What is the concept of lazy evaluation in Clojure?

Key takeaways:

  • Lazy evaluation in programming improves efficiency by delaying computation until necessary which reduce memory usage and processing waste.

  • Clojure uses lazy sequences for lazy evaluation which enable all collections to be transformed into sequences that only compute values as needed.

  • Lazy sequences can be finite or infinite, with finite sequences becoming fully realized after computation is complete, while infinite sequences continue to generate values on demand.

  • Advantages of lazy sequences include their ability to extend infinitely, avoid unnecessary interim results, and compatibility with various collection types.

  • The lazy-seq function allows developers to create lazy sequences by generating elements only when required or converting existing collections.

  • Lazy sequences enhance resource conservation and efficiency, making Clojure an effective choice for developers focusing on performance in their applications.

Lazy evaluation in programming languages is aimed at making them more efficient. The evaluation process in many programming languages is automatically triggered when a compiler receives code. The compiler performs the evaluation process to generate a result, even though the chunk of code may not be used anywhere in the program. This can consume a lot of memory and waste processing resources. Some programming languages offer lazy evaluation to try to overcome the wastage of evaluating unused code by keeping the code without evaluating it unless it is necessary.

How lazy evaluation works in Clojure

Although Clojure is not strictly a lazy language, it provides lazy sequencesLazy sequences are collections in Clojure that delay computation until their elements are actually needed, allowing for efficient memory usage and the ability to represent infinite sequences. that enable lazy evaluation, and these are widely used. In Clojure, lazy evaluation is implemented through lazy sequences, allowing computations to be deferred until the values are actually needed.

All collections in Clojure can become a sequence when converted with the core function lazy-seq, enabling them to exhibit this behavior. These sequences are computed only when needed, deferring computation until an element is accessed. This allows Clojure to work efficiently with infinite sequences, as only the required portion of the sequence is evaluated. For example, we can generate an infinite sequence of numbers, but Clojure will only compute as many values as we request, such as the first 10 elements. This approach helps optimize both performance and memory usage.

Advantages of lazy sequences

Lazy sequences offer several advantages:

  1. They can extend infinitely. For example, (take 5 (range)) returns the first five numbers of an infinite sequence without calculating beyond those elements.

  2. They enable the avoidance of fully realizing interim results. For instance, (map inc (range 1 1000000)) will not create a list of a million elements but will increment each number only as it is accessed.

  3. They are compatible with a wide range of collection types. For example, functions like map, filter, and reduce work seamlessly on lazy sequences generated from lists, vectors, and sets.

  4. There are many core functions designed to implement lazy sequences. Functions like lazy-seq, map, and filter inherently produce lazy sequences, enabling on-demand evaluation.

Producing lazy sequences in Clojure

We can use the lazy-seq function to create lazy sequences, and we can do this using two methods.

Generating lazy elements

One way to generate lazy sequences is by writing a function that generates elements lazily, meaning it only computes them when needed.

Code

Let’s have a look at the code to understand lazy sequences:

(defn lazy-fibonacci [limit]
(letfn [(fib [a b]
(lazy-seq
(when (< a limit)
(println "[" a "was REALIZED]")
(cons a (fib b (+ a b))))))]
(fib 0 1)))
(def first-ten-fib (lazy-fibonacci 10))
(println "take 5 Fibonacci numbers:" (take 5 first-ten-fib))
(println "the whole first-ten-fibonacci:" first-ten-fib)

Explanation

  • Lines 1–7: We define a function lazy-fibonacci that generates Fibonacci numbers lazily up to a specified limit. Inside the function, we define a local recursive function fib to generate Fibonacci numbers. We have initialized the Fibonacci sequence with initial values 0 and 1.

  • Line 9: We have created a lazy sequence of Fibonacci numbers up to a limit of 10 and store it in the function name first-ten-fib.

  • Line 11: We then print the first 5 Fibonacci numbers by taking them from first-ten-fib.

  • Line 13: We then print the entire lazy sequence first-ten-fib.

Output

In this code, lazy evaluation generates Fibonacci numbers only when needed. The call (take 5 first-ten-fib) computes and prints the first five numbers with [x was REALIZED], showing they’re calculated on demand. The when condition in the fib function checks whether the current Fibonacci number (a) is less than the limit (10 in this case). Once a reaches or exceeds the limit, the recursion stops, and no further numbers are generated. Printing first-ten-fib in full triggers the realization of all Fibonacci numbers up to the limit of 10. This approach conserves resources by calculating elements only as required.

Converting existing collections

Another way to produce lazy sequences is by converting an existing collection into a lazy sequence. We can use lazy-seq along with existing collections to avoid precomputing all elements and conserving resources.

Code

Here’s an example that uses range to create an infinite sequence of natural numbers and then convert it to a lazy sequence.

(def lazy-naturals
(lazy-seq (range)))
(println "Take the first 10 natural numbers:" (take 10 lazy-naturals))
(println "Take another 10 numbers starting after 10:" (take 10 (drop 10 lazy-naturals)))

Explanation

  • Line 2: The range function creates an infinite sequence of natural numbers, which could potentially be resource-intensive. Here, we wrap it in lazy-seq to ensure that the numbers are only generated when needed.

  • Line 4: Here, we retrieve and print the first 10 natural numbers from the lazy sequence, giving 0 through 9.

  • Line 6: This line takes the next 10 natural numbers starting after the first 10, effectively producing numbers 10 through 19.

Output

In this code, lazy evaluation generates natural numbers only as needed. (lazy-seq (range)) creates an infinite sequence without computing it all at once. The first call, (take 10 lazy-naturals), generates and prints only the first 10 numbers (0 to 9), while the second call, (take 10 (drop 10 lazy-naturals)), skips the first 10 and generates the next 10 (10 to 19). This approach efficiently computes only the required elements each time.

Take your skills further with the “Becoming a Functional Programmer with Clojure” course. Master core concepts, improve your coding skills, and earn a certificate of completion. Start your journey today!

Conclusion

In Clojure, lazy evaluation and lazy sequences play a vital role in enhancing the efficiency and performance of programs. By deferring the computation of values until they are required, developers can optimize resource usage and avoid unnecessary processing, particularly when dealing with large or potentially infinite data sets. Lazy sequences, generated through the lazy-seq function, provide a powerful mechanism for handling collections in a flexible manner, allowing for both finite and infinite sequences. This approach not only streamlines the coding process but also fosters a more resource-conscious programming paradigm, making Clojure a compelling choice for developers focused on building efficient applications.

Frequently asked questions

Haven’t found what you were looking for? Contact Us


What is the difference between lazy evaluation and strict evaluation?

Strict evaluation (also known as eager evaluation) evaluates expressions as soon as they are encountered, regardless of whether their results are immediately needed.  

Lazy evaluation (also known as call-by-need) delays the evaluation of an expression until its value is actually required.


What is the difference between eager evaluation and lazy evaluation?

Eager evaluation immediately computes values, while lazy evaluation postpones computation until necessary.


What is DAG and lazy evaluation?

A Directed Acyclic Graph (DAG) is a graphical representation of a computation where nodes represent operations and edges represent dependencies between operations. Lazy evaluation can be efficiently implemented using a DAG, as it allows for sharing of intermediate results and avoiding redundant computations. In a lazy evaluation context, a DAG can be used to track which parts of a computation have been evaluated and which parts can be deferred.


Free Resources

Copyright ©2025 Educative, Inc. All rights reserved