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