Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ To build the book, change to the [`book`](book/) directory and run `lake exe fp-
To read the book locally, serve that directory over HTTP and open the address that the server prints:

```
python3 -m http.server --directory book/_out/html-multi
lake exe verso-serve
```

## Publishing
Expand Down
3 changes: 1 addition & 2 deletions book/.verso/verso-xref-manifest.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
{"version": 0,
"sources":
{"manual":
{"updated": "2026-08-04:14:48:20.312161000",
"updateFrequency": "manual",
{"updateFrequency": "manual",
"shortName": "ref",
"root": "https://lean-lang.org/doc/reference/4.26.0/",
"longName": "Lean Language Reference"}},
Expand Down
22 changes: 11 additions & 11 deletions book/FPLean/Monads/IO.lean
Original file line number Diff line number Diff line change
Expand Up @@ -43,27 +43,27 @@ Nat.zero : Nat
Nat.succ : Nat → Nat
```
and
```anchor printCharIsAlpha
#print Char.isAlpha
```anchor printStringToLower
#print String.toLower
```
results in
```anchorInfo printCharIsAlpha
def Char.isAlpha : Char → Bool :=
fun c => c.isUpper || c.isLower
```anchorInfo printStringToLower
def String.toLower : String → String :=
fun s => String.map Char.toLower s
```

Sometimes, the output of {kw}`#print` includes Lean features that have not yet been presented in this book.
For example,
```anchor printListIsEmpty
#print List.isEmpty
```anchor printListHeadHuh
#print List.head?
```
produces
```anchorInfo printListIsEmpty
def List.isEmpty.{u} : {α : Type u} → List α → Bool :=
```anchorInfo printListHeadHuh
def List.head?.{u} : {α : Type u} → List α → Option α :=
fun {α} x =>
match x with
| [] => true
| head :: tail => false
| [] => none
| a :: tail => some a
```
which includes a {lit}`.{u}` after the definition's name, and annotates types as {anchorTerm names}`Type u` rather than just {anchorTerm names}`Type`.
This can be safely ignored for now.
Expand Down
97 changes: 36 additions & 61 deletions book/FPLean/ProgramsProofs/InsertionSort.lean
Original file line number Diff line number Diff line change
Expand Up @@ -85,12 +85,10 @@ def insertSorted [Ord α] (arr : Array α) (i : Fin arr.size) : Array α :=
match i with
| ⟨0, _⟩ => arr
| ⟨i' + 1, _⟩ =>
have : i' < arr.size := by
grind
match Ord.compare arr[i'] arr[i] with
| .lt | .eq => arr
| .gt =>
insertSorted (arr.swap i' i) ⟨i', by simp [*]⟩
insertSorted (arr.swap i' i) ⟨i', by grind⟩
```
If the index {anchorName insertSorted}`i` is {anchorTerm insertSorted}`0`, then the element being inserted into the sorted region has reached the beginning of the region and is the smallest.
If the index is {anchorTerm insertSorted}`i' + 1`, then the element at {anchorName insertSorted}`i'` should be compared to the element at {anchorName insertSorted}`i`.
Expand All @@ -103,19 +101,8 @@ If the element to the left is greater than the element being inserted, then the
{anchorName names}`Array.swap` takes both of its indices as {anchorName names}`Nat`s, using the same tactics as array indexing behind the scenes to ensure that they are in bounds.

Nonetheless, the {anchorName names}`Fin` used for the recursive call needs a proof that {anchorName insertSorted}`i'` is in bounds for the result of swapping two elements.
The {anchorTerm insertSorted}`simp` tactic's database contains the fact that swapping two elements of an array doesn't change its size, and the {anchorTerm insertSorted}`[*]` argument instructs it to additionally use the assumption introduced by {kw}`have`.
Omitting the {kw}`have`-expression with the proof that {anchorTerm insertSorted}`i' < arr.size` reveals the following goal:
```anchorError insertSortedNoProof
unsolved goals
α : Type ?u.7
inst✝ : Ord α
arr : Array α
i : Fin arr.size
i' : Nat
isLt✝ : i' + 1 < arr.size
⊢ i' < arr.size
```

The {anchorTerm insertSorted}`grind` tactic's database contains the fact that swapping two elements of an array doesn't change its size.
Combining this with the fact that {anchorTerm insertSorted}`i' + 1` is in bounds for the original array, {anchorTerm insertSorted}`grind` can conclude that {anchorName insertSorted}`i'` is in bounds after the swap.


# The Outer Loop
Expand Down Expand Up @@ -385,10 +372,10 @@ theorem insert_sorted_size_eq [Ord α]
(arr : Array α) (i : Fin arr.size) :
(insertSorted arr i).size = arr.size := by
fun_induction insertSorted with
| case1 arr isLt => skip
| case2 arr i isLt this isLt => skip
| case3 arr i isLt this isEq => skip
| case4 arr i isLt this isGt ih => skip
| case1 arr => skip
| case2 arr i this isLt => skip
| case3 arr i this isEq => skip
| case4 arr i this isGt ih => skip
```
The first goal is the case for index {anchorTerm insertSorted}`0`.
Here, the array is not modified, so proving that its size is unmodified will not require any complicated steps:
Expand All @@ -398,7 +385,7 @@ case case1
α : Type u_1
inst✝ : Ord α
arr✝ arr : Array α
isLt : 0 < arr.size
isLt✝ : 0 < arr.size
⊢ arr.size = arr.size
```
The next two goals are the same, and cover the {anchorName insertSorted}`.lt` and {anchorName insertSorted}`.eq` cases for the element comparison.
Expand All @@ -410,13 +397,12 @@ case case2
inst✝ : Ord α
arr✝ arr : Array α
i : Nat
isLt✝ : i + 1 < arr.size
this : i < arr.size
isLt : compare arr[i] arr[⟨i.succ, isLt✝⟩] = Ordering.lt
⊢ (match compare arr[i] arr[⟨i.succ, isLt✝⟩] with
this : i + 1 < arr.size
isLt : compare arr[i] arr[⟨i.succ, this⟩] = Ordering.lt
⊢ (match compare arr[i] arr[⟨i.succ, this⟩] with
| Ordering.lt => arr
| Ordering.eq => arr
| Ordering.gt => insertSorted (arr.swap i (↑⟨i.succ, isLt✝⟩) this ⋯) ⟨i, ⋯⟩).size =
| Ordering.gt => insertSorted (arr.swap i ↑⟨i.succ, this⟩ ⋯ ⋯) ⟨i, ⋯⟩).size =
arr.size
```
```anchorError insert_sorted_size_eq_funInd1
Expand All @@ -426,13 +412,12 @@ case case3
inst✝ : Ord α
arr✝ arr : Array α
i : Nat
isLt : i + 1 < arr.size
this : i < arr.size
isEq : compare arr[i] arr[⟨i.succ, isLt⟩] = Ordering.eq
⊢ (match compare arr[i] arr[⟨i.succ, isLt⟩] with
this : i + 1 < arr.size
isEq : compare arr[i] arr[⟨i.succ, this⟩] = Ordering.eq
⊢ (match compare arr[i] arr[⟨i.succ, this⟩] with
| Ordering.lt => arr
| Ordering.eq => arr
| Ordering.gt => insertSorted (arr.swap i (↑⟨i.succ, isLt⟩) this ⋯) ⟨i, ⋯⟩).size =
| Ordering.gt => insertSorted (arr.swap i ↑⟨i.succ, this⟩ ⋯ ⋯) ⟨i, ⋯⟩).size =
arr.size
```
In the final case, once the {anchorTerm insertSorted}`match` is reduced, there will be some work left to do to prove that the next step of the insertion preserves the size of the array.
Expand All @@ -444,26 +429,24 @@ case case4
inst✝ : Ord α
arr✝ arr : Array α
i : Nat
isLt : i + 1 < arr.size
this : i < arr.size
isGt : compare arr[i] arr[⟨i.succ, isLt⟩] = Ordering.gt
ih : (insertSorted (arr.swap i (↑⟨i.succ, isLt⟩) this ⋯) ⟨i, ⋯⟩).size = (arr.swap i (↑⟨i.succ, isLt⟩) this ⋯).size
⊢ (match compare arr[i] arr[⟨i.succ, isLt⟩] with
this : i + 1 < arr.size
isGt : compare arr[i] arr[⟨i.succ, this⟩] = Ordering.gt
ih : (insertSorted (arr.swap i ↑⟨i.succ, this⟩ ⋯ ⋯) ⟨i, ⋯⟩).size = (arr.swap i ↑⟨i.succ, this⟩ ⋯ ⋯).size
⊢ (match compare arr[i] arr[⟨i.succ, this⟩] with
| Ordering.lt => arr
| Ordering.eq => arr
| Ordering.gt => insertSorted (arr.swap i (↑⟨i.succ, isLt⟩) this ⋯) ⟨i, ⋯⟩).size =
| Ordering.gt => insertSorted (arr.swap i ↑⟨i.succ, this⟩ ⋯ ⋯) ⟨i, ⋯⟩).size =
arr.size
```
:::

:::paragraph
The Lean library includes the theorem {anchorName insert_sorted_size_eq_funInd}`Array.size_swap`, which states that swapping two elements of an array doesn't change its size.
By default, {tactic}`grind` doesn't use this fact, but once instructed to do so, it can take care of all four cases:
The {tactic}`grind` tactic can take care of all four cases:
```anchor insert_sorted_size_eq_funInd
theorem insert_sorted_size_eq [Ord α]
(arr : Array α) (i : Fin arr.size) :
(insertSorted arr i).size = arr.size := by
fun_induction insertSorted <;> grind [Array.size_swap]
fun_induction insertSorted <;> grind
```
:::

Expand Down Expand Up @@ -533,7 +516,7 @@ Adding calls to {anchorName dbgTraceIfSharedSig}`dbgTraceIfShared` at each point
Insertion sort has precisely one place that is at risk of copying rather than mutating: the call to {anchorName names}`Array.swap`.
Replacing {anchorTerm insertSorted}`arr.swap i' i` with {anchorTerm InstrumentedInsertionSort (module := Examples.ProgramsProofs.InstrumentedInsertionSort)}`(dbgTraceIfShared "array to swap" arr).swap i' i` causes the program to emit {lit}`shared RC array to swap` whenever it is unable to mutate the array.
However, this change to the program changes the proofs as well, because now there's a call to an additional function.
Adding a local assumption that {anchorName dbgTraceIfSharedSig}`dbgTraceIfShared` preserves the length of its argument and adding it to some calls to {anchorTerm InstrumentedInsertionSort (module:=Examples.ProgramsProofs.InstrumentedInsertionSort)}`simp` is enough to fix the program and proofs.
Adding a local assumption that {anchorName dbgTraceIfSharedSig}`dbgTraceIfShared` preserves the length of its argument and adding it to some calls to {anchorTerm InstrumentedInsertionSort (module:=Examples.ProgramsProofs.InstrumentedInsertionSort)}`grind` is enough to fix the program and proofs.

The complete instrumented code for insertion sort is:
```anchor InstrumentedInsertionSort (module := Examples.ProgramsProofs.InstrumentedInsertionSort)
Expand All @@ -542,33 +525,25 @@ def insertSorted [Ord α] (arr : Array α) (i : Fin arr.size) : Array α :=
| ⟨0, _⟩ => arr
| ⟨i' + 1, _⟩ =>
have : i' < arr.size := by
omega
grind
match Ord.compare arr[i'] arr[i] with
| .lt | .eq => arr
| .gt =>
have : (dbgTraceIfShared "array to swap" arr).size = arr.size := by
simp [dbgTraceIfShared]
grind [dbgTraceIfShared]
insertSorted
((dbgTraceIfShared "array to swap" arr).swap i' i)
⟨i', by simp [*]⟩

theorem insert_sorted_size_eq [Ord α] (len : Nat) (i : Nat) :
(arr : Array α) → (isLt : i < arr.size) → (arr.size = len) →
(insertSorted arr ⟨i, isLt⟩).size = len := by
induction i with
| zero =>
intro arr isLt hLen
simp [insertSorted, *]
| succ i' ih =>
intro arr isLt hLen
simp [insertSorted, dbgTraceIfShared]
split <;> simp [*]
⟨i', by grind [dbgTraceIfShared]⟩

theorem insert_sorted_size_eq [Ord α]
(arr : Array α) (i : Fin arr.size) :
(insertSorted arr i).size = arr.size := by
fun_induction insertSorted <;> grind [dbgTraceIfShared]

def insertionSortLoop [Ord α] (arr : Array α) (i : Nat) : Array α :=
if h : i < arr.size then
have : (insertSorted arr ⟨i, h⟩).size - (i + 1) < arr.size - i := by
rw [insert_sorted_size_eq arr.size i arr h rfl]
omega
grind [insert_sorted_size_eq]
insertionSortLoop (insertSorted arr ⟨i, h⟩) (i + 1)
else
arr
Expand Down Expand Up @@ -631,19 +606,19 @@ def main (args : List String) : IO UInt32 := do
| ["--shared"] => mainShared; pure 0
| ["--unique"] => mainUnique; pure 0
| _ =>
IO.println "Expected single argument, either \"--shared\" or \"--unique\""
IO.println "Expected either \"--shared\" or \"--unique\""
pure 1
```

Running it with no arguments produces the expected usage information:
```interaction «sort-sharing» "sort-demo"
{ command := "sort",
script := #[
.expect "Expected single argument, either \"--shared\" or \"--unique\"",
.expect "Expected either \"--shared\" or \"--unique\"",
.exitCode 1] }
---
$ sort
< Expected single argument, either "--shared" or "--unique"
< Expected either "--shared" or "--unique"
```

The file {lit}`test-data` contains the following rocks:
Expand Down
8 changes: 4 additions & 4 deletions book/lake-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"type": "git",
"subDir": null,
"scope": "",
"rev": "655d4f6e89bbf4c3c946e54625dcbe1539ea1107",
"rev": "aa44714115e9973999dfdde63130f725c3265a82",
"name": "verso",
"manifestFile": "lake-manifest.json",
"inputRev": "main",
Expand All @@ -15,7 +15,7 @@
"type": "git",
"subDir": null,
"scope": "",
"rev": "56958b3901ca108830de34fbce6cecd4b5757c1f",
"rev": "76f052847294d189dc9924a33466b4b677f47e67",
"name": "illuminate",
"manifestFile": "lake-manifest.json",
"inputRev": "main",
Expand All @@ -25,7 +25,7 @@
"type": "git",
"subDir": null,
"scope": "",
"rev": "b1c4a69a7e247ab7df20460212001673d74f08c0",
"rev": "38e9c3ce15cbb63c92e90bb9a92e4eb82131f669",
"name": "plausible",
"manifestFile": "lake-manifest.json",
"inputRev": "main",
Expand All @@ -45,7 +45,7 @@
"type": "git",
"subDir": null,
"scope": "",
"rev": "859ab80c32c5851151919a7d757d7c0c0b6e39d2",
"rev": "847084e80500726e4331dded5f17007ddaf89c31",
"name": "subverso",
"manifestFile": "lake-manifest.json",
"inputRev": "main",
Expand Down
2 changes: 1 addition & 1 deletion book/lean-toolchain
Original file line number Diff line number Diff line change
@@ -1 +1 @@
leanprover/lean4:v4.33.0-rc1
leanprover/lean4:v4.34.0-rc1
5 changes: 5 additions & 0 deletions book/verso-serve.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
banner = "Functional Programming in Lean"

[[mounts]]
path = "/"
dir = "_out/html-multi"
20 changes: 14 additions & 6 deletions examples/Examples/Monads/Class.lean
Original file line number Diff line number Diff line change
Expand Up @@ -891,20 +891,28 @@ instance : LawfulMonad (Reader ρ) where
map_const := by
simp [Functor.mapConst, Function.comp, Functor.map]
id_map x := by
simp [Functor.map]
funext
simp [Functor.map, id, Function.comp]
seqLeft_eq x _ := by
simp [SeqLeft.seqLeft, Seq.seq, Functor.map]
funext
simp [SeqLeft.seqLeft, Seq.seq, Functor.map, Function.comp]
seqRight_eq _ y := by
simp [SeqRight.seqRight, Seq.seq, Functor.map]
funext
simp [SeqRight.seqRight, Seq.seq, Functor.map, Function.comp]
pure_seq g x := by
simp [Seq.seq, Functor.map, pure]
funext
simp [Seq.seq, Functor.map, pure, Function.comp]
bind_pure_comp f x := by
simp [Functor.map, bind, pure]
funext
simp [Functor.map, bind, pure, Function.comp]
bind_map f x := by
simp [Seq.seq, bind, Functor.map]
funext
simp [Seq.seq, bind, Functor.map, Function.comp]
pure_bind x f := by
funext
simp [pure, bind]
bind_assoc x f g := by
funext
simp [bind]


Expand Down
26 changes: 13 additions & 13 deletions examples/Examples/Monads/IO.lean
Original file line number Diff line number Diff line change
Expand Up @@ -32,27 +32,27 @@ Nat.succ : Nat → Nat
-- ANCHOR_END: printNat


/-- info:
def Char.isAlpha : Char → Bool :=
fun c => c.isUpper || c.isLower
/--
info: def String.toLower : String → String :=
fun s => String.map Char.toLower s
-/
#check_msgs in
-- ANCHOR: printCharIsAlpha
#print Char.isAlpha
-- ANCHOR_END: printCharIsAlpha
-- ANCHOR: printStringToLower
#print String.toLower
-- ANCHOR_END: printStringToLower


/-- info:
def List.isEmpty.{u} : {α : Type u} → List α → Bool :=
/--
info: def List.head?.{u} : {α : Type u} → List α → Option α :=
fun {α} x =>
match x with
| [] => true
| head :: tail => false
| [] => none
| a :: tail => some a
-/
#check_msgs in
-- ANCHOR: printListIsEmpty
#print List.isEmpty
-- ANCHOR_END: printListIsEmpty
-- ANCHOR: printListHeadHuh
#print List.head?
-- ANCHOR_END: printListHeadHuh



Expand Down
Loading