Is it possible to overload Clojure multi-methods on arity?
Asked Answered
H

3

20

I have some code that uses multi-methods and would ideally like to overload the function (in this case, multi-function) so that I can pass in a higher order function to help with testing, for example.

Here's the example:

(ns multi)

(defn my-print [m] (println "The colour is" (:colour m)))

(defmulti which-colour-mm (fn [m f] (:colour m)))

(defmethod which-colour-mm :blue [m f] (f m))
(defmethod which-colour-mm :red [m f] (f m))
(defmethod which-colour-mm :default [m f] (println "Default: Neither Blue nor Red"))

(defn which-colour
  ([m] (which-colour-mm m my-print))
  ([m f] (which-colour-mm m f)))

(which-colour {:colour :blue :object :ball})
(which-colour {:colour :yellow :object :ball})
(which-colour {:colour :blue :animal :parrot} (fn [m] (println "The " (:animal m) "is" (:colour m))))

So my defn provides the arity overloading but I'm wondering if defmethod supports anything like this. (I guess you wouldn't want to do it for each defmethod declaration.)

Is this the most suitable (dare I say, idiomatic) approach, or is there a better way?

Hydroscope answered 25/4, 2012 at 10:16 Comment(0)
K
16

This is perfectly fine. There is the "user" interface and the "type" interface of a library. They may be identical, but they don't have to.

The "user" interface is in your case which-colour. The "type" interface is which-colour-mm (ok, not really, but just for the sake of the argument). The user of your library does not need to know about the multimethod.

On the other hand someone providing a new colour - say :purple - does not have to care about multi-arity boilerplate. This is handled for him in which-colour.

This is a perfectly valid design!

But of course there's a price tag: Suppose you have a colour, which has some more perfomant way to do things... Now, you are locked into a possible slower interface.

To clarify this a little: Suppose you have a collection interface. You provide a function - conj - which allows the user to add elements to the collection. It is implemented like this:

(defn conj
  [coll & elements]
  (reduce conj1 coll elements))

conj1 is the "type" interface (eg. a multimethod or protocol function): it adds one element to the collection. So someone supplying a new collection type has only to implement the simple case of adding a single argument. And automagically the new type will also support adding multiple elements.

But now suppose you have a collection type, which allows a faster way to add several elements than just adding one after the other. This capability cannot be used now.

So you make the multimethod/protocol function the function conj itself. Now the collection can use the faster way. But each implementation must provide the multiple elements boilerplate.

This is a trade-off and up to your decision. There is not Right Way(tm). Both can be considered idiomatic. (Although I personally would try to go with the first one as often as possible.)

YMMV.

Edit: An example of multi arity methods without coding in the dispatch value.

(defmulti which-colour-mm (fn [m & args] (:colour m)))
(defmethod which-colour-mm :blue
  ([m] (print m))
  ([m f] (f m)))
Keeper answered 25/4, 2012 at 12:17 Comment(1)
I like this and Ankur's answer, but this one uses arity overloading vs the other which uses the argument count to match the dispatch value. I guess it makes sense to use the defn approach if you want the same default function for each dispatch value (and avoid duplication) vs overloading at the defmethod level if you want a different default per dispatch value.Hydroscope
S
3

You can do that using multimethods as shown below example:

(defmulti which-colour-mm (fn [m & args] [(count args) (:colour m)]))
(defmethod which-colour-mm [0 :blue] [m] (print m))
(defmethod which-colour-mm [1 :blue] [m f] (f m))


user=> (which-colour-mm {:colour :blue :object :ball})
{:colour :blue, :object :ball}nil
user=> (which-colour-mm {:colour :blue :object :ball} print)
{:colour :blue, :object :ball}nil
Scherer answered 25/4, 2012 at 12:25 Comment(0)
N
2

Basically you can dispatch on anything, neither the type nor the number of args has to be consistent..like this:

(defn- map-classes [an-object]
     (let [cmap 
         {1 :thing
          2  666
          3  "yada"}
    the-class (class an-object)]
    (get cmap an-object the-class)))

(defn- mk-class [& args] (map #(map-classes %) args))
(defmulti play-thing mk-class )
(defmethod play-thing [:thing] [v] (= 1 v))
(defmethod play-thing [666] [v] (= 2 v))
(defmethod play-thing ["yada" String] [v x] (str x v))

The possibilities are endless

Novotny answered 22/5, 2012 at 23:57 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.