[Python-3000] Generic functions
Phillip J. Eby
pje at telecommunity.com
Tue Apr 4 18:38:33 CEST 2006
Nick Coghlan wrote:
>Unfortunately the rules for choosing *which* implementation to dispatch to,
>even if restricting things to concrete types only, are necessarily complex.
>
>As Walter pointed out, the deliberately simplistic sample code elsewhere in
>this thread dispatches to a different method depending on order of
>registration and hashing idiosyncrasies.
The simple code that I posted actually depends only on __mro__.
>To fix that, you either have to stop permitting subclasses of registered
>argument types,
Um, no. :) The single-dispatch case works quite nicely with __mro__
(or the moral equivalent thereof for classic classes).
>or else you have to define the idea of a "closest" signature
>match, at which point you've been forced to throw "simple" right out
>the window.
It's no more complex than Python's __mro__ computation is, actually.
>Given this type hierarchy:
> A
> B C
>D E F
>
>and a call containing (D(), E(), F()), is the type signature (B, B, C) a
>closer match than the signature (A, E, F)?
Neither. It's ambiguous if you're doing symmetric dispatch. The
correct algorithm for scanning a set of signatures like this is that
you must check *all* signatures for applicability, and then choose
the one that is "most specific" by throwing out dominated matches
(ones that are a superset of some other match). If you end up with
more than one applicable signature, you have a conflict.
This is almost exactly the same as Python's metaclass determination
algorithm, by the way: it goes through all the candidate metaclasses
and throws away dominated metaclasses. If at the end there is more
than one candidate remaining, it's a metaclass conflict.
Now, I'm not saying that you'd actually want to implement this
matching algorithm in such a naive way as a loop over all
possibilities. RuleDispatch creates a tree of tests that eliminates
entire groups of signatures from consideration while testing
conditions in parallel. So in your example, the D() argument has its
__mro__ scanned to eliminate any signatures that don't support a 'B'
, 'A', or 'D' in the first position. Then the E() is scanned, and
finally the F().
In each case, the equivalent of a single isinstance() check is done,
and the result used to choose a new sub-branch of the tree. The
final node after doing the three quasi-isinstance operations is a
node representing the set of applicable signatures. If there is more
than one dominant signature, it's an ambiguity error.
I'm simplifying a bit here, because RuleDispatch actually allows you
to create custom combiners to handle multiple applicable signatures
entirely according to your taste. So, you could actually define an
alternative resolution that didn't complain about ambiguous
methods. For example, you could decide that you would use a
left-to-right asymmetric resolution (ala Common Lisp) so that (B,B,C)
would be more specific than (A,E,F) because B->A in the first
position. Or you could decide that you would break ties using the
definition order, which is a popular approach if you're just using
the generic function as a kind of "listener" hook, where you plan to
call *all* the applicable signatures, even duplicates, because the
function is intended as a broadcast hook.
(Notice, by the way, that this makes another class of "registration"
or "registries" in the stdlib unnecessary; for example, you could
ditch the atexit module by just having sys.exitfunc be a generic
function that users register methods for!)
>In a certain sense, an adapting protocol is just a generic function that only
>permits dispatch of single argument functions based on type of that
>argument -
>as such, adaptation is necessarily simpler than full generic function support.
Adaptation is equivalent to a single-dispatch generic function,
yes. And multiple dispatch is certainly more complex to
implement. However, multiple dispatch is a lot easier to implement
if you have single-dispatch generic functions available. A huge
amount of RuleDispatch is built on single-dispatch generic functions
and adaptation.
If I were going to do it over again, I wouldn't use the adaptation
bit at all; I'd just start with single-dispatch generics. Or maybe
not even those. I've considered that I could actually write the
bootstrapping bits of RuleDispatch using a simple "naive"
test-all-signatures algorithm, because by the time RuleDispatch was
fully imported it would be able to replace those functions with its
tree-building algorithms.
But that's mostly fantasy at the moment, because RuleDispatch was
effectively a sabbatical project for me, and although I've gotten a
few nibbles about possible consulting projects built around it,
nothing has materialized. So RuleDispatch is unlikely to get any
serious development time from me in the near future.
More information about the Python-3000
mailing list