Preface: Why Specter?

Specter for selecting

Clojure has a a nice function, get-in, for looking deep into a data structure:

user=> (def x {:a [{:b 3}
                   {:b 4}
                   {:b 5 :c 3}]})
user=> (get-in x [:a 1 :b])
=> 4

Let’s say that the second argument of get-in is a path, which is composed of path elements. For get-in, every element must be a key into an associative structure. (Remember that, in Clojure, vectors are associative structures with integer keys, so 1 above is the same sort of element as :a and :b.)

get-in is a fine function, but it’s limited to traversal of associative structures to retrieve a single value (such as 4).

Specter is more powerful than get-in because it has more types of path elements. For example, ALL lets you select many leaves of the structure. Here’s how you select all values of :b from the structure above:

user=> (select [:a ALL :b] x)
=> [3 4 5]

You can combine ALL with arbitrary predicates to reduce the number of selected values to those that satisfy the predicate:

user=> (select [:a ALL :b odd?] x)
=> [3 5]

As a final teaser, walker is a path element for walking code. Here’s how you’d select every even integer in a nested expression:

user=> (select [(walker integer?) even?]
               '(+ 1 (- 2 3) (+ 4 (- 5 (+ 6 7)) 8)))
=> (2 4 6 8)

Specter for updating

transform is to select as update-in is to get-in: you provide a function that’s given each selected element, and the result is the original input “modified” with the results of the function. The following transforms even integers by multiplying them by 1000:

user=> (transform [(walker integer?) even?]    ; path
                  (partial * 1000)             ; transformer
                  '(+ 1 (- 2 3) (+ 4 (- 5 (+ 6 7)) 8)))
=> (+ 1 (- 2000 3) (+ 4000 (- 5 (+ 6000 7)) 8000))

As you can see from above, any of the variety of path elements you can use with select can also be used with transform. There are also elements especially useful in transform. For example, consider this structure:

user=> (def input [ {:a 1 :x {:y 2}}
                    {:a 3 :x {:y 4}} ])

We can change the :a values with the path elements you already know:

user=> (transform [ALL :a] (partial * 1000) input)
=> [{:a 1000, :x {:y 2}} {:a 3000, :x {:y 4}}]

… and do the same to the more-deeply-nested :y values:

user=> (transform [ALL :x :y] (partial * 1000) input)
=> [{:a 1, :x {:y 2000}} {:a 3, :x {:y 4000}}]

But how do we add 1000 times the :a values to the corresponding :y values? We can do that using the collect-one element, which stashes a value for later use. Before we get to its use with transform, consider collect-one in a select expression:

user=> (select [ALL (collect-one :a) :x :y] input)
=> [[1 2] [3 4]]

The result is not the :y values (2 and 4), but rather pairs of :a and :y values. (If you’re familiar with monads, you can think of this as something like the state monad.)

So we have a way to “grab” an intermediate value (:a) as well as the leaf value :y. Because of the use of collect-one, they will both be given to the transformation function, which allows us to combine the two:

user=> (transform [ALL (collect-one :a) :x :y]
                  (fn [a z] (+ (* 1000 a) z))
                  input)
=> [{:a 1, :x {:y 1002}} {:a 3, :x {:y 3004}}]

Specter is not as general-purpose as, say, zippers, because you can only visit a subtree once. (With zippers, you can navigate to a point in the tree and then, based on the value there, navigate backwards to revisit part of the tree.) However, many of the problems you’d otherwise use zippers for can be solved with Specter, often—I’d argue—more clearly. The above use of collect-one is one of many examples.

Specter for learning

In order to learn how Specter works, I decided to grow it myself, feature by feature, using the existing implementation as a guide. As I did, I kept having little shocks of recognition: “Ah, continuation-passing style. Clever!” or “Ah, recursively precomputing the call graph—I’ve seen that before!” I was recognizing design patterns1, and it occurred to me that this book could also introduce those patterns to people who perhaps hadn’t seen them before.

Originally, I planned to discuss Specter’s design in a Part II of the book, about extending specter with your own path elements. You need to understand something of the design to do that well. But as I was writing the first chapter, about the most basic Specter paths, I realized that I could make that chapter a lot less dry by showing the design, and—in fact—by having you implement Specter yourself (or, if you prefer, follow along with a narrated implementation).

That was the moment when the title switched from Using and Extending Specter to Extending and Using Specter, and when the book became both a book about a useful tool and also a little bit of a book about software design.