Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Iterate over clojure map pairs (loop)

I have a sequence of map-pairs like this (currently about 17000 pairs)

(def myseq '({:name "Peter" :rank 2222} {:name "Anna" :rank 111}))

I want to filter specific pairs into a new sequence with

(filter (fn [x] (> x 222)) (:rank (first myseq)))

I have been trying to iterate with loop like this but keep getting thread-death. Also if I use filter on a a single map collection it just returns a new sequence, not sure if I need to create one myself here?

(defn remove-lower [number myseq]
    (loop [i 0]
        (if (= i (count file))
            (println "done")
            (filter [x] (> x number))
                (:rank (first myseq))))
    (recur (rest myseq))))

Finally is looping the most efficient way to get the new sequence of pairs?

like image 799
C.A Avatar asked Jul 24 '12 07:07

C.A


2 Answers

No need for loop/recur here. filter already iterates through a seq for you:

(filter (fn [entry] (> (:rank entry) 220)) myseq)
like image 60
Joost Diepenmaat Avatar answered Oct 25 '22 15:10

Joost Diepenmaat


First thing to know is that (most of) the data structures in clojure are immutable and most functions are, well, functional. That means, that they don't have side effects. In your case filter doesn't change the sequence in any way, it returns a new one, containing only the unfiltered items.

So, to filter myseq you need to do something like:

(def filtered-seq (filter (fn [x] ...) myseq))

Filter will call the function repeatedly, binding xto the currently filtered item in myseq. That is, the first time it'll be bound to {:name "Peter" :rank 2222}, then to {:name "Anna" :rank 111}. The filtered-seq will contain only the elements, for which the function returned true. myseq will not be modified!

So, you want to leave only elements with :rank higher than 222:

(filter (fn [x] (> (:rank x) 222)) myseq)

That's it. And one more thing about filter is, that it's lazy. That is, items in the returned collection are "realized" (or computed) only when they are needed.

You don't need to use loop for this, as filter does the job nicely, and loop isn't lazy.

That said, your loop doesn't work because it has several problems:

  1. recur is outside the loop. In this case clojure will loop back to the beginning of the function.
  2. you need to construct a return value and you need to maintain the "current" element
  3. you need to properly check for the end condition

The code could look something like this (untested):

(defn remove-lower [number myseq]
  (loop [sq myseq res []]
     (if (empty? sq)
         res
         (let [current (first sq)]
           (if (> (:rank current) number)
              (recur (rest sq) (conj res current))
              (recur (rest sq) res))))))

Note how:

  1. recur is now inside the loop
  2. res contains the return value and sq contains the currently left sequence
  3. each recur passes the new values of sq and res for the next iteration
  4. sq is "shrinked" with each iteration, so the loop will eventually exit unless myseq is infinite. Contrast this to filter, which handles infinite sequences just fine.

As you see this is harder to read and less general than filter and also is eager (not lazy).

like image 23
ivant Avatar answered Oct 25 '22 15:10

ivant