This chapter introduces basic data structures and functions for working with
them. We place all these definitions in the Lists namespace to avoid name
clashes with Lean's standard library and with definitions from other chapters.
In an inductive type definition, each constructor can take
any number of arguments — none (as with true and 0),
one (as with Nat.succ), or more than one (as with Playground.Nibble and
the following):
inductiveNatProdwhere|pair(n1n2:Nat)
This declaration can be read: "The one and only way to
construct a pair of numbers is by applying the constructor NatProd.pair
to two arguments of type Nat."
Since pairs will be used heavily in what follows, it will be
convenient to write them with angle-bracket notation ⟨n, m⟩
instead of NatProd.pair n m. This notation is built into Lean and is
called "anonymous constructor syntax". It is available for any inductive
type with a single constructor, as long as the expected type is declared or
can be inferred from the context.
Note that pattern-matching on a pair (with angle brackets: ⟨x, y⟩)
is not to be confused with the "multiple pattern" syntax (with no
brackets: x, y) that we have seen previously. The above
examples illustrate pattern matching on a pair with elements x
and y, whereas, for example, the definition of sub for subtracting two Nats performs pattern matching on the values n and m:
The distinction is minor, but it is worth understanding that they
are not the same. For instance, the following definitions are
ill-formed:
defbad_fst(p:NatProd):Nat:=matchpwith|Too many patterns in match alternative: Expected 1, but found 2:x, yx,y=>x
Too many patterns in match alternative: Expected 1, but found 2:x, y
defbad_sub(nm:Nat):Nat:=matchn,mwith|Invalid `⟨...⟩` notation: The expected type `Nat` has more than one constructorNote: This notation can only be used when the expected type is an inductive type with a single constructorNot enough patterns in match alternative: Expected 2, but found 1:⟨0, .(_)⟩⟨0,_⟩=>0|⟨.succ_,0⟩=>n|⟨.succn',.succm'⟩=>subn'm'
Invalid `⟨...⟩` notation: The expected type `Nat` has more than one constructorNote: This notation can only be used when the expected type is an inductive type with a single constructor
As with the multi-argument match n, m with style used above in sub,
matching jointly on several values can combine what would otherwise be
several separate cases into a single match arm. This means the
simplification rules we define for such a function may not always match
one-to-one with the cases of its match construct.
A property like p = ⟨p.fst, p.snd⟩ can be proved by exposing
the structure of the pair, either with cases or by destructuring in
intro.
Notice that, unlike the behavior of cases on
Nats, where it generates two subgoals, cases generates just
one subgoal here. That's because NatProds can only be
constructed in one way.
Lean also provides a convenient way to define inductive structures like pairs
that have a single constructor but multiple ways to access their data,
using the structure keyword. The definition of NatProd' below is equivalent
to the NatProd definition from earlier, except that Lean automatically
generates the fst and snd accessors.
Generalizing the definition of pairs, we can describe the
type of lists of numbers like this: "A list is either the empty
list or else a pair of a number and another list."
By convention, we place the operations (functions) of an inductive type
inside the namespace implicitly created by that type's definition.
namespaceNatList
As with pairs, it is useful to give lists a symbolic
notation. The following declarations allow us to use :: as an
infix cons operator and square brackets as an "outfix" notation
for constructing lists.
List syntax
We first define :: as right-associative notation for cons,
and then define list notation as a macro,
allowing us to write [1,2] instead of 1::2::[].
The unexpander reverses the macro, translating list syntax back to
cons syntax.
In Lean, notation like ++, ==, and + is not
hardwired to particular definitions, which is the way we have
been defining notation so far. Instead, Lean defines this notation
using type classes — a mechanism that lets us overload operations
for different types.
We'll learn more about type classes in chapter Typeclasses.
For now, the key idea is just this:
a type class is like a Java-style interface, and an instance is an
implementation of that interface for a particular type.
We associate notation with a particular type class member, and then
instances of that typeclass inherit the notation for that member.
For example, ++ is defined via the HAppend type class's hAppend member.
Any type that provides an HAppend instance gets to use ++ for its
implementation of hAppend.
Lean's built-in List already has such an instance (using
List.append for hAppend), but since we've defined our own append function,
we can register it as the ++ operator within our namespace:
The equality test == on Nats is another example: it comes
from the BEq ("boolean equality") type class. One small but handy
fact about it, which several proofs below will need, is that == is
reflexive:
The head function returns the first element (the "head") of
the list, while tail returns everything but the first element (the
"tail"). Since the empty list has no first element, we pass
a default value to be returned in that case.
Complete the definitions of nonZeros, oddMembers, and
countOddMembers below. Have a look at the lemmas and examples to understand
what these functions should do.
The next definition uses bif, Lean's conditional for boolean tests.
The expression bif b then x else y evaluates to x when b is
true and to y when b is false.
Its characterizing lemmas are cond_true and cond_false.
In fact, as the entire proof is just plain computation, it can be done with a single rfl.
This is possible because all of the elements and lists are concrete — there are no variables involved.
Complete the following definition of alternate, which
interleaves two lists into one, alternating between elements taken
from the first list and elements from the second.
Hint: there are natural ways of writing alternate that fail to
satisfy Lean's requirement that all recursive definitions be
structurally recursive, as mentioned in Basics.
If you encounter this difficulty,
consider pattern matching against both lists at the same time.
Again, all these proofs could be completed with just rfl, because the proof is computationally straightforward — compute both sides of the equality and check whether they are the same.
As with numbers, simple facts about list-processing
functions can sometimes be proved entirely by cases and rewriting,
as shown for the following theorem.
Here, the nil case works because we've chosen to define
tail[]=[]. Notice that the cons case introduces two names,
n and l', corresponding to the fact that the cons constructor
for lists takes two arguments (the head and tail of the list it is
constructing).
Usually, though, interesting theorems about lists require
induction for their proofs. We'll see how to do this next.
(Micro-Sermon: As we get deeper into this material, simply
reading proof scripts will not help you very much. Rather, it
is important to step through the details of each one using Lean and
think about what each step achieves. Otherwise it is more or less
guaranteed that the exercises will make no sense when you get to
them. 'Nuff said.)
Proofs by induction over datatypes like NatList are a
little less familiar than standard natural number induction, but
the idea is equally simple. Each inductive declaration defines
a set of data values that can be built up using the declared
constructors. For example, a boolean can be either true or
false; a number can be either 0 or else Nat.succ applied to another
number; and a list can be either [] or else :: applied to a
number and a list. Moreover, applications of the declared
constructors to one another are the only possible shapes that
elements of an inductively defined set can have.
This last fact directly gives rise to a way of reasoning about
inductively defined sets: a number is either 0 or else it is Nat.succ
applied to some smaller number; a list is either [] or else
it is :: applied to some number and some smaller list;
etc. Thus, if we have in mind some proposition P that mentions a
list l and we want to argue that P holds for all lists, we
can reason as follows:
First, show that P is true of l when l is [].
Then show that P is true of l when l is n :: l' for
some number n and some smaller list l', assuming that P
is true for l'.
Since larger lists can always be broken down into smaller ones,
eventually reaching [], these two arguments together establish
the truth of P for all lists l.
In some situations, it is necessary to generalize a
statement in order to prove it by induction. Intuitively, the
reason is that a more general statement also yields a more general
(stronger) induction hypothesis. While the following
statement is true, we cannot prove it directly:
A first attempt to make progress would be to prove exactly
the statement that we are missing at this point. But this attempt
will fail because the induction hypothesis is not general enough.
It turns out that the above lemma is more specific than it
needs to be. We can strengthen the lemma to work not only on reversed
lists but on general lists.
We can also prove a more general form that gives the
length of any two appended lists. We could use this theorem rather
than append_length_succ to help prove length_reverse.
This follows directly from the definitions of length and ++
together with the induction hypothesis. QED.
Theorem: For all lists l, l.reverse.length = l.length.
Proof: By induction on l.
First, suppose l = []. We must show
[].reverse.length = [].length,
which follows directly from the definitions of length
and reverse.
Next, suppose l = n :: l', with
l'.reverse.length = l'.length
We must show
(n :: l').reverse.length = (n :: l').length.
By the definition of reverse, this follows from
(l'.reverse ++ [n]).length = l'.length + 1,
which, by the previous lemma, is the same as
l'.reverse.length + [n].length = l'.length + 1.
This follows directly from the induction hypothesis and the
definition of length. QED.
The style of these proofs is rather long-winded and pedantic.
After reading a couple like this, we might find it easier to
follow proofs that give fewer details (which we can easily work
out in our own minds or on scratch paper if necessary) and just
highlight the non-obvious steps. In this more compressed style,
the above proof might look like this:
Theorem: For all lists l, l.reverse.length = l.length.
Proof: First observe, by a straightforward induction on l₁,
that (l₁ ++ l₂).length = l₁.length + l₂.length for any l₁ and l₂. The main
property then follows by induction on l, using the
observation together with the induction hypothesis in the case
where l = n' :: l'. QED.
Which style is preferable in a given situation depends on
the sophistication of the expected audience and how similar the
proof at hand is to ones that they will already be familiar with.
The more pedantic style is a good default for our present purposes
because we're trying to be very clear about the details.
Write down an interesting theorem count_append about lists
involving the functions count and append, and prove it.
(You may find that the difficulty of the proof depends on how you defined count!)
Exercise★★★(involutive_injective) (Advanced)
Prove that every involution is injective.
Involutions were defined above in reverse_involutive. An injective
function is one-to-one: it maps distinct inputs to distinct
outputs, without any collisions.
Prove that reverse is injective. Do not prove this by induction —
that would be hard. Instead, reuse the same proof technique that
you used for involutive_injective. (But: Don't try to use that
exercise directly as a lemma: the types are not the same!)
Suppose we want to write a function that returns the nth
element of some list. If we give it type NatList→Nat→Nat,
then we'll have to choose some number to return when the list is
too short...
This solution is not so good, since in some cases this default value
of 42 could appear in the input list, and thus will not clearly
indicate that n was greater than the length of the list.
A better alternative is to change
the return type to include an error value as a possible outcome.
We call this new type NatOption.
We can then change the above definition of nthBad to
return NatOption.none when the list is too short and some a when the
list has enough members and a appears at position n. We call
this new function nth? to indicate that it may result in an
error.
As a final illustration of how data structures can be defined in
Lean, here is a simple partial map data type, analogous to the
map or dictionary data structures found in most programming
languages.
First, we define a new type MyId to serve as the "keys" of our
partial maps.
structureMyIdwhereval:Nat
Internally, a MyId is just a number. Introducing a separate type
by wrapping each Nat makes definitions more readable and gives us
flexibility to change representations later if we want to.
This declaration can be read: "There are two ways to construct a
PartialMap: either using the constructor empty to represent an
empty partial map, or applying the constructor record to
a key, a value, and an existing PartialMap to construct a
PartialMap with an additional key-to-value mapping."
namespacePartialMap
The update function overrides the entry for a given key in a
partial map by shadowing it with a new one (or simply adds a new
entry if the given key is not already present).
Last, the find function searches a PartialMap for a given
key. It returns none if the key was not found and some val if
the key was associated with val. If the same key is mapped to
multiple values, find will return the first one it encounters.