Extensible Double Dispatch for Racket
Sun 24 Jul 2016 18:59 EDT
Both Racket’s object system and its (separate!) generic interface system offer single-dispatch object-oriented programming: the choice of method body to execute depends on the type of just one of the arguments given to the method, usually the first one.
In some cases, the first thing that a method will do is to decide what to do next based on the type of a second argument. This is called double dispatch, and it has a long history in object-oriented programming languages—at least as far back as the original Smalltalk.
As an example, consider implementing addition for classes representing numbers. A different method body would be needed for each pair of representations of numbers.
I stumbled across the need for something like this when
implementing Operational Transformation (OT) for
Racket.
The macro
operation-transformer
in that code base is almost the double-dispatch
macro from
this post; the difference is that for operational transformation,
the method concerned yields two results, and if the arguments
are switched on the way in, they must be switched on the way out.
Basic Double Dispatch
Here’s a basic double-dispatch macro:
It assumes that it will be used in a method where dispatch has
already been done on arg1
, and that the next step is to inspect
arg2
. It applies the pred?
s in sequence until one of them
answers true, and then evaluates the corresponding body. If none
of the pred?
s hold, it signals an error.
It’s often convenient to use it inside a class definition or
generic interface implementation with the following macros, which
simply define op
to delegate immediately to double-dispatch
.
The first is to be used with Racket’s object system, where the
first argument is bound implicitly to this
and where predicates
should use Racket’s
is-a?
function. The second is to be used with Racket’s generic interface
system, where both arguments are explicitly specified and
predicates are more general.
Commutative Double Dispatch
For commutative operations like addition, it’s common to see the same code appear for adding an A to a B as for adding a B to an A.
The next macro automatically flips its arguments and tries again to see if B’s method has support for A, if it can’t find support for B within A’s method. That way, code for combining B with A need only be supplied in one place. It uses a parameter to keep track of whether it’s currently trying out a flipped pair of arguments.
Writing a simple wrapper works well for using
commutative-double-dispatch
in a class definition:
but a wrapper for use with the generic interface system needs to
take care not to accidentally shadow the outer dispatch mechanism.
This macro uses
define/generic
to make op*
an alias of op
that always does a full dispatch on
its arguments:
Examples
Let’s see the system in operation! First, using Racket’s object system, and then using Racket’s generic interfaces.
Example Scenario
We will first define two types of value foo and bar, each
responding to a single doubly-dispatched method, operator
which
produces results according to the following table:
| foo | bar |
-----|-----|-----|
foo | foo | bar |
bar | bar | foo |
-----|-----|-----|
Then, we’ll extend the system to include a third type, zot, which yields a zot when combined with any of the three types.
Double Dispatch with Classes
Some tests show that this is doing what we expect. Notice that we
get the right result when the first operand is a bar%
and the
second a foo%
, even though bar%
only explicitly specified the
case for when the second operand is also a bar%
. This shows the
automatic argument-flipping in operation.
Double Dispatch with Generic Interfaces
The tests show the same argument-flipping behavior as for the object system above.
Extending The Example
First, we implement and test class zot%
…
… and then implement and test struct zot
.
Conclusion
Double dispatch is a useful addition to the object-oriented programmer’s toolkit, and can be straightforwardly added to both of Racket’s object systems using its macro facility.
This post was written as executable, literate Racket. You can download the program from here.