Clojure's polymorphism

This post looks at Clojure's polymorphism mechanisms using types. There are essentially two constructs to define new types: deftype and defrecord. defstruct is a third, but it is recommended to use defrecord instead of defstruct . We will take some inspiration by the loom graph library, meaning our examples will be concerned with graphs.

(deftype SimpleGraph [nodes adj])
0.3s
Clojure
(defrecord SimpleGraphRecord [nodes adj])
0.7s
Clojure

The main difference between the two constructs is that defrecord supports everything that clojure.lang.PersistentMap implements whereas deftype only comes with a constructor and field accessors. deftype comes with options to make the field mutable. Only use these options when you know what you are doing and avoid it whenever possible.

(def simple-graph (SimpleGraph. #{1 2} {1 #{2} 2 #{1}}))
0.0s
Clojure

Both type constructors also generate a functional constructor, which is the type name prefixed by -> .

(->SimpleGraph #{1 2} {1 #{2} 2 #{1}})
0.0s
Clojure
(.nodes simple-graph)
0.0s
Clojure
(def simple-graph-record (->SimpleGraphRecord #{1 2} {1 #{2} 2 #{1}}))
0.0s
Clojure

Records work just like maps.

(:nodes simple-graph-record)
0.0s
Clojure

One can assoc existing fields as well as new ones.

(assoc simple-graph-record :nodes #{1 2 3} :components 2)
0.0s
Clojure

They also support metadata.

(meta (with-meta simple-graph-record {:created-at (java.util.Date.)}))
0.2s
Clojure

A protocol defines a set of named method plus their signatures.

(defprotocol Graph
  (nodes [g] "Returns a collection of the nodes in graph g")
  (edges [g] "Edges in g. May return each edge twice in an undirected graph"))
0.2s
Clojure
(defprotocol EditableGraph
  (add-edges* [g edges] "Add edges to graph g. See add-edges"))
0.1s
Clojure

Types can then be extended with a protocol via extend .

(extend SimpleGraph
  Graph
  {:nodes (fn [g] (.nodes g))
   :edges (fn [g] (.adj g))})
0.0s
Clojure
(nodes simple-graph)
0.0s
Clojure

There is also a shorthand for specifying the methods via the macro extend-type which gets rid of the anonymous functions.

(extend-type SimpleGraphRecord
  Graph
  (nodes [g] (:nodes g))
  (edges [g] (:adj g))
  
  EditableGraph
  (add-edges* [g edges]
    (reduce
      (fn [g [n1 n2]]
        (-> g
          (update-in [:nodes] conj n1 n2)
          (update-in [:adj n1] (fnil conj #{}) n2)
          (update-in [:adj n2] (fnil conj #{}) n1)))
      g edges)))
0.1s
Clojure
(add-edges* (->SimpleGraphRecord #{} {}) [[1 2] [2 3]])
0.1s
Clojure

In case you want to define the methods on a per protocol basis, one can use extend-protocol . It's also possible to extend types with a protocol right when they get declared.

(extend-protocol Graph
  SimpleGraph
  (nodes [g] (.nodes g))
  (edges [g] (.adj g))
  SimpleGraphRecord
  (nodes [g] (:nodes g))
  (edges [g] (:adj g)))
0.1s
Clojure

One might be wondering why you would want to use deftype, defrecord in combination with defprotocol when there are multimethods . Multimethods are simpler in the sense that they are just functions with a dispatch method. They let you dispatch on any computation of the arguments. This also makes them slower but also more powerful. One can think of multimethods as superset of protocols.

(defmulti nodes-multi class)
0.1s
Clojure

With protocols you hook into the low level support of the JVM which is a lot faster. The downside is one can only dispatch on the type of the first argument. An advantage of protocols is that you can group methods based on a protocol (see the Graph protocol above). satisfies? and extends? also let one check if a value satisfies a protocol.

(satisfies? Graph simple-graph) 
0.0s
Clojure
(satisfies? EditableGraph simple-graph)
0.0s
Clojure

Although you can achieve a similar result with derive and isa? for multimethods, it needs to be done explicitly.

Two constructs this post hasn't touched upon are defprotocol and reify. reify serves essentially to define one-off types.

Setup

{:deps {compliment {:mvn/version "0.3.10"}}}
deps.edn
Extensible Data Notation
Runtimes (1)