How to understand clojure lazy-seq

I am trying to understand the clojure lazy-seq operator and the concept of lazy evaluation in general. I know the basic idea of ​​the concept: Evaluation of an expression is delayed until a value is needed.

In general, this is achieved in two ways:

  • during compilation using macros or special forms;
  • at runtime using lambda functions

With lazy estimation methods, you can build endless data structures that are rated as consumed. These infinite sequences use lambdas, closures, and recursion. In clojure, these endless data structures are generated using the lazy-seq and cons forms.

I want to understand how lazy-seq does this magic. I know that this is actually a macro. Consider the following example.

 (defn rep [n] (lazy-seq (cons n (rep n)))) 

Here, the rep function returns a lazily evaluated sequence of the LazySeq type, which can now be converted and consumed (evaluated in this way) using the sequence API. This API provides take , map , filter and reduce functions.

In an expanded form, we can see how lambda is used to store a recipe for a cell without evaluating it immediately.

 (defn rep [n] (new clojure.lang.LazySeq (fn* [] (cons n (rep n))))) 
  • But how does the sequence API work with LazySeq ?
  • What actually happens in the following expression?

(reduce + (take 3 (map inc (rep 5))))

  • How the intermediate map operation is applied to a sequence,
  • how take limits the sequence and
  • how does terminal reduce operation evaluate the sequence?

Also, how do these functions work with either Vector or LazySeq ?

Also, is it possible to generate nested infinite data structures ?: a list containing lists containing lists containing lists ... going infinitely wide and deep, evaluated as consumed using an API sequence?

And the last question is, is there any practical difference between this

 (defn rep [n] (lazy-seq (cons n (rep n)))) 

and this?

 (defn rep [n] (cons n (lazy-seq (rep n)))) 
+5
source share
1 answer

This is a lot of questions!

How does the seq API work with LazySeq?

If you look at the LazySeq class source code, you will notice that it implements ISeq by providing methods such as first , more and next .

Functions like map , take and filter built using lazy-seq (they create lazy sequences) and first and rest (which in turn uses more ) and that they can work with lazy seq as their input set - using first and more implementations of the LazySeq class.

What actually happens in the following expression?

(reduce + (take 3 (map inc (rep 5))))

The key should look like LazySeq.first . It will call a wrapped function to retrieve and remember the result. In your case, it will be the following code:

(cons n (rep n))

Thus, it will be a cons cell with n as its value and another LazySeq instance (the result of a recursive call to rep ) as part of rest . It will become the implemented value of this LazySeq object, and first will return the value of the cached cons cell.

When you call more on it, it will in the same way guarantee that the value of a specific LazySeq object will be implemented (or a reused memoized value) and will call more on it (in this case, more in the cons cell containing another LazySeq object).

After receiving another instance of the LazySeq object with more story repeats when you call first on it.

map and take will create another lazy-seq , which will call the first and more collections passed as their argument (just another lazy seq), so this will be a similar story. The difference will only be how the values ​​passed to cons are generated (e.g. calling f to the value obtained with first , called by the LazySeq value displayed in map instead of an unprocessed value such as n in your rep function).

With reduce it is a little simpler, since it will use a loop with first and more to iterate over the input of lazy seq and use the reduce function to get the final result.

Since the actual implementation looks like map and take , I recommend that you check their source code - it's pretty easy to do.

How can the seq API work with different types of collections (e.g. lazy seq and persistent vector)?

As mentioned above, map , take and other functions work in terms of first and rest (a reminder - rest implemented on top of more ). Thus, we need to explain how first and rest / more can work with different types of collections: they check whether the collection ISeq (and then implements these functions directly) or they try to create a seq view of the collection and collapse its implementation first and more .

Is it possible to generate nested infinite data structures?

It is definitely possible, but I'm not sure what exact form of data you would like to receive. You mean get a lazy seq that generates a different sequence as a value (instead of a single value like n in your rep ), but returns it as a flat sequence?

 (defn nested-cons [n] (lazy-seq (cons (repeat nn) (nested-cons (inc n))))) (take 3 (nested-cons 1)) ;; => ((1) (2 2) (3 3 3)) 

which is likely to return (1 2 2 3 3 3) ?

For such cases, you can use concat instead of cons , which creates a lazy sequence of two or more sequences:

 (defn nested-concat [n] (lazy-seq (concat (repeat nn) (nested-concat (inc n))))) (take 6 (nested-concat 1)) ;; => (1 2 2 3 3 3) 

Is there any practical difference with this

 (defn rep [n] (lazy-seq (cons n (rep n)))) 

and this?

 (defn rep [n] (cons n (lazy-seq (rep n)))) 

In this particular case, this is not so. But in the case when the cons cell does not wrap the raw value, but is the result of calling a function to calculate it, the latter form is not completely lazy. For instance:

 (defn calculate-sth [n] (println "Calculating" n) n) (defn rep1 [n] (lazy-seq (cons (calculate-sth) (rep1 (inc n))))) (defn rep2 [n] (cons (calculate-sth n) (lazy-seq (rep2 (inc n))))) (take 0 (rep1 1)) ;; => () (take 0 (rep2 1)) ;; Prints: Calculating 1 ;; => () 

Thus, the last form will evaluate its first element, even if you may not need it.

+8
source

Source: https://habr.com/ru/post/1268064/


All Articles