Clojure multi-methods

9

A friend asked me a question about multi-methods and since the response was long-ish, I’m dumping it here in case it helps someone else:

Question: “Is it possible to write a multimethod that has defmethods which handle ranges of values? For example, say that a person has an age. Can I write a multimethod that accepts a person as a parameter and returns “child” if age < 16, "adult" if 16 <= age < 66 and "senior" if age >= 66?”

Answer:

Sure – how a multimethod “switches” to choose an implementation is abstracted by a function of course, specifically the dispatch function you give it when you create the multimethod. Very commonly, the dispatch function is just “class” to switch on type but it can be anything. Each defmethod specifies a specific value it matches on, so you can’t have a defmethod that directly matches a range (afaik).

In your example, you don’t really need a multimethod though – I would just use cond for that:

user=> (defn ticket [age] 
         (cond (< age 16) :child
               (>= age 66) :senior
               :else :adult))
#'user/ticket
user=> (ticket 10)
"child"
user=> (ticket 20)
"adult"
user=> (ticket 90)
"senior"

Now, you could use the ticket function above as the dispatch function for a multi-method if you wanted to switch behavior based on those kinds of tickets and that would make some sense. Here I create a print-name multimethod that switches behavior based for a Person record using the ticket function on the age:

user=> (defrecord Person [name age])
user.Person
user=> (defmulti print-name (fn [person] (ticket (:age person))))
nil
user=> (defmethod print-name :child [person] (str "Young " (:name person)))
#<MultiFn clojure.lang.MultiFn@44547842>
user=> (defmethod print-name :adult [person] (:name person))
#<MultiFn clojure.lang.MultiFn@44547842>
user=> (defmethod print-name :senior [person] (str "Old " (:name person)))
#<MultiFn clojure.lang.MultiFn@44547842>
user=> (print-name (Person. "Jimmy" 5))
"Young Jimmy"
user=> (print-name (Person. "Alex" 36))
"Alex"
user=> (print-name (Person. "Edna" 99))
"Old Edna"

Comments

9 Responses to “Clojure multi-methods”
  1. Defn says:

    Minor correction: Your return values from (ticket 10), 20, 90 should be keywords not strings.

  2. Alex says:

    @Defn: Ah yes, that was actually an intentional change b/c I thought it read nicer and set up the second example.

  3. Gabor Szabo says:

    An interesting re-implementation in Perl 6

  4. Vinoth Kumar says:

    Can i make multimethods to dispatch to a subset of functions say based on list of values
    (make-noise [:dog :cat :bird])

    has to call 3 dispatch function?

  5. Alex says:

    @Vinoth: Not sure what you mean exactly. The dispatch function can be any function so the answer to any question of the form “Can I make multimethod dispatch …” is generally yes. But I’m not sure what exactly you’re trying to do.

  6. Thanks for posting this. It is helpful. I am not sure why your ticket function on my 1.2 repl comes out with the tag, not the name in quotes.

    ba1-app=> (defn ticket [age] (cond (= age 66) :senior :else :adult))
    #’ba1-app/ticket
    ba1-app=> (ticket 66)
    :senior

  7. Alex Miller says:

    @octopusgrabbus: your results are expected – I think I might have rewritten my example and gotten a mixture into the blog of old results and new code.

  8. Timothy Baldridge says:

    The only issue I have with the above example, is that using multi-methods with the ticket fn don’t really allow you to extend the system and run-time. Or even outside of your library. For instance, if I want to add “young-adult”, then I have modify ticket as well as create a new multi-method. The better route may be to re-write ticket so that it gets its ranges from a hashmap. That way, when other namespaces load you can add new classifications at runtime.

    One of the main benefits of multimethods is that you can define a new method at any time.

  9. Alex Miller says:

    @Timothy: Correct, although you could make ticket a multimethod too. ;)