[Python-3000] Sane transitive adaptation
Phillip J. Eby
pje at telecommunity.com
Fri Apr 7 01:31:10 CEST 2006
At 02:15 PM 4/6/2006, Guido van Rossum wrote:
>On 4/6/06, Phillip J. Eby <pje at telecommunity.com> wrote:
> > I don't want to discourage people from working out their own ideas,
> > but a lot of the stuff that's being discussed here about protocol
> > adaptation is already implemented in PyProtocols.
>
>That's great. I believe that if we derive things from first principles
>here and arrive at the same choices as PyProtocols, we haven't wasted
>anybody's time;
I agree; that's why I said "I don't want to discourage people from
working out ... ideas". I just wanted to point out that for those
who prefer to learn from worked-out examples, there are some
available. One reason I like to steal designs from *built* systems
is that making something real forces lots of tradeoffs into the open,
that don't come up when you're just brainstorming.
That having been said, please consider this a "spoiler warning": the
rest of this message reveals where I think you're all going to
eventually end up. If it'll spoil anyone's fun and/or learning to
know this ahead of time, don't read the rest of this message. :)
Hint: premature optimization is the root of all evil. I was actually
trying to hint in my previous message that it would be valuable to
learn from PyProtocols' example by *not* repeating it! I was not
suggesting that PyProtocols is a model to which you should all
aspire, but rather consider why I wish I hadn't bothered to do it in
the first place!
>At the moment I'm doing the same for overloadable functions. However,
>instead of doing it in the privacy of a hotel room in a temp directory
>on a disconnected laptop, I'll blog about it. There'll be a lot of
>thinking aloud, a couple of dead alleys and red herrings, and every
>once in a while I'll have to buy a clue from PyProtocols. But I really
>see no other way to do it; when I look at the completed PyProtocols my
>eyes just start glazing over.
No criticism of your methods was intended or implied; it's just that
I personally hate to struggle to reinvent something that's already
been done. More specifically, I wish somebody could've pointed *me*
to PyProtocols' docs before I wrote them, or better yet, sat me down
and showed me why generic, uh, overloadable functions were an even
better idea and sold me on why I shouldn't bother writing
PyProtocols, but jump straight to the good stuff. :)
This is particularly so in the area of multiple-argument dispatch, as
some people have been bringing up implementation ideas that I see as
painful dead-ends that took me months to chew my way out of. So I've
actually been doing an *excellent* job of sitting on my hands so far
and not stealing anybody's learning opportunities. :)
However, continuing in the vein of "things I wished somebody told
me", I would say that the biggest hurdle to implementing a good
multiple-argument dispatch system, IMO, is realizing that there is
only one basic algorithm that works, and that there are no
shortcuts. You can find lots of ways to make the basic algorithm
efficient (indexing, dispatch trees, partial evaluation, etc.), but
there are really no shortcuts.
The good news, however, is that once you realize there is only one
basic algorithm (eliminate non-matching cases and prioritize what's
left), you find that there are a lot of implementation shortcuts,
especially if you have single-dispatch functions available to build
the multiple-dispatch implementation. You just can't directly
shoehorn multiple-dispatch into single-dispatch, although you can
waste a *lot* of time trying. I literally could've had RuleDispatch
done maybe 2 years sooner if I took all the time I spent thinking
about how to figure out something "easier" to implement than the
Chambers&Chen optimization of the One True Algorithm, and just worked
on implementing it instead.
Anyway, the point of my comment was not to interfere with anybody's
learning, it was just to mention stuff that I would have wished to
have, when I was trying to work these things out. I'll go back to
sitting on my hands now, except for occasional drooling at the idea
of a writable func_ast attribute. :)
Actually, I like the func_ast idea so much that it tempts me to write
something that would emulate it, like a pair of set_ast and get_ast
functions for Python 2.x. (I could use PyPy's abstract
interpretation approach to turn bytecode back into a pseudo-AST,
perhaps.) It would be useful as a proof-of-concept to demonstrate
how simple it would be to implement trivial overloading and then full
multiple/predicate dispatch using AST manipulation, because all this
mucking about with dictionaries is way too forest-for-the-trees if
your goal is to understand the *applications* for overloading.
Overloadable functions are easiest to understand first (IMO) in terms
of syntactic transforms. After you've got a grasp at that level, you
can come up with other optimizations. Better still, with the new AST
branch and PyPy, we now have a better chance of just optimizing the
ASTs (or compiling them to C) directly, rather than painfully working
out a bunch of low-level MRO and registry issues. Those are costly
premature optimizations, and I wish I had not been sucked into that
trap. If I had it to do over, I would proceed directly to AST
manipulation, and then work on ways to make it faster (like the
Chambers & Chen-inspired dispatch indexes, and by moving the AST
manipulations to C after they were tested).
So, I would gently suggest that, now that we have overloaded
functions on the table, that it might be better to focus on "what
should construct X translate to in naive Python code", than worrying
about implementation feasibility of optimized code. One of the
interesting findings of Chambers and Chen was that a significant
number of overloadable functions "in the wild" have a very small
number of methods, and then there are a few that have gobs and gobs
of methods. My focus on premature optimization thus blinded me to
the fact that the most efficient way to deal with those common cases
is just to translate it to a simple set of of if-then
statements. The cases where you need big guns like registries are
much fewer and further between. Indeed, if you examine PEAK, Zope,
or Twisted, you will also find that most interfaces have only a
handful of adapter implementations (or direct method implementations
in classes), and then there are some that have zillions.
And -- here's the key bit -- if you have a tool that lets you handle
those common cases (of only a few distinct methods), you can then
*use* them as a stepping stone to implement the more complex
approaches. RuleDispatch makes extensive use of simple
single-dispatch functions to bootstrap the multiple-dispatch
implementation. If I had a multiple dispatch implementation whose
only drawback was that it was slow if you had more than a handful of
overloads for the function, then creating an *efficient*
multi-dispatch system on top of that is a piece of cake, especially
if you make the "overloading function" itself overloadable. That is,
once the system is bootstrapped, you just overload "overload()" to be
able to use the more efficient implementation approaches whenever the
number of overloads reaches a certain threshold. (Analogy: 'type' is
its own type in today's Python, but it's built on a simpler C-level
notion of types as a data structure with function pointers that it
needs in order to be bootstrapped. In the same way, you can
bootstrap overload() using an extremely simple form of overloading,
not far removed from monkeypatching.)
The reason I haven't done much on documenting RuleDispatch's
internals to date is because I basically got to a point where the
implementation I have, although relatively efficient for large
overload sets, is not as efficient as the naive approach (unoptimized
if-then trees) for the common cases, and isn't as customizable or
optimized as I'd like for the "zillions of methods" cases. Its
internals are hard-wired for things in a way that makes it hard to
add more special-case optimizations. Thus, RuleDispatch is
effectively optimized for an excluded middle where you have enough
rules that the naive approach is too slow, but not so many rules that
you need advanced customization features or speed that matches what
can be done by hand-tuning. In other words: for use cases that don't
actually exist! (This same problem exists to a lesser degree for PyProtocols.)
Thus, I've pretty much decided that I need to go back and reimplement
the RuleDispatch internals so that they are themselves based on
generic functions, and my plan was to use a naive approach to code
generation for those functions, and then "overload the overload() function".
In truth, my suggestion for Py3K is that simply providing a writable
func_ast and a simple overloading implementation that uses it, and is
itself overloadable, seems like the simplest thing that would
possibly work, and it allows unlimited implementation-level
optimizations. For example, let's say that naive if-then overloading
is too slow for pickle() or pprint(). So you overload "overload()"
to allow overloads to pickle to modify a registry instead of
modifying code. Voila!
So this is why I think that worrying about protocol objects and MROs
and registries is losing the forest for the trees. It's easier to
think about those things when you're grounded in specific use
cases. And, an important feature of dynamic overloading is that you
get to change your mind about how things work - just create another overload!
This is something that was totally not obvious to me before I got to
a certain point of working on RuleDispatch. It took a while for my
thinking to shift to a world where overloading is *cheap*. In
today's Python overloading requires interfaces or registries or
adaptation; it's not as simple of a thing, so you don't use it unless
you know you have to. But if you can overload functions as easily as
breathing, then overloadability can be the *default* case, and then
you realize that any *single* implementation of a function is
necessarily a premature optimization! And just as important, trying
to only ever design *one* implementation of an algorithm is
necessarily a wasteful compromise!
You see, overloading allows you to write correct algorithms, and then
overload the abstract operations for a specific implementation
scenario. Almost like PyPy generating a custom VM -- you can create
a new "back-end" for any given special case. So special cases aren't
special enough to break the rules -- but with overloading, you can
bend the rules to accomodate them. Or as Uncle Timmy put it, "1)
Everything is a trivial special case of something else, and 2) death
is a bunch of blue spheres." :)
More information about the Python-3000
mailing list