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"}}}