Lazy lists are lists that may contain thunks.
The delayed constructor causes part of the list to be computed on demand.
inductive LazyList (α : Type u) where
| nil
| cons : α → LazyList α → LazyList α
| delayed : Thunk (LazyList α) → LazyList α
deriving Inhabited
Lazy lists can be converted to ordinary lists by forcing all the embedded thunks.
def LazyList.toList : LazyList α → List α
| .nil => []
| .cons x xs => x :: xs.toList
| .delayed xs => xs.get.toList
Many operations on lazy lists can be implemented without forcing the embedded thunks, instead building up further thunks.
The body of delayed does not need to be an explicit call to Thunk.mk because of the coercion.
def LazyList.take : Nat → LazyList α → LazyList α
| 0, _ => .nil
| _, .nil => .nil
| n + 1, .cons x xs => .cons x <| .delayed <| take n xs
| n + 1, .delayed xs => .delayed <| take (n + 1) xs.get
def LazyList.ofFn (f : Fin n → α) : LazyList α :=
Fin.foldr n (init := .nil) fun i xs =>
.delayed <| LazyList.cons (f i) xs
def LazyList.append (xs ys : LazyList α) : LazyList α :=
.delayed <|
match xs with
| .nil => ys
| .cons x xs' => LazyList.cons x (append xs' ys)
| .delayed xs' => append xs'.get ys
Laziness is ordinarily invisible to Lean programs: there is no way to check whether a thunk has been forced.
However, Lean.Parser.Term.dbgTrace : term`dbg_trace e; body` evaluates to `body` and prints `e` (which can be an
interpolated string literal) to stderr. It should only be used for debugging.
dbg_trace can be used to gain insight into thunk evaluation.
def observe (tag : String) (i : Fin n) : Nat :=
dbg_trace "{tag}: {i.val}"
i.val
The lazy lists xs and ys emit traces when evaluated.
def xs := LazyList.ofFn (n := 3) (observe "xs")
def ys := LazyList.ofFn (n := 3) (observe "ys")
Converting xs to an ordinary list forces all of the embedded thunks:
[0, 1, 2]xs: 0
xs: 1
xs: 2
#eval xs.toList
Likewise, converting xs.append ys to an ordinary list forces the embedded thunks:
[0, 1, 2, 0, 1, 2]xs: 0
xs: 1
xs: 2
ys: 0
ys: 1
ys: 2
#eval xs.append ys |>.toList
Appending xs to itself before forcing the thunks results in a single set of traces, because each thunk's code is evaluated just once:
[0, 1, 2, 0, 1, 2]xs: 0
xs: 1
xs: 2
#eval xs.append xs |>.toList
Finally, taking a prefix of xs.append ys results in only some of the thunks in ys being evaluated:
[0, 1, 2, 0]xs: 0
xs: 1
xs: 2
ys: 0
#eval xs.append ys |>.take 4 |>.toList