5. Elementary Number Theory

In this chapter, we show you how to formalize some elementary results in number theory. As we deal with more substantive mathematical content, the proofs will get longer and more involved, building on the skills you have already mastered.

5.1. Irrational Roots

5.2. Induction and Recursion

5.2.1. Factorial

Recursive definition of the factorial function.

def fac :     -- no := here
  | 0 => 1
  | n + 1 => (n + 1) * fac n
example : fac 0 = 1 :=
  rfl

example : fac 0 = 1 := by
  rw [fac]

example : fac 0 = 1 := by
  simp [fac]

example (n : ) : fac (n + 1) = (n + 1) * fac n :=
  rfl

example (n : ) : fac (n + 1) = (n + 1) * fac n := by
  rw [fac]

example (n : ) : fac (n + 1) = (n + 1) * fac n := by
  simp [fac]

5.2.2. Prove Induction using induction' n with n ih

Prove a general statement by proving that the statement holds of 0; and if it holds at \(n\), it also holds of \(n + 1\).

induction' n with n ih leads to two goals: 1) 0 < fac 0, 2) assuming ih : 0 < fac n, prove 0 < fac (n + 1).

theorem fac_pos (n : ) : 0 < fac n := by
  -- n defines the fac;
  -- ``with n' ih`` names the variable and the assumption for the inductive hypothesis
  induction' n with n' ih
  · rw [fac]
    exact zero_lt_one
  rw [fac]
  exact mul_pos n'.succ_pos ih

The induction' tactic can include hypotheses (ile in the example below) that depend on the induction variable as part of the induction hypothesis.

theorem dvd_fac {i n : } (ipos : 0 < i) (ile : i  n) : i  fac n := by
  -- ``ile`` depends on ``n``, the induction variable
  induction' n with n ih
  · exact absurd ipos (not_lt_of_ge ile)  -- "ex falso quodlibet"
  rw [fac]
  rcases Nat.of_le_succ ile with h | h  -- ile: i ≤ n i.e. i ≤ n.succ
  · apply dvd_mul_of_dvd_right (ih h)  -- dvd_mul_of_dvd_right takes ih; ih takes h
  · rw [h]; apply dvd_mul_right  -- n.succ = n+1

5.2.3. Finite Sums and Products

Induction is often used to prove identities involving finite sums and products.

Finset.sum s f: s : Finset α is a finite set of elements of the type α and f is a function defined on α.

You need import Algebra.BigOperators.Ring and open BigOperators to use notations like x s, f x x s, f x.

variable {α : Type*} (s : Finset ) (f :   ) (n : )

#check Finset.sum s f
#check Finset.prod s f

open BigOperators
open Finset

example : s.sum f =  x  s, f x :=
  rfl

example : s.prod f =  x  s, f x :=
  rfl

example : (range n).sum f =  x  range n, f x :=
  rfl

example : (range n).prod f =  x  range n, f x :=
  rfl

The facts Finset.sum_range_zero and Finset.sum_range_succ provide a recursive description of summation up to \(n\), and similarly for products.

example (f :   ) :  x  range 0, f x = 0 :=
  Finset.sum_range_zero f

example (f :   ) (n : ) :  x  range n.succ, f x =  x  range n, f x + f n :=
  Finset.sum_range_succ f n

example (f :   ) :  x  range 0, f x = 1 :=
  Finset.prod_range_zero f

example (f :   ) (n : ) :  x  range n.succ, f x = ( x  range n, f x) * f n :=
  Finset.prod_range_succ f n

5.2.4. Nature Number

In Lean’s core library, both the natural numbers and its addition and multiplication are defined using recursion, and their fundamental properties are also established using induction.

inductive Nat where  -- ``Nat`` is a Type defined by induction
  | zero : Nat
  | succ (n : Nat) : Nat


def Nat.add : Nat  Nat  Nat
  | a, Nat.zero   => a
  | a, Nat.succ b => Nat.succ (Nat.add a b)


-- Similar for multiplication
def Nat.mul : Nat  Nat  Nat
  | _, Nat.zero          => Nat.zero
  | a, Nat.succ b => Nat.add (Nat.mul a b) a

Thus:

n + 1
= Nat.add n 1
= Nat.add n (Nat.succ 0)
= Nat.succ (Nat.add n 0)
= Nat.succ n

5.3. Infinitely Many Primes

5.4. More Induction

In Section 5.2, we saw how to define the factorial function by recursion on the natural numbers.

def fac :   
  | 0 => 1
  | n + 1 => (n + 1) * fac n

We also saw how to prove theorems using the induction' tactic.

theorem fac_pos (n : ) : 0 < fac n := by
  induction' n with n ih
  · rw [fac]
    exact zero_lt_one
  rw [fac]
  exact mul_pos n.succ_pos ih

5.4.1. induction

  • The induction tactic (without the prime tick mark) allows for more structured syntax.

example (n : ) : 0 < fac n := by
  induction n
  case zero =>
    rw [fac]
    exact zero_lt_one
  case succ n' ih =>
    rw [fac]
    exact mul_pos n'.succ_pos ih

example (n : ) : 0 < fac n := by
  induction n with
  | zero =>
    rw [fac]
    exact zero_lt_one
  | succ n ih =>
    rw [fac]
    exact mul_pos n.succ_pos ih

5.4.2. More Ways to Define/Use Inductions

We can even prove a theorem (with the same notation) as if we are defining a recursive function.

theorem fac_pos' :  n, 0 < fac n  -- we insert ``∀ n``; also the absence of ``:=``
  | 0 => by  -- by
    rw [fac]
    exact zero_lt_one
  | n + 1 => by  -- by
    rw [fac]
    -- as if the theorem itself is a recursive function of n
    exact mul_pos n.succ_pos (fac_pos' n)  -- we make a recursive call for the inductive step
  • We can also define the Fibonacci function with multiple base cases:

@[simp] def fib :   
  | 0 => 0
  | 1 => 1
  | n + 2 => fib n + fib (n + 1)

The @[simp] annotation means that the simplifier will use the defining equations. You can also apply them by writing rw [fib]. Below it will be helpful to give a name to the n + 2 case.

theorem fib_add_two (n : ) : fib (n + 2) = fib n + fib (n + 1) := rfl

example (n : ) : fib (n + 2) = fib n + fib (n + 1) := by rw [fib]
  • Using Lean’s computational interpretation, we can evaluate the Fibonacci numbers.

#eval fib 6
-- ``|>``: forward pipe operator
-- ``x |> f`` means “take x and apply f to it”, i.e., f x.
#eval List.range 20 |>.map fib  -- ``(List.range 20).map fib`` or ``List.map fib (List.range 20)``

5.4.3. Tail-Recursive

The straightforward implementation of fib is computationally inefficient: It runs in time exponential in its argument.

Tail-Recursive defines recursion with pattern matching, running time is linear in n.

def fib' (n : Nat) : Nat :=
  -- pattern:
  -- aux remaining #recursions, F(0), F(1)
  -- aux remaining #recursions, value we should return, next value we should return
  aux n 0 1  -- aux is just a name, choose whatever you want
where aux  -- ``where`` implements ``aux`` by defining rules for matching patterns
  | 0,   x, _ => x
  | n+1, x, y => aux n y (x + y)  -- after each recursion, we count--, and shift (x, y) to (y, x + y)


def fib'' (n : Nat) : Nat :=
  aux n 0 1 1
where aux
  | 0,   x, _, _ => x
  | 1,   _, y, _ => y
  | n+1, x, y, z => aux n y z (x + y + z)

#eval fib' 10000
#eval fib'' 5

theorem fib'.aux_eq (m n : ) : fib'.aux n (fib m) (fib (m + 1)) = fib (n + m) := by
  induction n generalizing m with  -- ``induction ... with`` will intro hypo ``ih`` in the succ case
  | zero => simp [fib'.aux]
  -- ``generalizing m`` inserts ``∀ m`` in front of ``ih``
  | succ n ih => rw [fib'.aux, fib_add_two, ih, add_assoc, add_comm 1]