** 20. Multiple Dispatching ** ---------------------------------------------------------------------------- 20.1. We saw how contravariance prevented us from writing the following ColorPoint equals method: class Point { ... bool equals(Point arg) { return this.x == arg.x; } } class ColorPoint extends Point { color c; ... ColorPoint equals(ColorPoint arg) { // covariant argument overriding return this.x == arg.x && this.c == arg.c; } } and instead forced us to write (e.g. in Java): class ColorPoint extends Point { color c; ... ColorPoint equals(Point arg) { if (arg instanceof ColorPoint) { return this.x == arg.x && this.c == ((ColorPoint)arg).c; } else { ... do whatever should be done ... } } This is because dynamic dispatching only is performed on the receiver object, but not on any argument classes. What if we fixed this asymmetry, allowing dynamic dispatching on any argument(s)? And in fact treated the receiver just like any other argument? This leads to an OO language based on *multiple dispatching*, whose methods are called *multimethods*. [CommonLisp works this way, as does Cecil.] Here's how this code would be written in a Cecil-like language: class Point; field x(self:Point):int; method copy(self:Point):Point { return new Point(self.x) } method equals(self:Point, arg:Point):bool { return self.x == arg.x; } class ColorPoint extends Point; field c(self:ColorPoint):color; method copy(self@ColorPoint):ColorPoint { return new ColorPoint(self.x, self.c) } method equals(self@ColorPoint, arg@ColorPoint):bool { return self.x == arg.x && self.c == arg.c; } Things to note: 1) Fields & methods are no longer nested in classes, but written separately. 2) As a consequence of 1, the receiver argument is now explicit. We no longer need to build in any special self keyword (in fact, the formal parameter can be named anything we want, and it doesn't have to be the first argument either), nor is there any need for special syntax for implicit self. We instead have to make accesses of self's instance variables & methods explicit. 3) Method overriding and dynamic dispatching are indicated explicitly with the @ formal parameter declarations. E.g. the declaration "equals(self@ColorPoint, arg@ColorPoint)" means that this method overrides the previous equals methods *for the case when self and arg are instances of ColorPoint* (i.e., when the dynamic classes of self and arg are subclasses of ColorPoint). If this test fails, then the method doesn't apply, and so the original equals method is called. In effect, this @ specifies a kind of pattern-matching over dynamic classes, which if refuted continues on to more general method argument patterns. Example invocations: Point p = new Point(3); Point cp = new ColorPoint(3, blue); p.equals(p) --> invokes the Point*Point equals p.equals(cp) --> invokes the Point*Point equals cp.equals(p) --> invokes the Point*Point equals cp.equals(cp) --> invokes the ColorPoint*ColorPoint equals In the first three cases, only the first equals method satisfies the pattern-match (it always matches, since it doesn't use any @), while in the fourth case both satisfy the match, and the second is more specific than the first, so it is invoked. ---------------------------------------------------------------------------- 20.2. In general, methods are grouped into "generic functions"; in the example above, there are generic functions named "copy" and "equals". A generic function has a type and one or more methods in it. In the language used above, the generic functions were implicitly declared. If they needed to be explicitly declared, then we could have written: gf copy: Point -> Point; gf equals: Point * Point -> bool; The four method declarations in the example above put methods into these gfs. Each method has a pattern over the dynamic classes of its arguments that must be satisfied for the method to *apply* to some call. An invocation of a generic function (previously called a message send) does the following: 1) find all the methods in the generic function 2) determine which subset of the methods are applicable to the dynamic classes of the arguments [report a "message not understood" error if no cases are applicable] 3) select the most specific method [report a "message ambiguous" error if no case is more specific than all the others] 4) invoke the selected method Static typechecking of such a language would guarantee that neither message-not-understood nor message-ambiguous could occur at run-time. One way to avoid message-not-understood, for instance, is to require a default method in each gf, i.e., one with no @'s in its arguments. [But we want to have abstract methods, which means gfs with no default method, and things get tricky in this case.] What does "most specific method" mean? When is one set of restrictions more specific than another? To support method overriding in this model, we want (@ColorPoint, :int, :bool) to be more specific than (@Point, :int, :bool) We can extend this receiver-based dispatching to multiple dispatching: (@ColorPoint, @ColorPoint) should be more specific than (@Point, @Point) But which of the following is more specific? (@Point, @ColorPoint) (@ColorPoint, @Point) Here we have two choices: 1) we can say these two methods are ambiguous, leading to a message-ambiguous error if they are both applicable to some call (say equals(cp, cp)) and no other applicable method is more specific. This is what Cecil does. It forces the programmer to write a (@ColorPoint, @ColorPoint) method to resolve the ambiguity. Essentially, one pattern is more specific than another only if each of its arguments pointwise is at least as specific: Ci <= Di (forall i. 1 <= i <= n) Cj < Dj (for some j. 1 <= j <= n) ------------------------------------------ (@C1, ..., @Cn) overrides (@D1, ..., @Dn) 2) we can check arguments left-to-right (lexicographically), picking the method with the leftmost most-specific argument: C1 = D1 ... C(i-1) = D(i-1) Ci < Di (for some i. 1 <= i <= n) ------------------------------------------ (@C1, ..., @Cn) overrides (@D1, ..., @Dn) In the example above, it would pick the second method, since the leading @ColorPoint is more specific than the leading @Point, and we ignore any later arguments once we've found a difference. Common Lisp works this way. This strategy results in a total ordering over patterns, ruling out the possibility of message ambiguous errors, but at the cost of possibly weird behavior for (accidentally) weird programs. [FYI: the OOPSLA'00 paper on MultiJava proposes a way of extending Java naturally with multiple dispatching and the ability to write generic functions & methods outside of classes, and compiling & typechecking files modularly, and running efficiently on standard JVMs.]