True Multiple Dispatch
When I first started learning about Design Patterns some years ago, I found myself having to revisit the Visitor pattern over and over again. Something about it just didn’t click with me.
Several years later, I now have experience using the Visitor pattern in a few applications. But the idea of multiple dispatch still seems a bit foreign to me. The authors of Design Patterns claimed that Visitors allowed you to imitate multiple dispatch in languages that didn’t natively support it. That was a rather mind-blowing statement to me – how can a language natively support something as strange as multiple dispatch?
Lately, I’ve been learning a little bit about Lisp, and Common Lisp (CL) does natively support multiple dispatch. It turns out that understanding CL’s approach to object-orientation is useful for more than just a better understanding of the Visitor pattern.
The Fundamental Unit of Behavior
Common Lisp is class-based, but in CL, the class simply holds the data, like a C struct. When it comes to behavior, the method reigns supreme.
Putting methods into classes, which would have made CL look more like other object-oriented languages, never worked out very well. The message passing paradigm popularized by Smalltalk made methods and traditional Lisp functions semantically different, meaning much of the power that stems from Lisp’s functional roots was lost. So the authors of the Common Lisp object system (CLOS) unified methods with functions, giving birth to the generic function.
Generic functions are like abstract methods in other languages. They define an interface for behavior, but defer the implementation of that behavior. But – and this is an awfully big but – generic functions are not attached to any class.
(defgeneric area (shape) (:documentation "Returns the area of the given shape."))
In typical functional style, the
area function takes the object as a parameter, instead of sending the object an
area message as you would in traditional OO languages. So far, this isn’t too different from Python, which requires you to pass
self to all methods.
The actual implementation of a generic function is defined by methods. But methods don’t belong to classes; they belong to the generic function. Each method for a generic function is a specialization of that function for specific classes. For example:
(defmethod area ((shape square)) (* (width shape) (width shape))) (defmethod area ((shape circle)) (* (radius shape) (radius shape) *pi*))
In a traditional OO language, you would have the
Shape class define an abstract
Area method, which was implemented in both the
Circle subclasses. In contrast, when the
area generic function is called in CL, it compares the actual argument to the formal arguments in each of its applicable methods. If it finds a matching method, then its code is executed (and, although its outside the scope of this article, if it finds multiple matching methods, they are all called according to certain combination rules).
area example above is simple because it takes only one argument (equivalent to taking no arguments in message passing languages (except Python (of course!))).
It is obviously possible to have generic methods that operate on multiple arguments. And – and this is an awfully big and – methods can specialize on each of those arguments, not just the first one. Methods that specialize on multiple parameters are called multimethods (the following silly example was adapted from here):
(defgeneric test (x y) (:documentation "Returns the type of the two arguments)) (defmethod test ((x number) (y number)) '(num num)) (defmethod test ((i integer) (y number)) '(int num)) (defmethod test ((x number) (j integer)) '(num int)) (defmethod test ((i integer) (j integer)) '(int int)) (test 1 1) => (int int) (test 1 1/2) => (int num) (test 1/2 1) => (num int) (test 1/2 1/2) => (num num)
Specializing on more than one parameter doesn’t help with the combinatorial explosion, but it is occasionally useful And it’s not something you can do in a message passing language natively, which is why you need support from design patterns. The great thing about native support for multiple dispatch is that you can do it without any of the plumbing code that design patterns require (no confusing
Accept methods). There is a common complaint against design patterns that says they exist simply to cover up language limitations. At least in the case of the poor Visitor, that certainly seems to be the case.