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):
What we've done here is to use Lean's inductive
definition mechanism to characterize the property "Collatz holds
for..." by stating three different ways in which it can hold:
(1) Collatz holds for 1, (2) if Collatz holds for
div2 n and n is even, then Collatz holds for
n, and (3) if Collatz holds for (3 * n) + 1 and
n is odd, then Collatz holds for n.
This Lean definition directly corresponds to the three rules we
wrote informally above.
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.
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)
These rules say that there are two ways to show that a
number is less than or equal to another: either observe that
they are the same number, or, if the second has the form
m + 1, give evidence that the first is less than or
equal to m.
This definition is a bit simpler and more elegant than the
boolean function Nat.ble we defined in Basics.
As usual, Le and Nat.ble are equivalent, and there is
an exercise about that later.
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
Computing the transitive closure can be undecidable even for
a relation r that is decidable (e.g., the CollatzStep relation below, whose closure is CollatzStepMulti), so in
general we can't expect to define transitive closure as a boolean
function. Fortunately, Lean allows us to define transitive closure
as an inductive relation.
The transitive closure of a binary relation cannot, in general, be
expressed in first-order logic (see the Logic chapter),
since doing so would require quantifying over relations themselves.
The logic of Lean is, however, much
more powerful — being higher-order, as we saw there — and can easily
define such inductive relations.
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:
This CollatzStepMulti relation defined in terms of
ReflTransGen allows for more interesting derivations than the
linear ones of the directly defined CollatzHoldsFor relation:
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)
Intuitively these rules say that:
The number 0 is even.
If n is even, then n + 2 is even.
(Defining evenness in this way may seem a bit confusing,
since we have already seen two perfectly good ways of doing
it. It makes a convenient running example because it is
simple and compact, but we will soon return to the more compelling
examples above.)
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))))
In words, to show that 4 is even, by rule succ_succ, it
suffices to show that 2 is even. This, in turn, is again
guaranteed by rule succ_succ, as long as we can show that 0 is
even. But this last fact follows directly from the zero rule.
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:
Such definitions are interestingly different from previous uses of
inductive for defining inductive datatypes like Nat or List.
For one thing, we are defining not a Type (like Nat) or a
function yielding a Type (like List), but rather a function
from Nat to Prop — that is, a property of numbers. But what
is really new is that, because the Nat argument of Even appears
to the right of the colon on the first line, it is allowed to
take different values in the types of different constructors:
0 in the type of Even.zero and (n + 2)
in the type of Even.succ_succ.
Accordingly, the type of each constructor must be specified
explicitly (after a colon), and each constructor's type must have
the form Even n for some natural number n.
In contrast, recall the definition of List:
inductive`List` has already been declaredList(α:Type):Typewhere|nil|cons(x:α)(l:Listα)
or (equivalently but more explicitly):
inductive`List` has already been declaredList(α:Type):Typewhere|nil:Listα|cons(x:α)(l:Listα):Listα
This definition introduces the α parameter globally, to the
left of the colon, forcing the result of List.nil and
List.cons to be the same type (i.e., List α).
But if we had tried to bring Nat to the left of the colon in
defining Even, we would have seen an error:
inductiveWrongEven(n:Nat):PropwhereMismatched inductive type parameter inWrongEven0The provided argument0is not definitionally equal to the expected parameternNote: The value of parameter `n` must be fixed throughout the inductive declaration. Consider making this parameter an index if it must vary.|zero:WrongEven0|succ_succ(h:WrongEvenn):WrongEven(.succ(.succn))
Mismatched inductive type parameter inWrongEven0The provided argument0is not definitionally equal to the expected parameternNote: The value of parameter `n` must be fixed throughout the inductive declaration. Consider making this parameter an index if it must vary.
In an inductive definition, an argument to the type constructor
on the left of the colon is called a "parameter," whereas an
argument on the right is called an "index" or "annotation."
For example, in inductive List (α : Type) ..., the α is a
parameter, while in inductive Even : Nat → Prop ..., the
unnamed Nat argument is an index.
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 analyze a
hypothesis of the form Even n much as we do inductively defined
data structures; in particular, it should be possible to argue either by
case analysis or by induction on such evidence. Let's look at a
few examples to see what this means in practice.
Suppose we are proving some fact involving a number n, and
we are given Even n as a hypothesis. We already know how to
perform case analysis on n using cases or
induction, generating separate subgoals for the case where
n = 0 and the case where n = n' + 1 for some n'.
But for some proofs we may instead want to analyze the evidence for
Even ndirectly.
As a tool for such proofs, we can formalize the intuitive
characterization that we gave above for evidence of Even n,
using cases.
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.
Note how the inversion lemma produces two subgoals, which
correspond to the two ways of proving Even. The first subgoal is
a contradiction that is discharged with contradiction. The
second subgoal makes use of injections and subst.
The subst tactic takes an equation x = t and replaces
x by t in the context's hypotheses and in the goal, then removes
that equation from the context.
We've defined a handy tactic called inversion that factors out
this common pattern, saving us the trouble of explicitly stating
and proving an inversion lemma for every inductive definition we
make. (The details of how inversion is implemented are beyond the scope
of this course. Lean provides metaprogramming facilities that its
users can employ to write their own tactics, and these capabilities
are powerful enough that just about any algorithmic reasoning steps
can be implemented.)
Here, the inversion tactic can detect (1) that the first case,
where n = 0, does not apply and (2) that the n' that appears
in the Even.succ_succ case must be the same as n.
The inversion tactic can apply the principle of explosion to
"obviously contradictory" hypotheses involving inductively defined
properties, something that takes a bit more work using our
inversion lemma. Compare:
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.
Here is a useful way to think about inversion.
For an inductively defined hypothesis h, inversion h
starts with one case for each constructor, then uses the indices of the type of h
to eliminate impossible cases, and simplifies the remaining ones.
In the remaining cases, it solves these equations to force some expressions or substitute some variables.
If an equation cannot be solved, inversion leaves it in the context and we can
use it in the rest of the proof.
Quiz
Which tactics are needed to prove this goal, in addition to
apply or exact?
The Even.double exercise above allows us to easily show that
our new notion of evenness is implied by the two earlier ones.
In fact, by Nat.even_bool_prop in the Logic chapter,
we already know that those are equivalent to each other. To show that
Nat.Even, Even, and Nat.even coincide, we just need the following lemma.
We could try to proceed by cases or induction on n. But
since Even is mentioned in a premise, this strategy seems
unpromising, because (as we've noted before) the induction
hypothesis will talk about n - 1 (which is not even!). Thus, it
seems better to first try inversion on the evidence for Even.
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!
The behavior of induction on evidence is the same as its
behavior on data: it causes Lean to generate one subgoal for each
constructor that could have been used to build that evidence, while
providing an induction hypothesis for each recursive occurrence of
the property in question.
To prove that a property of n holds for all even numbers
(i.e., those for which Even n holds), we can use induction on
Even n. This requires us to prove two things, corresponding to
the two ways in which Even n could have been constructed. If it
was constructed by Even.zero, then n = 0 and the
property must hold of 0. If it was constructed by
Even.succ_succ, then the evidence of Even n
is of the form Even.succ_succ n' h', where n = n' + 2 and
h' is evidence for Even n'. In this case, the induction hypothesis
says that the property we are trying to prove holds for n'.
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! 🐙
Here, we can see that Lean produced an ih that corresponds
to h, the single recursive occurrence of Even in its own
definition. Since h' mentions n', the induction hypothesis
talks about n', as opposed to n or some other number.
The equivalence between the second and third definitions of
evenness now follows.
As we will see in later chapters, induction on evidence is a
recurring technique across many areas — in particular for
formalizing the semantics of programming languages.
The following exercises provide simpler examples of this
technique, to help you familiarize yourself with it.
theoremdeclaration uses `sorry`Even.of_add_left(nm:Nat)(h:Even(n+m))(hn:Evenn):Evenm:=byn:Natm:Nath:Even(n+m)hn:Evenn⊢ Evenm/- Hint: There are two pieces of evidence you could attempt to induct upon
here. If one doesn't work, try the other. -/sorryAll goals completed! 🐙
Exercise★★★(add_of_add_left) (Optional)
This exercise can be completed without induction or case analysis.
But you will need a clever have and some tedious rewriting.
Hint: Is (n + m) + (n + k) even?
Another example of a proposition that can be characterized both recursively and
inductively is the List.In predicate we defined in the Logic chapter.
As a reminder, the recursive definition we saw looked like this:
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.
A good exercise to test your understanding of induction on
evidence is to prove the equivalence of these definitions:
Something interesting happens here: there are two
induction hypotheses, ihxy and ihyz! If you think about it, it
is not that weird: we are in the case trans, which has
two recursive components, hxy, relating x to y, and hyz,
relating y to z. Hence we may want (and will actually need)
an induction hypothesis for hxy and one for hyz — they are
called ihxy and ihyz here. In general, Lean will always
generate one induction hypothesis per recursive premise of each
constructor of the type being inducted over.
Exercise★★★★(Even') (Advanced, Optional)
In general, there may be multiple ways of defining a
property inductively. For example, here's a (slightly contrived)
alternative definition for Even:
Prove that this definition is logically equivalent to the old one.
To streamline the proof, use the technique (from the Logic
chapter) of applying theorems to arguments, and note that the same
technique works with constructors of inductively defined
propositions.
We pause for a moment to point out that some tactics that accept an at clause can target
several locations at once, including the goal, written using the ⊢ symbol, by listing them
together after at — for instance, both rw and dsimp support this.
Instead of listing specific targets, you can also write at * to target all the
hypotheses and the goal. Here is another example, relevant to
the next exercise.
Recall the Le relation from earlier in this
chapter. Here are a number of facts about the ≤, <, and ≥
relations, and about Le's relationship to the boolean function
Nat.ble, that we are going to need later in the course; the
proofs make good practice for the case-analysis and induction
techniques from the last few sections.
theoremdeclaration uses `sorry`le_and_le_of_add_le(n₁n₂m:Nat)(h:n₁+n₂≤m):n₁≤m∧n₂≤m:=byn₁:Natn₂:Natm:Nath:n₁+n₂≤m⊢ n₁≤m∧n₂≤msorryAll goals completed! 🐙theoremdeclaration uses `sorry`le_or_le_of_add_le_add(nmpq:Nat)(h:n+m≤p+q):n≤p∨m≤q:=byn:Natm:Natp:Natq:Nath:n+m≤p+q⊢ n≤p∨m≤q/- Hint: May be easiest to prove by induction on `n`. -/sorryAll goals completed! 🐙
We can define three-place relations, four-place relations,
etc., in just the same way as binary relations. For example,
consider the following three-place relation on numbers:
A list is a subsequence of another list if all of the elements
in the first list occur in the same order in the second list,
possibly with some extra elements in between. For example,
Define an inductive proposition Subseq on ListNat that
captures what it means to be a subsequence. There are a number
of correct ways to do this. You should make sure that your
definition behaves correctly on all the positive and negative
examples above, but you do not need to prove this formally.
Prove Subseq.refl that subsequence is reflexive — that is,
any list is a subsequence of itself.
Prove Subseq.append that for any lists l₁, l₂, and l₃,
if l₁ is a subsequence of l₂, then l₁ is also a subsequence
of l₂ ++ l₃.
(Harder) Prove Subseq.trans that subsequence is transitive —
that is, if l₁ is a subsequence of l₂ and l₂ is a
subsequence of l₃, then l₁ is a subsequence of l₃.
inductiveSubseq:ListNat→ListNat→Propwhere-- FILL IN HEREnamespaceSubseqtheoremdeclaration uses `sorry`refl(l:ListNat):Subseqll:=byl:ListNat⊢ SubseqllsorryAll goals completed! 🐙theoremdeclaration uses `sorry`append(l₁l₂l₃:ListNat)(h:Subseql₁l₂):Subseql₁(l₂++l₃):=byl₁:ListNatl₂:ListNatl₃:ListNath:Subseql₁l₂⊢ Subseql₁(l₂++l₃)sorryAll goals completed! 🐙theoremdeclaration uses `sorry`trans(l₁l₂l₃:ListNat)(h12:Subseql₁l₂)(h23:Subseql₂l₃):Subseql₁l₃:=byl₁:ListNatl₂:ListNatl₃:ListNath12:Subseql₁l₂h23:Subseql₂l₃⊢ Subseql₁l₃/- Hint: be careful about what you are doing induction on and which
other things need to be generalized... -/sorryAll goals completed! 🐙endSubseq
Define an inductive binary relation TotalRelation that holds
between every pair of natural numbers.
inductiveTotalRelation:Nat→Nat→Propwhere-- FILL IN HEREtheoremdeclaration uses `sorry`total_relation_is_total(nm:Nat):TotalRelationnm:=byn:Natm:Nat⊢ TotalRelationnmsorryAll goals completed! 🐙
Exercise★★(empty_relation) (Optional)
Define an inductive binary relation EmptyRelation (on numbers)
that never holds.
inductiveEmptyRelation:Nat→Nat→Propwhere-- FILL IN HEREtheoremdeclaration uses `sorry`empty_relation_is_empty(nm:Nat):¬EmptyRelationnm:=byn:Natm:Nat⊢ ¬EmptyRelationnmsorryAll goals completed! 🐙
Formulating inductive definitions of properties is an important
skill you'll need in this course. Try to solve this exercise
without any help.
We say that a list "stutters" if it repeats the same element
consecutively. (This is different from not containing duplicates:
the sequence [1,4,1] has two occurrences of the element 1 but
does not stutter.) The property NoStutter l means that l does
not stutter. Formulate an inductive definition for NoStutter.
inductiveNoStutter{α:Type}:Listα→Propwhere-- FILL IN HERE
Make sure each of these tests succeeds, but feel free to change
the suggested proof (in comments) if the given one doesn't work
for you. Your definition might be different from ours and still
be correct, in which case the examples might need a different
proof. (You'll notice that the suggested proofs use a number of
tactics we haven't talked about, to make them more robust to
different possible ways of defining NoStutter. You can probably
just uncomment and use them as is, but you can also prove each
example with more basic tactics.)
declaration uses `sorry`example:NoStutter[3,1,4,1,5,6]:=by⊢ NoStutter[3,1,4,1,5,6]sorryAll goals completed! 🐙/- Suggested proof — uncomment and adapt:
constructor; intro contra; contradiction
constructor; intro contra; contradiction
constructor; intro contra; contradiction
constructor; intro contra; contradiction
constructor; intro contra; contradiction
constructor
-/declaration uses `sorry`example:NoStutter(@List.nilNat):=by⊢ NoStutter[]sorryAll goals completed! 🐙/- Suggested proof — uncomment and adapt:
constructor
-/declaration uses `sorry`example:NoStutter[5]:=by⊢ NoStutter[5]sorryAll goals completed! 🐙/- Suggested proof — uncomment and adapt:
constructor
-/declaration uses `sorry`example:¬(NoStutter[3,1,1,4]):=by⊢ ¬NoStutter[3,1,1,4]sorryAll goals completed! 🐙/- Suggested proof — uncomment and adapt:
intro contra
inversion contra with
| cons contra =>
inversion contra with
| cons _ h _ => contradiction
-/
Exercise★★★★(filter_challenge) (Advanced)
Let's prove that our definition of filter from the Poly
chapter matches an abstract specification. Here is the
specification, written out informally in English:
A list l is an "in-order merge" of l₁ and l₂ if it contains
all the same elements as l₁ and l₂, in the same order as l₁
and l₂, but possibly interleaved. For example,
[1, 4, 6, 2, 3]
is an in-order merge of
[1, 6, 2]
and
[4, 3].
Now, suppose we have a type α, a function test : α → Bool, and a
list l of type List α. Suppose further that l is an
in-order merge of two lists, l₁ and l₂, such that every item
in l₁ satisfies test and no item in l₂ satisfies test. Then
filter test l = l₁.
First define what it means for one list to be a merge of two
others. Do this with an inductive relation, not a def.
inductiveMerge{α:Type}:Listα→Listα→Listα→Propwhere-- FILL IN HEREtheoremdeclaration uses `sorry`merge_filter(α:Type)(test:α→Bool)(ll₁l₂:Listα)(h:Mergel₁l₂l)(h₁:l₁.allbtest)(h₂:l₂.allb(funx=>!testx)):filtertestl=l₁:=byα:Typetest:α→Booll:Listαl₁:Listαl₂:Listαh:Mergel₁l₂lh₁:List.allbtestl₁=trueh₂:List.allb(funx=>!testx)l₂=true⊢ filtertestl=l₁sorryAll goals completed! 🐙
A different way to characterize the behavior of filter goes like
this: Among all subsequences of l with the property that test
evaluates to true on all their members, filter test l is the
longest. Formalize this claim and prove it.
Exercise★★★★(palindromes) (Optional)
A palindrome is a sequence that reads the same backwards as
forwards.
Define an inductive proposition Pal on List α that
captures what it means to be a palindrome. (Hint: You'll need
three cases.)
Prove pal_append_reverse, which states that
∀ l, Pal (l ++ l.reverse).
Prove pal_reverse, which states that
∀ l, Pal l → l = l.reverse.
For extra credit, try proving the same theorems with an alternate
definition with a single constructor of this type:
Use the ∈ property to define a proposition Disjoint l₁ l₂,
which should be provable exactly when l₁ and l₂ are
lists (with elements of type α) that have no elements in
common.
Next, use ∈ to define an inductive proposition NoDup l,
which should be provable exactly when l is a list (with
elements of type α) where every member is different from every
other. For example, NoDup ([1, 2, 3, 4] : List Nat) and
NoDup ([] : List Bool) should be provable, while
NoDup ([1, 2, 1] : List Nat) and
NoDup ([true, true] : List Bool) should not be.
inductiveNoDup{α:Type}:Listα→Propwhere-- FILL IN HERE
Finally, state and prove one or more interesting theorems relating
Disjoint, NoDup, and ++ (list append).
The pigeonhole principle states a basic fact about counting: if
we distribute more than n items into n pigeonholes, some
pigeonhole must contain at least two items. As often happens, this
apparently trivial fact about numbers requires nontrivial
machinery to prove, but we now have enough...
Now define a property Repeats such that Repeats l asserts
that l contains at least one repeated element.
inductiveRepeats{α:Type}:Listα→Propwhere-- FILL IN HERE
Now, here's a way to formalize the pigeonhole principle. Suppose
list l₂ represents a list of pigeonhole labels, and list l₁
represents the labels assigned to a list of items. If there are
more items than labels, at least two items must have the same
label — i.e., list l₁ must contain repeats.
This proof is much easier if you use the excluded middle
to show that ∈ is decidable, i.e., ∀ x l, (x ∈ l) ∨ ¬ (x ∈ l).
Remember the by_cases tactic from Logic!