Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Clojure/FP: apply functions to each argument to an operator

Let's say I have several vectors

(def coll-a [{:name "foo"} ...])
(def coll-b [{:name "foo"} ...])
(def coll-c [{:name "foo"} ...])

and that I would like to see if the names of the first elements are equal.

I could

(= (:name (first coll-a)) (:name (first coll-b)) (:name (first coll-c)))

but this quickly gets tiring and overly verbose as more functions are composed. (Maybe I want to compare the last letter of the first element's name?)

To directly express the essence of the computation it seems intuitive to

(apply = (map (comp :name first) [coll-a coll-b coll-c]))

but it leaves me wondering if there's a higher level abstraction for this sort of thing.

I often find myself comparing / otherwise operating on things which are to be computed via a single composition applied to multiple elements, but the map syntax looks a little off to me.

If I were to home brew some sort of operator, I would want syntax like

(-op- (= :name first) coll-a coll-b coll-c)

because the majority of the computation is expressed in (= :name first).

I'd like an abstraction to apply to both the operator & the functions applied to each argument. That is, it should be just as easy to sum as compare.

(def coll-a [{:name "foo" :age 43}])
(def coll-b [{:name "foo" :age 35}])
(def coll-c [{:name "foo" :age 28}])

(-op- (+ :age first) coll-a coll-b coll-c)
; => 106
(-op- (= :name first) coll-a coll-b coll-c)
; => true

Something like

(defmacro -op- 
  [[op & to-comp] & args]
  (let [args' (map (fn [a] `((comp ~@to-comp) ~a)) args)]
    `(~op ~@args')))
  • Is there an idiomatic way to do this in clojure, some standard library function I could be using?
  • Is there a name for this type of expression?
like image 310
John Dorian Avatar asked Dec 23 '22 06:12

John Dorian


2 Answers

For your addition example, I often use transduce:

(transduce
  (map (comp :age first))
  +
  [coll-a coll-b coll-c])

Your equality use case is trickier, but you could create a custom reducing function to maintain a similar pattern. Here's one such function:

(defn all? [f]
  (let [prev (volatile! ::no-value)]
    (fn
      ([] true)
      ([result] result)
      ([result item]
       (if (or (= ::no-value @prev)
               (f @prev item))
         (do
           (vreset! prev item)
           true)
         (reduced false))))))

Then use it as

(transduce
  (map (comp :name first))
  (all? =)
  [coll-a coll-b coll-c])

The semantics are fairly similar to your -op- macro, while being both more idiomatic Clojure and more extensible. Other Clojure developers will immediately understand your usage of transduce. They may have to investigate the custom reducing function, but such functions are common enough in Clojure that readers can see how it fits an existing pattern. Also, it should be fairly transparent how to create new reducing functions for use cases where a simple map-and-apply wouldn't work. The transducing function can also be composed with other transformations such as filter and mapcat, for cases when you have a more complex initial data structure.

like image 153
exupero Avatar answered Dec 25 '22 23:12

exupero


You may be looking for the every? function, but I would enhance clarity by breaking it down and naming the sub-elements:

  (let [colls           [coll-a coll-b coll-c]
        first-name      (fn [coll] (:name (first coll)))
        names           (map first-name colls)
        tgt-name        (first-name coll-a)
        all-names-equal (every? #(= tgt-name %) names)]

all-names-equal => true

I would avoid the DSL, as there is no need and it makes it much harder for others to read (since they don't know the DSL). Keep it simple:

  (let [colls  [coll-a coll-b coll-c]
        vals   (map #(:age (first %)) colls)
        result (apply + vals)]

result => 106
like image 32
Alan Thompson Avatar answered Dec 26 '22 00:12

Alan Thompson