Higher-order if-then-else in Clojure?

200 Views Asked by At

I often have to run my data through a function if the data fulfill certain criteria. Typically, both the function f and the criteria checker pred are parameterized to the data. For this reason, I find myself wishing for a higher-order if-then-else which knows neither f nor pred.

For example, assume I want to add 10 to all even integers in (range 5). Instead of

(map #(if (even? %) (+ % 10) %) (range 5))

I would prefer to have a helper –let's call it fork– and do this:

(map (fork even? #(+ % 10)) (range 5))

I could go ahead and implement fork as function. It would look like this:

(defn fork
  ([pred thenf elsef]
   #(if (pred %) (thenf %) (elsef %)))
  ([pred thenf]
   (fork pred thenf identity)))

Can this be done by elegantly combining core functions? Some nice chain of juxt / apply / some maybe?

Alternatively, do you know any Clojure library which implements the above (or similar)?

4

There are 4 best solutions below

1
amalloy On BEST ANSWER

As Alan Thompson mentions, cond-> is a fairly standard way of implicitly getting the "else" part to be "return the value unchanged" these days. It doesn't really address your hope of being higher-order, though. I have another reason to dislike cond->: I think (and argued when cond-> was being invented) that it's a mistake for it to thread through each matching test, instead of just the first. It makes it impossible to use cond-> as an analogue to cond.

If you agree with me, you might try flatland.useful.fn/fix, or one of the other tools in that family, which we wrote years before cond->1.

to-fix is exactly your fork, except that it can handle multiple clauses and accepts constants as well as functions (for example, maybe you want to add 10 to other even numbers but replace 0 with 20):

(map (to-fix zero? 20, even? #(+ % 10)) xs)

It's easy to replicate the behavior of cond-> using fix, but not the other way around, which is why I argue that fix was the better design choice.


1 Apparently we're just a couple weeks away from the 10-year anniversary of the final version of fix. How time flies.

0
Alan Thompson On

Depending on the details, it is often easiest to accomplish this goal using the cond-> macro and friends:

(let [myfn (fn [val] 
             (cond-> val
               (even? val) (+ val 10))) ]

with result

  (mapv myfn (range 5)) => [10 1 14 3 18]

There is a variant in the Tupelo library that is sometimes helpful:

(mapv #(cond-it-> %
         (even? it) (+ it 10))
  (range 5))

that allows you to use the special symbol it as you thread the value through multiple stages.


As the examples show, you have the option to define and name the transformer function (my favorite), or use the function literal syntax #(...)

1
Rulle On

I agree that it could be very useful to have some kind of higher-order functional construct for this but I am not aware of any such construct. It is true that you could implement a higher order fork function, but its usefulness would be quite limited and can easily be achieved using if or the cond-> macro, as suggested in the other answers.

What comes to mind, however, are transducers. You could fairly easily implement a forking transducer that can be composed with other transducers to build powerful and concise sequence processing algorithms.

The implementation could look like this:

(defn forking [pred true-transducer false-transducer]
  (fn [step]
    (let [true-step (true-transducer step)
          false-step (false-transducer step)]
      (fn
        ([] (step))
        ([dst x] ((if (pred x) true-step false-step) dst x))
        ([dst] dst))))) ;; flushing not performed.

And this is how you would use it in your example:

(eduction (forking even?
                   (map #(+ 10 %))
                   identity)

          (range 20))
;; => (10 1 12 3 14 5 16 7 18 9 20 11 22 13 24 15 26 17 28 19)

But it can also be composed with other transducers to build more complex sequence processing algorithms:

(into []

      (comp (forking even?
                     (comp (drop 4)
                           (map #(+ 10 %)))
                     (comp (filter #(< 10 %))
                           (map #(vector % % %))
                           cat))
            (partition-all 3))
      
      (range 20))
;; => [[18 20 11] [11 11 22] [13 13 13] [24 15 15] [15 26 17] [17 17 28] [19 19 19]]
0
peter pun On

Another way to define fork (with three inputs) could be:

(defn fork [pred then else]
  (comp
    (partial apply apply)
    (juxt (comp {true then, false else} pred) list)))

Notice that in this version the inputs and output can receive zero or more arguments. But let's take a more structured approach, defining some other useful combinators. Let's start by defining pick which corresponds to the categorical coproduct (sum) of morphisms:

(defn pick [actions]
  (fn [[tag val]]
    ((actions tag) val)))

;alternatively
(defn pick [actions]
  (comp
    (partial apply apply)
    (juxt (comp actions first) rest)))

E.g. (mapv (pick [inc dec]) [[0 1] [1 1]]) gives [2 0]. Using pick we can define switch which works like case:

(defn switch [test actions]
  (comp
    (pick actions)
    (juxt test identity)))

E.g. (mapv (switch #(mod % 3) [inc dec -]) [3 4 5]) gives [4 3 -5]. Using switch we can easily define fork:

(defn fork [pred then else]
  (switch pred {true then, false else}))

E.g. (mapv (fork even? inc dec) [0 1]) gives [1 0]. Finally, using fork let's also define fork* which receives zero or more predicate and action pairs and works like cond:

(defn fork* [& args]
  (->> args
       (partition 2)
       reverse
       (reduce
         (fn [else [pred then]]
           (fork pred then else))
         identity)))

;equivalently
(defn fork* [& args]
  (->> args
       (partition 2)
       (map (partial apply (partial partial fork)))
       (apply comp)
       (#(% identity))))

E.g. (mapv (fork* neg? -, even? inc) [-1 0 1]) gives [1 1 1].