Search code examples
typesclojuremultimethod

How can I force dispatch to an existing multimethod implementation?


If I declare a multimethod for another namespace (a library which I cannot change), ns-a, for my type:

defmethod ns-a/method-a Y [y]

and there's an existing method for X defined in ns-a:

defmethod method-a X [x]

and the dispatch function is

(defmulti method-a type)

if my type Y is also an X, how could I dispatch to the implementation for X in my implementation for Y ?

edit: I found a hack:

(defmethod ns-a/method-a Y [y]
 (do-special-Y-stuff-here y)
    ; now do X stuff with Y:
    ((get-method ns-a/method-a X) y)
)

Solution

  • If you dispatching on type or class, consider revising your code to use Protocols.

    More detailed answer:

    Clojure's defmulti doesn't let you dispatch to a parent Java type if a precise subtype is also registered. This is intentional (it takes the side of "least suprise" in the Liskov substitution principle debate). Since there is already a registered multimethod for Y, if your object isa? exactly a Y, then you'll hit the method for Y - even though it's also an "X". In Java, classes can inherit from multiple interfaces, they can only ever be exactly one type. That's what you're bumping up against here.

    Per the documentation for multimethods

    Derivation is determined by a combination of either Java inheritance (for class values), or using Clojure's ad hoc hierarchy system. The hierarchy system supports derivation relationships between names (either symbols or keywords), and relationships between classes and names. The derive function create these relationships, and the isa? function tests for their existence. Note that isa? is not instance?.

    If you look in the source for MultiFn, you'll see that Clojure always uses the most specific Java class a given multimethod dispatch value (when dispatching on type).

    See this line in Clojure 1.4.0 source for MultiFn and specifically the implementation of dominates.

    At the REPL:

    user=> (defmulti foo #(class %))
    user=> (defmethod foo java.util.RandomAccess [x] "RandomAccess")
    user=> (defmethod foo java.util.Vector [x] "Vector")
    user=> (defmethod foo java.util.Stack [x] "Stack")
    

    Ok, this works as expected, and prefer-method can't override the class hierarchy, because isa? inspects the Java types first.

    user=> (prefer-method foo java.util.RandomAccess java.util.Stack)
    user=> (foo (new java.util.Stack))
    "Stack"
    

    Finally, in the source inspects MultiFn all the method keys that match the dispatch value type (as per isa?). If it finds more than one match, it inspects the type hierarchy for the "dominate" value. We see here that Stack dominates RandomAccess

    user=> (isa? java.util.Stack java.util.RandomAccess)
    true
    user=> (isa? java.util.RandomAccess java.util.Stack)
    false
    

    Now, if I define a new method bar as follows:

    user=> (defmulti bar #(class %))
    user=> (defmethod bar Comparable [x] "Comparable")
    user=> (defmethod bar java.io.Serializable [x] "Serializable")
    

    I get the following, due to ambiguity:

    user=> (bar 1)
    IllegalArgumentException Multiple methods in multimethod 'bar' match dispatch value: class java.lang.Long -> interface java.lang.Comparable and interface java.io.Serializable, and neither is preferred  clojure.lang.MultiFn.findAndCacheBestMethod (MultiFn.java:136)
    

    Now, I can solve this problem with prefer-method

    user=> (prefer-method bar Comparable java.io.Serializable)
    user=> (bar 1)
    "Comparable"
    

    However, if I register a new method for Long

    user=> (defmethod bar Long [x] "Long")
    user=> (bar 1)
    "Long"
    

    I can't get back to Comparable, even if I use prefer-method:

    user=> (prefer-method bar Comparable Long)
    user=> (bar 1)
    "Long"
    

    That seems to be what you've encountered here.

    Note that you have the option of remove-method - but I think that is a much more heavy-weight / dangerous (monkey-patching?) solution compared to the "hack" you devised.