Saturday, November 29, 2003

(via's Tales from the Red Shed)

Ryan Wilcox took PyObjC out for a spin last night and has written a wonderful article describing PyObjC. I don't agree with his criticisms of AppleScript Studio -- creating full fledged Cocoa app was not really an intended goal of Studio. Studio does wonders for putting powerful GUIs on top of the many workflow solutions built around AppleScript.

Further on in the article, Ryan discusses the mapping scheme used by PyObjC to map Objective-C selectors into Python. This both warrants correction and explanation because Ryan succinctly demonstrates two points of confusion regarding both ObjC selectors and the mapping solution used.

Notice that, unlike C/C++, Objective-C uses labeled parameters separated by colons.

In PyObjC you would call the same function [RWError errorWithString: myString going:kToFile to:filepath]) like this

RWError.errorWithString_going_to_(myString, kToFile, filepath)
Notice how the labels are now in the function name itself, and separated by _s instead of :s. (The underscores vs colons issue makes sense - colons are syntax elements in Python.)

Now Python does labelled parameters too, so I don't understand the reasoning behind having the labels in the function name syntax. Wouldn't something like the following be closer to what you get with Objective-C?

RWError.errorWithString(myString, going = kToFile, to= filepath)
I don't know. Maybe they implemented it this way because of speed issues (although a transformation like that shouldn't take much time).

Python does not support labeled parameters. The call errorWithString(myString, going = kToFile, to= filepath) actually passes a dictionary into the errorWithString method/function that will contain a 'going' and a 'to' field. Order is not defined or preserved, nor should it be.

Objective-C doesn't support labeled parameters either! The method used in the above demonstration has the canonical name of errorWithString:going:to:. Objective-C breaks up the invocation syntax such that the method name can help to describe the arguments passed to the method. However, errorWithString:, errorWithString:to:, and errorWithString:to:going: are all distinctly different methods than the one used in the example above.

Now, there are all kinds of hacks we could have done to have munged together errorWithString(myString, going = kToFile, to= filepath) into a call to the appropriate Objective-C method. However, it could never be done with 100% accuracy and it would have added a huge amount of overhead to method dispatch. Compare running through a string and substituting ':' for '_' versus having to parse all of the parameters, concatenating a potential selector name, seeing if it really exists, and retrying until the real method is found.

Alternatively, we could have implemented a solution that involved a hardwire map. This is the path that Cocoa/Java took. That is, we could have built a map file that effectively said "the 'errorWithString:going:to:' method is mapped to the 'errorWithString()' python function". This requires a tremendous amount of maintenance overhead and results in an API that is neither Native Cocoa nor Pure Python. It requires the developer to maintain two mental maps in their head without an automatic heuristic to span the maps; the first map goes from Obj-C to the new thing in the middle and the second goes from Python to the new thing in the middle.

Yet using Cocoa with Python is simple...

And that is goal #1 of the PyObjC project. We wanted it to be simple to use, simple to extend, and simple to maintain.

Once you have learned this simple rule...

To use an Obj-C method from Python, take the Objective-C method name and replace all ':' (colons) with '_' (underscores).

... you have the ability to use nearly all of Objective-C from Python. Furthermore, the developers of PyObjC only have to actively maintain the handful of special cases that do not otherwise map automatically (varargs, meaingful void *s, etc..).

It also results in more readable code. Compare the following two lines of code:

RWError.errorWithString_going_to_(myString, kToFile, filepath)
RWError.errorWithString(myString, going = kToFile, to= filepath)

The second naturally leads the developer to thinking they are calling the "errorWithString()" method of the RWError class/object. That may or may not be true depending on if RWError is a Python object or an ObjC object. If Python, then the developer is calling errorWithString() where the 'going' and 'to' parameters are generally expected to be optional. If ObjC, the developer has to know that key/value paramaters are actually a part of the method name and has to mentally parse and rewrite the line of code to understand what method will actually be invoked.

The first looks slightly alien, but it maps cleanly into both the ObjC and Python worlds. By "maps cleanly", I mean that there is a very simple rule that can be followed to understand how the method fits in on either side of the bridge.

This is a subject that has come up many times in the nearly 10 years that PyObjC has been in development. There have been many heated discussions regarding the syntax. I don't think any of us that actively work on PyObjC are particularly married to the syntax. The underscores are ugly. Personally, I would rather just use the straight Objective-C method name...

RWError.errorWithString:going:to:("foo", bar, baz)

... but that would require a change to the Python language itself. I hacked the python interpreter ages ago to do this-- 1.5.2-- and it worked really well. But requiring that the developer build/install a custom version of the python interpreter just to have access to PyObjC completely defeats the "simple" goal.

No one has yet to come up with a mapping scheme that is as easy to use or maintain. We are all eyes/ears, but I would suggest revisiting the various records of discussion on the subject (a google search of 'bbum pyobjc method' will reveal others, I'm sure).
9:42:21 AM  pontificate