Protocols in ClojureScript
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 Custom Protocols
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]]
A 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
The 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 seqs, but cljs.core doesn’t implement anything for NodeLists, so we have to do that ourselves, which allows us to pass the NodeList to map successfully:
(extend-type js/NodeList ISeqable (-seq [node-list] (array-seq node-list))) (map #(.-offsetTop %) item-nodes) ; => (8 26 44)
(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
(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 filter and 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 %))))
(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~ function.
(into #js process-words-input words-input) ; => #js["apple" "banana" "potato"]
- Adam Frey, May 2016