Cliff Hacks Things.

Tuesday, February 07, 2006

Multi-method dispatch, operators, and typecasting

I am suspicious of multi-method dispatch.

In a language like Smalltalk, when you send a message like

foo process: bar with: baz know that foo will respond to the message, generally executing some method identified with process:with:. bar and baz have no direct say.

In a language with multi-method dispatch, bar and baz — the arguments to the message — can also provide methods for handling the message. Generally, the most specific one is chosen at runtime: if foo has a method for dealing with arguments of any two types, but baz offers one that accepts the precise runtime types of all three objects, it is not foo's code which will execute, but baz's.

As I said, I'm suspicious of this.

I like the ease of reasoning that single-dispatch gives me, knowing that a given object's code will execute when I call a method. Particularly if the code for baz was loaded at runtime, the method may be effectively hijacked with a result I didn't intend.

However, I can think of two cases that are dramatically more elegant with MMD: operators, and casting (which is effectively an operator).

Smalltalk lets you name a method '+', to be called in the normal infix fashion, a + b. In that fragment, it is of course a's + method that will execute.

But let's say you define a new type to represent, say, money — we'll name it Money. You teach it to add integers, floats, or other numbers using a + method. This is all well and good, so long as you write it like this:

myMoneyAmount + someInteger

Great. The + method you defined for all objects of type Money runs, adds the integer, returns a Money. All is well.

However, if you write it

someInteger + myMoneyAmount

(which is, thanks to the commutative property, mathematically equivalent), what happens? The + method on the built-in class Integer fires up, and it may not know how to act on your Money object — and almost certainly will not return a new Money in response!

The traditional Smalltalk solution to this is, in my opinion, a hack. It's called double-dispatch. The code in Integer would notice that the other addend isn't a known type, so it passes control into the addend using some other method. Effectively, it converts that fragment into something like

myMoneyAmount addedWithInteger: someInteger

(Why doesn't it flip it around and call the + method on the argument? Because that could quickly become an infinite loop of back-and-forth message sends. addedWithInteger:, or whatever it's called, is defined as not performing double-dispatch.)

That's a simple solution for the built-in types, but if you've got two third-party types you want to add, each must sport an addedWithWhatever: method for the other — when the third-party types are probably not aware of one anothers' existence. The situation can spiral out of control.

In Smalltalk, specifically, this problem is mitigated by the fact that you can add methods to other peoples' classes once they're loaded into the system. You can construct this web of adding methods to ensure your types play nice.

This is not, to me, a solution, but a workaround.

You'll run into similar problems if you try to implement a generic cast method. Smalltalks frequently have asString or asArray methods to convert objects to some other type. If you wanted a generic method, like

foo as: Bar

you'd wind up having to resort to double-dispatch again. Your Money class can easily handle "as: Integer", but having Integer handle "as: Money" is more difficult.

Multi-method dispatch eliminates this problem entirely, by allowing your Money class to introduce an as: method that is used when a Money object is the argument, not just the receiver. (Yes, pedants, in MMD there is no receiver per se.)

This is one situation where I would like the dispatch of the method to be determined at runtime.

C++ solves it a little differently, by having both member functions for operators, and free-floating functions that overload operator+ depending on the argument types, but the result is the same as MMD.

So, for this sort of problem, MMD is appealing. I've been thinking about this for a while, and there's an obvious place in the Mongoose language to hook MMD if necessary (namely, by providing implementations directly for the Class-independent Method Contract Identifier — effectively, C++'s approach).

I suspect there's a compromise, or perhaps an alternative technique.


Post a Comment

<< Home