104 lines
2.8 KiB
Markdown
104 lines
2.8 KiB
Markdown
# Thunks, Tasks, and Threads
|
||
|
||
A `Thunk` is defined as
|
||
```lean
|
||
# namespace Ex
|
||
# universe u
|
||
structure Thunk (α : Type u) : Type u where
|
||
fn : Unit → α
|
||
# end Ex
|
||
```
|
||
A `Thunk` encapsulates a computation without evaluation.
|
||
That is, a `Thunk` stores the way of how the value would be computed.
|
||
The Lean runtime has special support for `Thunk`s. It caches their values
|
||
after they are computed for the first time. This feature is useful for implementing
|
||
data structures such as lazy lists.
|
||
Here is a small example using a `Thunk`.
|
||
```lean
|
||
def fib : Nat → Nat
|
||
| 0 => 0
|
||
| 1 => 1
|
||
| x+2 => fib (x+1) + fib x
|
||
|
||
def f (c : Bool) (x : Thunk Nat) : Nat :=
|
||
if c then
|
||
x.get
|
||
else
|
||
0
|
||
|
||
def g (c : Bool) (x : Nat) : Nat :=
|
||
f c (Thunk.mk (fun _ => fib x))
|
||
|
||
#eval g false 1000
|
||
```
|
||
The function `f` above uses `x.get` to evaluate the `Thunk` `x`.
|
||
The expression `Thunk.mk (fun _ => fib x)` creates a `Thunk` for computing `fib x`.
|
||
Note that `fib` is a very naive function for computing the Fibonacci numbers,
|
||
and it would an unreasonable amount of time to compute `fib 1000`. However, our
|
||
test terminates instantaneously because the `Thunk` is not evaluated when `c` is `false`.
|
||
Lean has a builtin coercion from any type `a` to `Thunk a`. You write the function `g` above as
|
||
```lean
|
||
# def fib : Nat → Nat
|
||
# | 0 => 0
|
||
# | 1 => 1
|
||
# | x+2 => fib (x+1) + fib x
|
||
# def f (c : Bool) (x : Thunk Nat) : Nat :=
|
||
# if c then
|
||
# x.get
|
||
# else
|
||
# 0
|
||
def g (c : Bool) (x : Nat) : Nat :=
|
||
f c (fib x)
|
||
|
||
#eval g false 1000
|
||
```
|
||
In the following example, we use the macro `dbg_trace` to demonstrate
|
||
that the Lean runtime caches the value computed by a `Thunk`.
|
||
We remark that the macro `dbg_trace` should be used for debugging purposes
|
||
only.
|
||
```lean
|
||
def add1 (x : Nat) : Nat :=
|
||
dbg_trace "add1: {x}"
|
||
x + 1
|
||
|
||
def double (x : Thunk Nat) : Nat :=
|
||
x.get + x.get
|
||
|
||
def triple (x : Thunk Nat) : Nat :=
|
||
double x + x.get
|
||
|
||
def test (x : Nat) : Nat :=
|
||
triple (add1 x)
|
||
|
||
#eval test 2
|
||
-- add1: 2
|
||
-- 9
|
||
```
|
||
Note that the message `add1: 2` is printed only once.
|
||
Now, consider the same example using `Unit -> Nat` instead of `Thunk Nat`.
|
||
```lean
|
||
def add1 (x : Nat) : Nat :=
|
||
dbg_trace "add1: {x}"
|
||
x + 1
|
||
|
||
def double (x : Unit -> Nat) : Nat :=
|
||
x () + x ()
|
||
|
||
def triple (x : Unit -> Nat) : Nat :=
|
||
double x + x ()
|
||
|
||
def test (x : Nat) : Nat :=
|
||
triple (fun _ => add1 x)
|
||
|
||
#eval test 2
|
||
-- add1: 2
|
||
-- add1: 2
|
||
-- 9
|
||
```
|
||
Now, the message `add1: 2` is printed twice.
|
||
It may come as a surprise that it was printed twice instead of three times.
|
||
As we pointed out, `dbg_trace` is a macro used for debugging purposes only,
|
||
and `add1` is still considered to be a pure function.
|
||
The Lean compiler performs common subexpression elimination when compiling `double`,
|
||
and the produced code for `double` executes `x ()` only once instead of twice.
|
||
This transformation is safe because `x : Unit -> Nat` is pure.
|