Yet Another Python Multimethod Decorator, Part 6: But Wait, There’s More!

Somehow this ended up unposted and thus out-of-order, bah…

So, we’ve now got a reasonably complete implementation of multi-methods for Python. I can feel some objections brewing in the minds of some readers – aspects of a “proper” multiple dispatch mechanism that I haven’t addressed here – but hush, there are some points I’ll arrive at soon which will hopefully address those. Before we do that, I want to introduce a new feature. There’s a good reason why I’m doing this first… you’ll just have to trust me. 🙂

So, we’ve implemented way of dynamically dispatching methods based on the types of their parameters. Let’s remind ourselves of how we’re doing it: we’re building an object which is callable, examines the object for either its class or the presence of certain attributes (depending on whether we require a class or duckface), finds the first implementation which the object satisfies, and then returns the result of calling that implementation.

Or, to put it another way, we build a list of predicates for each implementation, and dispatch to the implementation where the arguments match the predicates. When we put it like that, that prompts the question: Why are we limiting ourselves to predicates about type?

Sure, there are times when we want to apply different implementations based on type. Other times we want to apply different implementations based on other criteria. It would be nice to be able to do things like, say:

@case(1)
def factorial(x): return 1

@default
def factorial(n): return n * factorial(n-1)

Or even:

def positive_integer(x): return isinstance(x, int) && x > 0

@case(1)
def factorial(x): return 1

@case(positive_integer)
def factorial(x): return n * factorial(n-1)

# this isn't necessary for functionality, it just adds clarity
# without this we would just throw a TypeError as no appropriate target exists
@default
def factorial(x): raise RuntimeError("Factorial can only be called with a positive integer: called with " + n)

It’s arguable that we’re still dealing with type concerns here, if we think of types as sets of values with common properties – but that’s kind of moot, because I don’t see an reason why it’s compelling to allow dispatch on type concerns but not other concerns here, other than historically that’s what we’re used to dispatching on.

The good news is that our implementation very nearly already does this. It already converts the argument into a predicate, based on its type (one predicate for classes, another for duckfaces): we only need to add another couple of cases to handle, namely to assume anything callable is a predicate (we need to be careful here as classes are callable, so those have priority over general callables) and treat anything else as a value (the sorts of values we want to dispatch on should not normally be callable).

We may even, for clarity and avoidance of unexpected cases (such as wanting to match a callable by value), only want to accept one type – predicates – as arguments for the @case decorator, and provide helper predicates of eq(x), class(x) or duckface(x) for the value, class and duckface cases. The downside of that is it makes a syntactic sugaring less sugary in the use case it was originally introduced for, but we’ve got an obvious fix in the wings in case those cases become actual problems.

Despite this being basically the same implementation, though, our philosophy has changed substantially. We’re no longer just trying to address the requirements of multimethods: we’ve got something more akin to function patterns (without deconstruction, but allowing general predicates). Now our decorator names (@case and @default) seem even more apt. We’ve introduced a syntactic sugar around arbitrary conditionals, raising them out from the bodies of functions to create many smaller fuctions, each addressing one case.

What are the implications of that? Well, the first implication is what it does to method resolution order, which is a white elephant I deliberately haven’t addressed up to this point. The short answer is it means you can’t come up with a sensible, consistent method resolution order based on the cases any more.

The rebuttal to that, of course, is that ship sailed a long time ago.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s