4

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?
2
  • 1
    Writing a macro is often the idiomatic way, providing it isn't otherwise possible with a standard function. If you want a syntax like this, you're pretty much forced to use a macro, and this is basically creating a DSL, which isn't necessarily bad. Commented Jun 26, 2018 at 4:51
  • 1
    No, there is absolutely no need for a macro here, unless you are hell bent on creating a DSL when you don't really need one. Commented Jun 26, 2018 at 6:43

4 Answers 4

7

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.

Sign up to request clarification or add additional context in comments.

2 Comments

I was just preparing similar answer but you beat me to it (=,<,> with transcduce can be really tricky, indeed). Let me just add fn -op-tr-, which replaces OP's -op- macro: (defn -op-tr- [[op & to-comp] & args] (transduce (apply comp (map #(map %) (reverse to-comp))) op args)) (-op-tr- [+ :age first] coll-a coll-b coll-c) (-op-tr- [(all? =) :name first] coll-a coll-b coll-c)
Cool! I wouldn't have thought to apply transduce here. Treating the arguments as a sequence seems pleasingly lisp-ey to me and it abstracts at the right levels. Thanks!
2

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 

1 Comment

Thanks for the suggestion! I think breaking up the parts does make it more readable, but I'm really looking for an abstraction over both i) the transformation for each arg (comp :name first) ii) the operator =. This solution works for equality testing but doesn't abstract a different operator that takes multiple things and returns a single thing. (I.e. = has the shape [& args] => boolean) Updated the question to clarify.
1

I don't think you need a macro, you just need to parameterize your op function and compare functions. To me, you are pretty close with your (apply = (map (comp :name first) [coll-a coll-b coll-c])) version.

Here is one way you could make it more generic:

(defn compare-in [op to-compare & args] (apply op (map #(get-in % to-compare) args))) (compare-in + [0 :age] coll-a coll-b coll-c) (compare-in = [0 :name] coll-a coll-b coll-c) ;; compares last element of "foo" (compare-in = [0 :name 2] coll-a coll-b coll-c) 

I actually did not know you can use get on strings, but in the third case you can see we compare the last element of each foo.

This approach doesn't allow the to-compare arguments to be arbitrary functions, but it seems like your use case mainly deals with digging out what elements you want to compare, and then applying an arbitrary function to those values.

I'm not sure this approach is better than the transducer version supplied above (certainly not as efficient), but I think it provides a simpler alternative when that efficiency is not needed.

1 Comment

I think you hit the nail on the head describing my use case. I like this as a better alternative to my macro!
0

I would split this process into three stages:

  1. transform items in collections into the data in collections you want to operate on - (map :name coll);
  2. Operate on transformed items in collections, returning collection of results - (map = transf-coll-a transf-coll-b transf-coll-c)
  3. Finally, selecting which result in resulting collection to return - (first calculated-coll)

When playing with collections, I try to put more than one item into collection:

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

For example, matching items by second char in :name and returning result for items in second place:

(let [colls [coll-a coll-b coll-c] transf-fn (comp #(nth % 1) :name) op = fetch second] (fetch (apply map op (map #(map transf-fn %) colls)))) ;; => false 

In transducers world you can use sequence function which also works on multiple collections:

(let [colls [coll-a coll-b coll-c] transf-fn (comp (map :name) (map #(nth % 1))) op = fetch second] (fetch (apply sequence (map op) (map #(sequence transf-fn %) colls)))) 

Calculate sum of ages (for all items at the same level):

(let [colls [coll-a coll-b coll-c] transf-fn (comp (map :age)) op + fetch identity] (fetch (apply sequence (map op) (map #(sequence transf-fn %) colls)))) ;; => (106 112) 

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.