Created
August 23, 2019 13:10
-
-
Save maxfischer2781/7ab8f910ac133c43cd5bfd29a5058a44 to your computer and use it in GitHub Desktop.
Extended class based variant of switch statements in Python
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| r""" | |
| Another class-based implementation of a ``switch`` type | |
| Toy example for building types that emulate ``switch``-statements. | |
| Supports type-based matching, and can be extended to destructuring/pattern | |
| matching and value-based matching. | |
| ``switch``-types must derive from the ``Switch`` class. Each match case | |
| is a method ``def case(self, name: pattern [, name: pattern [, ...]]):``. | |
| Cases are seemingly evaluated in-order, without guarantee on side-effects. | |
| .. code:: python3 | |
| class Magnitude(Switch): | |
| @destruct(Vector3) | |
| def case(self, x, y, z): | |
| "match(Vector(1, 2, 3)) => 14" | |
| return x*x + y*y + z*z | |
| def case(self, arg: list): | |
| "match([1, 2, 3]) => 6" | |
| return sum(arg) | |
| def case(self, a: int, b: int): | |
| "match(3, 6) => 9" | |
| return a, b | |
| :note: The `destruct`\ ering decorator is a non-working draft. | |
| """ | |
| from typing import Type, Generic, TypeVar | |
| from functools import partial | |
| T = TypeVar('T') | |
| class CaseError(LookupError): | |
| """No case matched a given value""" | |
| def __init__(self, values, switch): | |
| self.values = values | |
| self.switch = switch | |
| super().__init__(f'values {values!r} did not match any case') | |
| class Destructure: | |
| """ | |
| Type of cases that destructure their match | |
| :param tp: the types by which to match and destructured | |
| :param case: the case to evaluated a successful match | |
| """ | |
| # TODO: complete me | |
| def __init__(self, *tp, case): | |
| self.types = tp | |
| self.case = case | |
| class AliasingNamespace(dict): | |
| """Namespace that allows multiple values for the fields ``aliases``""" | |
| def __init__(self, *aliases): | |
| super().__init__({alias: [] for alias in aliases}) | |
| self.aliases = aliases | |
| def __setitem__(self, key, value): | |
| if key in self.aliases: | |
| self[key].append(value) | |
| else: | |
| super().__setitem__(key, value) | |
| class SwitchType(type): | |
| def __new__(mcs, name, bases, namespace, cases='case', **kwargs): | |
| namespace = namespace.copy() | |
| namespace['__cases__'] = namespace.pop(cases) | |
| cls = super().__new__(mcs, name, bases, namespace, **kwargs) | |
| return cls | |
| @classmethod | |
| def __prepare__(mcs, name, bases, cases='case', **kwargs): | |
| # TODO: make __once__=True matches while traversing the namespace | |
| return AliasingNamespace(cases) | |
| class Switch(Generic[T], metaclass=SwitchType): | |
| """ | |
| Baseclass for Switch types | |
| Provides the case analysis and matching facilities. | |
| """ | |
| def __new__(cls, *values, __once__=False) -> T: | |
| # TODO: build a proper matching table if __once__=False | |
| return cls.__match_once(values) | |
| @classmethod | |
| def __match_once(cls, values): | |
| for dispatch, case in cls.__get_cases(): | |
| if len(dispatch) != len(values): | |
| continue | |
| if all( | |
| isinstance(value, tp) for value, tp | |
| in zip(values, dispatch) | |
| ): | |
| return case(*values) | |
| raise CaseError(values, cls) | |
| # TODO: get pattern and value cases as well | |
| @classmethod | |
| def __get_cases(cls): | |
| """Get every ``case`` of this class""" | |
| for case in cls.__cases__: | |
| yield cls.__get_dispatch(case) | |
| @staticmethod | |
| def __get_dispatch(func): | |
| """Get the type-dispatch for ``func``""" | |
| if isinstance(func, Destructure): | |
| return func.types, func.case | |
| assert not isinstance(func, staticmethod) | |
| arguments = func.__code__.co_varnames[1:] | |
| annotations = func.__annotations__ | |
| try: | |
| dispatch = tuple(annotations[arg] for arg in arguments) | |
| except KeyError: | |
| raise TypeError( | |
| f'missing annotations for {func.__module__}{func.__qualname__}' | |
| ) | |
| return dispatch, partial(func, None) | |
| def destruct(*tp): | |
| """ | |
| Mark a case a destructuring its match | |
| .. code:: python3 | |
| @destruct(Vector3, Vector3) | |
| def case(x1, x2, x3, y1, y1, y3): | |
| return Vector3(x1 - y1, x2 - y2, x3 - y3) | |
| """ | |
| return partial(Destructure, *tp) | |
| def match(*values): | |
| """ | |
| Decorator to match against a ``Switch`` type definition | |
| This decorator is applied with a value to a type definition. | |
| It instantly evaluates the match - the result is assigned to | |
| the name of the class. | |
| .. code:: python3 | |
| @match(15) | |
| class result(Switch): | |
| def case(self, arg: str): ... | |
| def case(self, arg: int): ... | |
| """ | |
| def match_now(switch: Type[Switch[T]]) -> T: | |
| return switch(*values, __once__=True) | |
| return match_now | |
| # example of directly matching a case | |
| # the result is assigned to the class name | |
| # note that we could '@print' this directly as well | |
| @match(3, 5) | |
| class Magnitude(Switch): | |
| def case(self, arg: list): return sum(arg) | |
| def case(self, a: int, b: int): return a, b | |
| print(Magnitude) | |
| # example of matching a predefined case | |
| class Magnitude(Switch): | |
| def case(self, arg: list): return sum(arg) | |
| def case(self, a: int, b: int): return a, b | |
| print(Magnitude(7, 8)) | |
| print(Magnitude([1, 2, 3, 4])) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment