-- Lecture Notes for October 6, 2026 set_option autoImplicit false set_option linter.unusedVariables false set_option inductive.autoPromoteIndices false -- ---------------------------------------------------- -- * Inductive types, continued namespace MyNat -- Last time, we defined our own version of the -- natural numbers. inductive N : Type where | zero : N | succ : N → N open N -- We also defined some recursive functions, including an -- addition function. def add : N → N → N | n, zero => n | n, succ m => succ (add n m) -- Now, let's prove a theorem about addition. Let's -- prove that 1 + m = succ m. Since addition was -- defined by induction on m, the most natural proof -- is also by induction on m. -- -- `congrArg` is Lean's version of the `congruence` -- theorem we proved last time. theorem zero_add : ∀ (m : N), add (succ zero) m = succ m | zero => rfl | succ m' => calc add (succ zero) (succ m') _ = succ (add (succ zero) m') := rfl _ = succ (succ m') := congrArg (f := succ) (zero_add m') -- Note: In proof of `zero_add`, the "calc" part was -- just to make the equational reasoning more -- readable. Here's a shorter, perhaps less readable -- version. theorem zero_add' : ∀ (m : N), add (succ zero) m = succ m | zero => rfl | succ m' => congrArg (f := succ) (zero_add' m') -- Recursion and induction don't work unless the -- argument in the recursive call is smaller! The -- following definition is not accepted by Lean. def bad : N → N | n => bad (succ n) --> ERROR: failed to prove termination -- Neither is the following theorem. theorem bad_thm : ∀ n : N, n = succ n | n => bad_thm n --> ERROR: failed to prove termination -- When we define an inductive datatype such as `N` above, Lean -- automatically generates three things: -- -- (1) The type `N : Type`. -- (2) The constructors `N.zero : N` and `N.succ : N → N`. -- (3) An induction/recursion principle `N.rec`. #check N --> MyNat.N : Type #check zero --> MyNat.N.zero : N #check succ --> MyNat.N.succ : N → N #check @N.rec -- @N.rec : {motive : N → Sort u} → -- motive zero → -- (∀ (n : N), motive n → motive (succ n)) → -- ∀ (n : N), motive n -- Try to understand what `N.rec` is saying. -- -- • The "motive" is "the statement we are proving by -- induction". Specifically, `motive n` is the -- statement we are trying to prove for all `n`. -- -- • The base case is a proof of `motive zero`. -- -- • The induction step is a proof of `motive (succ n)`, -- assuming `motive n`. -- -- • If we provide all three (a motive, a base case, -- and an induction step), then we may conclude -- `∀ (n : N), motive n`. -- -- The above explanation was for induction (where -- `motive n : Prop` is a proposition). If -- `motive n : Type` is a type, the corrsponding thing -- is recursion. -- We can define addition in terms of `N.rec`, -- without using explicit recursion. noncomputable def add' : N → N → N := fun n m => N.rec (motive := fun m => N) n (fun m add_n_m => succ add_n_m) m -- Compare this with the recursive definition of `add`: -- def add : N → N → N -- | n, zero => n -- | n, succ m => succ (add n m) -- Fortunately, the elaborator usually does this job -- on our behalf. We can write recursive definitions, -- and the elaborator figures out how to tell the -- kernel to use the corresponding -- recursion/induction principle. end MyNat namespace MyList -- Let's define another inductive type: the type of lists. -- Since `List α` is already predefined, we call our type `L α`. -- -- The `L` type takes a _parameter_ `α : Type`. inductive L (α : Type) : Type where | nil : L α | cons : α → L α → L α -- In English: for any fixed type `α`: -- -- • `nil : L α` is a list, namely the empty list. -- -- • Whenever `x : α` and `xs : L α`, then `cons x xs : L α` is a list, -- namely the list whose first element is `x` and whose remaining -- elements are `xs`. -- We open the `L` namespace, so that we don't have -- to keep writing `L.nil` and `L.cons` everywhere. open L #check @cons --> @cons : {α : Type} → α → L α → L α #check @nil --> @nil : {α : Type} → L α -- Now we can define some functions on lists, by recursion. -- The length of a list. def length {α : Type} : L α → Nat | nil => 0 | cons x xs => (length xs) + 1 -- Append two lists. def append {α : Type} : L α → L α → L α | nil, ys => ys | cons x xs, ys => cons x (append xs ys) -- A theorem about the length of `append xs ys`. theorem length_append {α : Type} : ∀ xs ys : L α, length (append xs ys) = length ys + length xs | nil, ys => rfl | cons x xs, ys => congrArg Nat.succ (length_append xs ys) namespace Distraction -- When I first tried to define the `length` -- function in class, I got an error message I did -- not understand. I had put `{α : Type}` after the -- colon instead of before it. I expected this to -- work, but it did not: def length : {α : Type} → L α → Nat | nil => 0 --> ERROR | cons x xs => (length xs) + 1 -- The answer is simple. If `{α : Type}` is after -- the colon, it becomes an argument to the -- function. When you define a function by -- recursion, you must specify all of its arguments -- (implicit or explicit). So the following -- actually works. Notice the extra `α` in each -- of the two cases of the definition: def length_fixed : {α : Type} → L α → Nat | α, nil => 0 | α, cons x xs => (length xs) + 1 end Distraction -- -------------------------------------------------------------- -- * Notations -- Lean has many built-in notations. We already saw that we can -- write `2` instead of `Nat.succ (Nat.succ Nat.zero)`, and -- `1 + 2` instead of `Nat.add 1 2`. We also saw notations such -- as `[1, 2, 3]` or even `1 :: 2 :: 3 :: []` instead of -- `List.cons 1 (List.cons 2 (List.cons 3 List.nil))`. #eval 2 #eval Nat.succ (Nat.succ Nat.zero) #eval 1 + 2 #eval Nat.add 1 2 #eval [1, 2, 3] #eval 1 :: 2 :: 3 :: [] #eval List.cons 1 (List.cons 2 (List.cons 3 List.nil)) -- We can also define our own notations. The relevant -- commands are `notation` (for the general case) and -- `infix`, `infixr`, `infixl`, `prefix`, and -- `postfix` (for special cases). -- Let's define our own notations for `L.nil` and `L.cons`. notation "⟦⟧" => L.nil infixr:67 " ::: " => L.cons #check ⟦⟧ --> ⟦⟧ : L ?m.1 #check 1 ::: 2 ::: 3 ::: ⟦⟧ --> 1 ::: 2 ::: 3 ::: ⟦⟧ : L Nat -- Several remarks are in order: -- -- • `infixr` is for infix operators that associate -- to the right. There is also `infixl` for -- operators (such as `+`) that associate to the -- left, and `infix` for operators (such as `==`) -- that don't associate at all. #check (1 + 2) + 3 --> 1 + 2 + 3 : Nat #check 1 + (2 + 3) --> 1 + (2 + 3) : Nat #eval (1 == 2) == true --> false #eval 1 == 2 == true --> ERROR: unexpected token -- • The `:67` after `infixr` indicates a _precedence_. Operators -- of higher precedence bind more tightly. For example, -- `+` has precedence 65, and `*` has precedence 70. -- Therefore `1 + 2 * 3` means `1 + (2 * 3)` and -- `1 * 2 + 3` means `(1 * 2) + 3`, as expected. -- -- • We wrote `" ::: "` instead of `":::"`. The extra spaces -- are only used by Lean for printing. We do not have to -- use spaces when using the notation: #check 1:::2:::⟦⟧ --> 1 ::: 2 ::: ⟦⟧ : L Nat -- We can also define `prefix` and `postfix` notations. -- Suppose we have a square root and a factorial function. axiom sqrt : Nat → Nat axiom factorial : Nat → Nat prefix:max "√" => sqrt postfix:max "!" => factorial #check √2 --> √2 : Nat #check 2! --> 2! : Nat -- The `:max` indicates the highest available precedence. -- So something like `1 + 2!` should always mean `1 + (2!)`. -- Finally, the most general case, what is called -- _mixfix_ notation: notation:70 "add" "my" a ", " b ", " "and" c "please" => a + b + c #check add my 1, 2, and 3 please --> 1 + 2 + 3 : Nat #eval add my 1, 2, and 3 please --> 6 -- Pretty cool, huh? Some math notations work like -- that. Consider divisibility: def divides n m := ∃ k : Nat, n*k = m -- We can now define an infix notation: infix:60 " | " => divides -- We can also define congruence mod n: notation:70 x " ≡ " y " (mod " n ")" => n | y - x #check 1 ≡ 5 (mod 4) --> 4 | 1 - 5 : Prop example : 1 ≡ 5 (mod 4) := ⟨1, rfl⟩