Logical Foundations

3. Induction: Proof by Induction🔗

This chapter shows how to carry out proofs by induction, one of the most fundamental reasoning tools in computer science and mathematics, in Lean.

3.1. Separate Compilation🔗

Before getting started on this chapter, we need to import all of our definitions from the previous chapter:

import LF.Basics

For this import to work, Lean needs to be able to find a compiled version of the previous chapter (Basics.lean). This compiled version, called Basics.olean, is analogous to the .class files compiled from .java source files and the .o files compiled from .c files.

When using Lake (Lean's build system), the file lakefile.toml specifies dependencies and build configuration. Running lake build will compile all necessary files in the correct order.

If you are using VS Code with the Lean 4 extension, compilation happens automatically in the background. When you open a file, the extension compiles its dependencies as needed.

Troubleshooting:

  • If you get complaints about missing imports, make sure you have run lake build from the project root directory in a terminal, at least once.

  • If you modify Basics.lean, VS Code will automatically recompile it when you save. You may need to reopen this file or wait for recompilation to finish.

  • If you get errors that seem inconsistent with the source, try running lake clean followed by lake build to recompile everything from scratch.

    (If you are using the Lean 4 extension for VS Code, you can also restart the extension on the current file via the Restart File button in the InfoView. The extension should prompt you to do this if you change things upstream in the dependency tree.)

3.2. Review🔗

We reopen the namespace from the previous chapter to group this chapter's definitions and theorems with the custom natural-number development and keep their names distinct from the standard library.

Now let's review what we learned in Basics using some quiz questions and an exercise.

namespace NatPlayground.Nat
Quiz

Recall the definition of or, which has notation || and is not marked @[irreducible]:

def or (b1 : Bool) (b2 : Bool) : Bool :=
  match b1 with
  | true => true
  | false => b2

To prove the following theorem, which tactics will we need besides rfl?

theorem review₁ : (true || false) = true

(A) none

(B) rewrite

(C) cases

(D) both rewrite and cases

(E) can't be done with the tactics we've seen.

Show solution
theorem review₁ : (true || false) = true := ⊢ (true || false) = true All goals completed! 🐙
Quiz

What about the next one?

theorem review₂ (b : Bool) : (true || b) = true

Which tactics do we need besides rfl?

(A) none

(B) rewrite

(C) cases

(D) both rewrite and cases

(E) can't be done with the tactics we've seen.

Show solution
theorem review₂ (b : Bool) : (true || b) = true := b:Bool⊢ (true || b) = true All goals completed! 🐙
Quiz

What if we change the order of the arguments of ||?

theorem review₃ (b : Bool) : (b || true) = true

Which tactics do we need besides rfl?

(A) none

(B) rewrite

(C) cases

(D) both rewrite and cases

(E) can't be done with the tactics we've seen.

Show solution
theorem review₃ (b : Bool) : (b || true) = true := b:Bool⊢ (b || true) = true cases b with ⊢ (false || true) = true All goals completed! 🐙 ⊢ (true || true) = true All goals completed! 🐙
def add (n : Nat) (m : Nat) : Nat := match m with | zero => n | succ m' => succ (add n m')add_zero : ∀ n : Nat, n + zero = nadd_succ : ∀ n m : Nat, n + (succ m) = succ (n + m)
Quiz

What about this one? Recall that our add function has notation + and is marked @[irreducible].

theorem review₄ (n : Nat) : n + zero = n

(A) none

(B) rewrite

(C) cases

(D) both rewrite and cases

(E) can't be done with the tactics we've seen.

Show solution
theorem review₄ (n : Nat) : n + zero = n := n:Nat⊢ n + zero = n n:Nat⊢ n = n All goals completed! 🐙
Quiz

What about this?

theorem review₅ (n : Nat) : zero + n = n

(A) none

(B) rewrite

(C) cases

(D) both rewrite and cases

(E) can't be done with the tactics we've seen.

Show solution

This one cannot be proved by rfl, cases, or rewriting alone — it needs induction! (We'll see why below.)

Exercise★(succ_eq_add_one)

One more warm-up exercise. Prove the following theorem, using theorems from Basics:

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

3.3. Proof by Induction🔗

We will introduce proofs by induction on natural numbers, first motivating why induction is needed, and then explaining what it is and how you do it in Lean.

3.3.1. Motivation🔗

We defined add to recurse on its second argument:

def add (n : Nat) (m : Nat) : Nat := match m with | zero => n | succ m' => succ (add n m')

For the add_zero simplification rule, we were able to prove that zero is a neutral element for + on the right using just rfl.

theorem add_zero : ∀ (n : Nat), n + zero = n := by
  intro n
  rfl

This worked because n + zero reduces to n by definition. What if we wanted to prove a rule that zero is also a neutral element on the left? Just applying rfl doesn't work, since the n in zero + n is an arbitrary unknown number, so the match in the definition of + can't be reduced.

example (n : Nat) : zero + n = n := n:Nat⊢ zero + n = n Tactic `rfl` failed: The left-hand side zero + n is not definitionally equal to the right-hand side n n:Nat⊢ zero + n = nn:Nat⊢ zero + n = n -- doesn't work here!
Tactic `rfl` failed: The left-hand side
  zero + n
is not definitionally equal to the right-hand side
  n

n:Nat⊢ zero + n = n

And reasoning by cases using cases on n doesn't get us much further: the branch of the case analysis where we assume n = zero goes through just fine, but in the branch where n = n' + 1 for some n' we get stuck in exactly the same way.

example (n : Nat) : zero + n = n := unsolved goals n':Nat⊢ zero + succ n' = succ n'n:Nat⊢ zero + n = n cases n with ⊢ zero + zero = zero /- n = zero -/ ⊢ zero = zero All goals completed! 🐙 -- so far so good... | succ n' => /- n = succ n' -/ _ -- ...but we're stuck on zero + n'
unsolved goals
n':Nat⊢ zero + succ n' = succ n'

We could use cases on n' to get a bit further, but, since n can be arbitrarily large, we'll never get all the way there if we just go on like this.

3.3.2. Induction: In Principle and in Lean🔗

To prove interesting facts about numbers, lists, and other inductively defined sets, we often need a more powerful reasoning principle: induction.

Recall (from a discrete math course, probably) the principle of induction over natural numbers: If P(n) is some proposition involving a natural number n and we want to show that P holds for all numbers n, we can reason like this:

  • show that P(zero) holds;

  • show that, for any n', if P(n') holds, then so does P(succ n');

  • conclude that P(n) holds for all n.

In Lean, the steps are the same: we begin with the goal of proving P(n) for all n and use the induction tactic to break it down into two separate subgoals: one where we must show P(zero) and another where we must show P(n') → P(succ n'). Here's how this works for the theorem at hand...

theorem zero_add (n : Nat) : zero + n = n := n:Nat⊢ zero + n = n induction n with ⊢ zero + zero = zero /- n = zero -/ ⊢ zero = zero All goals completed! 🐙 n':Natih:zero + n' = n'⊢ zero + succ n' = succ n' /- n = succ n' -/ /- Goal: zero + (succ n') = succ n' We can rewrite `zero + (succ n')` to `succ (zero + n')`. Then we can rewrite with the induction hypothesis. -/ n':Natih:zero + n' = n'⊢ succ n' = succ n' All goals completed! 🐙

Like cases, the induction tactic takes a with clause that specifies the names of the variables to be introduced in the subgoals. Since there are two subgoals (for zero and succ), the with clause has two branches.

In the first subgoal, n is replaced by zero. The goal becomes zero + zero = zero, which follows by rewrite [add_zero] and rfl.

In the second subgoal, n is replaced by succ n', and the induction hypothesis ih : zero + n' = n' is added to the context. The goal becomes zero + (succ n') = succ n'. add_succ tells us that a + (succ b) = succ (a + b), so rewrite [add_succ] transforms the goal to succ (zero + n') = succ n'. Then rewrite [ih] rewrites zero + n' to n', and the goal becomes succ n' = succ n', which closes with reflexivity.

Here's another theorem to try, this time involving equality on natural numbers.

theorem beq_self (n : Nat) : (n == n) = true := n:Nat⊢ (n == n) = true induction n with ⊢ (zero == zero) = true ⊢ true = true All goals completed! 🐙 n':Natih:(n' == n') = true⊢ (succ n' == succ n') = true n':Natih:(n' == n') = true⊢ (n' == n') = true All goals completed! 🐙
Exercise★★(basic_induction)

Prove the following using induction. You might need previously proven results.

theorem declaration uses `sorry`zero_mul (n : Nat) : zero * n = zero := n:Nat⊢ zero * n = zero All goals completed! 🐙 theorem declaration uses `sorry`succ_add (n m : Nat) : (succ n) + m = succ (n + m) := n:Natm:Nat⊢ succ n + m = succ (n + m) All goals completed! 🐙 theorem declaration uses `sorry`add_comm (n m : Nat) : n + m = m + n := n:Natm:Nat⊢ n + m = m + n All goals completed! 🐙 theorem declaration uses `sorry`add_assoc (n m p : Nat) : n + (m + p) = (n + m) + p := n:Natm:Natp:Nat⊢ n + (m + p) = n + m + p All goals completed! 🐙

3.3.3. Tip: The rw Tactic🔗

As you've probably noticed, a common pattern in Lean proofs is rewrite [...] followed by rfl. Lean also provides a tactic that combines these two steps: rw [...] will automatically close the goal if the rewrite makes the goal true by definition. For example, instead of

rewrite [double_zero]; rfl

we could write this:

rw [double_zero]

One small caveat: rw [...] only performs a quick reflexivity check after rewriting; it does not unfold every definition. So, in some cases, rw may leave a goal that can actually be solved immediately by rfl. For example, rw does not unfold the definition of aliasOfTwo in the following example, and thus needs an explicit rfl.

def aliasOfTwo := two example (n : Nat) (h : n = aliasOfTwo) : n = two := n:Nath:n = aliasOfTwo⊢ n = two n:Nath:n = aliasOfTwo⊢ aliasOfTwo = two /- The remaining goal is `aliasOfTwo = two`. -/ All goals completed! 🐙

Let's get some practice with using rw.

set_option pp.fieldNotation false
Exercise★★(double_add)

Consider the following function, which doubles its argument:

def double (n : Nat) : Nat := match n with | zero => zero | succ n' => succ (succ (double n')) theorem double_zero : double zero = zero := ⊢ double zero = zero All goals completed! 🐙 theorem double_succ n : double (succ n) = succ (succ (double n)) := n:Nat⊢ double (succ n) = succ (succ (double n)) All goals completed! 🐙 attribute [irreducible] double

Use induction to prove this simple fact about double. Try using rw instead of rewrite.

theorem declaration uses `sorry`double_add (n : Nat) : double n = n + n := n:Nat⊢ double n = n + n All goals completed! 🐙

3.4. Proofs Within Proofs🔗

In Lean, as in informal mathematics, large proofs are often broken into sequences of theorems, with later proofs referring to earlier theorems. But sometimes a proof will involve some miscellaneous fact that is too trivial and of too little general interest to bother giving it its own top-level name. In such cases, it is convenient to simply state and prove the required fact "in place." The have tactic allows us to do this.

theorem mul_zero_add' (n m : Nat) : ((zero + n) + zero) * m = n * m := n:Natm:Nat⊢ (zero + n + zero) * m = n * m n:Natm:Nath:zero + n + zero = n⊢ (zero + n + zero) * m = n * m All goals completed! 🐙

The have tactic introduces a local lemma into the proof. We prove it immediately, and it's available as a hypothesis for the rest of the proof.

As another example, suppose we want to prove that (n + m) + (p + q) = (m + n) + (p + q). The only difference between the two sides of the = is that the arguments m and n to the first inner + are swapped, so it seems we should be able to use the commutativity of addition (add_comm) to rewrite one into the other. However, the rw tactic is not very smart about where it applies the rewrite. There are three uses of + here, and rw [add_comm] may choose the wrong one...

example (n m p q : Nat) : (n + m) + (p + q) = (m + n) + (p + q) := unsolved goals n m p q:Nat⊢ p + q + (n + m) = m + n + (p + q)n:Natm:Natp:Natq:Nat⊢ n + m + (p + q) = m + n + (p + q) /- We just need to swap (n + m) for (m + n)... seems like add_comm should do the trick! But `rw [add_comm]` might rewrite the wrong `+`! -/ n:Natm:Natp:Natq:Nat⊢ p + q + (n + m) = m + n + (p + q)
unsolved goals
n m p q:Nat⊢ p + q + (n + m) = m + n + (p + q)

To use add_comm at the point where we need it, we can supply explicit arguments: rw [add_comm n m] tells Lean exactly which + to rewrite. (We can also use have to establish the specific equation we want, then rewrite with it.)

theorem add_rearrange (n m p q : Nat) : (n + m) + (p + q) = (m + n) + (p + q) := n:Natm:Natp:Natq:Nat⊢ n + m + (p + q) = m + n + (p + q) All goals completed! 🐙

3.5. Formal vs. Informal Proof🔗

"Informal proofs are algorithms; formal proofs are code."

What constitutes a successful proof of a mathematical claim?

The question has challenged philosophers for millennia, but a rough and ready answer could be this: A proof of a mathematical proposition P is a text that instills in the reader the certainty that P is true. That is, a proof is an act of communication.

Acts of communication may involve different sorts of readers. On one hand, the reader can be a program like Lean, in which case the "belief" that is instilled is that P can be mechanically derived from a certain set of formal logical rules, and the proof is a recipe that guides the program in checking this fact. Such recipes are formal proofs.

Alternatively, the reader can be a human being, in which case the proof will probably be written in English or some other natural language and will thus necessarily be informal. Here, the criteria for success are less clearly specified. A "valid" proof is one that makes the reader believe P. But the same proof may be read by many different readers, some of whom may be convinced by a particular way of phrasing the argument, while others may not be. Some readers may be unfamiliar with the area and need the argument spelled out in detail. Other readers, more familiar with the area, may find that extra detail makes it harder to follow the argument; all they want is to be told the main ideas, since it is easier for them to fill in the details for themselves than to wade through a written presentation of them. Ultimately, there is no universal standard, because there is no single way of writing an informal proof that will convince every conceivable reader.

In practice, mathematicians have developed a rich set of conventions and idioms for writing about complex mathematical objects that — at least within a certain community — make communication pretty reliable. The conventions of this stylized form of communication give a reasonably clear standard for judging proofs good or bad.

Because we are using Lean in this course, we will be working heavily with formal proofs. But this doesn't mean we can completely forget about informal ones! Formal proofs are useful in many ways, but they are typically not the most efficient ways of communicating ideas between human beings.

For example, here is a proof that addition is associative (you might have written something like it yourself, recently...):

theorem add_assoc' (n m p : Nat) : n + (m + p) = (n + m) + p := n:Natm:Natp:Nat⊢ n + (m + p) = n + m + p induction p with n:Natm:Nat⊢ n + (m + zero) = n + m + zero All goals completed! 🐙 n:Natm:Natp':Natih:n + (m + p') = n + m + p'⊢ n + (m + succ p') = n + m + succ p' All goals completed! 🐙

Lean is perfectly happy with this. For a human, however, it is difficult to make much sense of it. We can pass arguments to the add_succ theorem to show the structure more clearly...

theorem add_assoc'' (n m p : Nat) : add n (add m p) = add (add n m) p := n:Natm:Natp:Nat⊢ n + (m + p) = n + m + p induction p with n:Natm:Nat⊢ n + (m + zero) = n + m + zero /- p = zero -/ All goals completed! 🐙 n:Natm:Natp':Natih:n + (m + p') = n + m + p'⊢ n + (m + succ p') = n + m + succ p' /- p = succ p', in other words p = p' + 1 -/ All goals completed! 🐙

... and if you're used to Lean you might be able to step through the tactics one after the other in your mind and imagine the state of the context and goal stack at each point, but, if the proof were even a little bit more complicated, this would be next to impossible.

On paper, a (somewhat pedantic) mathematician might write the proof like this:

  • Theorem: For any n, m, and p,

  n + (m + p) = (n + m) + p.

Proof: By induction on p.

  • First, suppose p = zero. We must show that

  n + (m + zero) = (n + m) + zero.

This follows directly from the definition of + (since x + zero = x for any x).

  • Next, suppose p = p' + 1 (i.e., p = succ p'), where

  n + (m + p') = (n + m) + p'.

We must now show that

  n + (m + (p' + 1)) = (n + m) + (p' + 1).

By definition of +, both sides rewrite (via add_succ) to

  (n + (m + p')) + 1   and   ((n + m) + p') + 1

respectively, which are equal by the induction hypothesis. QED.

The overall form of the formal and informal proofs is basically similar, and of course this is no accident: Lean has been designed so that its induction tactic generates the same sub-goals, in the same order, as the bullet points that a mathematician would usually write. But there are significant differences of detail: the formal proof is much more explicit in some ways (e.g., the sequence of rewrites) and less explicit in others. In particular, the "proof state" at any given point in the Lean proof is completely implicit, whereas the informal proof reminds the reader several times where things stand.

Exercise★★(add_comm_informal) (Advanced, Optional, Manually graded)

Translate your solution for add_comm into an informal proof:

Theorem: Addition is commutative.

Proof: ...

Exercise★★(beq_refl_informal) (Optional, Manually graded)

Write an informal proof of the following theorem, using the informal proof of add_assoc as a model. Don't just paraphrase the Lean tactics into English!

Theorem: (n == n) = true for any n.

Proof:

3.6. Aside: Using Code Actions to Generate Match Skeletons🔗

Lean's language server can suggest code actions, which are small editor commands that modify the source code.

In VS Code, a lightbulb icon appears on the left when a code action is available at your cursor.

You can click the icon or open the code action menu with Ctrl + . on Windows/Linux or Command + . on macOS. For more information, see the Lean 4 VSCode extension manual.

For example, code actions can generate the explicit branches needed for pattern matching. This can be especially useful when working with match expressions or with tactics such as cases and induction, which we saw earlier in the book.

Let's look at a code action for induction. Suppose we start with the following incomplete proof:

example (n : Nat) : Nat.beq n n = true := unsolved goals ⊢ (zero == zero) = true n✝:Natn_ih✝:(n✝ == n✝) = true⊢ (succ n✝ == succ n✝) = truen:Nat⊢ (n == n) = true ⊢ (zero == zero) = truen✝:Natn_ih✝:(n✝ == n✝) = true⊢ (succ n✝ == succ n✝) = true

Put your cursor on induction n and open the code action menu.

You should see "Generate an explicit pattern match for 'induction'." in the list. If you choose this action, Lean adds an explicit branch for each constructor:

declaration uses `sorry`example (n : Nat) : Nat.beq n n = true := n:Nat⊢ (n == n) = true induction n with ⊢ (zero == zero) = true All goals completed! 🐙 n:Natih:(n == n) = true⊢ (succ n == succ n) = true All goals completed! 🐙

This gives us the basic structure of the proof without requiring us to write each branch by hand. We can then focus on proving each case.

One possible proof is the following.

example (n : Nat) : Nat.beq n n = true := n:Nat⊢ (n == n) = true induction n with ⊢ (zero == zero) = true All goals completed! 🐙 n:Natih:(n == n) = true⊢ (succ n == succ n) = true All goals completed! 🐙

The same trick also works for match expressions. For example, suppose we start with

def isZero (n : Nat) : Bool := match n unexpected end of input; expected 'with'

Lean can generate the missing branches:

def isZero (n : Nat) : Bool := match n with | .zero => don't know how to synthesize placeholder context: n:Nat⊢ Bool_ | .succ n => don't know how to synthesize placeholder context: n✝ n:Nat⊢ Bool_

Now you just have to replace the holes _ with your definition. You can use code actions freely to fill out induction, case, and match branches while working with this book.

One note: Sometimes the variables the code action chooses are not ideal, so you might want to change them.

For example, here is what we get from the code action for add_comm

theorem declaration uses `sorry`add_comm' (n m : Nat) : n + m = m + n := n:Natm:Nat⊢ n + m = m + n induction m with n:Nat⊢ n + zero = zero + n All goals completed! 🐙 n✝:Natn:Natih:n✝ + n = n + n✝⊢ n✝ + succ n = succ n + n✝ All goals completed! 🐙 -- bad choice of variable `n`, want `m` or `m'` !

Notice that the action chose n for the succ case, even though we are inducting on m. Manually updating this variable to either m or m' will make your proof easier to read.

3.7. More Exercises🔗

Exercise★(mul_one)
theorem declaration uses `sorry`mul_one (p : Nat) : one * p = p := p:Nat⊢ one * p = p All goals completed! 🐙

By default, rewrite and rw rewrite left to right, i.e., they transform the goal (or a hypothesis) from the form on the left side of the equality to the right side. To rewrite from right to left, use rewrite [← h] or rw [← h], where ← is entered as \l or \<-.

Exercise★★(mul_two)
theorem declaration uses `sorry`mul_two (p : Nat) : two * p = p + p := p:Nat⊢ two * p = p + p All goals completed! 🐙
Exercise★★★(mul_comm)

Use have (or rw with explicit arguments) to help prove add_shuffle3. You don't need to use induction.

theorem declaration uses `sorry`add_shuffle3 (n m p : Nat) : n + m + p = n + p + m := n:Natm:Natp:Nat⊢ n + m + p = n + p + m All goals completed! 🐙 theorem declaration uses `sorry`succ_mul (m n : Nat) : (succ n) * m = (n * m) + m := m:Natn:Nat⊢ succ n * m = n * m + m All goals completed! 🐙

Now prove commutativity of multiplication.

theorem declaration uses `sorry`mul_comm (m n : Nat) : m * n = n * m := m:Natn:Nat⊢ m * n = n * m All goals completed! 🐙
Exercise★★★(more_exercises) (Optional)

Take a piece of paper. For each of the following theorems, first think about whether (a) it can be proved using only simplification and rewriting, (b) it also requires case analysis (cases), or (c) it also requires induction. Write down your prediction. Then fill in the proof. (There is no need to turn in your piece of paper; this is just to encourage you to reflect before you hack!)

theorem declaration uses `sorry`ble_refl (n : Nat) : Nat.ble n n = true := n:Nat⊢ ble n n = true All goals completed! 🐙 theorem declaration uses `sorry`andb_false (b : Bool) : (b && false) = false := b:Bool⊢ (b && false) = false All goals completed! 🐙 theorem declaration uses `sorry`all3_spec (b c : Bool) : ((b && c) || ((!b) || (!c))) = true := b:Boolc:Bool⊢ (b && c || (!b || !c)) = true All goals completed! 🐙 theorem declaration uses `sorry`right_distrib (n m p : Nat) : (n + m) * p = (n * p) + (m * p) := n:Natm:Natp:Nat⊢ (n + m) * p = n * p + m * p All goals completed! 🐙 theorem declaration uses `sorry`left_distrib (n m p : Nat) : p * (n + m) = (p * n) + (p * m) := n:Natm:Natp:Nat⊢ p * (n + m) = p * n + p * m All goals completed! 🐙 theorem declaration uses `sorry`mul_assoc (n m p : Nat) : n * (m * p) = (n * m) * p := n:Natm:Natp:Nat⊢ n * (m * p) = n * m * p All goals completed! 🐙

3.8. A New Tactic Combinator: <;>🔗

Before moving on to the next batch of exercises, let's introduce a simple tactic combinator. A tactic combinator combines tactics to form a larger tactic.

If t₁ and t₂ are tactics, then t₁ <;> t₂ means: first run t₁, then run t₂ on every subgoal produced by t₁.

This is useful when the first tactic splits the goal into several subgoals and all of them can be finished by the second.

example (b : Bool) : (b || true) = true := b:Bool⊢ (b || true) = true ⊢ (false || true) = true⊢ (true || true) = true ⊢ (false || true) = true⊢ (true || true) = true All goals completed! 🐙

This is short for:

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

We can also chain <;>s. In the next example, cases on b creates two goals; in each of them, cases on c splits the goal again; then rfl solves all four remaining goals.

example (b c : Bool) : (b && c) = (c && b) := b:Boolc:Bool⊢ (b && c) = (c && b) c:Bool⊢ (false && c) = (c && false)c:Bool⊢ (true && c) = (c && true) c:Bool⊢ (false && c) = (c && false)c:Bool⊢ (true && c) = (c && true) ⊢ (true && false) = (false && true)⊢ (true && true) = (true && true) ⊢ (false && false) = (false && false)⊢ (false && true) = (true && false)⊢ (true && false) = (false && true)⊢ (true && true) = (true && true) All goals completed! 🐙

For the moment, you should use <;> only when the generated subgoals really do have the same proof. If different branches need different arguments, it is usually clearer to write the cases explicitly. We'll discuss some other tactic combinators in the Automation chapter.

3.9. Nat to Bin and Back🔗

namespace NatToBin

Recall the Bin type we defined in Basics:

inductive Bin : Type where | z | b0 (n : Bin) | b1 (n : Bin)

Before you start working on the next exercise, replace the stub definitions of incr and binToNat, below, with your solution from Basics, so that this file can be graded on its own.

def declaration uses `sorry`incr (m : Bin) : Bin := sorry theorem declaration uses `sorry`incr_z : incr .z = .b1 .z := sorry theorem declaration uses `sorry`incr_b0 m : incr (.b0 m) = .b1 m := sorry theorem declaration uses `sorry`incr_b1 m : incr (.b1 m) = .b0 (incr m) := sorry def declaration uses `sorry`binToNat (m : Bin) : Nat := sorry theorem declaration uses `sorry`binToNat_z : binToNat .z = zero := sorry theorem declaration uses `sorry`binToNat_b0 m : binToNat (.b0 m) = mul (binToNat m) two := sorry theorem declaration uses `sorry`binToNat_b1 m : binToNat (.b1 m) = add (mul (binToNat m) two) one := sorry
attribute [pp_nodot] Bin.b0 Bin.b1

In Basics, we did some unit testing of binToNat, but we didn't prove its correctness. Now we'll do so.

Exercise★★★(binary_commute)

Prove that the following diagram commutes — that is, incrementing a binary number and then converting it to a (standard, unary) natural number yields the same result as first converting it to a natural number and then incrementing:

                      incr
          Bin ------------------------> Bin
           |                             |
binToNat   |                             |  binToNat
           |                             |
           v                             v
          Nat ------------------------> Nat
                      succ

If you want to change your previous definitions of incr or binToNat to make the property easier to prove, feel free!

theorem declaration uses `sorry`bin_to_nat_pres_incr (b : Bin) : binToNat (incr b) = (binToNat b) + one := b:Bin⊢ binToNat (incr b) = binToNat b + one All goals completed! 🐙
Exercise★★★(nat_bin_nat)

Write a function to convert natural numbers to binary numbers. Also write some simplification lemmas for it.

def declaration uses `sorry`natToBin (n : Nat) : Bin := sorry -- FILL IN HERE -- FILL IN HERE unexpected end of input

Prove that, if we start with any Nat, convert it to Bin, and convert it back, we get the Nat that we started with.

Hint: This proof should go through smoothly using the previous exercise about incr as a lemma. If not, revisit your definitions of the functions involved and consider whether they are more complicated than necessary: the shape of a proof by induction will match the recursive structure of the program being verified, so make the recursion as simple as possible.

theorem declaration uses `sorry`nat_bin_nat (n : Nat) : binToNat (natToBin n) = n := n:Nat⊢ binToNat (natToBin n) = n All goals completed! 🐙

3.10. Bin to Nat and Back (Advanced)🔗

The opposite direction — starting with a Bin, converting to Nat, then converting back to Bin — turns out to be problematic: the expected "theorem" does not hold.

example (b : Bin) : natToBin (binToNat b) = b := unsolved goals b:Bin⊢ natToBin (binToNat b) = bby

Let's explore why it fails and how to prove a modified version of it. We'll start with some lemmas that might seem unrelated but will turn out to be relevant.

Exercise★★(double_bin) (Advanced)

Prove this lemma about double, which we defined earlier in the chapter.

theorem declaration uses `sorry`double_incr (n : Nat) : double (succ n) = (double n) + two := n:Nat⊢ double (succ n) = double n + two All goals completed! 🐙

Now define a similar doubling function for Bin.

def declaration uses `sorry`doubleBin (b : Bin) : Bin := sorry

Fill in the characterizing lemmas for this definition below:

-- FILL IN HERE -- FILL IN HERE unexpected end of input

Check that your function correctly doubles zero.

theorem declaration uses `sorry`double_bin_zero : doubleBin .z = .z := sorry

Prove this lemma, which corresponds to double_incr.

theorem declaration uses `sorry`double_incr_bin (b : Bin) : doubleBin (incr b) = incr (incr (doubleBin b)) := b:Bin⊢ doubleBin (incr b) = incr (incr (doubleBin b)) All goals completed! 🐙

Let's return to our desired theorem:

example (b : Bin) : natToBin (binToNat b) = b := unsolved goals b:Bin⊢ natToBin (binToNat b) = bby

The theorem fails because there are some Bins for which we won't necessarily get back to the original Bin, but instead to an "equivalent" Bin. (We deliberately leave this notion informal here so that you can think about it.)

Explain in a comment, below, why this failure occurs. Your explanation will not be graded, but it's important that you get it clear in your mind before going on to the next part. If you're stuck on this, think about alternative implementations of doubleBin that might have failed to satisfy double_bin_zero yet otherwise seem correct.

To solve this problem, we can introduce a normalization function that selects the simplest Bin out of all the equivalent Bins. Then we can prove that the conversion from Bin to Nat and back again produces that normalized, simplest Bin.

Exercise★★★★(bin_nat_bin) (Advanced)

Define normalize. Keep its definition as simple as possible so that later proofs go through smoothly. Do not use binToNat or natToBin, but do use doubleBin.

Hint: Structure the recursion such that it always reaches the end of the Bin and only processes each bit once. Do not try to "look ahead" at future bits, as this will complicate the proof.

def declaration uses `sorry`normalize (b : Bin) : Bin := sorry

Also specify the characterizing lemmas for this definition:

-- FILL IN HERE -- FILL IN HERE unexpected end of input

Next, it would be a good idea to do some example proofs to check that your definition of normalize works the way you intend before you proceed. They won't be graded, but do fill in a few below.

-- FILL IN HERE -- FILL IN HERE unexpected end of input

Now that we have defined all of our functions and their characterizing lemmas, we mark the definitions irreducible as usual. From here on, proofs about these definitions should use rewrite or rw, not rfl.

attribute [irreducible] normalize doubleBin natToBin incr binToNat

Finally, prove the main theorem. The inductive cases could be a bit tricky.

Hint: Start by trying to prove the main statement, see where you get stuck, and see if you can find a lemma — perhaps requiring its own inductive proof — that will allow the main proof to make progress. We have one lemma for the b0 case (which also makes use of double_incr_bin) and another for the b1 case.

-- FILL IN HERE -- FILL IN HERE theorem declaration uses `sorry`bin_nat_bin (b : Bin) : natToBin (binToNat b) = normalize b := b:Bin⊢ natToBin (binToNat b) = normalize b All goals completed! 🐙
end NatToBin end NatPlayground.Nat
Source revision: e85fe77, committed 2026-10-06 21:16 UTC