[Python-3000] Draft pre-PEP: function annotations (original) (raw)

Phillip J. Eby pje at telecommunity.com
Sat Aug 12 18:39:15 CEST 2006


At 12:33 AM 8/12/2006 -0400, Collin Winter wrote:

I don't see the point of this. A decorator should be responsible for manipulating the signature of its return value. Meanwhile, the semantics for combining annotations should be defined by an overloaded function like "combineAnnotations(a1,a2)" that returns a new annotation. There is no need to have a special chaining decorator.

May I suggest that you try using Guido's Py3K overloaded function prototype? I expect you'll find that if you play around with it a bit, it will considerably simplify your view of what's required to do this. It truly isn't necessary to predefine what an annotation is, or even any structural constraints on how they will be combined, since the user is able to define for any given type how such things will be handled. I've looked at Guido's overloaded function prototype, and while I think I'm in the direction of understanding, I'm not quite there 100%. Could you illustrate (in code) what you've got in mind for how to apply overloaded functions to this problem space?

You just define an overloadable function for whatever operation you want to perform on annotations. Then you define methods that implement the operation for known types, and a default method that ignores unknown types. Then you're done.

If somebody wants to do more than one thing with the annotations on their functions, then everything "just works", since there is only one annotation per argument (per the PEP), and each operation is ignoring types it doesn't understand.

This leaves only one problem: the possibility of incompatible interpretations for a given type of annotation -- and it is easily solved by using some container or wrapper type, for which methods can be added to the respective operations.

So, let's say I'm using two decorators that have a common (and incompatible) interpretation for type "str". I need only create a type that is unique to my program, and then define methods for the overloaded functions those decorators expose.

QED: any incompatibility can be trivially solved by introducing a new type. However, the most likely source of conflict is the need to specify multiple, unrelated annotations for a given argument. So, it's likely that most operations will want to interpret a list of annotations as just that: a list of annotations.

But there is no requirement that they do so. Someone writing a library of their own that has a special use for lists is under no obligation to adhere to that pattern. Remember: any conflict can be trivially solved by introducing a new type.

If you'd like me to sketch this out in code, fine, but you define the specific example you'd like to see. To me, this all seems as obvious and straightforward as 2+2=4 implying that 4-2=2. And it doesn't even have anything specifically to do with overloaded functions!

If you replace overloaded functions with functions that expect to call certain method names on the objects, the exact same principles apply. As long as each operation gets a unique method name, any conflict can be trivially solved by introducing a new type that implements both methods.

The key here is that introspection and explicit dispatching are bad. Code like this:

  def decorate(func):
      ...
      if isinstance(annotation,str):
           # do something with string

is wrong, wrong, wrong. It should simply be doing the equivalent of:

      annotation.doWhatIWant()

Except in the overloaded function case, it's 'doWhatIWant(annotation)'. The latter spelling has the advantage that you don't have to be able to modify the 'str' class to add a 'doWhatIWant()' method.

Is this clearer now? This is known, by the way, as the "tell, don't ask" pattern. In Python, we use the variant terms "duck typing" and "EAFP" (easier to ask forgiveness than permission), but "tell, don't ask" refers specifically to the idea that you should never dig around in an object's guts to perform an operation, and instead always delegate the operation to it.

Of course, delegation is impossible in the case of a "third-party" object being used -- i.e., one that can't be modified to add the necessary method. Overloaded functions remove that restriction.

(This, by the way, is why I think Python should ultimately add an overloading syntax -- so that we could ultimately replace things like 'def str(self)' with something like 'defop str(self)'. But that's not relevant to the immediate discussion.)



More information about the Python-3000 mailing list