Protocols
Prerequisites: Java interop
A protocol is a named set of functions that different types can
implement — Clojure's answer to interfaces, but open: you can extend a
protocol to types you didn't write, including Java classes and nil.
(defprotocol Describable
(describe [this] "Returns a human-readable description.")
(tag [this]))
(extend-protocol Describable
String
(describe [s] (str "text of length " (count s)))
(tag [_] :string)
Number
(describe [n] (str "the number " n))
(tag [_] :number)
clojure.lang.IPersistentMap
(describe [m] (str "a map with keys " (keys m)))
(tag [_] :map)
nil
(describe [_] "nothing at all")
(tag [_] :nil))
(doseq [x ["hello" 42 {:a 1} nil]]
(println (tag x) "-" (describe x)))
:string - text of length 5
:number - the number 42
:map - a map with keys (:a)
:nil - nothing at all
defprotocoldeclares the functions; the first parameter is always the value being dispatched on.extend-protocolimplements it for several types at once;extend-typeimplements several protocols for one type.- Dispatch is on the type of the first argument and is very fast.
Calling an unimplemented protocol
(defprotocol Shape (area [s]))
(try
(area [1 2])
(catch IllegalArgumentException e
(println (first (clojure.string/split-lines (ex-message e))))))
(println (satisfies? Shape [1 2]))
No implementation of method: :area of protocol: #'user/Shape found for class: clojure.lang.PersistentVector
false
Protocols and records
Protocols are usually implemented by records (next lesson) right in their definition:
(defprotocol Shape
(area [s])
(perimeter [s]))
(defrecord Circle [r]
Shape
(area [_] (* Math/PI r r))
(perimeter [_] (* 2 Math/PI r)))
(defrecord Rect [w h]
Shape
(area [_] (* w h))
(perimeter [_] (* 2 (+ w h))))
(doseq [s [(->Circle 1) (->Rect 2 3)]]
(println (.getSimpleName (class s))
(format "area %.2f perimeter %.2f" (double (area s)) (double (perimeter s)))))
(println (reduce + (map area [(->Rect 1 1) (->Rect 2 2)])))
Circle area 3.14 perimeter 6.28
Rect area 6.00 perimeter 10.00
5
When to use a protocol
- You need polymorphism on type with good performance — for example, a storage API with in-memory and database implementations.
- You want to add behavior to existing types (Java classes, other libraries' records) without changing them.
If you only need different behavior for different values (like an
order's :status), multimethods are more flexible.