Skip to content
AgileLogics
6 min read

Multimethods

Prerequisites: Protocols

A multimethod chooses an implementation by calling a dispatch function on its arguments. The dispatch can use any data — a key, a combination of values, or a computed category — not just the type.

(defmulti shipping-cost :method)

(defmethod shipping-cost :standard [order]
  (if (>= (:total order) 50) 0 5))

(defmethod shipping-cost :express [order]
  15)

(defmethod shipping-cost :pickup [_]
  0)

(defmethod shipping-cost :default [order]
  (throw (ex-info "Unknown shipping method" {:method (:method order)})))

(doseq [order [{:method :standard :total 30}
               {:method :standard :total 80}
               {:method :express :total 10}
               {:method :pickup :total 99}]]
  (println (:method order) (:total order) "->" (shipping-cost order)))

(println (try (shipping-cost {:method :drone})
              (catch Exception e (ex-data e))))
:standard 30 -> 5
:standard 80 -> 0
:express 10 -> 15
:pickup 99 -> 0
{:method :drone}
  • (defmulti name dispatch-fn) — here the dispatch function is the keyword :method.
  • (defmethod name dispatch-value [args] body) adds one case.
  • :default handles unmatched values.
  • New methods can be added from any namespace, later — the set of cases is open.

Dispatching on several values

(defmulti collide (fn [a b] [(:kind a) (:kind b)]))

(defmethod collide [:asteroid :ship] [_ _] "ship destroyed")
(defmethod collide [:ship :asteroid] [a b] (collide b a))
(defmethod collide [:asteroid :asteroid] [_ _] "asteroids merge")
(defmethod collide :default [_ _] "nothing happens")

(println (collide {:kind :ship} {:kind :asteroid}))
(println (collide {:kind :asteroid} {:kind :asteroid}))
(println (collide {:kind :ship} {:kind :ship}))
ship destroyed
asteroids merge
nothing happens

Hierarchies

(derive ::cat ::mammal)
(derive ::dog ::mammal)
(derive ::mammal ::animal)
(derive ::parrot ::animal)

(defmulti sound :species)
(defmethod sound ::mammal [_] "some mammal noise")
(defmethod sound ::dog [_] "woof")
(defmethod sound ::animal [_] "...")

(doseq [s [::cat ::dog ::parrot]]
  (println (name s) "->" (sound {:species s})))
(println (isa? ::cat ::animal))
cat -> some mammal noise
dog -> woof
parrot -> ...
true

derive builds an "is-a" hierarchy of keywords; a multimethod picks the most specific matching method. ::cat is a keyword qualified with the current namespace (:user/cat here).

Protocols vs. multimethods

Protocol Multimethod
Dispatch on type of the first argument any function of all arguments
Speed very fast a map lookup per call
Groups functions yes, several per protocol one function each
Typical use abstractions over types business rules over values