[PEAK] API Tradeoffs (was RE: A ridiculously simple mapping layer)
Phillip J. Eby
pje at telecommunity.com
Tue Jul 27 13:50:38 EDT 2004
At 11:53 AM 7/27/04 -0400, Phillip J. Eby wrote:
>Right now I'm going over the requirements for both providing all the
>current functionality of DM's, while expanding the basic
>capabilities. And I'm also trying to balance things in my mind between
>making the class conversion a general-purpose AOP facility, and making it
>extremely specific to data mapping.
Hm. I'm beginning to sense a pattern here... whenever there are tradeoffs
required as to the degree of flexibility an operation requires, consider
using generic functions.
That is, I'm trying to balance the class conversion mechanism between
extremes of simplicity and generality, trying not to pay a high price for
early generalization, but not ruling it out in the future. So, what if the
mapping from abstract modules to workspace classes was done by a generic
function?
This would in effect create a general-purpose AOP framework, of the form:
componentClass = genericAdvisingFunction(abstractModule)
As it happens, generic functions are composable, so
'genericAdvisingFunction' can be created from an arbitrary number of
specific cases, just as aspects should be composable. And, because generic
functions (at least the ones in PyProtocols) allow testing arbitrary
conditions, it's effectively possible to use them to specify the equivalent
of AspectJ's *static* pointcuts. That is, one could define a single
advising function that applies to several methods of multiple classes,
based on a pattern of the class and methods names, or any of several other
things.
There are some additional features I'd need to add to generic functions to
make this practical, mainly in the area of allowing actual composition of
generic functions. But they're not particularly a big deal, and I figured
I'd implement them eventually anyway.
The main tradeoff of using generic functions to resolve the "generality vs.
simplicity" conflict is economy of expression. That is, expressing things
in terms of generic functions is often quite verbose and repetitive. But,
this is countered by the relative ease of defining higher-level functions
that operate by creating closures and adding them to one or more generic
functions on your behalf, so that you can express yourself in
domain-specific terms. For example, by calling functions to
'declareField()' or 'mapTable()' or some such, rather than defining methods
directly. However, the fact that the implementation is in terms of generic
functions means that you can define your own high-level "macros" or even
code directly to the generic function when needed.
The other tradeoff that generic functions introduce are the issues of
desigining the function signature(s) and method combination rule(s). That
is, generic functions are *so* generic that you could quite literally use
one generic function for *everything*, just by giving it an extra parameter
to say what it is you're supposed to be doing. :)
Naturally, that's a bad idea. But having too many functions can also be
problematic, because a given domain-specific operation like 'mapTable()'
might need to add methods to generic functions for loading an object,
saving an object, reading or modifying an object's fields, and so on. And
that means that the DSO (domain-specific operation) needs access to all
those functions, and we don't want you to have to pass them in. You want
to say something like 'mapTable(schema,...)', where that schema object
carries all the needed generic functions. (Of course, objects could
provide DSO's as methods, e.g. 'schema.mapTable(...)', but that's just a
different way of spelling the same thing.)
Anyway, this seems to me to mean that a common case for generic functions
is that they will live on some instance, possibly "inheriting" an initial
definition from their class. For example, a "relational workspace" class
would define several generic functions for the operations needed by
objects, and supply the initial default implementations for the parts of
those operations that are not schema-specific. Schema-specific methods
would then be added as a result of calling DSO methods like
'workspaceClass.mapTable(...)', but could also be defined directly by
calling 'workspaceClass.someOperation.addMethod(predicate,function)'.
To make things even weirder, some of those generic functions will be the
runtime behavior of the workspace (e.g. get and set object attributes), but
some will be "build-time" behavior, i.e. defining how to transform model
classes into implementation classes.
Ow. That makes my own head hurt. :) I think I'm going to need some
"separation of church and state" (behavior and state?) there. More
specifically, I need to get a clearer idea of how and when the functions at
each level are defined. I just caught a glimpse of how everything is
"generic functions all the way down", and it made me dizzy. However, I
expect it's probably something that can be gotten used to. After all,
since version 2.2, Python is "metaclasses all the way down", but most of
the time nobody notices or cares about that fact. :)
There's a lot more that needs to go into this train of thought, but I'll
need to continue it later.
More information about the PEAK
mailing list