Logical Foundations

2. Basics: Functional Programming in Lean🔗

This chapter introduces some of Lean's most essential features for writing functional programs and proving things about how they behave.

2.1. Introduction🔗

2.2. Data and Functions🔗

In Lean, we can build practically everything from first principles using inductive definitions.

2.2.1. Days of the Week (Enumerated Types)🔗

An inductive definition for an enumerated type:

inductive Day : Type where | monday | tuesday | wednesday | thursday | friday | saturday | sunday

A function on days:

def nextWorkingDay (d : Day) : Day := match d with | Day.monday => Day.tuesday | Day.tuesday => Day.wednesday | Day.wednesday => Day.thursday | Day.thursday => Day.friday | Day.friday => Day.monday | Day.saturday => Day.monday | Day.sunday => Day.monday

Evaluation:

Day.monday#eval nextWorkingDay Day.friday
Day.monday
Day.tuesday#eval nextWorkingDay (nextWorkingDay Day.saturday)
Day.tuesday

We can also record what we expect the result of calling a function to be in the form of a Lean example:

example : nextWorkingDay (nextWorkingDay Day.saturday) = Day.tuesday := ⊢ nextWorkingDay (nextWorkingDay Day.saturday) = Day.tuesday All goals completed! 🐙

The rfl tactic is used to observe that both sides of an equal sign evaluate to the same value.

2.2.2. Aside: Using the VS Code Lean Extension🔗

2.2.3. Booleans🔗

Note to developers (Benjamin Pierce @bcpierce00)

This still doesn't explain WHY we are doing things two different ways. And I don't understand why myself, so I can't explain it. :-)

Another familiar enumerated type; we'll switch to Lean's built-in Bool later:

inductive MyBool : Type where | true | false

This command opens the namespace associated with the MyBool type:

namespace MyBool

Functions over booleans can be defined in the same way as functions over days of the week.

def not (b : MyBool) : MyBool := match b with | MyBool.true => MyBool.false | MyBool.false => MyBool.true
def and (b1 : MyBool) (b2 : MyBool) : MyBool := match b1 with | MyBool.true => b2 | MyBool.false => MyBool.false def or (b1 : MyBool) (b2 : MyBool) : MyBool := match b1 with | MyBool.true => MyBool.true | MyBool.false => b2

Note the syntax for defining multi-argument functions (and and or).

example : or MyBool.true MyBool.false = MyBool.true := b:MyBooln:Natm:Nat⊢ or true false = true All goals completed! 🐙 example : or MyBool.false MyBool.false = MyBool.false := b:MyBooln:Natm:Nat⊢ or false false = false All goals completed! 🐙 example : or MyBool.false MyBool.true = MyBool.true := b:MyBooln:Natm:Nat⊢ or false true = true All goals completed! 🐙 example : or MyBool.true MyBool.true = MyBool.true := b:MyBooln:Natm:Nat⊢ or true true = true All goals completed! 🐙

Lean also allows us to define symbolic notations for these functions.

local prefix:40 (priority := high) "!" => not local infixl:35 (priority := high) " && " => and local infixl:30 (priority := high) " || " => or example : (MyBool.false || MyBool.false || MyBool.true) = MyBool.true := b:MyBooln:Natm:Nat⊢ (false || false || true) = true All goals completed! 🐙 example : (!MyBool.false) = MyBool.true := b:MyBooln:Natm:Nat⊢ (!false) = true All goals completed! 🐙
Exercise★(nand)

The sorry keyword is a placeholder for an incomplete proof or definition. We use it in exercises to indicate the parts that we're leaving for you — i.e., your job is to replace sorry with real definitions and proofs.

Remove sorry below and complete the definition of the function. The function should return MyBool.true if either or both of its inputs are MyBool.false. Make sure that the example assertions below can be verified by Lean.

def declaration uses `sorry`nand (b1 : MyBool) (b2 : MyBool) : MyBool := sorry theorem declaration uses `sorry`nand_test1 : nand MyBool.true MyBool.false = MyBool.true := sorry theorem declaration uses `sorry`nand_test2 : nand MyBool.false MyBool.false = MyBool.true := sorry theorem declaration uses `sorry`nand_test3 : nand MyBool.false MyBool.true = MyBool.true := sorry theorem declaration uses `sorry`nand_test4 : nand MyBool.true MyBool.true = MyBool.false := sorry

Going forward, most exercises will be omitted from the "terse" version of the notes used in lecture. The "full" version (used online and for homeworks) contains both longer explanations and all the exercises.

2.3. A First Taste of Proofs🔗

Let's prove something simple about booleans:

theorem true_and : ∀ (b : MyBool), (MyBool.true && b) = b := ⊢ ∀ (b : MyBool), (true && b) = b b:MyBool⊢ (true && b) = b All goals completed! 🐙

And now let's see it in a bit more detail:

theorem true_and_explained : ∀ (b : MyBool), (MyBool.true && b) = b := ⊢ ∀ (b : MyBool), (true && b) = b /- Move your cursor (click) here to see the initial proof state in the InfoView. If you are viewing the book online, click instead on the white button after `by`. The context (before the ⊢) is empty. The goal is `∀ (b : MyBool), (MyBool.true && b) = b`. -/ b:MyBool⊢ (true && b) = b /- Now click here (or the white button after `intro b`) to see the new proof state that results from the tactic. Notice how `intro b` has changed the _context_: it now contains `b : MyBool`. The `intro` tactic is used to name variables quantified by a `∀`. Since we are trying to prove a property of all `MyBool`s, we proceed by introducing an unknown `MyBool` `b` and proving the property holds for this particular `b`. Informally, this move can be read, "We want to prove <some property> for all `MyBool`s `b`. So suppose `b` is some arbitrary `MyBool`... <and then go on to prove the property for this particular `b`>..." Since `b` was chosen arbitrarily, we've now proved the property for all `b`. A proof of a theorem beginning with a ∀ will typically start with an `intro`. As in the `example`s above, we can use the `rfl` tactic, which closes goals about equality where both sides are equal to one another according to the principle of reflexivity. Now, inspecting our goal will show that it is `(MyBool.true && b) = b`, which may not appear to be "true by reflexivity", since the two sides of the equality are not textually identical. However, the tactic _evaluates_ both sides of the equality before comparing them. In this case, if we look at the definition of `and`, we can see that, when its first argument is `MyBool.true`, the result is its second argument. So the two terms `MyBool.true && b` and `b` are in fact equal because one evaluates to the other. -/ All goals completed! 🐙 /- The proof is now done! The Lean InfoView tells us there are "No goals". -/

Now we'll switch to Lean's definition of booleans.

end MyBool

2.3.1. Aside: Unicode in Lean🔗

2.3.2. Types🔗

We can use #check to check the type of an expression:

Bool.true : Bool#check Bool.true
Bool.true : Bool
true : Bool#check (Bool.true : Bool) !true : Bool#check (Bool.not Bool.true : Bool)
true : Bool
!true : Bool
Bool.not (x : Bool) : Bool#check Bool.not

2.3.3. New Types from Old🔗

A more interesting type definition:

inductive RGB : Type where | red | green | blue inductive Color : Type where | black | white | primary (p : RGB)

We can define functions on colors using pattern matching, just as we did for Day and MyBool.

def monochrome (c : Color) : Bool := match c with | Color.black => Bool.true | Color.white => Bool.true | Color.primary Variable name `p` is not explicitly referenced. Hint: The binding can be removed (if unused) or named `_` (if used implicitly). Alternatively, prefix the name with `_` to silence this warning: [apply] _p Note: This linter can be disabled with `set_option linter.unusedVariables false`p => Bool.false

We can use a wildcard pattern _ to match something we don't care about:

def monochrome' (c : Color) : Bool := match c with | Color.black => Bool.true | Color.white => Bool.true | Color.primary _ => Bool.false

We can use a constant argument to Color.primary to match a specific primary color:

def isRed (c : Color) : Bool := match c with | Color.black => Bool.false | Color.white => Bool.false | Color.primary RGB.red => Bool.true | Color.primary _ => Bool.false

An alternative way to write the same function would be to explicitly nest match statements:

def isRed' (c : Color) : Bool := match c with | Color.black => Bool.false | Color.white => Bool.false | Color.primary r => match r with | RGB.red => Bool.true | _ => Bool.false

This isRed' function produces the same result as isRed. It also illustrates the use of a pattern variable in the corresponding branch.

2.3.4. Namespaces🔗

namespace declarations create separate namespaces.

def myFoo : Bool := true namespace Playground def myFoo : RGB := RGB.blue end Playground myFoo : Bool#check myFoo Playground.myFoo : RGB#check Playground.myFoo
myFoo : Bool
Playground.myFoo : RGB
namespace Playground def myBar : RGB := myFoo end Playground Playground.myBar : RGB#check Playground.myBar
Playground.myBar : RGB

The names of an inductive type's constructors are prefixed by the type's name.

namespace RGB def myBlue : RGB := blue end RGB

Top-level definitions can also be prefixed by a namespace, which opens the namespace temporarily for the body of the definition.

-- The following works because the definition is qualified by `RGB.` def RGB.myOtherBlue : RGB := myBlue RGB.myBlue : RGB#check RGB.myBlue RGB.myOtherBlue : RGB#check RGB.myOtherBlue
RGB.myBlue : RGB
RGB.myOtherBlue : RGB
-- This doesn't work: the identifier is undefined #check Unknown identifier `myBlue`myBlue
Unknown identifier `myBlue`
def Day.nextWorkingDay' (d : Day) : Day := match d with | monday => tuesday | tuesday => wednesday | wednesday => thursday | thursday => friday | friday => monday | saturday => monday | sunday => monday

open brings definitions from a namespace into scope.

namespace MyNamespace def myDef : Bool := Bool.true end MyNamespace open MyNamespace MyNamespace.myDef : Bool#check myDef
MyNamespace.myDef : Bool

If we only want to bring some, rather than all, of the definitions of a namespace into the current scope, we can use the open (...) form:

namespace MyOtherNamespace def myHiddenDef : Bool := Bool.true def myVisibleDef : Bool := Bool.false end MyOtherNamespace open MyOtherNamespace (myVisibleDef) -- `myVisibleDef` is now usable without qualification: MyOtherNamespace.myVisibleDef : Bool#check myVisibleDef
MyOtherNamespace.myVisibleDef : Bool

But myHiddenDef, which we did not include in the open, still needs to be qualified:

#check Unknown identifier `myHiddenDef`myHiddenDef
Unknown identifier `myHiddenDef`

Lean's prelude exports common names from the Bool namespace.

Bool.true : Bool#check Bool.true Bool.true : Bool#check true
Bool.true : Bool
Bool.true : Bool

Lean can often use the expected type to resolve a name beginning with .:

def nextWorkingDay' (d : Day) : Day := match d with | .monday => .tuesday | .tuesday => .wednesday | .wednesday => .thursday | .thursday => .friday | .friday => .monday | .saturday => .monday | .sunday => .monday

Here, Lean can't figure out which version of .true we mean.

#check Invalid dotted identifier notation: The expected type of `.true` could not be determined Hint: Using one of these would be unambiguous: [apply] `true` [apply] `MyBool.true` [apply] `Lake.Toml.true` [apply] `Lean.LBool.true` [apply] `Std.Do.ExceptConds.true` [apply] `Lean.Meta.Grind.Filter.true`.true
Invalid dotted identifier notation: The expected type of `.true` could not be determined

Hint: Using one of these would be unambiguous:
  [apply] `true`
  [apply] `MyBool.true`
  [apply] `Lake.Toml.true`
  [apply] `Lean.LBool.true`
  [apply] `Std.Do.ExceptConds.true`
  [apply] `Lean.Meta.Grind.Filter.true`

But in the following example, because Bool.not takes a Bool argument, Lean knows that .true must here be a Bool:

!true : Bool#check (Bool.not .true)
!true : Bool

2.3.5. Constructors with Multiple Parameters (Tuple Types)🔗

namespace Playground

A Nibble is half a byte — four bits.

inductive Bit : Type where | b1 | b0 inductive Nibble : Type where | bits (x0 x1 x2 x3 : Bit) Nibble.bits Bit.b1 Bit.b0 Bit.b1 Bit.b0 : Nibble#check Nibble.bits .b1 .b0 .b1 .b0
Nibble.bits Bit.b1 Bit.b0 Bit.b1 Bit.b0 : Nibble

We can deconstruct a Nibble by pattern matching.

def allZero (nb : Nibble) : Bool := match nb with | .bits .b0 .b0 .b0 .b0 => true | .bits _ _ _ _ => false example : allZero (.bits .b1 .b0 .b1 .b0) = false := b:MyBooln:Natm:Natc:Colorr:RGB⊢ allZero (Nibble.bits Bit.b1 Bit.b0 Bit.b1 Bit.b0) = false All goals completed! 🐙 example : allZero (.bits .b0 .b0 .b0 .b0) = true := b:MyBooln:Natm:Natc:Colorr:RGB⊢ allZero (Nibble.bits Bit.b0 Bit.b0 Bit.b0 Bit.b0) = true All goals completed! 🐙 end Playground

2.3.5.1. Structures🔗

An inductive type with just one constructor can alternatively be defined as a structure, an analog of a record type in other programming languages.

structure NibbleStruct : Type where x0 : Playground.Bit x1 : Playground.Bit x2 : Playground.Bit x3 : Playground.Bit { x0 := Playground.Bit.b0, x1 := Playground.Bit.b0, x2 := Playground.Bit.b0, x3 := Playground.Bit.b0 } : NibbleStruct#check NibbleStruct.mk .b0 .b0 .b0 .b0
{ x0 := Playground.Bit.b0, x1 := Playground.Bit.b0, x2 := Playground.Bit.b0, x3 := Playground.Bit.b0 } : NibbleStruct

The .mk constructor is created for us.

def zeroNibble : NibbleStruct := { x0 := .b0 x1 := .b0 x2 := .b0 x3 := .b0 }

We can "update" a structure like this:

def setFirstTwoBits (old : NibbleStruct) (newX0 : Playground.Bit) (newX1 : Playground.Bit) : NibbleStruct := { old with x0 := newX0, x1 := newX1 }

When variables and field names match, construction is easier.

def makeNibbleStruct (x0 x1 x2 x3 : Playground.Bit) : NibbleStruct := { x0, x1, x2, x3 }

2.3.6. Natural Numbers🔗

namespace NatPlayground

For simplicity in proofs, we choose a unary representation of natural numbers.

inductive Nat : Type where | zero | succ (n : Nat)
Library Nat to SFL Nat coerciondef ofNat : _root_.Nat → Nat | .zero => .zero | .succ n => .succ (ofNat n) instance (n : _root_.Nat) : OfNat Nat n := ⟨ofNat n⟩ attribute [pp_nodot] Nat.succ

Eventually we'll swap to Lean's definition of natural numbers, which is very similar to this.

namespace Nat def one : Nat := succ zero def two : Nat := succ one def three : Nat := succ two def four : Nat := succ three

We can also write functions on Nat.

def pred (n : Nat) : Nat := match n with | zero => zero | succ n' => n' def minusTwo (n : Nat) : Nat := match n with | zero => zero | succ (zero) => zero | succ (succ n') => n' NatPlayground.Nat.succ (NatPlayground.Nat.succ (NatPlayground.Nat.zero))#eval minusTwo four

Look at the types of succ, pred, and minusTwo:

succ : Nat → Nat#check (succ) pred : Nat → Nat#check (pred) minusTwo : Nat → Nat#check (minusTwo)
succ : Nat → Nat
pred : Nat → Nat
minusTwo : Nat → Nat

These are all things that can be applied to a number to yield a number. But there is a difference between succ and the other two.

Here are some recursive functions on natural numbers:

def even (n : Nat) : Bool := match n with | zero => true | succ (zero) => false | succ (succ n') => even n' example : even one = false := b:MyBooln:Natm:Natc:Colorr:RGB⊢ even one = false All goals completed! 🐙 example : even four = true := b:MyBooln:Natm:Natc:Colorr:RGB⊢ even four = true All goals completed! 🐙

We could define odd by a similar recursive declaration, but here is a simpler way:

def odd (n : Nat) : Bool := not (even n) example : odd one = true := b:MyBooln:Natm:Natc:Colorr:RGB⊢ odd one = true All goals completed! 🐙 example : odd four = false := b:MyBooln:Natm:Natc:Colorr:RGB⊢ odd four = false All goals completed! 🐙

This function takes multiple parameters, recursing on the second:

def add (n : Nat) (m : Nat) : Nat := match m with | zero => n | succ m' => succ (add n m') NatPlayground.Nat.succ (NatPlayground.Nat.succ (NatPlayground.Nat.succ (NatPlayground.Nat.zero)))#eval add one two -- succ (succ (succ zero)) -- aka, three!
NatPlayground.Nat.succ (NatPlayground.Nat.succ (NatPlayground.Nat.succ (NatPlayground.Nat.zero)))

We can also define infix notation for our add function.

scoped infixl:65 " + " => add NatPlayground.Nat.succ (NatPlayground.Nat.succ (NatPlayground.Nat.succ (NatPlayground.Nat.zero)))#eval one + two -- succ (succ (succ zero)) -- aka, three again.
NatPlayground.Nat.succ (NatPlayground.Nat.succ (NatPlayground.Nat.succ (NatPlayground.Nat.zero)))

2.4. Proof by Rewriting🔗

2.4.1. Proving Properties about Functions in Lean🔗

We can prove properties of recursive functions like add:

theorem add_zero : ∀ n : Nat, n + zero = n := ⊢ ∀ (n : Nat), n + zero = n n:Nat⊢ n + zero = n All goals completed! 🐙 NatPlayground.Nat.add_zero (n : Nat) : n + zero = n#check add_zero
NatPlayground.Nat.add_zero (n : Nat) : n + zero = n

Using our simplification rule add_zero, we can carry out a simple proof about natural numbers.

theorem add_zero_zero : ∀ n : Nat, n + zero + zero = n := ⊢ ∀ (n : Nat), n + zero + zero = n n:Nat⊢ n + zero + zero = n n:Nat⊢ n + zero = n n:Nat⊢ n = n All goals completed! 🐙

2.4.2. Proof State and Tactics🔗

Here is the previous proof in more detail:

theorem add_zero_zero_explained : ∀ n : Nat, n + zero + zero = n := ⊢ ∀ (n : Nat), n + zero + zero = n n:Nat⊢ n + zero + zero = n /- After introducing `n`, our goal is `n + zero + zero = n`. What can we do to simplify this expression? If you hover your cursor over the `add_zero` in the rewrite below, you can see its type: `n + zero = n`. So, we can use that simplification rule to transform an appearance of `n + zero` in the goal to `n`. -/ n:Nat⊢ n + zero = n /- Now click here to see the new proof state that results from the tactic. Notice how `n + zero + zero` changes to `n + zero` in the goal. -/ n:Nat⊢ n = n /- Again the goal changes, from `n + zero` to `n`. Now the proof state is an equality with both sides equal, so it can be closed by the tactic `rfl`. -/ All goals completed! 🐙 /- The proof is now done! The Lean InfoView tells us there are "No goals". -/

Give this proof a try (it's similar):

theorem declaration uses `sorry`add_zero_zero_zero : ∀ n : Nat, n + zero + zero + zero = n := ⊢ ∀ (n : Nat), n + zero + zero + zero = n All goals completed! 🐙

2.4.3. The rewrite tactic🔗

2.4.4. The rfl tactic🔗

The rfl closes a goal that looks like a = a, reducing both sides of the equality in the process.

2.4.5. A New add Rule🔗

Here's another rule we can use for add:

theorem add_succ : ∀ n m : Nat, n + (succ m) = succ (n + m) := ⊢ ∀ (n m : Nat), n + succ m = succ (n + m) n:Natm:Nat⊢ n + succ m = succ (n + m) All goals completed! 🐙

Now, let's use add_succ in a proof:

theorem add_one (n : Nat) : n + (succ zero) = succ n + zero := n:Nat⊢ n + succ zero = succ n + zero n:Nat⊢ succ (n + zero) = succ n + zero n:Nat⊢ succ n = succ n + zero n:Nat⊢ succ n = succ n All goals completed! 🐙

2.4.6. Irreducibility, Rewriting, and Proof Engineering🔗

After proving the theorems that characterize a definition, we mark the definition irreducible to require rewriting by them instead of using rfl.

attribute [irreducible] add

These simplification rules also follow a particular pattern. Let's look again at the definition of add, without the + notation for maximum clarity:

namespace AddPlayground def add (n : Nat) (m : Nat) : Nat := match m with | zero => n | succ m' => succ (add n m') theorem add_zero : ∀ (n : Nat), add n zero = n := ⊢ ∀ (n : Nat), add n zero = n n:Nat⊢ add n zero = n All goals completed! 🐙 theorem add_succ : ∀ (n m : Nat), add n (succ m) = succ (add n m) := ⊢ ∀ (n m : Nat), add n (succ m) = succ (add n m) n:Natm:Nat⊢ add n (succ m) = succ (add n m) All goals completed! 🐙 end AddPlayground

Each branch of a definition's control flow gets one simplification rule. Here are the two for pred:

theorem pred_zero : pred zero = zero := ⊢ pred zero = zero All goals completed! 🐙 theorem pred_succ (n : Nat) : pred (succ n) = n := n:Nat⊢ pred (succ n) = n All goals completed! 🐙

Now that we have defined and proved pred's simplification rules, we can mark it irreducible to enforce rewriting by these lemmas.

attribute [irreducible] pred

Similarly, for each of the three branches of the definition of even, we need one simplification rule:

theorem even_zero : even zero = true := rfl theorem even_one : even (succ zero) = false := rfl theorem even_succ_succ (n : Nat) : even (succ (succ n)) = even n := rfl attribute [irreducible] even odd

From here on, we pair each definition with its simplification rules and rewrite by those rules rather than rfl-ing through the definition.

2.4.7. Working with Numerals🔗

We know from our definitions above that one is just succ zero, two is succ one, and so on. We can write rules for these equalities too:

theorem one_eq_succ_zero : one = succ zero := ⊢ one = succ zero All goals completed! 🐙 theorem two_eq_succ_one : two = succ one := ⊢ two = succ one All goals completed! 🐙 theorem three_eq_succ_two : three = succ two := ⊢ three = succ two All goals completed! 🐙 theorem four_eq_succ_three : four = succ three := ⊢ four = succ three All goals completed! 🐙

2.4.7.1. Multiplication🔗

def mul (n m : Nat) : Nat := match m with | zero => zero | succ m' => (mul n m') + n scoped infixl:70 " * " => mul
Exercise★(mul_simpl_rules)

Multiplication, like any function we will prove properties about, also has simplification rules.

Remove sorry and prove the simplification rules for mul below. You will likely find the proofs of the simplification rules for add to be helpful as a model.

theorem declaration uses `sorry`mul_zero : ∀ n : Nat, n * zero = zero := ⊢ ∀ (n : Nat), n * zero = zero All goals completed! 🐙 theorem declaration uses `sorry`mul_succ : ∀ n m : Nat, n * (succ m) = (n * m) + n := ⊢ ∀ (n m : Nat), n * succ m = n * m + n All goals completed! 🐙 attribute [irreducible] mul

Prove this theorem using rewriting with the simplification rules.

theorem declaration uses `sorry`zero_add_one : (zero + one : Nat) = one := ⊢ zero + one = one ⊢ zero + succ zero = succ zero All goals completed! 🐙

2.4.7.2. Equality and Ordering🔗

Here is a function beq that tests natural numbers for equality, yielding a boolean.

def beq (n m : Nat) : Bool := match n with | zero => match m with | zero => true | succ _ => false | succ n' => match m with | zero => false | succ m' => beq n' m'

We could also write this by pattern matching on both n and m at the same time:

def beq' (n m : Nat) : Bool := match n, m with | zero, zero => true | zero, succ _ => false | succ _, zero => false | succ n', succ m' => beq n' m'

The definitions of beq and beq' are equivalent.

Similarly, the ble function tests whether its first argument is less than or equal to its second argument, yielding a boolean.

def ble (n m : Nat) : Bool := match n with | zero => true | succ n' => match m with | zero => false | succ m' => ble n' m' theorem zero_ble (n : Nat) : ble zero n = true := n:Nat⊢ ble zero n = true All goals completed! 🐙 theorem succ_ble_zero (n : Nat) : ble (succ n) zero = false := n:Nat⊢ ble (succ n) zero = false All goals completed! 🐙 theorem succ_ble_succ (n m : Nat) : ble (succ n) (succ m) = ble n m := n:Natm:Nat⊢ ble (succ n) (succ m) = ble n m All goals completed! 🐙 example : ble two two = true := b:MyBooln:Natm:Natc:Colorr:RGB⊢ ble two two = true All goals completed! 🐙 example : ble two four = true := b:MyBooln:Natm:Natc:Colorr:RGB⊢ ble two four = true All goals completed! 🐙 example : ble four two = false := b:MyBooln:Natm:Natc:Colorr:RGB⊢ ble four two = false All goals completed! 🐙

We'll be using beq a lot, so let's give it an infix notation.

scoped infixl:30 " == " => beq

Note that == and = are different; the former means beq whereas the latter is a logical claim. Here are our simplification rules.

theorem zero_beq_zero : (zero == zero) = true := ⊢ (zero == zero) = true All goals completed! 🐙 theorem zero_beq_succ (n : Nat) : (zero == (succ n)) = false := n:Nat⊢ (zero == succ n) = false All goals completed! 🐙 theorem succ_beq_zero (n : Nat) : ((succ n) == zero) = false := n:Nat⊢ (succ n == zero) = false All goals completed! 🐙 theorem succ_beq_succ (n m : Nat) : ((succ n) == (succ m)) = (n == m) := n:Natm:Nat⊢ (succ n == succ m) = (n == m) All goals completed! 🐙 attribute [irreducible] beq

2.4.8. General Proofs about Natural Numbers🔗

A (slightly) more interesting theorem:

theorem add_id_example : ∀ n m : Nat, n = m → n + n = m + m := ⊢ ∀ (n m : Nat), n = m → n + n = m + m n:Natm:Nat⊢ n = m → n + n = m + m n:Natm:Nath:n = m⊢ n + n = m + m n:Natm:Nath:n = m⊢ m + m = m + m All goals completed! 🐙

2.4.8.1. Displaying Theorem Statements🔗

The #check command can also be used to examine the statements of previously declared lemmas and theorems.

NatPlayground.Nat.mul_zero (n : Nat) : n * zero = zero#check mul_zero NatPlayground.Nat.mul_succ (n m : Nat) : n * succ m = n * m + n#check mul_succ
NatPlayground.Nat.mul_zero (n : Nat) : n * zero = zero
NatPlayground.Nat.mul_succ (n m : Nat) : n * succ m = n * m + n

Lean displays universally quantified variables as binders before the colon, which is the preferred declaration-header style in Lean.

2.5. Proof by Case Analysis🔗

Sometimes simple calculation and rewriting are not enough...

example (n : Nat) : (succ zero + n == zero) = false := unsolved goals b:MyBooln✝ m:Natc:Colorr:RGBn:Nat⊢ (succ zero + n == zero) = falseby /- We can't rewrite by any lemmas here: `add`'s definition matches on its *second* argument, and here that argument is the unknown `n`! -/

We can use cases to perform case analysis:

theorem add_one_neb_zero (n : Nat) : (succ zero + n == zero) = false := n:Nat⊢ (succ zero + n == zero) = false cases n with ⊢ (succ zero + zero == zero) = false ⊢ false = false All goals completed! 🐙 n':Nat⊢ (succ zero + succ n' == zero) = false n':Nat⊢ false = false All goals completed! 🐙

Another example, using booleans:

theorem not_involutive (b : Bool) : (!!b) = b := b:Bool⊢ (!!b) = b cases b with ⊢ (!!false) = false ⊢ false = false All goals completed! 🐙 ⊢ (!!true) = true ⊢ true = true All goals completed! 🐙

Some of the above proofs use standard library lemmas; later on we will discuss how to search for them yourself.

We can also have nested case analysis:

theorem and_commutative (b c : Bool) : (b && c) = (c && b) := b:Boolc:Bool⊢ (b && c) = (c && b) cases b with c:Bool⊢ (true && c) = (c && true) cases c with ⊢ (true && true) = (true && true) ⊢ true = true All goals completed! 🐙 ⊢ (true && false) = (false && true) ⊢ false = false All goals completed! 🐙 c:Bool⊢ (false && c) = (c && false) cases c with ⊢ (false && true) = (true && false) ⊢ false = false All goals completed! 🐙 ⊢ (false && false) = (false && false) ⊢ false = false All goals completed! 🐙 theorem and3_exchange (b c d : Bool) : ((b && c) && d) = ((b && d) && c) := b:Boolc:Boold:Bool⊢ (b && c && d) = (b && d && c) cases b with c:Boold:Bool⊢ (false && c && d) = (false && d && c) cases c with d:Bool⊢ (false && true && d) = (false && d && true) cases d with ⊢ (false && true && false) = (false && false && true) ⊢ false = (false && true) All goals completed! 🐙 ⊢ (false && true && true) = (false && true && true) ⊢ (false && true) = (false && true) All goals completed! 🐙 d:Bool⊢ (false && false && d) = (false && d && false) cases d with ⊢ (false && false && false) = (false && false && false) ⊢ (false && false) = (false && false) All goals completed! 🐙 ⊢ (false && false && true) = (false && true && false) ⊢ false = (false && false) All goals completed! 🐙 c:Boold:Bool⊢ (true && c && d) = (true && d && c) cases c with d:Bool⊢ (true && true && d) = (true && d && true) cases d with ⊢ (true && true && false) = (true && false && true) ⊢ false = false All goals completed! 🐙 ⊢ (true && true && true) = (true && true && true) ⊢ (true && true) = (true && true) All goals completed! 🐙 d:Bool⊢ (true && false && d) = (true && d && false) cases d with ⊢ (true && false && false) = (true && false && false) ⊢ false = false All goals completed! 🐙 ⊢ (true && false && true) = (true && true && false) ⊢ false = (true && false) All goals completed! 🐙

As you can see, proofs by cases can become very verbose. We will introduce some tactics for writing shorter proofs by case analysis in the Tactics chapter.

2.5.1. New Tactics: rewrite ... at and exact🔗

You will need the rewrite ... at and exact tactics to complete some exercises.

2.5.2. Structural Recursion (Optional)🔗

2.5.3. Binary Numerals🔗

end Nat

2.6. More Exercises🔗

2.6.1. Warmups🔗

2.6.2. Airport Exercise🔗

Source revision: e85fe77, committed 2026-10-06 21:16 UTC