Lean 语言参考

20.21. 调用计算🔗

thunk 延迟值的计算。 特别是,Thunk 类型用于延迟编译代码中值的计算,直到明确请求该值为止 - 该请求称为 forcing thunk。 计算出的值会被保存,因此后续请求不会导致重新计算。 当明确请求时,最多计算一次值称为 lazyvaluation. 此缓存对于 Lean 的逻辑是不可见的,其中 Thunk 相当于 Unit 中的函数。

20.21.1. 逻辑模型🔗

thunk 被建模为包含 Unit 中的函数的单字段结构。 该结构体的字段是私有的,因此不能直接访问该函数本身。 相反,应使用 Thunk.get。 从逻辑上看,它们是等价的; Thunk.get 的存在将在编译器中被实现惰性求值的平台原语覆盖。

🔗structure
Thunk.{u} (α : Type u) : Type u
Thunk.{u} (α : Type u) : Type u

Delays evaluation. The delayed code is evaluated at most once.

A thunk is code that constructs a value when it is requested via Thunk.get, Thunk.map, or Thunk.bind. The resulting value is cached, so the code is executed at most once. This is also known as lazy or call-by-need evaluation.

The Lean runtime has special support for the Thunk type in order to implement the caching behavior.

Constructor

Thunk.mk.{u}

Constructs a new thunk from a function Unit α that will be called when the thunk is first forced.

The result is cached. It is re-used when the thunk is forced again.

Fields

fn : Unit  α

Extract the getter function out of a thunk. Use Thunk.get instead.

20.21.2. 运行时表示🔗

m_header Lean object header m_value Saved valuelean_object * m_closure Closurelean_object *
Memory layout of thunks

Thunk 是 Lean 运行时支持的原始对象类型之一。 对象头包含一个特定的标记,指示对象是一个 thunk。

Thunk 有两个字段:

  • m_value 是指向已保存值的指针,如果尚未计算该值,则该指针为空指针。

  • m_closure 是一个闭包,在计算值时将调用它。

运行时系统维护闭包或保存的值是空指针的不变量。 如果两者都是空指针,则 thunk 被强制作用在另一个线程上。

当 thunk 为 forced 时,运行时系统首先检查保存的值是否已经计算过,如果是则返回。 否则,它会尝试通过原子地将闭包与空指针交换来获取闭包上的锁。 如果获取了锁,则调用它来计算值;计算出的值存储在保存的值字段中,并且对闭包的引用被删除。 如果没有,则另一个线程已经在计算该值;系统会等待直到计算完毕。

20.21.3. 强制🔗

存在从任何类型 αThunk α 的强制转换,将术语 e 转换为 Thunk.mk fun () => e。 由于精化器展开强制,原始项 e 的求值被延迟;强制转换不等于 Thunk.pure

Lazy Lists

惰性列表是可能包含 thunk 的列表。 delayed 构造函数导致根据需要计算列表的一部分。

inductive LazyList (α : Type u) where | nil | cons : α LazyList α LazyList α | delayed : Thunk (LazyList α) LazyList α deriving Inhabited

通过强制所有嵌入的 thunk,可以将惰性列表转换为普通列表。

def LazyList.toList : LazyList α List α | .nil => [] | .cons x xs => x :: xs.toList | .delayed xs => xs.get.toList

惰性列表上的许多操作可以在不强制嵌入 thunk 的情况下实现,而是构建更多的 thunk。 由于强制转换,delayed 的主体不需要是对 Thunk.mk 的显式调用。

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

Lean 程序通常看不到惰性:无法检查 thunk 是否已被强制。 但是,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 可用于深入了解 thunk 评估。

def observe (tag : String) (i : Fin n) : Nat := dbg_trace "{tag}: {i.val}" i.val

惰性列表 xsys 在求值时会发出痕迹。

def xs := LazyList.ofFn (n := 3) (observe "xs") def ys := LazyList.ofFn (n := 3) (observe "ys")

xs 转换为普通列表会强制所有嵌入的 thunk:

[0, 1, 2]xs: 0 xs: 1 xs: 2 #eval xs.toList
xs: 0
xs: 1
xs: 2
[0, 1, 2]

同样,将 xs.append ys 转换为普通列表会强制嵌入 thunk:

[0, 1, 2, 0, 1, 2]xs: 0 xs: 1 xs: 2 ys: 0 ys: 1 ys: 2 #eval xs.append ys |>.toList
xs: 0
xs: 1
xs: 2
ys: 0
ys: 1
ys: 2
[0, 1, 2, 0, 1, 2]

在强制 thunk 之前将 xs 附加到自身会产生一组跟踪,因为每个 thunk 的代码仅计算一次:

[0, 1, 2, 0, 1, 2]xs: 0 xs: 1 xs: 2 #eval xs.append xs |>.toList
xs: 0
xs: 1
xs: 2
[0, 1, 2, 0, 1, 2]

最后,采用 xs.append ys 前缀会导致仅评估 ys 中的部分 thunk:

[0, 1, 2, 0]xs: 0 xs: 1 xs: 2 ys: 0 #eval xs.append ys |>.take 4 |>.toList
xs: 0
xs: 1
xs: 2
ys: 0
[0, 1, 2, 0]

20.21.4. API 参考🔗

🔗def
Thunk.get.{u_1} {α : Type u_1} (x : Thunk α) : α
Thunk.get.{u_1} {α : Type u_1} (x : Thunk α) : α

Gets the thunk's value. If the value is cached, it is returned in constant time; if not, it is computed.

Computed values are cached, so the value is not recomputed.

🔗def
Thunk.map.{u_1, u_2} {α : Type u_1} {β : Type u_2} (f : α β) (x : Thunk α) : Thunk β
Thunk.map.{u_1, u_2} {α : Type u_1} {β : Type u_2} (f : α β) (x : Thunk α) : Thunk β

Constructs a new thunk that forces x and then applies x to the result. Upon forcing, the result of f is cached and the reference to the thunk x is dropped.

🔗def
Thunk.pure.{u_1} {α : Type u_1} (a : α) : Thunk α
Thunk.pure.{u_1} {α : Type u_1} (a : α) : Thunk α

Stores an already-computed value in a thunk.

Because the value has already been computed, there is no laziness.

🔗def
Thunk.bind.{u_1, u_2} {α : Type u_1} {β : Type u_2} (x : Thunk α) (f : α Thunk β) : Thunk β
Thunk.bind.{u_1, u_2} {α : Type u_1} {β : Type u_2} (x : Thunk α) (f : α Thunk β) : Thunk β

Constructs a new thunk that applies f to the result of x when forced.