5. The Booleans¶
The logic we have introduced so far was very generic. We fix this in
this chapter by looking at a very simple type, the booleans bool
which has just two elements tt
(for true) and ff
(for false) and functions on this
type. Then we are going to use predicate logic to prove some simple
theorems about booleans.
In the lean prelude bool
is defined as an inductive type:
inductive bool : Type
| ff : bool
| tt : bool
This declaration means:
- There is a new type
bool : Type
, - There are two elements
tt ff : bool
, - These are the only elements of
bool
, tt
andff
are different elements ofbool
.
Inductive
is quite versatile we can use it to define other finite
types, infinite types like ℕ
and type constructors like maybe
or
list
. It is similar to the data
type constructor in
Haskell but not exactly since there are data
definitions in
Haskell which are not permitted in Lean.
5.1. Functions on bool¶
Let’s define negation on booleans this is a function bnot : bool →
bool
. By a function here we mean something which we can feed an
element of the input type (here bool
) and it will return an
element of the output type (here bool
again. We can do this by
matching all possible inputs:
def bnot : bool → bool
| tt := ff
| ff := tt
To define a function with two inputs, like and for booleans band
we use currying, that is band
applied to a boolean returns a
function which applied to the 2nd boolean returns a boolean, hence
band : bool → bool → bool
. As we have already seen →
is right
associative hence putting the extra brackets in the type is
band :bool → (bool → bool)
.
We could list all the four cases, reproducing the truth table, but we get away with just two matching only on the first argument:
def band : bool → bool → bool
| tt b := b
| ff b := ff
If the first argument is tt
, that is we look at band tt : bool →
bool
then this is just the identity on the snd argument, because
band tt tt = tt
and band tt ff = ff
. If the first argument is
ff
then the outcome wil be ff
whatever the 2nd argument
its. With other words band ff : bool → bool
is the constant
function which will always return ff
.
Symmetrically we can define bor : bool → bool → bool
with just two
cases:
def bor : bool → bool → bool
| tt b := tt
| ff b := b
In this case if the first argument is tt
, bor tt : bool → bool
is always tt
while if the first argument is ff
, bor ff :
bool → bool
is just the identity.
The lean prelude also introduces the standard infix notation for
operations on bool
:
x && y := band x y
and
x || y := bor x y
.
We can evaluate boolean expressions using #reduce
:
#reduce ff && (tt || ff)
#reduce tt && (tt || ff)
When I defined the binary boolean functions my choice to match on the first argument was quite arbitrary, I could have used the 2nd argument just as well:
def band2 : bool → bool → bool
| b tt := b
| b ff := ff
def bor2 : bool → bool → bool
| b tt := tt
| b ff := b
These functions produce the same truth table as the ones we have defined before but their computational behaviour is different which is important when we prove something about them. This can be seen by applying them to a variable:
variable x : bool
#reduce band tt x
#reduce band2 tt x
#reduce band x tt
#reduce band2 x tt
We see that band tt x
reduces to x
because it matches on the
first argument, while band2 x tt
is stuck, it needs to see what
x
turns out to be. If we fix the 2nd argument it is just the other
way around, this time band2
is better behaved.
When we are doing proofs it is important to remember how the function is defined because when we are doing case analysis we should instantiate the arguments which allow us to reduce the function, if possible.
5.2. Proving some basic properties¶
To reason about bool we can use cases x
to analyze a variable x :
bool
which means that there are two possibilities tt
and ff
. For
example we can state and prove in predicate logic that every element of bool
is either tt
or ff
:
example : ∀ x : bool, x=tt ∨ x=ff :=
begin
assume x,
cases x,
right,
refl,
left,
refl,
end
After assume x we are in the following state:
x : bool
⊢ x = tt ∨ x = ff
And after cases x we get two subgoals:
2 goals
case bool.ff
⊢ ff = tt ∨ ff = ff
case bool.tt
⊢ tt = tt ∨ tt = ff
Both of them are straightforward to prove. Now lets prove that both
are different. The idea is to define a predicate is_tt : bool →
Prop
by case analysis:
def is_tt : bool → Prop
| ff := false
| tt := true
Now we can use is_tt
to show that tt
and ff
cannot be
equal:
theorem cons : tt ≠ ff :=
begin
assume h,
change is_tt ff,
rewrite ← h,
trivial,
end
Here I am using tt ≠ ff
which just a shorthand for ¬ (tt =
ff)
(and which again is just short for tt = ff → false
).
I am also using a new tactic change
which replaces the current
goal with another one that is definitionally the same. In our case we
have::
h : tt = ff
⊢ false
and we know that is_tt ff = false
hance we can say change is_tt
ff
to replace the goal:
h : tt = ff ⊢ is_tt ff
But now we can use rewrite ← h
to change the goal to is_tt tt
which is equal to true
and hence provable by trivial
.
However, since this is a common situation Lean provides the tactic
contradiction
which we can use to prove goals like this:
example : tt ≠ ff :=
begin
assume h,
contradiction,
end
contradiction
will solve the current goal if there is an
inconsistent assumption like assuming that two different constructors
of an inductive type are equal.
5.3. Proving equations about bool¶
Ok next let’s prove some interesting equalities. We are going to revisit our old friend, distributivity but this time for booleans:
theorem distr_b : ∀ x y z : bool,
x && (y || z) = x && y || x && z :=
begin
assume x y z,
cases x,
dsimp [band],
dsimp [bor],
refl,
dsimp [band],
refl,
end
After assume x y z,
we are in the following state:
x y z : bool
⊢ x && (y || z) = x && y || x && z
We do case analysis on x
which can be either tt
or
ff
. Hence after cases x
we are left with two subgoals:
2 goals
case bool.ff
y z : bool
⊢ ff && (y || z) = ff && y || ff && z
case bool.tt
y z : bool
⊢ tt && (y || z) = tt && y || tt && z
Let’s just discuss the first one. We can instruct Lean to use the
definition of &&
(i.e. band
) by saying dsimp [band]
, we
are left with
y z : bool
⊢ ff = ff || ff
Now we just need to apply the definition of ||
(i..e bor) using dsimp [bor]
:
y z : bool
⊢ ff = ff
and now we only need to use refl
to dispose of this goal.
Can you see why in the 2nd case it is enough to use dsimp [band]
.
We can apply several definitions in one tactic, i.e. in the first case
dsimp [band,bor]
would have done the same. But actually in this
proof there is no need for dsimp
at all because refl
will
automatically reduce its arguments, hence we could have just written:
theorem distr_b : ∀ x y z : bool,
x && (y || z) = x && y || x && z :=
begin
assume x y z,
cases x,
refl,
refl,
end
However, when doing proofs interactively it may be helpful to see the
reductions. Also, later we will encounter cases where using dsimp
is actually necessary.
Could we have used another variable than x
. Let’s see what about
z
?
example : ∀ x y z : bool,
x && (y || z) = x && y || x && z :=
begin
assume x y z,
cases z,
sorry
end
After cases zz
we are in stuck in the following state:
x y : bool
⊢ x && (y || ff) = x && y || x && ff
No reduction is possible because we have defined the functions
matching on the first argument. Using cases y
would have done a
bit reduction but not enough. cases x
was the right choice.
Now have a go at the de Morgan laws for bool
yourselves, both are
provable just using case analysis:
theorem dm1_b : ∀ x y : bool, bnot (x || y) = bnot x && bnot y :=
begin
sorry,
end
theorem dm2_b : ∀ x y : bool, bnot (x && y) = bnot x || bnot y :=
begin
sorry,
end
5.4. Relating bool and Prop¶
I am sure it has not escaped you that we seem to define logical
operations twice: once for Prop
and once for bool
. How are the two
related? Indeed we can use is_tt
for example to relate ∧
and
&&
:
theorem and_thm : ∀ x y : bool, is_tt x ∧ is_tt y ↔ is_tt (x && y) :=
begin
assume x y,
constructor,
assume h,
cases h with xtt ytt,
cases x,
cases xtt,
cases y,
cases ytt,
constructor,
assume h,
cases x,
cases h,
cases y,
cases h,
constructor,
constructor,
constructor,
end
I recommend to step through the proof, we are only using tactics I
have already explained. Note that cases x
is used for two things:
for doing case analysis on booleans and to eliminate inconsistent
assumptions.
I leave it is an exercise to prove the corresponding facts about negation and disjunction:
theorem not_thm : ∀ x : bool, ¬ (is_tt x) ↔ is_tt (bnot x) :=
begin
sorry,
end
theorem or_thm : ∀ x y : bool, is_tt x ∨ is_tt y ↔ is_tt (x || y) :=
begin
sorry,
end
Another challenge is to define
implb : bool → bool → bool
and
eqb : bool → bool → bool
and show that they implement →
and ↔
for bool
.