Language: Laziness
This guide covers:
- What are lazy sequences
- Pitfalls with lazy sequences
- How to create functions that produce lazy sequences
- How to force evaluation
This work is licensed under a Creative Commons Attribution 3.0 Unported License (including images & stylesheets). The source is available on Github.
What Version of Clojure Does This Guide Cover?
This guide covers Clojure 1.11.
Overview
Clojure is not a lazy language.
However, Clojure supports lazily evaluated sequences. This means that sequence elements are not available ahead of time and are produced as the result of a computation. The computation is performed as needed. Evaluation of lazy sequences is known as realization.
Lazy sequences can be infinite (e.g., the sequence of Fibonacci numbers, a sequence of dates with a particular interval between them, and so on). If a lazy sequence is finite, when its computation is completed, it becomes fully realized.
When it is necessary to fully realize a lazy sequence, Clojure provides a way to force evaluation (force realization).
Benefits of Lazy Sequences
Lazy sequences have two main benefits:
- They can be infinite
- Full realization of interim results can be avoided
Producing Lazy Sequences
Lazy sequences are produced by functions. Such functions either use the clojure.core/lazy-seq
macro
or other functions that produce lazy sequences.
clojure.core/lazy-seq
accepts one or more forms that produce a sequence, or nil
when the sequence
is fully realized, and returns a seqable data structure that evaluates the body the first time
the value is needed and then caches the result.
For example, the following function produces a lazy sequence of random UUIDs strings:
(defn uuid-seq
[]
(lazy-seq
(cons (str (random-uuid))
(uuid-seq))))
Note: the
random-uuid
function was added to Clojure in version 1.11 but was previously available in ClojureScript.
Another example:
(defn fib-seq
"Returns a lazy sequence of Fibonacci numbers"
([]
(fib-seq 0 1))
([a b]
(lazy-seq
(cons b (fib-seq b (+ a b))))))
Both examples use clojure.core/cons
which prepends an element to a sequence. The sequence
can in turn be lazy, which both of the examples rely on.
Even though both of these sequences are infinite, taking first N elements from each does return successfully:
(take 3 (uuid-seq))
(take 10 (fib-seq))
(take 20 (fib-seq))
Realizing Lazy Sequences (Forcing Evaluation)
Lazy sequences can be forcefully realized with clojure.core/dorun
and
clojure.core/doall
. The difference between the two is that dorun
throws away all results and is supposed to be used for side effects,
while doall
returns computed values:
(dorun (map inc [1 2 3 4]))
(doall (map inc [1 2 3 4]))
Commonly Used Functions That Produce Lazy Sequences
Multiple frequently used clojure.core
functions return lazy sequences,
most notably:
map
filter
remove
range
take
take-while
drop
drop-while
The following example uses several of these functions to return 10 first even numbers in the range of [0, n):
(take 10 (filter even? (range 0 100)))
Several functions in clojure.core
are designed to produce lazy
sequences:
repeat
iterate
cycle
For example:
(take 3 (repeat "ha"))
(take 5 (repeat "ha"))
(take 3 (cycle [1 2 3 4 5]))
(take 10 (cycle [1 2 3 4 5]))
(take 3 (iterate inc 1))
(take 5 (iterate inc 1))
Lazy Sequences Chunking
There are two fundamental strategies for implementing lazy sequences:
- Realize elements one-by-one
- Realize elements in groups (chunks, batches)
In Clojure, most lazy sequences are chunked (realized in chunks).
For example, in the following code
(take 10 (map inc (range)))
one-by-one realization would realize one element 10 times. With chunked sequences, elements are realized ahead of time in chunks (32 elements at a time).
You can see this in action:
(take 10 (map #(do (println %) (inc %)) (range 100)))
What you'll see here is that even tho' the result is a sequence of 10 elements, 32 elements are printed. This is because realizing the first element causes the whole of the first chunk to be realized.
This typically reduces the number of realizations and, for many common workloads, improves efficiency of lazy sequences.
Clojure on the JVM has optimized some functions, such as range
, to produce
individual elements efficiently without chunking:
user=> (take 10 (map #(do (println %) (inc %)) (range)))
(0
1
1 2
2 3
3 4
4 5
5 6
6 7
7 8
8 9
9 10)
user=>
What you'll see here is that only 10 elements are printed, not 32, and they are printed one-by-one, as each element of the sequence is realized.
If you used (range 100)
instead of (range)
in the above example, you'd see
32 elements printed, as before.
Contributors
Michael Klishin michael@defprotocol.org, 2013 (original author)