Turning Expressions Inside Out with Threading Macros

Some of Clojure’s most fun built-in macros are the ones that allow you to rewrite deeply nested expressions in a way that more closely reflects their execution order. In this section, we’ll take a look at how we can use these threading macros to simplify our code and how those macros work their magic. And to avoid confusion: threading in this context has nothing to do with JVM/OS threads; the two concepts just have an unfortunate naming collision.

The most common of these macros is ->, which threads the result of each expression into the next one as the first argument (converting symbols to lists where the next expression is not a list). Perhaps you’ve seen this macro before, when composing Ring[27] middleware:

control_flow/threading_ring_middleware.clj
 
(​def​ app-1
 
(wrap-head
 
(wrap-file-info
 
(wrap-resource (wrap-session
 
(wrap-flash
 
(wrap-params app-handler)))
 
"public"​))))
 
 
 
;; The ,,, placeholders represent the result from the previous line
 
(​def​ app-2
 
(​->​ app-handler ​;; app-handler
 
wrap-params ​;; (wrap-params ,,,)
 
wrap-flash ​;; (wrap-flash ,,,)
 
wrap-session ​;; (wrap-session ,,,)
 
(wrap-resource ​"public"​) ​;; (wrap-resource ,,, "public")
 
wrap-file-info ​;; (wrap-file-info ,,,)
 
wrap-head)) ​;; (wrap-head ,,,)

With its much flatter look, the app-2 version is what most Clojure folks prefer for these kinds of expressions. It’s not that we hate parentheses—it’s just that we prefer the clarity of this pipelined approach. Now, I said that -> was nice because it clarifies execution order, but can you see why the Ring approach might be confusing for newcomers?

If you’ve used Ring before, you know that the wrap-head middleware has the first opportunity to respond to a web request, and app-handler is actually last in line to handle the request, though it’s first in the threading macro. I’ve found that for newcomers (and for old-timers too!), this can be tricky to get your head around. It works this way for Ring middleware because we’re threading functions through the expressions, and those functions don’t actually get evaluated until later, when a web request comes in.

Now let’s take a look at the implementation of ->, which turns expressions inside out:

control_flow/threading_implementation.clj
 
(​defmacro​ ​->
 
"Threads the expr through the forms. Inserts x as the
 
second item in the first form, making a list of it if it is not a
 
list already. If there are more forms, inserts the first form as the
 
second item in second form, etc."
 
{:added ​"1.0"​}
 
[x & forms]
 
(​loop​ [x x, forms forms]
 
(​if​ forms
 
(​let​ [form (​first​ forms)
 
threaded (​if​ (​seq?​ form)
 
(​with-meta​ `(~(​first​ form) ~x ~@(​next​ form)) (​meta​ form))
 
(​list​ form x))]
 
(​recur​ threaded (​next​ forms)))
 
x)))

Again, we see loop/recur used to iterate through the given forms, preserving metadata and transforming symbols to lists, and building up the result as x, the eventual return value from the macro (and thus, the expression to be evaluated). Take a few minutes to macroexpand a few sample expressions in your head, like these (don’t hesitate to use macroexpand-1 if you get stuck!):

control_flow/threading_examples.clj
 
(​->​ ​"hi"​)
 
;=> ???
 
 
(​->​ 4
 
(​+​ 3)
 
(​*​ 2))
 
;=> ???
 
 
(​->​ 10
 
^clojure.lang.LazySeq ​range
 
.​iterator
 
(​doto​ ​.​​next​ ​.​​next​)
 
.​​next​)
 
;=> ???

Now that you’ve seen how -> does its magic, let’s take a moment to think about what it would take to do this kind of rewriting without macros. As usual, we can get reasonably close with functions:

control_flow/threading_as_functions.clj
 
(​->​ 1
 
(​+​ 2)
 
(​*​ 3)
 
(​+​ 4)
 
(​*​ 5))
 
;=> 65
 
 
(​defn​ thread-first-fn [x & fns]
 
(​reduce​ (​fn​ [acc f] (f acc))
 
x
 
fns))
 
 
(thread-first-fn 1
 
#(​+​ % 2)
 
#(​*​ % 3)
 
#(​+​ % 4)
 
#(​*​ % 5))
 
;=> 65
 
 
;; or even:
 
(​defn​ thread-first-fn' [x & fns]
 
((​apply​ ​comp​ (​reverse​ fns)) x))

Once again, this isn’t bad at all. It looks even better for things like middleware composition, where most of the threaded elements are named functions already. It does take a bit more syntax for cases like the previous one, since we need to deal in functions, not just expressions. One thing we can do with -> that we can’t do with either thread-first-fn variant is to insert a macro into the pipeline using only its name. If we tried that with thread-first-fn, we’d run into our old friend, CompilerException java.lang.RuntimeException: Can’t take value of a macro.

There are plenty of other useful threading macros that you can dig into more: ->> has been in clojure.core for years, and clojure.core/some->, clojure.core/as->. Swiss Arrows[28] really goes to town and provides a whole tool belt of arrow macros similar to these. But this idea of rewriting the input expressions (as opposed to just wrapping the expression up to be evaluated or not, as we see fit) is a huge step. We’re starting to get into the territory of macros that do things that functions can’t.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.217.206.112