Sequences and Laziness
Prerequisites: map, filter, and reduce
Almost every collection function in Clojure works through one
abstraction: the sequence (seq). A seq is anything that can give you
its first item and the rest. Vectors, lists, maps, sets, strings,
Java collections, and files read line by line can all be viewed as seqs.
(println (seq [1 2 3]) (seq {:a 1}) (seq "hi") (seq #{:x}))
(println (first {:a 1 :b 2}) (rest "abc"))
(println (map inc (java.util.ArrayList. [1 2 3])))
(1 2 3) ([:a 1]) (h i) (:x)
[:a 1] (b c)
(2 3 4)
That's why map, filter, and reduce work on everything: they ask
for a seq, and every collection can produce one.
Lazy sequences
Most sequence functions are lazy: they describe a computation and only do the work when items are actually needed.
(println (take 5 (range))) ; (range) is infinite
(println (take 5 (iterate #(* 2 %) 1))) ; 1, 2, 4, 8, ...
(println (take 4 (cycle [:red :green])))
(println (take 3 (repeat "x")))
(println (take 5 (filter even? (map #(* % %) (range)))))
(println (first (drop 1000000 (range))))
(0 1 2 3 4)
(1 2 4 8 16)
(:red :green :red :green)
(x x x)
(0 4 16 36 64)
1000000
Infinite sequences are fine as long as you only ask for a finite part. Laziness lets you write "all the squares, filtered" and then take what you need.
Laziness and side effects
(def results (map (fn [x] (println "processing" x) (* x 10)) '(1 2 3)))
(println "nothing processed yet")
(println (first results))
(println "realized so far:" (realized? results))
(println (doall results))
nothing processed yet
processing 1
10
realized so far: true
processing 2
processing 3
(10 20 30)
Nothing ran until first asked for an item. That surprises people when
a lazy map with side effects (printing, saving to a database) seems to
"do nothing". Rules of thumb:
- Use
doseqorrun!for side effects — they're eager. - Force a lazy seq with
doall(keep the result) ordorun(discard it). - Use
mapv,filterv, orinto []when you want an eager vector.
Chunking
(def xs (map (fn [x] (print x "") x) (range 100)))
(println "first:" (first xs))
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 first: 0
Asking for one item computed 32. Sequences over vectors and ranges are realized in chunks of 32 for speed. Never depend on exactly how much of a lazy seq gets computed.
Building your own lazy sequence
(defn fibs
([] (fibs 0N 1N))
([a b] (lazy-seq (cons a (fibs b (+ a b))))))
(println (take 10 (fibs)))
(println (nth (fibs) 100))
(0N 1N 1N 2N 3N 5N 8N 13N 21N 34N)
354224848179261915075N
lazy-seq wraps code that produces the rest of the sequence only when
it's needed, so fibs describes an infinite sequence safely.
Practice
Print the first 5 numbers greater than 1000 that are divisible by both 7 and 11.
Show solution
(println (->> (range 1001 Long/MAX_VALUE)
(filter #(zero? (mod % 77)))
(take 5)))
(1001 1078 1155 1232 1309)