Search code examples
clojuremacrospromisemutation

"Joy of Clojure" 2 edition. Listing 11.9 about promises


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


Solution

  • 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.