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])
(defrecord SimpleGraphRecord [nodes adj])
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}}))
Both type constructors also generate a functional constructor, which is the type name prefixed by ->
.
(->SimpleGraph {1 2} {1 {2} 2 {1}})
(.nodes simple-graph)
(def simple-graph-record (->SimpleGraphRecord {1 2} {1 {2} 2 {1}}))
Records work just like maps.
(:nodes simple-graph-record)
One can assoc existing fields as well as new ones.
(assoc simple-graph-record :nodes {1 2 3} :components 2)
They also support metadata.
(meta (with-meta simple-graph-record {:created-at (java.util.Date.)}))
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"))
(defprotocol EditableGraph
(add-edges* [g edges] "Add edges to graph g. See add-edges"))
Types can then be extended with a protocol via extend
.
(extend SimpleGraph
Graph
{:nodes (fn [g] (.nodes g))
:edges (fn [g] (.adj g))})
(nodes simple-graph)
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)))
(add-edges* (->SimpleGraphRecord {} {}) [[1 2] [2 3]])
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)))
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)
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)
(satisfies? EditableGraph simple-graph)
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"}}}