doc: do notation
This commit is contained in:
parent
cb9574b086
commit
9608ea5aef
3 changed files with 380 additions and 3 deletions
|
|
@ -8,8 +8,8 @@
|
|||
- [Functions](./functions.md)
|
||||
- [Sections](./sections.md)
|
||||
- [Namespaces](./namespaces.md)
|
||||
- [The `do` Notation](./do.md)
|
||||
- [String interpolation](./stringinterp.md)
|
||||
- [The `do` Notation]()
|
||||
|
||||
# Tools
|
||||
|
||||
|
|
|
|||
377
doc/do.md
Normal file
377
doc/do.md
Normal file
|
|
@ -0,0 +1,377 @@
|
|||
# The `do` notation
|
||||
|
||||
Lean is a pure functional programming language, but you can write effectful code using the `do` embedded domain specific language (DSL). The following simple program prints two strings "hello" and "world" in the standard output and terminates with exit code 0. Note that the type of the program is `IO UInt32`. You can read this type as the type of values that perform input-output effects and produce a value of type `UInt32`.
|
||||
|
||||
```lean
|
||||
def main : IO UInt32 := do
|
||||
IO.println "hello"
|
||||
IO.println "world"
|
||||
return 0
|
||||
```
|
||||
The type of `IO.println` is `String → IO Unit`. That is, it is a function from `String` to `IO Unit` which indicates it may perform input-output effects and produce a value of type `Unit`. We often say that functions that may perform effects are *methods*.
|
||||
We also say a method application, such as `IO.println "hello"` is an *action*.
|
||||
Note that the examples above also demonstrates that braceless `do` blocks are whitespace sensitive.
|
||||
If you like `;`s and curly braces, you can write the example above as
|
||||
```lean
|
||||
def main : IO UInt32 := do {
|
||||
IO.println "hello";
|
||||
IO.println "world";
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
Semicolons can be used even when curly braces are not used. They are particularly useful when you want to "pack" more than one action in a single line.
|
||||
```lean
|
||||
def main : IO UInt32 := do
|
||||
IO.println "hello"; IO.println "world"
|
||||
return 0
|
||||
```
|
||||
Whitespace sensitivity in programming languages is a controversial topic
|
||||
among programmers. You should use your own style. We, the Lean developers, **love** the
|
||||
braceless and semicolon-free style.
|
||||
We believe it is clean and beautiful.
|
||||
|
||||
The `do` DSL expands into the core Lean language. Let's inspect the different components using the commands `#print` and `#check`.
|
||||
|
||||
```lean
|
||||
# def main : IO UInt32 := do
|
||||
# IO.println "hello"
|
||||
# IO.println "world"
|
||||
# return 0
|
||||
|
||||
#check IO.println
|
||||
-- String -> IO Unit
|
||||
#print main
|
||||
-- Output contains the infix operator `>>=` and `pure`
|
||||
-- The following `set_option` disables notation such as `>>=` in the output
|
||||
set_option pp.notation false in
|
||||
#print main
|
||||
-- Output contains `bind` and `pure`
|
||||
#print bind
|
||||
-- bind : {m : Type u → Type v} → [self : Bind m] → {α β : Type u} →
|
||||
-- m α → (α → m β) → m β
|
||||
#print pure
|
||||
-- pure : {m : Type u → Type v} → [self : Pure m] → {α : Type u} →
|
||||
-- α → m α
|
||||
|
||||
-- IO implements the type classes `Bind` and `Pure`.
|
||||
#check (inferInstance : Bind IO)
|
||||
#check (inferInstance : Pure IO)
|
||||
```
|
||||
The types of `bind` and `pure` may look daunting at first sight.
|
||||
They both have many implicit arguments. Let's focus first on the explicit arguments.
|
||||
`bind` has two explicit arguments `m α` and `α → m β`. The first one should
|
||||
be viewed as an action with effects `m` and producing a value of type `α`.
|
||||
The second is a function that takes a value of type `α` and produces an action
|
||||
with effects `m` and a value of type `β`. The result is `m β`. The method `bind` is composing
|
||||
these two actions. We often say `bind` is an abstract semicolon. The method `pure` converts
|
||||
a value `α` into an action that produces an action `m α`.
|
||||
|
||||
Here is the same function being defined using `bind` and `pure` without the `do` DSL.
|
||||
```lean
|
||||
def main : IO UInt32 :=
|
||||
bind (IO.println "hello") fun _ =>
|
||||
bind (IO.println "world") fun _ =>
|
||||
pure 0
|
||||
```
|
||||
|
||||
The notations `let x <- action1; action2` and `let x ← action1; action2` are just syntax sugar for `bind action1 fun x => action2`.
|
||||
Here is a small example using it.
|
||||
```lean
|
||||
def isGreaterThan0 (x : Nat) : IO Bool := do
|
||||
IO.println s!"value: {x}"
|
||||
return x > 0
|
||||
|
||||
def f (x : Nat) : IO Unit := do
|
||||
let c <- isGreaterThan0 x
|
||||
if c then
|
||||
IO.println s!"{x} is greater than 0"
|
||||
else
|
||||
pure ()
|
||||
|
||||
#eval f 10
|
||||
-- value: 10
|
||||
-- 10 is greater than 0
|
||||
```
|
||||
|
||||
|
||||
## Nested actions
|
||||
|
||||
Note that we cannot write `if isGreaterThan0 x then ... else ...` because the condition in a `if-then-else` is a **pure** value without effects, but `isGreaterThan0 x` has type `IO Bool`. You can use the nested action notation to avoid this annoyance. Here is an equivalent definition for `f` using a nested action.
|
||||
```lean
|
||||
# def isGreaterThan0 (x : Nat) : IO Bool := do
|
||||
# IO.println s!"x: {x}"
|
||||
# return x > 0
|
||||
|
||||
def f (x : Nat) : IO Unit := do
|
||||
if (<- isGreaterThan0 x) then
|
||||
IO.println s!"{x} is greater than 0"
|
||||
else
|
||||
pure ()
|
||||
|
||||
#print f
|
||||
```
|
||||
Lean "lifts" the nested actions and introduces the `bind` for us.
|
||||
Here is an example with two nested actions. Note that both actions are executed
|
||||
even if `x = 0`.
|
||||
```lean
|
||||
# def isGreaterThan0 (x : Nat) : IO Bool := do
|
||||
# IO.println s!"x: {x}"
|
||||
# return x > 0
|
||||
|
||||
def f (x y : Nat) : IO Unit := do
|
||||
if (<- isGreaterThan0 x) && (<- isGreaterThan0 y) then
|
||||
IO.println s!"{x} and {y} are greater than 0"
|
||||
else
|
||||
pure ()
|
||||
|
||||
#eval f 0 10
|
||||
-- value: 0
|
||||
-- value: 10
|
||||
|
||||
-- The function `f` above is equivalent to
|
||||
def g (x y : Nat) : IO Unit := do
|
||||
let c1 <- isGreaterThan0 x
|
||||
let c2 <- isGreaterThan0 y
|
||||
if c1 && c2 then
|
||||
IO.println s!"{x} and {y} are greater than 0"
|
||||
else
|
||||
pure ()
|
||||
|
||||
theorem fgEqual : f = g :=
|
||||
rfl -- proof by reflexivity
|
||||
```
|
||||
Here are two ways to achieve the short-circuit semantics in the example above
|
||||
```lean
|
||||
# def isGreaterThan0 (x : Nat) : IO Bool := do
|
||||
# IO.println s!"x: {x}"
|
||||
# return x > 0
|
||||
|
||||
def f1 (x y : Nat) : IO Unit := do
|
||||
if (<- isGreaterThan0 x <&&> isGreaterThan0 y) then
|
||||
IO.println s!"{x} and {y} are greater than 0"
|
||||
else
|
||||
pure ()
|
||||
|
||||
-- `<&&>` is the effectful version of `&&`
|
||||
-- Given `x y : IO Bool`, `x <&&> y` : m Bool`
|
||||
-- It only executes `y` if `x` returns `true`.
|
||||
|
||||
#eval f1 0 10
|
||||
-- value: 0
|
||||
#eval f1 1 10
|
||||
-- value: 1
|
||||
-- value: 10
|
||||
-- 1 and 10 are greater than 0
|
||||
|
||||
def f2 (x y : Nat) : IO Unit := do
|
||||
if (<- isGreaterThan0 x) then
|
||||
if (<- isGreaterThan0 y) then
|
||||
IO.println s!"{x} and {y} are greater than 0"
|
||||
else
|
||||
pure ()
|
||||
else
|
||||
pure ()
|
||||
```
|
||||
|
||||
## `if-then` notation
|
||||
|
||||
In the `do` DSL, we can write `if c then action` as a shorthand for `if c then action else pure ()`. Here is the method `f2` using this shorthand.
|
||||
```lean
|
||||
# def isGreaterThan0 (x : Nat) : IO Bool := do
|
||||
# IO.println s!"x: {x}"
|
||||
# return x > 0
|
||||
|
||||
def f2 (x y : Nat) : IO Unit := do
|
||||
if (<- isGreaterThan0 x) then
|
||||
if (<- isGreaterThan0 y) then
|
||||
IO.println s!"{x} and {y} are greater than 0"
|
||||
```
|
||||
|
||||
## Reassignments
|
||||
|
||||
When writing effectful code, it is natural to think imperatively.
|
||||
For example, suppose we want to create an empty array `xs`,
|
||||
add `0` if some condition holds, add `1` if another condition holds,
|
||||
and then print it. In the following example, we use variable
|
||||
"shadowing" to simulate this kind of "update".
|
||||
|
||||
```lean
|
||||
def f (b1 b2 : Bool) : IO Unit := do
|
||||
let xs := #[]
|
||||
let xs := if b1 then xs.push 0 else xs
|
||||
let xs := if b2 then xs.push 1 else xs
|
||||
IO.println xs
|
||||
|
||||
#eval f true true
|
||||
-- #[0, 1]
|
||||
#eval f false true
|
||||
-- #[1]
|
||||
#eval f true false
|
||||
-- #[0]
|
||||
#eval f false false
|
||||
-- #[]
|
||||
```
|
||||
|
||||
We can use tuples to simulate updates on multiple variables.
|
||||
|
||||
```lean
|
||||
def f (b1 b2 : Bool) : IO Unit := do
|
||||
let xs := #[]
|
||||
let ys := #[]
|
||||
let (xs, ys) := if b1 then (xs.push 0, ys) else (xs, ys.push 0)
|
||||
let (xs, ys) := if b2 then (xs.push 1, ys) else (xs, ys.push 1)
|
||||
IO.println s!"xs: {xs}, ys: {ys}"
|
||||
|
||||
#eval f true false
|
||||
-- xs: #[0], ys: #[1]
|
||||
```
|
||||
|
||||
We can also simulate the control-flow above using *join-points*.
|
||||
A join-point is a `let` that is always tail called and fully applied.
|
||||
The Lean compiler implements them using `goto`s.
|
||||
Here is the same example using join-points.
|
||||
|
||||
```lean
|
||||
def f (b1 b2 : Bool) : IO Unit := do
|
||||
let jp1 xs ys := IO.println s!"xs: {xs}, ys: {ys}"
|
||||
let jp2 xs ys := if b2 then jp1 (xs.push 1) ys else jp1 xs (ys.push 1)
|
||||
let xs := #[]
|
||||
let ys := #[]
|
||||
if b1 then jp2 (xs.push 0) ys else jp2 xs (ys.push 0)
|
||||
|
||||
#eval f true false
|
||||
-- xs: #[0], ys: #[1]
|
||||
```
|
||||
|
||||
You can capture complex control-flow using join-points.
|
||||
The `do` DSL offers the variable reassignment feature to make this kind of code more comfortable to write. In the following example, the `mut` modifier at `let mut xs := #[]` indicates that variable `xs` can be reassigned. The example contains two reassignments `xs := xs.push 0` and `xs := xs.push 1`. The reassignments are compiled using join-points. There is no hidden state being updated.
|
||||
|
||||
```lean
|
||||
def f (b1 b2 : Bool) : IO Unit := do
|
||||
let mut xs := #[]
|
||||
if b1 then xs := xs.push 0
|
||||
if b2 then xs := xs.push 1
|
||||
IO.println xs
|
||||
|
||||
#eval f true true
|
||||
-- #[0, 1]
|
||||
```
|
||||
The notation `x <- action` reassigns `x` with the value produced by the action. It is equivalent to `x := (<- action)`
|
||||
|
||||
## Iteration
|
||||
|
||||
The `do` DSL provides a unified notation for iterating over datastructures. Here are a few examples.
|
||||
|
||||
```lean
|
||||
def sum (xs : Array Nat) : IO Nat := do
|
||||
let mut s := 0
|
||||
for x in xs do
|
||||
IO.println s!"x: {x}"
|
||||
s := s + x
|
||||
return s
|
||||
|
||||
#eval sum #[1, 2, 3]
|
||||
-- x: 1
|
||||
-- x: 2
|
||||
-- x: 3
|
||||
-- 6
|
||||
|
||||
-- We can write pure code using the `do` DSL too.
|
||||
def sum' (xs : Array Nat) : Nat := do
|
||||
let mut s := 0
|
||||
for x in xs do
|
||||
s := s + x
|
||||
return s
|
||||
|
||||
#eval sum' #[1, 2, 3]
|
||||
-- 6
|
||||
|
||||
def sumEven (xs : Array Nat) : IO Nat := do
|
||||
let mut s := 0
|
||||
for x in xs do
|
||||
if x % 2 == 0 then
|
||||
IO.println s!"x: {x}"
|
||||
s := s + x
|
||||
return s
|
||||
|
||||
#eval sumEven #[1, 2, 3, 6]
|
||||
-- x: 2
|
||||
-- x: 6
|
||||
-- 8
|
||||
|
||||
def splitEvenOdd (xs : List Nat) : IO Unit := do
|
||||
let mut evens := #[]
|
||||
let mut odds := #[]
|
||||
for x in xs do
|
||||
if x % 2 == 0 then
|
||||
evens := evens.push x
|
||||
else
|
||||
odds := odds.push x
|
||||
IO.println s!"evens: {evens}, odds: {odds}"
|
||||
|
||||
#eval splitEvenOdd [1, 2, 3, 4]
|
||||
-- evens: #[2, 4], odds: #[1, 3]
|
||||
|
||||
def findNatLessThan (x : Nat) (p : Nat → Bool) : IO Nat := do
|
||||
-- [:x] is notation for the range [0, x)
|
||||
for i in [:x] do
|
||||
if p i then
|
||||
return i -- `return` from the `do` block
|
||||
throw (IO.userError "value not found")
|
||||
|
||||
#eval findNatLessThan 10 (fun x => x > 5 && x % 4 == 0)
|
||||
-- 8
|
||||
|
||||
def sumOddUpTo (xs : List Nat) (threshold : Nat) : IO Nat := do
|
||||
let mut s := 0
|
||||
for x in xs do
|
||||
if x % 2 == 0 then
|
||||
continue -- it behaves like the `continue` statement in imperative languages
|
||||
IO.println s!"x: {x}"
|
||||
s := s + x
|
||||
if s > threshold then
|
||||
break -- it behave like the `continue` statement in imperative languages
|
||||
IO.println s!"result: {s}"
|
||||
return s
|
||||
|
||||
#eval sumOddUpTo [2, 3, 4, 11, 20, 31, 41, 51, 107] 40
|
||||
-- x: 3
|
||||
-- x: 11
|
||||
-- x: 31
|
||||
-- result: 45
|
||||
-- 45
|
||||
```
|
||||
|
||||
TODO: describe `forIn`
|
||||
|
||||
## Try-catch
|
||||
|
||||
TODO
|
||||
|
||||
## Pattern matching
|
||||
|
||||
TODO
|
||||
|
||||
## Monads
|
||||
|
||||
TODO
|
||||
|
||||
## ReaderT
|
||||
|
||||
TODO
|
||||
|
||||
## StateT
|
||||
|
||||
TODO
|
||||
|
||||
## StateRefT
|
||||
|
||||
TODO
|
||||
|
||||
## ExceptT
|
||||
|
||||
TODO
|
||||
|
||||
## MonadLift and automatic lifting
|
||||
|
||||
TODO
|
||||
|
|
@ -1114,7 +1114,7 @@ hljs.registerLanguage("lean", function(hljs) {
|
|||
'universe universes variable variables ' +
|
||||
'import open theory prelude renaming hiding exposing ' +
|
||||
'calc match with do by let extends ' +
|
||||
'for in unless try catch finally mutual ' +
|
||||
'for in unless try catch finally mutual mut return continue break where' +
|
||||
'fun ' +
|
||||
'#check #eval #reduce #print ' +
|
||||
'section namespace end',
|
||||
|
|
@ -1130,7 +1130,7 @@ hljs.registerLanguage("lean", function(hljs) {
|
|||
'contradiction by_contradiction by_contra trivial exfalso ' +
|
||||
'symmetry transitivity destruct constructor econstructor ' +
|
||||
'left right split injection injections ' +
|
||||
'repeat try continue skip swap solve1 abstract all_goals any_goals done ' +
|
||||
'repeat skip swap solve1 abstract all_goals any_goals done ' +
|
||||
'fail_if_success success_if_fail guard_target guard_hyp ' +
|
||||
'have replace at suffices show from ' +
|
||||
'congr congr_n congr_arg norm_num ring ',
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue