Skip to content

Instantly share code, notes, and snippets.

@eric-wieser
Last active June 24, 2020 16:57
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save eric-wieser/da679ff9b1b1e99aa660d54cb0dbd517 to your computer and use it in GitHub Desktop.
Save eric-wieser/da679ff9b1b1e99aa660d54cb0dbd517 to your computer and use it in GitHub Desktop.
Pattern matching without PEP 622

This shows how the walrus operator (:=) can be exploited to write pattern matching without needing PEP622 at all.

Supported patterns:

  • M._ - wildcard which matches anything
  • (x := M._) - bound match, result is stored in x.value
  • M[Class](*args, **kwargs) - Match an instance of a specific class
  • M([a, *M._, c]) - Match against [a, *_, c]
  • M([a, *(rest := M._), c]) - Match against [a, *rest, c]

Not implemented but straightforward:

  • M({a, b, *(rest := M._)}) - Match against {a, b, *rest}, by adding ClassMatcher._known[set].
  • M({'a': a, 'b': b, **(rest := M._)}) - Match against {a, b, **rest}. This would be implemented by making Any.keys() return StarStarMatcher(self), Any.__getitem__(self, StarStarMatcher) return None, and then handling StarStarMatcher in ClassMatcher._known[dict].

Patterns are matched with obj in pattern:

if obj in M(...):
   ...
elif obj in M(...):
   ...

Main differences from PEP 622:

  • No special syntax
  • Bound values must be accessed with .value

See examples.py for some examples.

# some example uses
obj = [1, 2, [3, 4]]
assert obj in M([M._, *(rest := M._), M._])
assert rest.value == [2]
# does not have two elements
assert not obj in M([M._, M._])
assert obj in M([1, M._, [M._, four := M._]])
assert four.value == 4
# second alternation is a match
assert obj in M([M._, M._]) | M([M._, M._, M._])
# custom classes can implement this too
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
@staticmethod
def __match__(obj, x, y):
return isinstance(obj, Point) and obj.x in M(x) and obj.y in M(y)
p = Point(1, 2)
assert p in M[Point](1, 2)
assert p in M[Point](x := M._, 2)
assert x.value == 1
""" See readme for documentation """
_none = object()
class _MObject:
def __call__(self, arg=_none):
if isinstance(arg, list):
return self[list]([self(a) for a in arg])
elif isinstance(arg, tuple):
return self[tuple](tuple([self(a) for a in arg]))
elif isinstance(arg, (_MatchBase, _StarMatcher)):
return arg
else:
return _Exactly(arg)
def __getitem__(self, cls):
def f(*args, **kwargs):
return _ClassMatcher(cls, *args, **kwargs)
return f
@property
def _(self):
return _Any()
M = _MObject()
class _MatchBase:
def __or__(self, other):
return _Either(self, other)
class _ClassMatcher(_MatchBase):
_known = {}
@classmethod
def register(cls, c):
def decorator(f):
cls._known[c] = f
return f
return decorator
def __init__(self, cls, *args, **kwargs):
self._cls = cls
self._args = args
self._kwargs = kwargs
def __contains__(self, other):
try:
m = type(self)._known[self._cls]
except:
m = self._cls.__match__
if m(other, *self._args, **self._kwargs):
self.value = other
return True
return False
class _StarMatcher:
def __init__(self, any):
self._any = any
def __contains__(self, other):
raise TypeError("Can't be used directly")
def _register_sequence(t):
@_ClassMatcher.register(t)
def _t_matcher(other, arg):
assert isinstance(arg, t)
if not isinstance(other, t):
return False
n_arg = len(arg)
star = None
for i, v in enumerate(arg):
if isinstance(v, _StarMatcher):
star = (i, v)
if star:
i, v = star
arg_begin, arg_end = arg[:i], arg[i+1:]
if other[:i] not in M(arg_begin):
return False
if other[len(other)-len(arg_end):] not in M(arg_end):
return False
v._any.value = other[i:len(other)-len(arg_end)]
return True
else:
if len(arg) != len(other):
return False
return all(
oi in M(ai) for oi, ai in zip(other, arg)
)
return arg == other
_register_sequence(list)
_register_sequence(tuple)
class _Either(_MatchBase):
def __init__(self, *options):
self._options = options
def __contains__(self, other):
if any(other in o for o in self._options):
self.value = other
return True
return False
class _Any(_MatchBase):
def __contains__(self, other):
self.value = other
return True
def __iter__(self):
yield _StarMatcher(self)
class _Exactly(_MatchBase):
def __init__(self, value):
self.value = value
def __contains__(self, other):
return self.value == other
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment