5

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)
)
Hendekagon
  • 4,565
  • 2
  • 28
  • 43
  • What is your dispatch function? Is it `type` ? – Ankur Oct 15 '12 at 04:16
  • it is class....or type will do ok – Hendekagon Oct 15 '12 at 04:21
  • class or type wont work as the actual type of the object will still be Y and it will go in infinite loop if you call the method-a again in Y by type casting the object to X type – Ankur Oct 15 '12 at 04:38
  • 1
    I know - that's why I'm asking! I tried type-hinting but I can't think of a way to explicitly tell it to use the other implementation. I want to do some things for my type, then delegate over to the existing method to do the rest – Hendekagon Oct 15 '12 at 04:40
  • Then you should not use `type` or `class`, rather have a custom dispatch function that can do all this chaning – Ankur Oct 15 '12 at 04:52
  • 1
    but say the namespace in which the defmulti is defined uses type and I can't change it – Hendekagon Oct 15 '12 at 04:59
  • Then the method was not designed for what you are trying to do and hence you should not do it – Ankur Oct 15 '12 at 05:14
  • 1
    now I want to do it even more... – Hendekagon Oct 15 '12 at 05:16
  • 1
    ok did it, but not how I thought I wanted to. I just did ns-a/method-a (create-an-X y)), where create-an-X creates an X from my y (even though my y is already an instance of X, seems wasteful but it works) – Hendekagon Oct 15 '12 at 06:11
  • Isn't your question just the description of what the dispatch function should really do instead of just using `class`? – skuro Oct 15 '12 at 10:15
  • 1
    no - the dispatch function is a given, it has been defined by a library which I cannot change. I would like to extend the behaviour of the library to support my types - apparently a classic use case for multimethods – Hendekagon Oct 15 '12 at 23:20
  • Favor composition over inheritance. – noahlz Oct 16 '12 at 01:36
  • 1
    @noahz, when I say "extend" I don't mean in terms of inheritance, I mean I need to add defmethods to handle my types, but I would like to use the existing ones too. Nor do I mean X and Y are related by inheritance, they could be anything. Am I misunderstanding you ? – Hendekagon Oct 16 '12 at 02:59
  • The principle is the same. Seems like that library wasn't designed to be extended. Wrap it in an "adapter" function. – noahlz Oct 16 '12 at 03:15
  • 1
    I'd have thought that defining a multimethod is an invitation to extend! And I see nothing bad in using one public function from another. – Hendekagon Oct 16 '12 at 05:06

3 Answers3

2

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.

noahlz
  • 10,202
  • 7
  • 56
  • 75
  • I cannot, as the dispatch function is given by a library I cannot change – Hendekagon Oct 15 '12 at 23:21
  • thanks for the comprehensive answer! This serves as good documentation of multimethod behaviour and expectations. The thing about prefer-method is, even if it could be used to redirect dispatch to a given type (abstract or concrete), it probably wouldn't be a good solution for my problem as prefer-method seems to signal an indefinite, global preference in behaviour of multimethods, which doesn't seem like a good idea just to use a function locally and temporarily. – Hendekagon Oct 16 '12 at 23:08
  • marking this as the accepted answer because of the effort that went into it! Very good explanation of multimethods. – Hendekagon Dec 06 '12 at 02:56
2

This should be possible using prefer-method (doc):

Usage: (prefer-method multifn dispatch-val-x dispatch-val-y)

Causes the multimethod to prefer matches of dispatch-val-x over dispatch-val-y when there is a conflict

In your case, you would say:

(prefer-method ns-a/method-a X Y)

This should cause the method defined for dispatch-val X in ns-a to be called if something is both X and Y, and your method to be called if something is Y but not X.

EDIT: Turns out prefer-method is for resolving conflicts where there's no exact match for the dispatch value, and multiple parents match with neither one deriving the other. If there's an exact match of the dispatch value in the method table, that method is always used. So this will not solve OP's use case.

Alex
  • 13,811
  • 1
  • 37
  • 50
  • There's no conflict in that gist. `type` returns one precise value: `Long` – noahlz Oct 16 '12 at 03:46
  • ...and it appears `prefer-method` is for ad-hoc hierarchies built with `derive `, not Java classes. – noahlz Oct 16 '12 at 03:54
  • 1
    @noahz upon closer examination of the MultiFn source, the issue is that prefer-method has no effect when there's an exact match for the dispatch value. It only comes into play when there's no exact match for the dispatch val, and multiple parents match with neither one deriving the other. – Alex Oct 16 '12 at 14:56
  • 1
    Also, prefer-method does work with the default Java class hierarchy. See https://gist.github.com/3899779 – Alex Oct 16 '12 at 14:56
0

Came here looking for an idiomatic solution to the same problem. Since, according to @noahiz, idiomatically this should not be done, I'll just post our current workaround to be used at the discretion of a developer. Just give access to the concrete method via defn and feed a proxy to defmulti.

;;price/basic.cljs

(defn get-final-price-impl [{amount :amount}]
  amount) ;;normally this would be a complicated computation we want to keep DRY

(defmulti get-final-price :type)
(defmethod get-final-price :price/price-basic-tag [price-map]
  (get-final-price-impl price-map))
;;price/foreign-currency.cljs

(defmethod price-basic/get-final-price :price/price-foreign-currency-tag
  [price-map]
  (* (price-basic/get-final-price-impl price-map) (:conversion-rate price-map))) 

P.S. one of the comments suggested to override the type and call the multimethod recursively. I.E., if Y derives from X, inside ns-a/method-a for Y we should do (ns-a/method-a (create-an-X y)). DO NOT DO THIS. If ns-a/method-a for X itself calls other multimethods, those will be dispatched for X instead of Y - obscure logical bugs are likely to follow.

Ivan Koshelev
  • 3,830
  • 2
  • 30
  • 50