Skip to content

Instantly share code, notes, and snippets.

What would you like to do?
Functional language style pattern matching in python with a decorator
from collections import defaultdict
class BadMatch(NameError):
"""Exception when your args don't match a pattern"""
class Any(object):
>>> 'wutup' == Any()
def __eq__(mcs, _other):
return True
Any = Any()
class OfType:
>>> 3 == OfType(int, str, bool)
>>> 'ok' == OfType(int)
def __init__(self, *types):
self.type = types
def __eq__(self, other):
return isinstance(other, self.type)
class Where:
>>> 'ok' == Where(str.isupper)
>>> [] == Where(len)
def __init__(self, predicate):
self.predicate = predicate
def __eq__(self, other):
return bool(self.predicate(other))
return False
class WhereNot(Where):
def __eq__(self, other):
return not Where.__eq__(self, other)
class PatternMatcher(object):
Keeps a dict of lists where key is the function name
and val is a list of functions with that name.
Whenever it decorates a function, it adds that function
to its dict of lists, then replace it with a new function
that calls the right function in the list based on passed
def __init__(self):
self.funcs = defaultdict(list)
def find_match(self, name):
"""Return a function that knows how to call the right
function based on the args passed in.
my_funcs = self.funcs[name]
def wrapper(*args):
# TODO: can we handle **kwargs??
for function in my_funcs:
if len(args) == len(function.__defaults__):
if all(passed == spec for (passed, spec) in zip(args, function.__defaults__)):
return function(*args)
raise BadMatch("function `{0}` has no match for args ({1})".format(name, args))
return wrapper
def __call__(self, func):
"""Decorator: add to func to self.funcs"""
keyword_args = func.__defaults__ or 0
if func.__code__.co_argcount != len(keyword_args):
raise SyntaxError("Every argument must have default parameter for pattern matching")
return self.find_match(func.__code__.co_name)
ifmatches = PatternMatcher()
if __name__ == "__main__":
# pylint has NO IDEA what I'm doing here
# it's all like 'OMGWTF?!'
def my_func(test="hey"):
print("this is my_func with test=hey")
def my_func(test=2):
print("this is my_func with test=2")
def my_func(test=OfType(int)):
print("this is my_func with int test!=2")
def my_func(test=Any):
print("this is my_func with a non-int")
def psum(l=[]):
return 0
def psum(l=[Any]):
return l[0]
def psum(l=Where(lambda x: len(x) > 1)):
head, tail = l[0], l[1:]
return head + psum(tail)
def count_letters(string=""):
return 0
def count_letters(string=OfType(str)):
return count_letters(string[0], string[1:])
def count_letters(x=Where(str.isalpha), xs=OfType(str)):
return 1 + count_letters(xs)
def count_letters(x=WhereNot(str.isalpha), xs=OfType(str)):
return count_letters(xs)
assert count_letters("ACD123") == 3
assert count_letters("") == 0
assert count_letters("123424") == 0
assert False
except BadMatch:
assert True
import doctest
* Generate classes like WhereNot automatically with class decorator?
* Haskell style deconstruction, perhaps like:
def count(first_arg=Match(x=Head, *xs=Rest)):
print x, xs
Will require some nasty hacks like
* Implement boolean operators for stuff like:
def example(name=OfType(str) & Where(str.isalpha)):

This comment has been minimized.

Copy link

admk commented Nov 6, 2013

Hi, I am inspired by your work, I have created patmat, which is a similar idea but allows recursive matching, supports lists, tuples and dictionaries, as well as *args and **kwargs in function calls. You can find it here:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.
You signed in with another tab or window. Reload to refresh your session. You signed out in another tab or window. Reload to refresh your session.