Skip to content
AgileLogics
7 min read

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 doseq or run! for side effects — they're eager.
  • Force a lazy seq with doall (keep the result) or dorun (discard it).
  • Use mapv, filterv, or into [] 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)