Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

cond-> with multiple values

Tags:

clojure

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.

like image 578
Maarten Truyens Avatar asked Apr 08 '26 02:04

Maarten Truyens


1 Answers

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.

like image 67
Alan Thompson Avatar answered Apr 16 '26 17:04

Alan Thompson



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!