Search code examples
clojure

cond-> with multiple values


I encounter quite a few situations where a vector of two (or even three) values would need to be "updated" if a certain condition is met, and otherwise left alone. Example:

(let [val1 some-value
      val2 some-other-value
      [val1, val2] (if something-true 
                       (first-calculation val1 val2 some-other-arg)
                       [val1, val2])
      [val1, val2] (if something-else-true 
                       (second-calculation some-other-arg val1 val2)
                       [val1, val2])
      ...etc...)

where the assumption is that first-calculation and second-calculation return a vector [val1, val2] with possibly updated values.

This code style is not only clunky, but probably also has some unnecessary overhead due to the vector creation and destructuring each time.

Does anybody have a suggestion on how to improve this code with vanilla Clojure, without creating a macro? In other words, I'm looking for a kind of cond-> for multiple values.


Solution

  • For this we can copy a trick often seen in graphics processing and other use cases where functions always accept a context map as the first arg (or, in our case, a context vector). Try rewriting it like the following. Note the change in args to second-calculation:

    (defn first-calculation
      [ctx               ; first arg is the context (vec here, usually a map)
       some-other-arg]
      (let [[val1 val2] ctx]  ; destructure the context into locals
           ...
        [val1-new val2-new] ))   ; return new context
    
    (defn second-calculation
      [ctx               ; first arg is the context (vec here, usually a map)
       some-other-arg]
      (let [[val1 val2] ctx]  ; destructure the context into locals
        ...
        [val1-new val2-new] ))   ; return new context
    
    (let [ctx [some-value some-other-value]
       (cond-> ctx
          something-true       (first-calculation  some-other-arg)
          something-else-true  (second-calculation some-other-arg)
          ...etc... ))
    

    Here is a more concrete example:

    (defn inc-foo [ctx amount]
      (let [{:keys [foo bar]} ctx
            foo-new (+ foo amount)
            ctx-new (assoc ctx :foo foo-new)]
           ctx-new ))
    
    (defn inc-bar [ctx amount]
      (let [{:keys [foo bar]} ctx
            bar-new (+ bar amount)
            ctx-new (assoc ctx :bar bar-new)]
         ctx-new ))
    
    (dotest
      (loop [i   0
             ctx {:foo 0 :bar 0}]
        (let [{:keys [foo bar]} ctx
              >>      (println (format "i =%2d   foo =%3d   bar =%3d   " i foo bar))
              ctx-new (cond-> ctx
                        (zero? (mod i 2)) (inc-foo i)
                        (zero? (mod i 3)) (inc-bar i))]
             (if (< 9 i)
               ctx-new
               (recur (inc i) ctx-new)))))
    

    with result:

    i = 0   foo =  0   bar =  0   
    i = 1   foo =  0   bar =  0   
    i = 2   foo =  0   bar =  0   
    i = 3   foo =  2   bar =  0   
    i = 4   foo =  2   bar =  3   
    i = 5   foo =  6   bar =  3   
    i = 6   foo =  6   bar =  3   
    i = 7   foo = 12   bar =  9   
    i = 8   foo = 12   bar =  9   
    i = 9   foo = 20   bar =  9   
    i =10   foo = 20   bar = 18   
    

    You could probably write a macro like (with-context [foo bar] ... to remove some of the boilerplate if you used this a lot.