Clojure from the ground up

Passing the value along

The two arrows that unroll a nest of calls into lines you can read: the value goes in first with one arrow, last with the other.

Part 10 of 12

Advanced16 min readBack to Courses

Reading inside out is hard work

Nested calls read from the innermost bracket outward, so by the time you reach the front of the line you have forgotten the value you started with. The threading arrow fixes the reading order: (->> nums (filter odd?) (sort)) says take the numbers, keep the odd ones, sort them — in the order you would say it out loud. A pipeline can be read aloud from top to bottom, which is also how you would explain the job to a colleague.

Each step is written as its own small list, so a pipeline can be read one line at a time, and a wrong step is easy to point at. Moving a line moves one step and nothing else, which makes a pipeline easy to rearrange while you are still working out the shape of the answer.

Which end does the value go in

-> puts the value in as the first argument of the next call. That suits arithmetic and anything whose subject comes first: (-> 5 (+ 3) (* 2)) means (* (+ 5 3) 2), which is 16. The value inserted is the whole form on the left, not its first argument, so (-> (+ 1 2) (* 3)) is (* (+ 1 2) 3).

->> puts the value in as the last argument, which is what the sequence functions want: map, filter and sort-by all take the sequence last. (->> nums (filter odd?) (map #(* % %)) (sort)) is exactly (sort (map #(* % %) (filter odd? nums))), written in the order the work happens. If you read ->> as "and then", the pipeline says: take the numbers, and then keep the odd ones, and then square them, and then sort.

The arrow is not magic

Nothing new is computed: the arrow rearranges the brackets and then gets out of the way. If you cannot tell which one a line needs, ask where the sequence would go — first for arithmetic, last for a sequence function. A useful check is to write the plain call first: if the value would be the first argument, use ->; if it would be the last, use ->>.

The arrows nest too, and they can be mixed, but a pipeline long enough to need mixing is usually a pipeline worth splitting into a named function per step.

The arrow is a rewrite rule

-> and ->> are macros: they take the form on the left and move it into each step before the whole thing is evaluated. No new runtime machinery is involved, and the result is exactly the nested call you would have written by hand. Because it is a macro, the arrow is expanded before evaluation, so a mistake in the rewrite shows up as a call with its arguments in the wrong place rather than as a special arrow error.

Because it is a rewrite, the position matters. -> inserts the value as the first argument, which suits a function whose subject comes first. ->> inserts it last, which suits every sequence function that takes the collection at the end.

Read the pipeline in the order the work happens

The value at the top is the input, each line is one transformation, and the last line is the answer. That is the order you would explain the job to someone, which is why the arrow is worth the extra line. Everything between is a step you can check on its own by running it in the pane.

A pipeline with one step is usually not worth writing, and a pipeline that needs both arrows in the same chain is usually two pipelines. Split it and name the middle value; the reader gets a place to stand.

The same work, written twice

(def nums [4 1 7 2 11])

(println (sort (map #(* % %) (filter odd? nums))))
(println (->> nums (filter odd?) (map #(* % %)) (sort)))

(println (+ (* 2 5) 3))
(println (-> (* 2 5) (+ 3)))
(println (-> 5 (+ 3) (* 2)))

One pipeline, read top to bottom

Run it and read the six lines. Then add a step that keeps only the numbers over ten, and write the same pipeline with plain nested calls to see that the two agree.

Output
Click "Run" to execute your code

The two arrows are not interchangeable. Put the value in the wrong end and a call gets a sequence where it wanted a number, or the other way round. The error names the function that complained, and the fix is to switch the arrow.

Three threading mistakes. Using -> with filter puts the sequence where the function belongs, so the call fails or, worse, quietly means something else. Using ->> with subtraction reverses the operands: (->> 10 (- 3)) is (- 3 10), which is -7, not 7. And using ->> with a function that wants the subject first, such as assoc, turns (->> m (assoc :k 1)) into (assoc :k 1 m), which fails.

Try these next

  • Rewrite (sort (filter odd? nums)) with ->> and check the output is the same.
  • Write (-> 5 (* 2) (+ 1)) and then the nested form it stands for.
  • Build a pipeline that maps a vector of words to lengths and keeps the ones over three.

A pipeline over words

Run it and read the five lines. Then add a word and a step that keeps only lengths over four, and write the same pipeline with nested calls to check they agree.

Output
Click "Run" to execute your code

Check what you learned

One question at a time. A wrong answer explains itself — take another go.

Part 1 of 5

What does (-> 10 (- 3) (+ 2)) work out to?