Multimethods and Hierarchies

Table of Contents

Clojure eschews the traditional object-oriented approach of creating a new data type for each new situation, instead preferring to build a large library of functions on a small set of types. However, Clojure fully recognizes the value of runtime polymorphism in enabling flexible and extensible system architecture. Clojure supports sophisticated runtime polymorphism through a multimethod system that supports dispatching on types, values, attributes and metadata of, and relationships between, one or more arguments.

A Clojure multimethod is a combination of a dispatching function, and one or more methods. When a multimethod is defined, using defmulti, a dispatching function must be supplied. This function will be applied to the arguments to the multimethod in order to produce a dispatching value. The multimethod will then try to find the method associated with the dispatching value or a value from which the dispatching value is derived. If one has been defined (via defmethod), it will then be called with the arguments and that will be the value of the multimethod call. If no method is associated with the dispatching value, the multimethod will look for a method associated with the default dispatching value (which defaults to :default), and will use that if present. Otherwise the call is an error.

The multimethod system exposes this API: defmulti creates new multimethods, defmethod creates and installs a new method of multimethod associated with a dispatch-value, remove-method removes the method associated with a dispatch-value and prefer-method creates an ordering between methods when they would otherwise be ambiguous.

Derivation is determined by a combination of either Java inheritance (for class values), or using Clojure’s ad hoc hierarchy system. The hierarchy system supports derivation relationships between names (either symbols or keywords), and relationships between classes and names. The derive function creates these relationships, and the isa? function tests for their existence. Note that isa? is not instance?.

You can define hierarchical relationships with (derive child parent). Child and parent can be either symbols or keywords, and must be namespace-qualified:

Note the :: reader syntax, ::keywords resolve namespaces.

-> :user/rect

derive is the fundamental relationship-maker

(derive ::rect ::shape)
(derive ::square ::rect)

parents / ancestors / descendants and isa? let you query the hierarchy

(parents ::rect)
-> #{:user/shape}

(ancestors ::square)
-> #{:user/rect :user/shape}

(descendants ::shape)
-> #{:user/rect :user/square}

(= x y) implies (isa? x y)

(isa? 42 42)
-> true

isa? uses the hierarchy system

(isa? ::square ::shape)
-> true

You can also use a class as the child (but not the parent, the only way to make something the child of a class is via Java inheritance).

This allows you to superimpose new taxonomies on the existing Java class hierarchy:

(derive java.util.Map ::collection)
(derive java.util.Collection ::collection)

(isa? java.util.HashMap ::collection)
-> true

isa? also tests for class relationships:

(isa? String Object)
-> true

as do parents / ancestors (but not descendants, since class descendants are an open set)

(ancestors java.util.ArrayList)
-> #{java.lang.Cloneable java.lang.Object java.util.List
    java.util.RandomAccess java.util.AbstractList}

isa? works with vectors by calling isa? on their corresponding elements:

(isa? [::square ::rect] [::shape ::shape])
-> true

isa? based dispatch

Multimethods use isa? rather than = when testing for dispatch value matches. Note that the first test of isa? is =, so exact matches work.

(defmulti foo class)
(defmethod foo ::collection [c] :a-collection)
(defmethod foo String [s] :a-string)

(foo [])

(foo (java.util.HashMap.))

(foo "bar")

prefer-method is used for disambiguating in case of multiple matches where neither dominates the other. You can just declare, per multimethod, that one dispatch value is preferred over another:

(derive ::rect ::shape)

(defmulti bar (fn [x y] [x y]))
(defmethod bar [::rect ::shape] [x y] :rect-shape)
(defmethod bar [::shape ::rect] [x y] :shape-rect)

(bar ::rect ::rect)
-> Execution error (IllegalArgumentException) at user/eval152 (REPL:1).
   Multiple methods in multimethod 'bar' match dispatch value:
   [:user/rect :user/rect] -> [:user/shape :user/rect]
   and [:user/rect :user/shape], and neither is preferred

(prefer-method bar [::rect ::shape] [::shape ::rect])
(bar ::rect ::rect)
-> :rect-shape

All of the examples above use the global hierarchy used by the multimethod system, but entire independent hierarchies can also be created with make-hierarchy, and all of the above functions can take an optional hierarchy as a first argument.

This simple system is extremely powerful. One way to understand the relationship between Clojure multimethods and traditional Java-style single dispatch is that single dispatch is like a Clojure multimethod whose dispatch function calls getClass on the first argument, and whose methods are associated with those classes. Clojure multimethods are not hard-wired to class/type, they can be based on any attribute of the arguments, on multiple arguments, can do validation of arguments and route to error-handling methods etc.

Note: In this example, the keyword :Shape is being used as the dispatch function, as keywords are functions of maps, as described in the Data Structures section.

(defmulti area :Shape)
(defn rect [wd ht] {:Shape :Rect :wd wd :ht ht})
(defn circle [radius] {:Shape :Circle :radius radius})
(defmethod area :Rect [r]
    (* (:wd r) (:ht r)))
(defmethod area :Circle [c]
    (* (. Math PI) (* (:radius c) (:radius c))))
(defmethod area :default [x] :oops)
(def r (rect 4 13))
(def c (circle 12))
(area r)
-> 52
(area c)
-> 452.3893421169302
(area {})
-> :oops