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