Last active
August 29, 2015 14:13
-
-
Save niwinz/27a91ae399e5de5dba10 to your computer and use it in GitHub Desktop.
clojure like multimethods implemented in python3
This file contains 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
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