Bob Ippolito (@etrepum) on Haskell, Python, Erlang, JavaScript, etc.

Five-minute Multimethods In Python (using dispatch)


While PyProtocols dispatch more or less implements multimethods out of the box, I thought it would probably be useful to demonstrate that Guido's implementation of Five-minute Multimethods in Python can be cloned using it.

from dispatch.strategy import default, Argument, Signature
from dispatch.predicates import Getattr, Pointer
from dispatch.functions import GenericFunction

def multimethod_signature(args, kwargs):
    def isClass((arg, cls)):
        return Getattr(arg, '__class__'), Pointer(cls)
    def parse():
        for i, typ in enumerate(args):
            yield Argument(i), typ
        for name, typ in kwargs.iteritems():
            yield Argument(name=name), typ
    return Signature(map(isClass, parse()))

def multimethod(*args, **kwargs):
    def register(function):
        name = function.__name__
        oldfunc = function.func_globals.get(name, function)
        if not hasattr(oldfunc, 'addMethod'):
            oldfunc = GenericFunction(function).delegate
            def not_applicable(*a, **kw):
                raise TypeError("No Applicable Methods")
            oldfunc.addMethod(default, not_applicable)
        elif oldfunc is function:
            function = getattr(function, '_last_multimethod', function)
        sig = multimethod_signature(args, kwargs)
        oldfunc.addMethod(sig, function)
        oldfunc._last_multimethod = function
        return oldfunc
    return register

@multimethod(int, int)
def foo(a, b):
    print "code for two ints"

@multimethod(float, float)
def foo(a, b):
    print "code for two floats"

@multimethod(unicode, unicode)
@multimethod(str, str)
def foo(a, b):
    print (a, b)
    print "code for two strings"
    print "or unicodes..."

Note that it would be slightly less code if the type arguments were allowed to use isinstance(...), because that's what dispatch wants to do, but I wanted to clone Guido's implementation such that the class of the argument must be equal to the given type.

The other differences from Guido's implementation are:

  • No global registry, the registry is the func globals (could be extended to also look in locals).. so you could use this from multiple modules and not have to worry about having a flat namespace for multimethod names in your interpreter!
  • If you use default arguments, it's going to use the defaults specified on the first multimethod
  • You don't have to specify the type of every argument, you could multimethod dispatch on the first argument, or even named arguments

Note that would likely be more sensible and less code to use isinstance(...) style multimethod dispatch instead, but I just wanted to make sure that the (weird?) semantics were preserved. I'm guessing he used type identity because you could dispatch on a dict, so the implementation would be short :)

The following is a more "natural" implementation of how this would be implemented using PyProtocols dispatch. This version will accept subclasses, and can not be stacked. Its syntax is not as terse as @multimethod, but it's far more useful as you can dispatch on expressions, not just type.

import dispatch

def foo(a, b):
    foo is a generic method that takes two
    parameters and dispatches based on type

@foo.when("isinstance(a, int) and isinstance(b, int)")
def foo(a, b):
    print "code for two ints"

@foo.when("isinstance(a, float) and isinstance(b, float)")
def foo(a, b):
    print "code for two floats"

    # note that you can't use multi-line strings
    # in here without backslash continuation.
    # dispatch's current parser hates them(?!)
    "(isinstance(a, str) and isinstance(b, str)) or "
    "(isinstance(a, unicode) and isinstance(b, unicode))")
def foo(a, b):
    # what makes this more interesting than dispatching on
    # basestring is that you can't mix str/unicode here.
    # also note that this is equivalent to stacking...
    print (a, b)
    print "code for two strings"
    print "or unicodes..."