A clj snippet #6

Today we look at another threading macro which is a natural extension to the ones shipped with clojure.core. condp-> works essentially just like cond-> except that the expression is also threaded through the test expressions. This is useful when you want to conditionally thread expressions but the conditions are dependent on the current value being threaded instead of something known upfront.

One way to implement condp-> is as follows:

(defmacro condp-> [x & forms]
  (loop [x x forms forms]
    (if (seq forms)
      (let [thread-first (fn [form] (if (seq? form)
                                      `(~(first form) ~x ~@(next form))
                                      (list form x)))
            threaded-condition (thread-first (first forms))
            threaded-form (thread-first (second forms))]
        (recur `(if ~threaded-condition ~threaded-form ~x) (drop 2 forms)))
      x)))
0.1s
Clojure
'user/condp->

One could have also used the standard -> thread first macro instead of writing an explicit thread-first function inside the macro. It follows a rather mundane example

(condp-> 100
   number? inc
   even? (* 2)
   odd? (- 100)
   pos? (- 1000))
0.1s
Clojure
-999
(clojure.pprint/pprint 
 (macroexpand 
  '(condp-> 100
     number? inc
     even? (* 2)
     odd? (- 100)
     pos? (- 1000))))
0.9s
Clojure
nil

If you want to use the condp-> there is a commons library providing condp-> as well as condp->>. Interestingly their implementation is just a little different, but makes use of the standard -> macro.

(defmacro condp-> [expr & clauses]
  (assert (even? (count clauses)))
  (let [g (gensym)
        pstep (fn [[pred step]] `(if (-> ~g ~pred) (-> ~g ~step) ~g))]
    `(let [~g ~expr
           ~@(interleave (repeat g) (map pstep (partition 2 clauses)))]
       ~g)))
0.1s
Clojure
'user/condp->

One interesting difference is that in the first version the expanded code grows exponentially with the number of test/form pairs. The later implementation remains linear with respect to test/form pairs. As the macro expansion happens before compile time this shouldn't make much of a difference. It would still be interesting if the former version has some performance degradation with respect to the later if the number of tests/forms exceeds some threshold.

(clojure.pprint/pprint 
 (macroexpand 
  '(condp-> 100
     number? inc
     even? (* 2)
     odd? (- 100)
     pos? (- 1000))))
0.5s
Clojure
nil
Runtimes (1)