[PEAK] Moving forward with PyProtocols and generic functions

Phillip J. Eby pje at telecommunity.com
Wed Nov 3 18:47:53 EST 2004


About three weeks ago, I floated a proposal for a simplified "convenience" 
declaration API in PyProtocols:

   http://www.eby-sarna.com/pipermail/peak/2004-October/001828.html

So far, the only feedback has been from Ulrich, who expressed confusion 
about the 'performs' proposal, and opined that 'for_types' and 
'for_protocols' were less explicit than the current 'asAdapterForX' 
keywords.  So, here's a revised proposal:

     @protocols.function_implements(IFoo)
     def foo(x,y,z):
         """Do whatever IFoo.__call__ says this should do"""

     @protocols.adapter_function(IFoo, adapts_types=[int])
     def int_as_foo(anInteger):
         # return an IFoo implementation for anInteger

     class Foo:
         protocols.instances_provide(
            IFoo, adapts_protocols=[IBar]
         )

I'm assuming that the other items I proposed, like module_provides and 
class_provides, are remaining the same as I proposed them.  Also, keep in 
mind that in Python 2.2 and 2.3, the '@' usages above will be spelled:

     [protocols.function_implements(IFoo)]
     def foo(x,y,z):
         """Do whatever IFoo.__call__ says this should do"""

     [protocols.adapter_function(IFoo, adapts_types=[int])]
     def int_as_foo(anInteger):
         # return an IFoo implementation for anInteger

Any comments on these proposals?

Also, after much reflection on where generic functions should go, I think 
I've settled on creating another package, but it will be called "dispatch" 
rather than "generics", and it will still be part of the PyProtocols 
distribution.  Like 'protocols', it will also be considered part of the 
PEAK core API, so you'll be able to do, e.g.:

     from peak.api import *

     [dispatch.when("some(condition)")]
     def whatever(some):
         # blah

I will also move the currently-experimental 'as' decorator to the 
'dispatch' package, so that e.g.:

     [dispatch.as(classmethod)]
     def something(cls, etc):
         ...

can be used as a substitute for @classmethod (or 'something = 
classmethod(something)' in current Python versions.

My current plan for the dispatch package is to include not only the current 
predicate-dispatch prototype there, but also a simple protocol-based 
single-dispatch generic function.  Basically, any place in PEAK where we 
have an interface with only one method, it's an excellent candidate for 
replacement with a single-dispatch generic function.  For example, here's 
some recent code from peak.config:


# interfaces.py

class IStreamSource(Interface):
     """A way to load configuration from a file, URL, or other stream"""

     def getFactory(context):
         """Return a 'naming.IStreamFactory', using 'context' for any 
lookups"""


# config_components.py

class StreamSource(protocols.Adapter):

     protocols.advise(
         instancesProvide=[IStreamSource],
         asAdapterForTypes=[str,unicode]
     )

     def getFactory(self, context):
         from peak.naming.factories.openable import FileFactory,FileURL
         try:
             url = FileURL.fromFilename(self.subject)
         except exceptions.InvalidName:
             url = naming.toName(self.subject, FileURL.fromFilename)
         if isinstance(url,FileURL):
             return FileFactory(filename=url.getFilename())
         return naming.lookup(context,url)


class FactorySource(protocols.Adapter):

     protocols.advise(
         instancesProvide=[IStreamSource],
         asAdapterForProtocols=[IStreamFactory]
     )

     def getFactory(self, context):
         return self.subject


If the above were implemented using a single-dispatch generic function, it 
would look something like:

getStreamFactory = dispatch.SimpleGeneric(
     """Return a 'naming.IStreamFactory' for 'source'

     Usage::

         factory = config.getStreamFactory(source,context)

     Built-in cases::

         If 'source' is a 'naming.IStreamFactory', it is simply returned.
         If it is a string or Unicode object, it will be interpreted as
         either a filename or URL.  If it is a URL, it will be looked up
         in 'context'.

     You may define additional cases for this function using
     'dispatch.when', e.g.::

         from peak.config.api import getStreamFactory

         [dispatch.when(MyType)]
         def getStreamFactory(source,context):
             '''Return a stream factory for 'source' (a 'MyType' instance)'''
     """
)


[dispatch.when([str,unicode])]
def getStreamFactory(source,context)
     from peak.naming.factories.openable import FileFactory,FileURL
     try:
         url = FileURL.fromFilename(source)
     except exceptions.InvalidName:
         url = naming.toName(source, FileURL.fromFilename)
     if isinstance(url,FileURL):
         return FileFactory(filename=url.getFilename())
     return naming.lookup(context,url)


[dispatch.when(naming.IStreamFactory)]
def getStreamFactory(source, context):
     return source


This is not only more succinct (code-wise), but it also doesn't require 
creating an adapter instance each time the function is invoked.  (It will 
create a temporary tuple down in the implementation somewhere, but that's 
of little consequence.)

The above is not a finalized dispatch API, though.  For example, I'm not 
sure that you'll be able to use a list or a protocol directly as arguments 
to 'when'.  I'm not even positive you'll be able to use 'when' with 
single-dispatch functions, or whether there will be separate declarations 
for single and multiple-dispatch generics.

Anyway, as you can see, this approach would eliminate the need for many 
"one-method" interfaces in PEAK, while making it more obvious how to extend 
many of PEAK's APIs.  Documentation is also more straightforward, because 
we can describe the "built-in" behaviors in the docstring for the generic 
function itself.  (I've never felt comfortable with such "doc bundling" for 
interfaces.)

In addition to these things, I'll also be working to finish out the 
predicate-dispatch generic functions.  One currently annoying issue is that 
predicate dispatch functions don't respect 'adapt()'-ability fully.  That 
is, they 1) do not use __conform__ and 2) the selected implementation is 
passed the original object, not an adapted object.  In effect, 'when("x in 
IFoo")' simply means that 'x' will be an instance of a class that can be 
*adapted* to IFoo, not that it is an object whose __conform__() returns a 
value for IFoo, or that the 'x' received by the method actually implements 
IFoo.  In effect, you need to always do:

      [when("x in IFoo")]
      def something(x,y):
          x = IFoo(x)

before using the 'x' in question.  However, this is *not* the case for 
single-dispatch generic functions, which can be implemented directly via 
'adapt()' (since there's only one parameter to dispatch on).

I'm not sure what to do about this, though, because 1) '__conform__' calls 
can't really be indexed in any meaningful way, and 2) I don't see a 
straightforward way to allow a multi-dispatch function's criteria to munge 
the function's parameters.  That might have to be something that's done by 
some sort of wrapper to the individual method, but that adds another 
calling layer versus just adding the explicit 'x = IFoo(x)' to the top of 
the method.  The only time it would be more efficient to do parameter 
munging is if there are other very expensive calculations that are part of 
the predicate, whose values are then needed in the method body.

I did, however, previously have an idea for such a syntax:

     [when("let(x=IFoo(x,None)) in (x is not None)")]

I was originally thinking that this would mainly be to make it easier to 
refer to a complex expression more than once, but in principle it could 
also be used to actually change or add arguments to specialized cases.

Of course, once such a mechanism existed, it could perhaps be exploited to 
let criteria do the munging as well, such that:

      [when("x in IFoo")]

is effectively shorthand for:

     [when("let(x=IFoo(x,None)) in (x is not None)")]

This still doesn't address '__cohasattr(xnform__', but it's progress.

Finally, there's one other feature I plan to add, and that's the ability to 
use generic functions as methods in a class.  After much thinking recently 
about schema mappings, view registration, and other matters, I've concluded 
that the "best" way to do these things is by defining generic functions in 
a class, and then subclassing it to form alternate contexts.  For example, 
I could create a class that implements a relational mapping workspace over 
a generic schema, then subclass it to create a specific relational mapping 
that overrides some defaults.

The way that this would work is that since methods take a 'self' argument, 
we can actually use the *same* generic function for *all subclasses* of the 
original class.  However, if we tweak generic functions so that 
'SomeClass.someGeneric' returns a "bound" generic function that 
automatically adds "and self in SomeClass" to the criteria, then cases 
defined for subclasses override those defined in superclasses (as long as 
the remaining criteria are as specific or more specific than the criteria 
being overridden).

Of course, for many of these uses, you'll never directly add cases to the 
generic functions; you'll likely use domain-specific higher-order functions 
like 'registerView()' or 'mapTable()' that will add closures to the generic 
functions.

Anyway, the 'when()' function will still probably get some additional 
tweaking to make it relatively easy to add these "override cases" to a 
generic method in a subclass, when you're not using domain-specific APIs to 
define them.

This ability to bundle generic functions into classes and to subclass with 
selective overriding should make it possible to create some really 
interesting frameworks, especially those that are metadata driven.  For 
example, I expect to eventually replace virtually all of the current 
peak.security implementation this way, because all the funky adapters and 
temporary protocols should be replaceable with generic functions built into 
the Interaction, and easy to extend/override rules in subclasses.

All of this generic function stuff is pretty much a requirement for future 
work on peak.web, which really "wants" views and menus (and possibly 
certain other things) to be generic function-based.  It's also a 
prerequisite for implementing the "new metadata" concept I described in:

    http://www.eby-sarna.com/pipermail/peak/2004-October/001886.html

Even though that post spoke in terms of adapting to protocols, the API and 
implementations will be cleaner if classes with generic functions are 
used.  This is because "interface inheritance" is in the "opposite 
direction" from functional inheritance.  Currently, to inherit the 
functionality of already-defined adapters, one creates a 
protocol.Variation() of the existing protocol.  However, to do the same 
thing with generic functions, one can simply subclass the class that 
contains the generic functions.  So, if one has a 'Syntax' class that holds 
default syntax metadata for arbitrary application schemas, one can easily 
subclass it to create e.g. 'XMLSyntax', and add override cases to its 
metadata functions.

This is a more straightforward approach than having to deal in some sort of 
"registry" or "context" instances and deriving them from each other 
somehow.  First, it's obvious what the objects do, because they're objects 
you can use to do something.  For example, one might say 's = 
Syntax(SomeClass)' to create a Syntax instance that knows how to parse and 
format instances of SomeClass.  Second, it's obvious how to extend them, 
because you just subclass them.

Anyway, that's the current PyProtocols state of the union, and plans for 
the future.  Comments, anyone?




More information about the PEAK mailing list