It would be convenient to declare the variables below so that inline
prose throughout this chapter can use n, n', m, m', k, α,
x, y, l, l₁, l₂, and l₃ without repeating their type
annotations, but the same problem described in Logic
applies: an unused variable is silently added to the local context
in basically every proof from here on, even when the theorem never
mentions it, which makes theorem hover-overs in the HTML book
unusable. Until we have a way to declare variables visible only for
inline prose (rather than for every lean block), we leave this
commented out:
-- variable
-- (n n' m m' k : Nat)
-- (α : Type)
-- (x y : α)
-- (l l₁ l₂ l₃ : List α)
Next, we look at what happens when we repeatedly apply collatzStep to
some given starting number. For example, collatzStep12 is 6, and
collatzStep6 is 3, so by repeatedly applying collatzStep we get the
sequence 12, 6, 3, 10, 5, 16, 8, 4, 2, 1.
Similarly, if we start with 19, we get the longer sequence 19,
58, 29, 88, 44, 22, 11, 34, 17, 52, 26, 13, 40, 20, 10, 5, 16, 8,
4, 2, 1.
Both of these sequences eventually reach 1. The question posed
by Collatz was: Is the sequence starting from any positive
natural number guaranteed to reach 1 eventually?
To formalize this question in Lean, we might try to define a
recursive function that calculates the total number of steps
that it takes for such a sequence to reach 1. You can write
this definition in a standard programming language, but it is
rejected by Lean's termination checker, since the argument to
the recursive call, collatzStep n, is not "obviously smaller" than n.
deffail to show termination forreaches1Inwith errorsfailed to infer structural recursion:Cannot use parameter n:failed to eliminate recursive applicationreaches1In(collatzStepn)failed to prove termination, possible solutions: - Use `have`-expressions to prove the remaining goals - Use `termination_by` to specify a different well-founded relation - Use `decreasing_by` to specify your own tactic for discharging this kind of goaln:Nat⊢ collatzStepn<nreaches1In(n:Nat):Nat:=bifn==1then0else1+reaches1In(collatzStepn)
fail to show termination forreaches1Inwith errorsfailed to infer structural recursion:Cannot use parameter n:failed to eliminate recursive applicationreaches1In(collatzStepn)failed to prove termination, possible solutions: - Use `have`-expressions to prove the remaining goals - Use `termination_by` to specify a different well-founded relation - Use `decreasing_by` to specify your own tactic for discharging this kind of goaln:Nat⊢ collatzStepn<n
Indeed, this isn't just a pointless limitation: functions in Lean
are required to be total, to ensure logical consistency.
Moreover, we can't fix it by devising a more clever termination
checker: deciding whether this particular function is total
would be equivalent to settling the Collatz conjecture!
Another idea could be to express the concept "eventually reaches
1 in the Collatz sequence" as a recursively defined property
of numbers CollatzHoldsFor : Nat → Prop. This is also rejected
by the termination checker. In principle, we could convince Lean
that div2 n is smaller than n by supplying an
appropriate proof. However, we still can't convince it that
(3 * n) + 1 is smaller than n!
deffail to show termination forCollatzHoldsForwith errorsfailed to infer structural recursion:Cannot use parameter n:failed to eliminate recursive applicationCollatzHoldsFor(div2n)failed to prove termination, possible solutions: - Use `have`-expressions to prove the remaining goals - Use `termination_by` to specify a different well-founded relation - Use `decreasing_by` to specify your own tactic for discharging this kind of goalnx✝:Nat⊢ div2n<x✝CollatzHoldsFor(n:Nat):Prop:=matchnwith|0=>False|1=>True|_=>bifn.eventhenCollatzHoldsFor(div2n)elseCollatzHoldsFor((3*n)+1)
fail to show termination forCollatzHoldsForwith errorsfailed to infer structural recursion:Cannot use parameter n:failed to eliminate recursive applicationCollatzHoldsFor(div2n)failed to prove termination, possible solutions: - Use `have`-expressions to prove the remaining goals - Use `termination_by` to specify a different well-founded relation - Use `decreasing_by` to specify your own tactic for discharging this kind of goalnx✝:Nat⊢ div2n<x✝
Fortunately, there is another way to do it: we can express the
concept "reaches 1 eventually in the Collatz sequence" as an
inductively defined property of numbers. Intuitively, this
property is defined by a set of rules:
So there are three ways to prove that a number n eventually
reaches 1 in the Collatz sequence:
n is 1;
n is even and div2 n eventually reaches 1;
n is odd and (3 * n) + 1 eventually reaches 1.
We can prove that a number reaches 1 by constructing a (finite)
derivation using these rules. For instance, here is the
derivation proving that 12 reaches 1
(where we leave out the evenness/oddness premises):
For particular numbers, we can now prove that the Collatz
sequence reaches 1 (we'll look more closely at how it works a
bit later in the chapter). Each step applies a rule and
discharges the boolean evenness premise by rfl; the recursive
premise is then reduced by the kernel from
CollatzHoldsFor(div212) to CollatzHoldsFor6, etc.
The Collatz conjecture then states that the sequence beginning
from any positive number reaches 1:
defCollatz:=∀n:Nat,n≠0→CollatzHoldsForn
If you succeed in proving this conjecture, you've got a bright
future as a number theorist! But don't spend too long on it —
it's been open since 1937.
Note to developers (Chris Henson @chenson2018)
We may want to add an exercise later proving false if one assumes
Collatz' conjecture without the n ≠ 0 assumption. We had that
mistake in the script for years and no one noticed, wow!
9.1.2. Example: Binary Relation for Comparing Numbers🔗
A binary relation on a set α has Lean type α → α → Prop.
This is a family of propositions parameterized by two elements
of α — i.e., a proposition about pairs of elements of α.
For example, one familiar binary relation on Nat is
Le : Nat → Nat → Prop, the less-than-or-equal-to relation,
which can be inductively defined by the following two rules:
─────── (le_refl)
Le n n
Le n m
──────────── (le_step)
Le n (m + 1)
Another example: the transitive closure of a relation r is the
smallest relation that contains r and that is transitive. This can
be defined by the following two rules:
r x y
─────────────── (t_step)
TransGen r x y
TransGen r x y TransGen r y z
──────────────────────────────────── (t_trans)
TransGen r x z
As another example, the reflexive and transitive closure of a
relation r is the smallest relation that contains r and that is
reflexive and transitive. This can be defined by the following three
rules (where we added a reflexivity rule to TransGen):
r x y
——————————————————————— (step)
ReflTransGen r x y
——————————————————————— (refl)
ReflTransGen r x x
ReflTransGen r x y ReflTransGen r y z
—————————————————————————————————————————————— (trans)
ReflTransGen r x z
For instance, this enables an equivalent definition of the Collatz
conjecture. First we define a binary relation corresponding to
the "Collatz step function" collatzStep:
This Collatz step relation can be used in conjunction with the
reflexive and transitive closure operation to define a Collatz
multi-step relation, expressing that a number n
reaches another number m in zero or more Collatz steps:
The familiar mathematical concept of permutation also has an
elegant formulation as an inductive relation. For simplicity,
let's focus on permutations of lists with exactly three
elements.
We can define such permutations by the following rules:
We've already seen two ways of stating a proposition that a number
n is even: We can say
(1) Nat.even n = true (using the recursive boolean function Nat.even), or
(2) ∃ k, n = Nat.double k (using an existential quantifier).
A third possibility, which we'll use as a simple running example
in this chapter, is to say that a number is even if we can
establish its evenness from the following two rules:
────────── (zero)
Even 0
Even n
—————————————— (succ_succ)
Even (n + 2)
To illustrate how this new definition of evenness works, let's
imagine using it to show that 4 is even:
──────── (zero)
Even 0
─────────────────────── (succ_succ)
Even (.succ (.succ 0))
────────────────────────────────────────────── (succ_succ)
Even (.succ (.succ (.succ (.succ 0))))
We can translate the informal definition of evenness from above
into a formal inductive declaration, where each "way that a
number can be even" corresponds to a separate constructor:
There are both similarities and a few differences between
inductive properties like Even and the inductive types like
Nat or List that we have been using throughout the course:
inductive`List` has already been declaredList(α:Type):Typewhere|nil:Listα|cons(x:α)(l:Listα):Listα
The most important difference is that the constructors of Even,
Even.zero and Even.succ_succ, yield different types
(Even0 and Even (n + 2)), whereas the List
constructors both build List α values.
We can think of the inductive definition of Even as defining a
Lean property Even : Nat → Prop, together with two "evidence
constructors":
These evidence constructors can be thought of as "primitive evidence
of evenness," and they can be used later on just like proven theorems.
In particular, we can use Lean's apply and exact
tactics with the constructor names to obtain evidence for Even of
particular numbers...
And again we can equivalently use function application syntax to
combine several constructors. (Note that the Lean type checker can
infer not only types, but also Nats and Lists,
when they are clear from the context.)
So the informal derivation trees we drew above are not too far
from what's happening formally. Formally, we're using the evidence
constructors to build evidence trees, similar to the finite trees we
built using the constructors of data types such as Nat,
List, binary trees, etc.
Besides constructing evidence that numbers are even, we can also
destruct such evidence, reasoning about how it could have been
built — i.e., we can introduce and eliminateEven
evidence, in the sense of Logic.
Defining Even with an inductive declaration tells Lean not
only that the constructors Even.zero and Even.succ_succ
are valid ways to build evidence that some number is Even,
but also that these two constructors are the only ways to build
evidence that numbers are Even.
In other words, if someone gives us evidence e for the proposition
Even n, then we know that e must be one of two things:
e = Even.zero and n = 0, or
e = Even.succ_succ n' e' and n = n' + 2, where e' is
evidence for Even n'.
This suggests that it should be possible to do case
analysis and even induction on evidence of evenness...
Facts like this are often called "inversion lemmas" because they
allow us to "invert" some given information to reason about all
the different ways it could have been derived.
Quiz
Which tactics are needed to prove this goal?
∀ (n : Nat), Even n → n = 1 → true = false
(A) cases
(B) contradiction
(C) Both cases and contradiction
(D) These tactics are not sufficient to solve the goal.
For the inductively defined propositions we use,
inversion behaves much like cases:
it performs case analysis on the constructors of the hypothesis's inductive type.
However, when the case analysis on an indexed proposition gives unsolvable equations
between its indices, cases itself fails, whereas inversion leaves
such equations in the context.
Let's try to show that our new notion of evenness implies
our earlier notion (the one based on Nat.double).
example(n:Nat)(h:Evenn):Nat.Evenn:=byn:Nath:Evenn⊢ n.Evenunsolved goalssucc_succn':Nath':Evenn'⊢ (n'+2).Eveninversionhwith|zero=>exists0All goals completed! 🐙-- The first case can be solved trivially.|succ_succn'h'=>unexpected end of input; expected '{'
Unfortunately, the second case is harder. We need to show
∃ n₀, n' + 2 = double n₀, but the only available assumption is
h', which states that Even n' holds.
In other words, what we need here is precisely the result we
are trying to prove, but applied to the smaller evidence h'.
If this story feels familiar, it is no coincidence: we
encountered similar problems in the Induction chapter,
when trying to use case analysis to prove results that required
induction. And once again the solution is... induction!
Let's try proving that lemma again:
theoremEven.nat_even(n:Nat)(h:Evenn):Nat.Evenn:=byn:Nath:Evenn⊢ n.Eveninductionhwith|zero=>zeron:Nat⊢ Nat.Even0exists0All goals completed! 🐙-- (`0 = double 0` is closed by `exists`'s final `rfl`)|succ_succh'ih=>succ_succn:Natn✝:Nath':Evenn✝ih:n✝.Even⊢ (n✝+2).Evenlet⟨k,hk⟩:=ihsucc_succn:Natn✝:Nath':Evenn✝ih:n✝.Evenk:Nathk:n✝=k.double⊢ (n✝+2).Evenexistsk+1succ_succn:Natn✝:Nath':Evenn✝ih:n✝.Evenk:Nathk:n✝=k.double⊢ n✝+2=(k+1).double;rw[Nat.double_succ,succ_succn:Natn✝:Nath':Evenn✝ih:n✝.Evenk:Nathk:n✝=k.double⊢ n✝+2=k.double+2hksucc_succn:Natn✝:Nath':Evenn✝ih:n✝.Evenk:Nathk:n✝=k.double⊢ k.double+2=k.double+2]All goals completed! 🐙
Recall the definition of List.In from last chapter:
In fact, this is exactly how Lean defines this proposition,
which it calls Membership.mem and which is written x ∈ l.
Its negation ¬ x ∈ l is also written as x ∉ l.
The characterizing lemmas for ∈ are called
List.mem_nil_iff and List.mem_cons.