Skip to content

Instantly share code, notes, and snippets.

@maxfischer2781
Created August 23, 2019 13:10
Show Gist options
  • Select an option

  • Save maxfischer2781/7ab8f910ac133c43cd5bfd29a5058a44 to your computer and use it in GitHub Desktop.

Select an option

Save maxfischer2781/7ab8f910ac133c43cd5bfd29a5058a44 to your computer and use it in GitHub Desktop.
Extended class based variant of switch statements in Python
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