eploko February 2016

How to pass a list to clojure's `->` macro?

I'm trying to find a way to thread a value through a list of functions.

Firstly, I had a usual ring-based code:

(defn make-handler [routes]
  (-> routes
      (wrap-json-body)
      (wrap-cors)
      ;; and so on
      ))

But this was not optimal as I wanted to write a test to check the routes are actually wrapped with wrap-cors. I decided to extract the wrappers into a def. So the code became as follows:

(def middleware
  (list ('wrap-json-body)
        ('wrap-cors)
        ;; and so on
        ))

(defn make-handler [routes]
  (-> routes middleware))

This apparently doesn't work and is not supposed to as the -> macro doesn't take a list as the second argument. So I tried to use the apply function to resolve that:

(defn make-handler [routes]
  (apply -> routes middleware))

Which eventually bailed out with:

CompilerException java.lang.RuntimeException: Can't take value of a macro: #'clojure.core/->

So the question arises: How does one pass a list of values to the -> macro (or, say, any other macro) as one would do with apply for a function?

Answers


leetwinski February 2016

you can make a macro for that:

;; notice that it is better to use a back quote, to qoute function names for macro, as it fully qualifies them.
(def middleware
  `((wrap-json-body)
    (wrap-cors))
    ;; and so on
   )

(defmacro with-middleware [routes]
  `(-> ~routes ~@middleware))

for example this:

(with-middleware [1 2 3])

would expand to this:

(-> [1 2 3] (wrap-json-body) (wrap-cors))


galdre February 2016

This is an XY Problem.

The main point of -> is to make code easier to read. But if one writes a new macro solely in order to use -> (in code nobody will ever see because it exists only at macro-expansion), it seems to me that this is doing a lot of work for no benefit. Moreover, I believe it obscures, rather than clarifies, the code.

So, in the spirit of never using a macro where functions will do, I suggest the following two equivalent solutions:

Solution 1

(reduce #(%2 %) routes middleware)

Solution 2

((apply comp middleware) routes)

A Better Way

The second solution is easily simplified by changing the definition of middleware from being a list of the functions to being the composition of the functions:

(def middleware
    (comp wrap-json-body
          wrap-cors
          ;; and so on
          ))

(middleware routes)

When I began learning Clojure, I ran across this pattern often enough that many of my early projects have an freduce defined in core:

(defn freduce
   "Given an initial input and a collection of functions (f1,..,fn),
   This is logically equivalent to ((comp fn ... f1) input)."
   [in fs]
   (reduce #(%2 %) in fs))

This is totally unnecessary, and some might prefer the direct use of reduce as being more clear. However, if you don't like staring at #(%2 %) in your application code, adding another utility word to your language is fine.

Post Status

Asked in February 2016
Viewed 2,023 times
Voted 14
Answered 2 times

Search




Leave an answer