Skip to content

Instantly share code, notes, and snippets.

@niwinz
Last active August 29, 2015 14:13
Show Gist options
  • Save niwinz/27a91ae399e5de5dba10 to your computer and use it in GitHub Desktop.
Save niwinz/27a91ae399e5de5dba10 to your computer and use it in GitHub Desktop.
clojure like multimethods implemented in python3
from threading import Lock
from inspect import isclass
from functools import partial
def isa(cls_child: "class", cls_parent: "class") -> bool:
if not isclass(cls_child):
return False
if not isclass(cls_parent):
return False
return issubclass(cls_child, cls_parent)
class _multimethod:
def __init__(self, dispatch):
self.__name__ = dispatch.__name__
self.__doc__ = dispatch.__doc__
self._dispatch = dispatch
self._dispatch_entries = []
self._dispatch_cache = {}
self._dispatch_default = None
self._mutex = Lock()
self._notfound = object()
def register(self, value, func=None):
if func is None:
return partial(self.register, value)
with self._mutex:
_callable = _multimethod_callable(value, func)
self._dispatch_entries.append(_callable)
return self
def register_default(self, func):
self._dispatch_default = _multimethod_callable(None, func)
return self
def __call__(self, *args, **kwargs):
self._mutex.acquire()
# Calculate the dispatch value
dispatch_match = self._dispatch(*args, **kwargs)
# If a dispatch resolution is already
# exists in cache, use it as is. It
# just an optimization for avoid compute
# the resoultion in each call.
if dispatch_match in self._dispatch_cache:
dispatch_func = self._dispatch_cache[dispatch_match]
# Explicit release lock befor execute the method.
self._mutex.release()
return dispatch_func(*args, **kwargs)
# If no resolution found on cache, start the first
# search iteration using isa? method.
for dispatch_func in self._dispatch_entries:
dispatch_value = getattr(dispatch_func, "_dispatch_value", self._notfound)
if dispatch_value is self._notfound:
continue
if isa(dispatch_value, dispatch_match):
self._dispatch_cache[dispatch_match] = dispatch_func
self._mutex.release()
return dispatch_func(*args, **kwargs)
# If no resolution foun on first iteration, go to
# the second iteration using == operator.
for dispatch_func in self._dispatch_entries:
dispatch_value = getattr(dispatch_func, "_dispatch_value", self._notfound)
if dispatch_value is self._notfound:
continue
if dispatch_value == dispatch_match:
self._dispatch_cache[dispatch_match] = dispatch_func
self._mutex.release()
return dispatch_func(*args, **kwargs)
# If we are here, so no match is found.
self._mutex.release()
if not self._dispatch_default:
raise RuntimeError("No match found.")
return self._dispatch_default(*args, **kwargs)
class _multimethod_callable:
"""
Callable wrapper. The main purpose of this
callable container is not mutate the registered
function in a multimethod with dispatch value.
"""
def __init__(self, dispatch_value, func):
self._callable = func
self._dispatch_value = dispatch_value
def __call__(self, *args, **kwargs):
return self._callable(*args, **kwargs)
def multimethod(func):
"""Decorator that creates multimethods."""
return _multimethod(func)
##############################################################################
# Example
##############################################################################
## Say Hello
@multimethod
def say_hello(person):
return person.get("lang", "es")
@say_hello.register("es")
def say_hello(person):
return "Hola {}".format(person["name"])
@say_hello.register("en")
def say_hello(person):
return "Hello {}".format(person["name"])
person_es = {"name": "Foo", "lang": "es"}
person_en = {"name": "Bar", "lang": "en"}
## Dispatch on multiple values
@multimethod
def do_stuff(data):
return (data.get("a"), data.get("b"))
@do_stuff.register((1,2))
def do_stuff(_):
return "foo"
@do_stuff.register((2,2))
def do_stuff(_):
return "bar"
@do_stuff.register_default
def do_stuff(_):
return "baz"
if __name__ == "__main__":
print(say_hello(person_en))
print(say_hello(person_en))
print(say_hello(person_es))
print()
print(do_stuff({"a": 1, "b": 2}))
print(do_stuff({"a": 2, "b": 2}))
print(do_stuff({"a": 3, "b": 2}))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment