Multimethods with multiple dispatch values per method?

1,384 views
Skip to first unread message

darren...@gmail.com

unread,
Jun 14, 2008, 5:08:15 PM6/14/08
to Clojure
Hey folks,

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, however, for dispatch values of 2, 3, or 4 I want
it to call method-b. Is there a way to do this with the existing
multimethod mechanism?

If not, would it make sense to allow a sequence of values for the
dispatch value in defmethod? For example, something like:

(defmulti foo some-dispatch-function)

(defmethod foo 1 (...))

(defmethod foo (2 3 4) (...))

Thanks,
--Darren

Stephen C. Gilardi

unread,
Jun 14, 2008, 7:20:13 PM6/14/08
to clo...@googlegroups.com

On Jun 14, 2008, at 5:08 PM, darren...@gmail.com wrote:

> Hey folks,
>
> 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, however, for dispatch values of 2, 3, or 4 I want
> it to call method-b. Is there a way to do this with the existing
> multimethod mechanism?

Yes, here's one way:

(defmulti foo
[nil
:method-1,
:method-2,
:method-2,
:method-2])

(defmethod foo :method-1
[x]
(println "method-1"))

(defmethod foo :method-2
[x]
(println "method-2"))

----------------------

user=> (dotimes i 4 (foo (inc i)))
method-1
method-2
method-2
method-2
nil
user=>

----------------------

The nil at index 0 in the vector is a little ugly... you could use a
map instead or a function that uses "if".

--Steve

Tom Hicks

unread,
Jun 17, 2008, 4:01:01 PM6/17/08
to Clojure
On Jun 14, 4:20 pm, "Stephen C. Gilardi" <scgila...@gmail.com> wrote:
Which I am thinking would look something like this (with the
minor addition of a default dispatch value):

(defmulti foo
(fn [x]
(let [m {1 :method-1 2 :method-2 3 :method-2 4 :method-2} ]
(or (m x) :default))))

(defmethod foo :method-1
[x]
(println "method-1"))

(defmethod foo :method-2
[x]
(println "method-2"))

(defmethod foo :default
[x]
(println "method-default"))


I'm still learning so please let me know if there is a
simpler or more concise way to use the map for multi-method dispatch.
cheers,
-tom

Stephen C. Gilardi

unread,
Jun 17, 2008, 4:15:35 PM6/17/08
to clo...@googlegroups.com
On Jun 17, 2008, at 4:01 PM, Tom Hicks wrote:

I'm still learning so please let me know if there is a
simpler or more concise way to use the map for multi-method dispatch.

Since maps can be called as functions, the most succinct way I can think of to duplicate the behavior you described (including the :default method) using a map is:

(defmulti foo
  {1 :method-1 2 :method-2 3 :method-2 4 :method-2})

(defmethod foo :method-1
  [x]
  (println "method-1"))

(defmethod foo :method-2
  [x]
  (println "method-2"))

(defmethod foo :default
  [x]
  (println "method-default"))

When called as a function with 1 argument, the map will either return a value for the argument's key or "nil" if the map doesn't contain a key "=" to the argument.  defmulti allows you to specify a default method, but if you don't, it will call the method which is associated with the keyword ":default".  In this way "nil" will be mapped to a call to "foo :default".

Here's an example run:

user=> (dotimes i 10 (foo i))
method-default
method-1
method-2
method-2
method-2
method-default
method-default
method-default
method-default
method-default
nil

--Steve

Rich Hickey

unread,
Jun 17, 2008, 4:38:45 PM6/17/08
to Clojure


On Jun 17, 4:15 pm, "Stephen C. Gilardi" <scgila...@gmail.com> wrote:
> On Jun 17, 2008, at 4:01 PM, Tom Hicks wrote:
>
> > I'm still learning so please let me know if there is a
> > simpler or more concise way to use the map for multi-method dispatch.
>
> Since maps can be called as functions, the most succinct way I can
> think of to duplicate the behavior you described (including
> the :default method) using a map is:
>
> (defmulti foo
> {1 :method-1 2 :method-2 3 :method-2 4 :method-2})
>
> (defmethod foo :method-1
> [x]
> (println "method-1"))
>
> (defmethod foo :method-2
> [x]
> (println "method-2"))
>
> (defmethod foo :default
> [x]
> (println "method-default"))
>
> When called as a function with 1 argument, the map will either return
> a value for the argument's key or "nil" if the map doesn't contain a
> key "=" to the argument. defmulti allows you to specify a default
> method, but if you don't, it will call the method which is associated
> with the keyword ":default". In this way "nil" will be mapped to a
> call to "foo :default".
>

Sorry I didn't chime in sooner, but I can't stress enough that putting
the mapping in the dispatch function defeats the purpose of
multimethods, which is to provide an _open_ mapping to methods. So,
let the multimethod handle that part and use helper functions to share
implementation details:

(defmulti foo identity)

(defn- method-b [x] (println "method-b"))

(defmethod foo :default [x] (println "method-default"))

(defmethod foo 1 [x] (println "method-1"))
(defmethod foo 2 [x] (method-b x))
(defmethod foo 3 [x] (method-b x))
(defmethod foo 4 [x] (method-b x))

Rich


Tom Hicks

unread,
Jun 17, 2008, 5:38:01 PM6/17/08
to Clojure
Thanks for the caveat Rich....it really does defeat
the purpose to embed the map (or the list), doesn't it?

Thanks Steve for the important info that the dispatch
value nil will cause dispatch to the defmethod :default. I
should have deduced that from your original list example.
regards,
-tom

Graham Fawcett

unread,
Jun 17, 2008, 10:13:11 PM6/17/08
to Clojure
On Jun 17, 4:38 pm, Rich Hickey <richhic...@gmail.com> wrote:
> Sorry I didn't chime in sooner, but I can't stress enough that putting
> the mapping in the dispatch function defeats the purpose of
> multimethods, which is to provide an _open_ mapping to methods. So,
> let the multimethod handle that part and use helper functions to share
> implementation details:
>
> (defmulti foo identity)
>
> (defn- method-b [x] (println "method-b"))
>
> (defmethod foo :default [x] (println "method-default"))
>
> (defmethod foo 1 [x] (println "method-1"))
> (defmethod foo 2 [x] (method-b x))
> (defmethod foo 3 [x] (method-b x))
> (defmethod foo 4 [x] (method-b x))

It would be very nice if (defmulti) supported a :comparator option,
with #'= as the default, but which could be replaced with any function
of the type (fn [darg method-value]) => bool, where darg is the result
of the dispatch function applied to the argument. This could allow for
richer method-dispatch options, e.g. dispatching on instance
relationships:

(defmulti foo identity :comparator (fn [a m] (instance? m a)))

(defmethod foo BufferedReader [x] (...))
(defmethod foo File [x] (...))
(defmethod foo ISeq [x] (...))

I'm not sure the interface is quite right (#'identity seems
superfluous here), but what do you think of the general idea?

Graham

darren...@gmail.com

unread,
Jun 18, 2008, 12:08:13 AM6/18/08
to Clojure
Thanks folks. In line with Rich's comment from the latest sceencast
that if something isn't in the language I can add it myself, I have
thrown this together:

(defmacro defmethod-for-values
[multifn dispatch-values-seq & fn-tail]
`(let [pvar# (var ~multifn)
method# (fn ~@fn-tail)]
(doseq dispatch-val# ~dispatch-values-seq
(. pvar# (commuteRoot (fn [#^clojure.lang.MultiFn mf#]
(. mf# (assoc dispatch-val# method#))))))))

Which allows you to do something like:

(defmulti foo identity)
(defmethod foo :default [x] (println "method-default"))

(defmethod foo 1 [x] (println "method-1"))
(defmethod-for-values foo '(2 3 4) [x] (println "method-b"))

To achieve the same thing as Rich's example above with a little less
duplication. I'm still just getting my feet wet with Clojure, so any
improvements are welcome.

Thanks,
--Darren

Rich Hickey

unread,
Jun 18, 2008, 9:11:02 AM6/18/08
to Clojure
I understand the desire for matching at other than the most-derived
level, and it's a limitation of the current multimethods that you
cannot do that. But when you look at this more generally, you'll see
that it is not merely a matter of a different comparator, it is moving
from 1:1 equality dispatch to predicate dispatch, instance? being one
sub-flavor of the latter. The issue with predicate dispatch is that
more than one predicate can be true of a particular argument set. For
instance, in your example, what if methods were defined for Iterable,
Object and Comparable?

In a predicate dispatch system some matches can dominate others, and
such domination can be determined in advance, due to logical
implication, e.g. every Iterable is an Object, so Iterable is more
specific. Similarly < 10 implies < 100, etc. Others cannot be
determined, e.g. Iterable vs Comparable. A system would either have to
deny such possibilities, provide a mechanism for ordering things with
no implications, or generate runtime errors should a target satisfy
more than one match.

So, I hope you see predicate dispatch is substantially more complex
both at compile-time and runtime than the current system. But it is
interesting to me, and I had predicate dispatch in an early Clojure
prototype. It used a mini-Prolog for implication. I hope to revisit it
soon.

Rich

Graham Fawcett

unread,
Jun 18, 2008, 10:21:47 AM6/18/08
to Clojure
On Jun 18, 9:11 am, Rich Hickey <richhic...@gmail.com> wrote:
> On Jun 17, 10:13 pm, Graham Fawcett <graham.fawc...@gmail.com> wrote:
> > It would be very nice if (defmulti) supported a :comparator option,
> > with #'= as the default, but which could be replaced with any function
> > of the type (fn [darg method-value]) => bool, where darg is the result
> > of the dispatch function applied to the argument. This could allow for
> > richer method-dispatch options, e.g. dispatching on instance
> > relationships...
> I understand the desire for matching at other than the most-derived
> level, and it's a limitation of the current multimethods that you
> cannot do that. But when you look at this more generally, you'll see
> that it is not merely a matter of a different comparator, it is moving
> from 1:1 equality dispatch to predicate dispatch, instance? being one
> sub-flavor of the latter. The issue with predicate dispatch is that
> more than one predicate can be true of a particular argument set. For
> instance, in your example, what if methods were defined for Iterable,
> Object and Comparable?

I suspected that's why you had chosen 1:1 dispatch. Reasoning about
predicate-based dispatch without disjoint predicates is tricky, to say
the least. :-) It is good to hear that you had worked on predicate
dispatch in an earlier version, and might revisit it. A good solution
will be hard to find, but would provide a substantially more powerful
multimethod system.

> In a predicate dispatch system some matches can dominate others, and
> such domination can be determined in advance, due to logical
> implication, e.g. every Iterable is an Object, so Iterable is more
> specific. Similarly < 10 implies < 100, etc. Others cannot be
> determined, e.g. Iterable vs Comparable. A system would either have to
> deny such possibilities, provide a mechanism for ordering things with
> no implications, or generate runtime errors should a target satisfy
> more than one match.

(FWIW, I favour a runtime error: it's in the spirit of a dynamic
language.) Simplistic ordering would be convenient -- having, say,
three priority levels (:default, :override and :fallback) with
undefined ordering within each level, but ensuring that :override
dispatch rules were triggered before :default, and those before
:fallback. You wouldn't build a bridge on such a flimsy basis, but you
could build a lot of software. Introspection on the dispatch rules
would also be helpful.

On a tangent, some interesting work was done in Python on a 'component
adaptation' system (dynamically adapting objects via a series of
transitive rules, in order to implement some 'interface'). While not
strictly a generic-function system, it was designed to fulfill a
similar need, and might offer inspiration:

http://peak.telecommunity.com/protocol_ref/ref.html

Thanks, Rich,

Graham

MikeM

unread,
Jun 22, 2008, 11:51:50 AM6/22/08
to Clojure

> Sorry I didn't chime in sooner, but I can't stress enough that putting
> the mapping in the dispatch function defeats the purpose of
> multimethods, which is to provide an _open_ mapping to methods.

I was experimenting to see if I understood this issue, and I learned
something - defmulti captures the current definition of the dispatch
function, and doesn't know if you redefine it:

(defn tst-dispatch [& args] (let [f (first args)]
(cond
(= f 0) :a
(= f 1) :b
:else :default)))
(defmulti tst tst-dispatch)
(defmethod tst :a [& args] (println ":a"))
(defmethod tst :b [& args] (println ":b"))

(tst 2);=>exception since no default method and 2 not handled by tst-
dispatch

(defn tst-dispatch [& args] (let [f (first args)]
(cond
(= f 0) :a
(>= f 1) :b
:else :default)))
(tst 2);=>still get exception

But, you can get around this by wrapping your dispatch function:

(defn flex-dispatch [& args] (let [f (first args)]
(cond
(= f 0) :a
(= f 1) :b
:else :default)))
(defn tst-dispatch [& args] (apply flex-dispatch args))

(defmulti tst tst-dispatch)
(defmethod tst :a [& args] (println ":a"))
(defmethod tst :b [& args] (println ":b"))

(tst 2);=>exception

;re-defn to handle a new case
(defn flex-dispatch [& args] (let [f (first args)]
(cond
(= f 0) :a
(>= f 1) :b
:else :default)))
(tst 2) ;=> :b

I was wondering if it would make sense to have defmulti capture the
var (if provided; would still want to have the ability to just provide
a fn) instead of the function that the var points to?

Rich Hickey

unread,
Jun 22, 2008, 12:09:06 PM6/22/08
to clo...@googlegroups.com

You are in control here, and everywhere you store a reference to a
function, as to whether you want to capture the var's function value
or maintain a connection to the var that contains its current
definition. To get the latter effect, just pass the var itself rather
than its value:

(defmulti tst #'tst-dispatch)

vars implement IFn by delegating to their values:

user=> (first [1 2])
1
user=> (#'first [1 2]) ;call through var
1
user=> ((var first) [1 2]) ;previous is a shorthand for this
1

so doing the above allows you to fix tst-dispatch and have the
multimethod pick up the fix.

Rich

Reply all
Reply to author
Forward
0 new messages