method lookup
A multimethod is a Clojure function (an instance of palisades.lakes.multimethods.java.MultiFn
which implements clojure.lang.IFn
) which, when invoked:
-
First applies a dispatch function to the arguments, returning a dispatch value.
-
Finds the appplicable methods from a table mapping dispatch values to functions.
-
Finds the minima of a preference partial ordering of the applicable methods.
-
If there is a unique minimal method, it applies that to the arguments. If there are no applicable methods, or more than one minimal method, an exception is thrown.
The purpose of this document is to explain the process in more detail.
Note: I believe what I describe here is more complicated than necessary, due to a desire to remain (mostly) consistent with Clojure 1.8.0 behavior. I’ve changed certain aspects of the behavior that I believe are unintended bugs — see changes for a detailed discussion. of the differences.
dispatch function
The dispatch function is responsible for returning legal dispatch values.
Warning: Because we want method lookup to be fast, the current implementation (like Clojure 1.8.0) doesn’t validate dispatch values before method lookup. Instead, a possibly mysterious looking exception will be thrown some time later. You can use palisades.lakes.multimethods\legal-dispatch-value?
in your dispatch function, permanently if you can afford the cost, or at least in unit tests, and during development or debugging.
legal dispatch values
Legal dispatch values are one of (my terminology):
-
atomic
- an instance of
Class
. - a namespace-qualified instance of
Named
(aSymbol
orKeyword
).
- an instance of
-
:default
: a special case of a non-namespace-qualified keyword, for consistency with Clojure 1.8.0. Used to define a default method which is called when there are no other applicable methods. Dispatch functions may return:default
to short cut method lookup. -
recursive
an instance of
clojure.lang.IPersistentVector
whose elements are legal dispatch values, either atomic or recursive. -
signature
an instance of
palisades.lakes.multimethods.java.Signature
, effectively an immutablelist
of classes.Atomic and recursive dispatch values are supported by Clojure 1.8.0 multimethods.
Signatures, which are not recursive, are added here to optimize the common special case of pure class-based method lookup for multiple arguments. The current version of signatures is not recursive, and only supports single arity method functions with simple arglists (no destructuring).
(It may work to use a
Signature
as a leaf in a recursive dispatch value, but I haven’t tested it, don’t recommend it, and don’t plan to support it. However, I open to arguments for why it would be worth the trouble.)
dispatch value ordering
applicable methods (isa<=)
Applicable methods are determined using a partial ordering of dispatch values I’m calling isa<=
. isa<=
depends in general on a Var
pointing to a shared, mutable Clojure hierarchy.
The premise is that any method of f
defined for dispatch value y
could be applied to arguments resulting in dispatch value x
, as long as (isa<= f x y)
.
(faster-multimethods has a function of the same name, but that’s provided for unit tests/debugging and isn’t called during method lookup.)
-
(isa<= f s0 s1)
if(.isAssignableFrom s1 s0)
, whens0
ands1
are classes or signatures. -
(isa<= f r0 r1)
, whenr0
andr1
are recursive dispatch values, ifr0
andr1
are the same shape, and(isa<= f a0 a1)
is true for every pair of corresponding atomic leaves ofr0
andr1
. -
(isa<= f a0 a1)
if(clojure.core\isa? (.hierarchy f) a0 a1)
whena0
anda1
are atomic.-
When
a0
anda1
are both classes,isa?
ignores the hierarchy, and is just(.isAssignableFrom a1 a0)
. -
When
a0
isNamed
anda1
is aClass
,isa?
returns false. -
Otherwise, the hierarchy is used. A hierarchy is a directed acyclic graph created with calls to
clojure.core/derive
. Edges in this graph connect 2Named
or aClass
and aNamed
. Theisa?
relation is the transitive closure of the child-parent edge relation, that is, the descendant-ancestor relation.
Note: that
(isa? hierarchy c n)
may be true for aClass
c
and aNamed
n
, but not the other way around. -
preferred methods (dominates<)
The dominates<
(pseudo-code) relation extends isa<=
with additional explicit child-parent pairs, created by calling prefer-method
.
The explicit prefer-method
child-parent pairs may have any two dispatch values for child and parent, Class
, Named
, or recursive.
(prefer-method d0 d1)
checks that d1
is not already preferred to d0
, but otherwise allows any pair of values, which might or might not be legal dispatch values, might not be the same shapes, etc.
(prefer-method d0 d1)
is only called if there is some d2
such that (isa<= d2 d0)
and (isa<= d2 d1)
, so edges relating illegal dispatch values, or dispatch values of differing shapes, will have no effect.
On the other hand, the lack of validation in prefer-method
is likely to cause difficult-to-debug surprises later.
Note: The ordering of recursive dispatch values can only be changed for individual multimethods, making it harder to ensure consistent behavior in related multimethods, but at least limiting the damage radius of ill-considered changes.
default dispatch value
Finally, every multimethod has a special default dispatch value (defaulting to :default
). A method defined for the default dispatch value will be called if there are no applicable methods.
method lookup
Each multimethod contains a table mapping certain dispatch values to the defined methods, themselves Clojure functions.
The second step in calling a multimethod has 3 parts:
-
determine which of the defined methods are
isa<=
applicable to the arguments’ dispatch value.If there are no applicable methods, throw an exception.
-
find the
dominates<
minima (most preferred) among the applicable methods.If there is more than one minimal method, throw an exception.
-
apply the chosen method to the arguments.