Altometrics Blog

Intelligence Amplification, Data Visualization

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:

1
2
3
(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.

Now we can specify that existing JavaScript types fulfill the protocol we created. Here we create different implementations of invert for JavaScript numbers and strings.

1
2
3
4
5
6
7
8
9
10
(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.

1
2
(defn get-todo-nodes []
  (js/document.querySelectorAll ".todo-item"))

When we execute this function we get back a NodeList object from the browser.

1
(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:

1
(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:

1
2
3
4
5
(extend-type js/NodeList
  ISeqable
  (-seq [node-list] (array-seq node-list)))

  (map #(.-offsetTop %) item-nodes) ; => (8 26 44)

IFn Protocol

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 IFn protocol and have it work every place that a built-in function type can be used. In this example we make JavaScript RegExp types act like functions:

1
2
3
4
5
6
7
8
(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")

Working with JavaScript arrays and Transducers

Protocols also allow us to efficiently process native data structures. Let’s imagine that we have a large JavaScript array of strings coming from some source, and we need to:

  1. filter that array down to the strings that are longer than three characters
  2. downcase each remaining string
  3. pass the results into a rendering function as a JavaScript array.

One solutions is to transform the JavaScript array into a ClojureScript seq and then use ClojureScript transformation functions like map, and filter, each one producing a new ClojureScript collection. Then when the transformation is finished we could call clj->js to transform our final ClojureScript collection back into a JavaScript array. But as you might sense there are a lot of unnecessary intermediate collections in that solution, which could be slow if our input becomes too large.

Here’s our input JavaScript array:

1
(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:

1
2
3
4
(def process-words-input
  (comp
    (filter #(< 3 (count %)))
    (map #(.toLowerCase %))))

One thing that we need to do to make JavaScript arrays work seamlessly with Transducers is extend them with the ICollection protocol which defines the conj function. conj takes a collection and a variable number of items and returns a collection of the same type which has the items added to the end. Unlike ClojureScript vectors, JavaScript arrays are mutable, so an efficient implementation of conj for arrays is to use the push method to add an item to the array an then return it.

1
2
3
4
5
6
(extend-type array
  ICollection
  (-conj ([this x]
          ;; Mutable!
          (doto this
            (.push x)))))

With our protocol extended, now we can use our transducer function to transform our input into our desired JavaScript array output without ever creating an intermediate ClojureScript collection:

1
2
(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.

1
2
(into #js[] process-words-input words-input)
; => #js["apple" "banana" "potato"]

Closing

Protocols can be a powerful tool to create cohesion between functions and datatypes that we otherwise have no control over, such as built-in JavaScript types and the ClojureScript core library. Using them enable us to write efficient and idiomatic ClojureScript code.

Agree? Disagree? Let us know! Email us your comments at admin@altometrics.com.