Protocols are a feature in Clojure and ClojureScript that provide high-performance, dynamic, typed-based polymorphism. That’s a lot of computer words, but what it really means is that Clojure(Script) will help us in creating functions that process in different ways arguments of different types. They are similar to Java Interfaces, but with a few key differences.
This post will focus on using protocols in ClojureScript, so if you would like to play around with the examples, you can replicate them in a ClojureScript REPL, like clojurescript.io
Let’s create a basic protocol:
(defprotocol Invertible "A protocol for data types that are 'invertible'" (invert [this] "Invert the given item."))
As shown, a protocol is given a name (
Invertible) and a function (invert) the is connected to that protocol. Only one function is defined for our example, but protocols can be associated with multiple functions.
(extend-type number Invertible (invert [this] (if (zero? this) 0 (/ 1 this)))) (extend-type string Invertible (invert [this] (apply str (reverse this)))) (invert 4) ; => 0.25 (invert "backwards") ; => "sdrawkcab"
It’s also possible to create custom types for our programs and have those types extend protocols, but that is outside the scope of this post.
Working with the DOM
Let’s say in our ClojureScript app we need to query some DOM elements that have the class
todo-item to change them.
(defn get-todo-nodes  (js/document.querySelectorAll ".todo-item"))
When we execute this function we get back a
NodeList object from the browser.
(get-todo-nodes) ; => #object[NodeList [object NodeList]]
NodeList is sequential like a ClojureScript vector, but unfortunately we can’t use all the functions we know and love from the ClojureScript core library:
(map #(.-offsetTop %) (get-todo-nodes)) ; => Error: [object NodeList] is not ISeqable
map function above as defined in
cljs.core calls attempts to translate our
NodeList object into an instance of the
seq abstraction in order to process it. Many of the built-in ClojureScript types such as vectors implement the
ISeqable protocol, and thus are able to convert themselves into
cljs.core doesn’t implement anything for
NodeLists, so we have to do that ourselves, which allows us to pass the
(extend-type js/NodeList ISeqable (-seq [node-list] (array-seq node-list))) (map #(.-offsetTop %) item-nodes) ; => (8 26 44)
ClojureScript’s provided protocols offer a painless way to extend the core language giving us a flexible nature missing in most languages. For instance, in ClojureScript the concept of invoking a function is itself a protocol, named
IFn. For any given type, we can extend the
RegExp types act like functions:
(extend-type js/RegExp IFn (-invoke ([this s] (re-matches this s)))) (#"foo.*" "foobar") ; => "foobar" (#"zoo.*" "foobar") ; => nil (filter #".*foo.*" ["foobar" "goobar" "foobaz"]) ; => ("foobar" "foobaz")
- filter that array down to the strings that are longer than three characters
- downcase each remaining string
filter, each one producing a new ClojureScript collection. Then when the transformation is finished we could call
(def words-input #js["Apple" "To" "Banana" "Potato" "At" "The"])
We can create a better solution with Transducers, a feature in ClojureScript which provide efficient data transformations without creating intermediate collections. We can create a transducer that represents our desired transformation by composing individual transducer functions like
map which represent a specific transformation but are not tied to any specific concrete input our output collections. Here’s a transducer that will work for our problem:
(def process-words-input (comp (filter #(< 3 (count %))) (map #(.toLowerCase %))))
ICollection protocol which defines the
conj for arrays is to use the
push method to add an item to the array an then return it.
(extend-type array ICollection (-conj ([this x] ;; Mutable! (doto this (.push x)))))
(transduce process-words-input conj #js words-input) ; => #js["apple" "banana" "potato"]
ClojureScript also offers a shortcut version of the above call via the
(into #js process-words-input words-input) ; => #js["apple" "banana" "potato"]
Agree? Disagree? Let us know! Email us your comments at