murtaza52
murtaza52

Reputation: 47441

idiomatic way to only update the first elem matching a pred in a coll

I have a seq, (def coll '([:a 20] [:b 30] [:c 50] [:d 90]))

I want to iterate through the seq, and modify only the first element that matches a predicate.

The predicate (def pred (fn [[a b]] (> b 30)))

(f pred (fn [[a b]] [a (+ b 2)]) coll) => ([:a 20] [:b 30] [:c 52] [:d 90])

f is the fn I want, which takes a pred, and a fn to apply to the first elem which matches the pred. All the rest of the elems are not modified and returned in the seq.

What is the idiomatic way to do the above?

Upvotes: 6

Views: 269

Answers (4)

sloth
sloth

Reputation: 101142

One possible way is to split the collection with split-with, apply the function f to the first element of the second collection returned by split-with, and concat the elements together again.

(defn apply-to-first [pred f coll]
    (let [[h t] (split-with (complement pred) coll)]
        (concat h (list (f (first t))) (rest t))))

Note that the pred function in your example should probably look like this:

(def pred #(> (second %) 30))

Upvotes: 5

amalloy
amalloy

Reputation: 92117

This function is not hard to write recursively "from scratch". Not only is this a good learning exercise, it also produces the best solution: it is as lazy as possible, and does the absolute minimum amount of computation. So far, only one answer to this question is lazy, and that one calls pred twice on all the items before the update occurs: once in the take-while, and once in the drop-while, parts of split-with.

(defn update-first [pred f coll]
  (lazy-seq
   (when-let [coll (seq coll)]
     (if (pred (first coll))
       (cons (f (first coll))
             (rest coll))
       (cons (first coll)
             (update-first pred f (rest coll)))))))

Upvotes: 4

Grzegorz Luczywo
Grzegorz Luczywo

Reputation: 10502

To keep it straightforward: find first element, find its index and use assoc to "update" the element at index:

(let [e (first (filter pred coll))
      ind (.indexOf coll e)] 
  (assoc (vec coll) ind ((fn [[a b]] [a (+ b 2)]) e) ))

Dominic's note about pred applies:

(def pred #(> (second %) 30))

Upvotes: 0

leonardoborges
leonardoborges

Reputation: 5629

As with most problems, there is a number of ways to solve it. This is but one of them.

If you're running Clojure 1.5, give this a try:

(reduce
 (fn [acc [a b]]
   (if (pred b)
     (reduced (concat (:res acc) [[a (+ b 2)]] (rest (:coll acc))))
     (assoc acc
       :res (conj (:res acc) [a b])
       :coll (rest (:coll acc)))))
 {:coll coll :res []}
 coll)

;; ([:a 20] [:b 30] [:c 52] [:d 90])

The key in this algorithm is the use of the reduced (note the 'd') function - it essentially tells reduce to halt the iteration and return the result. From its doc string:

-------------------------
clojure.core/reduced
([x])
  Wraps x in a way such that a reduce will terminate with the value x

The code is a bit terse, but it should give you the basic idea.

Hope this helps.

Upvotes: 4

Related Questions