I'm examining the Listing 11.9 of the named book (p.269 of pdf).
Could anyone explain me how tests
value is being set (line [tests all-tests :as results]
)?
thanks
To set the context of the question for people without The Joy of Clojure (a book I do enjoy btw), the macro in question is:
(defmacro with-promises [[n tasks _ as] & body]
(when as
`(let [tasks# ~tasks
n# (count tasks#)
promises# (take n# (repeatedly promise))]
(dotimes [i# n#]
(dothreads!
(fn []
(deliver (nth promises# i#)
((nth tasks# i#))))))
(let [~n tasks#
~as promises#]
~@body))))
And is used thusly:
(defn run-tests [& all-tests]
(with-promises
[tests all-tests :as results]
(into (TestRun. 0 0 0)
(reduce #(merge-with + %1 %2) {}
(for [r results]
(if @r
{:run 1 :passed 1}
{:run 1 :failed 1}))))))
and the final call to run-tests is like:
(run-tests pass fail fail fail pass)
=> #user.TestRun{:run 5, :passed 2, :failed 3}
Ultimately, the latter part of the macro is doing a let assignment and running the body, so you end up with
(let [tests tasks#
results promises#]
(into (TestRun. 0 0 0)
;; rest of body
In the macro, ~n is unquoting the starting back-tick around the '(let
so you can just read it as n
which is the first parameter to the macro (well, the first parameter of the vector that is the first parameter to the macro).
This all happens after the macro has setup the promises using the custom dothreads! function that uses a thread pool - non of which is important to understand the macro.
You can determine more about the macro by wrapping it in a (pprint (macroexpand-1 '(with-promises ...
which generates something like (I've replaced the auto generated names with something simpler, v1, n1, p1 and i1):
(clojure.core/let
[v1 all-tests
n1 (clojure.core/count v1)
p1 (clojure.core/take n1 (clojure.core/repeatedly clojure.core/promise))]
(clojure.core/dotimes
[i1 n1]
(user/dothreads!
(clojure.core/fn
[]
(clojure.core/deliver
(clojure.core/nth p1 i1)
((clojure.core/nth v1 i1))))))
(clojure.core/let
[tests v1
results p1]
(into
(TestRun. 0 0 0)
;; ... rest of main body
which clearly shows the parameters passed in are used as variables in the final let bindings.
However, in this example usage (i.e. run-tests
function), the tests
variable isn't actually used in the body of the with-promises call, only results
is, so you're right to question it, it simply isn't needed.
Looking at the macro definition, there may be further optimizations for this case, as the tasks# binding doesn't seem to give anything extra than wrapping tasks
. At first I wondered if this was about immutability in the dothreads! call, or macro-niceness for providing a closure around the usage, rather than directly using the parameter to the macro.
I tried changing the macro to remove tasks#
completely and directly use ~tasks
, which seems to still work, and as "tests" isn't a required binding variable in the body of run-tests, you can drop both the n
parameter from the macro, and the ~n tasks#
part of the final let binding without issue.
Actually after reading it several times it finally dawned on me it's to make the whole vector read like a standard destructuring binding.
EDIT: some more explanation on "tests".
This is just a name, it could be "foo-tests", "foo-bar", because ultimately it's used to define something in a let binding.
Had the run-tests body been something like:
(defn run-tests [& all-tests]
(with-promises
[foo all-tests :as results]
(println "foo was set to" foo)
(into (TestRun. 0 0 0)
;; rest of body
you can see how foo (and results) are just used to ultimately define variables (eck - you know what i mean) that can be used in the body part of the call to the macro. The body being everything after the initial vector [foo all-tests :as results]
but in the original code, tests
is declared but unused.