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 and ff are different elements of bool.

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.