Asher
Asher

Reputation: 1267

Clojure multimethod with multiple dispatch values

I have a case where I want several dispatch values in a multimethod to map to the same method. For example, for a dispatch value of 1 I want it to call method-a, and for dispatch values of 2, 3, or 4 I want it to call method-b.

After some Googling, I ended up writing the following macro:

(defmacro defmethod-dispatch-seq [mult-fn dispatch-values & body]
  `(do (map
     (fn [x#] (defmethod ~mult-fn x# ~@body))
     ~dispatch-values)))

You can then use it like this:

(defmulti f identity)

(defmethod f 1 [x] (method-a x))
(defmethod-dispatch-seq f [2 3 4] [x] (method-b x))

Which allow you you to call the following:

(f 1) => (method-a 1)
(f 2) => (method-b 2)
(f 3) => (method-b 3)
(f 4) => (method-b 4)

Is this a good idea?

Upvotes: 6

Views: 3687

Answers (3)

ClojureMostly
ClojureMostly

Reputation: 4713

This is actually exactly what hierarchies in clojure was made for. The defmulti takes a :hierarchy parameter that can establish x is a y relationships.

Only slight problem is that hierarchies don't work with numbers. You can't tell it that the number 0 "is a" :a for instance. However, hoping that you really only chose the numbers for simplicity and asking the question and possibly have keywords in your actual case, you can do something like this:

(def hh (-> (make-hierarchy)
            (derive :0 :a)
            (derive :1 :a)
            (derive :2 :b)
            (derive :3 :b)
            atom))

(isa? @hh :3 :b) ;; => true

(defmulti mm
          "Demo of using hierarchy"
          (comp keyword str)
          :hierarchy hh)

(defmethod mm :b [orig] {:b orig})
(defmethod mm :a [orig] {:a orig})
(defmethod mm :default [orig] :oopsie)

(mm 2) ;; => {:b 2}
(mm 1) ;; => {:a 1}
(mm 4) ;; => :oopsie
;; Cool thing is we can steer this at RUNTIME!
(swap! hh derive :4 :b)
(mm 4) ;; => {:b 4}

;; So we can keep the data close to the definition:
(mapv #(swap! hh derive % :c) [:5 :6 :7])
(defmethod mm :c [orig] {:c orig})
(mm 6) ;; => {:c 6}

Notes:

  • I use atom instead of (var hh) since it works much better with clojurescript (cljs doesn't like vars in production).

  • The performance should be decent. Everything the dispatch function outputs something the mapping to which actual method it resolves to (walking the hierarchy) is cached.

Upvotes: 5

bsvingen
bsvingen

Reputation: 2759

I would rather do something like this:

(defn dispatch-function
  [value]
  (if (= 1 value) :case-a :case-b))

(defmulti f dispatch-function)

(defmethod f :case-a
  [x]
  :doing-something)

(defmethod f :case-b
  [x]
  :doing-something-else)

That way you avoid the macro, and you use the dispatch function for its intended purpose.

Upvotes: 5

sbensu
sbensu

Reputation: 1511

Since it is an open question ("is this a good idea?") I'll try to address the 2 concerns that come to mind:

  1. Efficiency: since it results in the same code, it is as efficient typing defmethod for different values and using the same function for them.

  2. DRY, readability, code quality: it is better than typing n times the same code with a different match values.

So, if that is the way your function behaves, it looks like a good idea but the fact that your function behaves that way might indicate something flawed in your model:

  • your data (dispatch arguments) might be modeled in a better way that reflected that behavior or
  • the multimethod might be doing either too much or to little, resulting on the awkward dispatch call.

I would use such a dispatch mechanism after making sure that is how my data/functions should work.

Upvotes: 2

Related Questions