Clojure - Lazy Seqs
Lazy Seqs
Many functions that work on seqs return lazy seqs. Members of a lazy seq aren’t computed until they are needed. This has a benefit of making a program more efficient and allowing programmers to construct infinite sequences.
Efficiency
1(def database
2 {0 {:name "Bill" :age 40 :has-job? true}
3 1 {:name "Anna" :age 20 :has-job? false}
4 2 {:name "Dave" :age 17 :has-job? true}
5 3 {:name "Joe" :age 29 :has-job? false}})
6
7(defn get-details
8 [social-security-number]
9 (Thread/sleep 1000)
10 (get database social-security-number))
11
12(defn can-hire?
13 [record]
14 (and (not (:has-job? record))
15 (> (:age record) 18)
16 record))
17
18(defn identify-potential-hire
19 [social-security-numbers]
20 (filter can-hire?
21 (map get-details social-security-numbers)))
22
23(time (get-details 0))
24; => "Elapsed time: 1001.051 msecs"
25; => {:name "Anna", :age 20, :has-job? false}
Now if map wasn’t lazy it would need to process every member of social-security-numbers before passing
it to filter. If we had one million entries in our database this would take one milion seconds, or 12 days.
But luckily map is lazy so it doesn’t need to do that.
1(time (def mapped-details (map get-details (range 0 1000000))))
2; => "Elapsed time: 0.037 msecs"
3; => #'user/mapped-details'
1(time (first mapped-details))
2; => "Elapsed time: 32020.812 msecs"
3; => {:name "Anna", :age 20, :has-job? false}
You would think that getting one entry from our lazy seq would take 1 second but it actually took 32 seconds. This is because Clojure chunks lazy seqs meaning Clojure will prepare the next 31 elements when getting one.
Infinite sequences
We can use repeat to create a sequence whose every member is an argument you pass.
1(take 5 (repeat "x"))
2; => ("x" "x" "x" "x" "x")
We can use repeatedly to create a sequence which will call the provided function to generate
each element in the sequence.
1(take 3 (repeatedly (fn [] (rand-int 10))))
2; => (7 1 0)