module Data.Bool where
open import Prim.Data.Bool public
The booleans🔗
The type of booleans is interesting in homotopy type theory because
it is the simplest type where its automorphisms in Type
are non-trivial. In particular,
there are two: negation, and the identity.
But first, true isn’t false.
: ¬ true ≡ false
true≠false = subst P p tt where
true≠false p : Bool → Type
P = ⊥
P false = ⊤ P true
It’s worth noting how to prove two things are not
equal. We write a predicate that distinguishes them by mapping one to
⊤
, and one to ⊥
. Then we can substitute under P along
the claimed equality to get an element of ⊥
- a contradiction.
Basic algebraic properties🔗
The booleans form a Boolean algebra, as one might already expect, given its name. The operations required to define such a structure are straightforward to define using pattern matching:
: Bool → Bool
not = false
not true = true
not false
: Bool → Bool → Bool
and or = false
and false y = y
and true y
= y
or false y = true or true y
Pattern matching on only the first argument might seem like a somewhat inpractical choice due to its asymmetry - however, it shortens a lot of proofs since we get a lot of judgemental equalities for free. For example, see the following statements:
: (x y z : Bool) → and x (and y z) ≡ and (and x y) z
and-associative = refl
and-associative false y z = refl
and-associative true y z
: (x y z : Bool) → or x (or y z) ≡ or (or x y) z
or-associative = refl
or-associative false y z = refl
or-associative true y z
: (x y : Bool) → and x y ≡ and y x
and-commutative = refl
and-commutative false false = refl
and-commutative false true = refl
and-commutative true false = refl
and-commutative true true
: (x y : Bool) → or x y ≡ or y x
or-commutative = refl
or-commutative false false = refl
or-commutative false true = refl
or-commutative true false = refl
or-commutative true true
: (x : Bool) → and x true ≡ x
and-truer = refl
and-truer false = refl
and-truer true
: (x : Bool) → and x false ≡ false
and-falser = refl
and-falser false = refl
and-falser true
: (x : Bool) → and true x ≡ x
and-truel = refl
and-truel x
: (x : Bool) → or x false ≡ x
or-falser = refl
or-falser false = refl
or-falser true
: (x : Bool) → or x true ≡ true
or-truer = refl
or-truer false = refl
or-truer true
: (x : Bool) → or false x ≡ x
or-falsel = refl
or-falsel x
: (x y : Bool) → and x (or x y) ≡ x
and-absorbs-orr = refl
and-absorbs-orr false y = refl
and-absorbs-orr true y
: (x y : Bool) → or x (and x y) ≡ x
or-absorbs-andr = refl
or-absorbs-andr false y = refl
or-absorbs-andr true y
: (x y z : Bool) → and x (or y z) ≡ or (and x y) (and x z)
and-distrib-orl = refl
and-distrib-orl false y z = refl
and-distrib-orl true y z
: (x y z : Bool) → or x (and y z) ≡ and (or x y) (or x z)
or-distrib-andl = refl
or-distrib-andl false y z = refl
or-distrib-andl true y z
: (x : Bool) → and x x ≡ x
and-idempotent = refl
and-idempotent false = refl
and-idempotent true
: (x : Bool) → or x x ≡ x
or-idempotent = refl
or-idempotent false = refl
or-idempotent true
: (x y z : Bool) → and (or x y) z ≡ or (and x z) (and y z)
and-distrib-orr =
and-distrib-orr true y z (or-absorbs-andr z y) ∙ ap (or z) (and-commutative z y)
sym = refl
and-distrib-orr false y z
: (x y z : Bool) → or (and x y) z ≡ and (or x z) (or y z)
or-distrib-andr = refl
or-distrib-andr true y z =
or-distrib-andr false y z (and-absorbs-orr z y) ∙ ap (and z) (or-commutative z y) sym
All the properties above hold both in classical and constructive mathematics, even in minimal logic that fails to validate both the law of the excluded middle as well as the law of noncontradiction. However, the boolean operations satisfy both of these laws:
: (x : Bool) → not (not x) ≡ x
not-involutive = false
not-involutive false i = true
not-involutive true i
: (x y : Bool) → not (and x y) ≡ or (not x) (not y)
not-and≡or-not = refl
not-and≡or-not false y = refl
not-and≡or-not true y
: (x y : Bool) → not (or x y) ≡ and (not x) (not y)
not-or≡and-not = refl
not-or≡and-not false y = refl
not-or≡and-not true y
: (x : Bool) → or (not x) x ≡ true
or-complementl = refl
or-complementl false = refl
or-complementl true
: (x : Bool) → and (not x) x ≡ false
and-complementl = refl
and-complementl false = refl and-complementl true
Furthermore, note that not
has no fixed points.
: ∀ {x} → x ≡ not x → ⊥
not-no-fixed {x = true} p = absurd (true≠false p)
not-no-fixed {x = false} p = absurd (true≠false (sym p)) not-no-fixed
Exclusive disjunction (usually known as XOR) also yields additional structure - in particular, it can be viewed as an addition operator in a ring whose multiplication is defined by conjunction:
: Bool → Bool → Bool
xor = y
xor false y = not y
xor true y
: (x y z : Bool) → xor x (xor y z) ≡ xor (xor x y) z
xor-associative = refl
xor-associative false y z = refl
xor-associative true false z = not-involutive z
xor-associative true true z
: (x y : Bool) → xor x y ≡ xor y x
xor-commutative = refl
xor-commutative false false = refl
xor-commutative false true = refl
xor-commutative true false = refl
xor-commutative true true
: (x : Bool) → xor x false ≡ x
xor-falser = refl
xor-falser false = refl
xor-falser true
: (x : Bool) → xor x true ≡ not x
xor-truer = refl
xor-truer false = refl
xor-truer true
: (x : Bool) → xor x x ≡ false
xor-inverse-self = refl
xor-inverse-self false = refl
xor-inverse-self true
: (x y z : Bool) → and (xor x y) z ≡ xor (and x z) (and y z)
and-distrib-xorr = refl
and-distrib-xorr false y z = and-falser (not y) ∙ sym (and-falser y)
and-distrib-xorr true y false = (and-truer (not y)) ∙ ap not (sym (and-truer y)) and-distrib-xorr true y true
Material implication between booleans also interacts nicely with many of the other operations:
: Bool → Bool → Bool
imp = true
imp false y = y
imp true y
: (x : Bool) → imp x true ≡ true
imp-truer = refl
imp-truer false = refl imp-truer true
Furthermore, material implication is equivalent to the classical definition.
: ∀ x y → or (not x) y ≡ imp x y
imp-not-or = refl
imp-not-or false y = refl imp-not-or true y
Discreteness🔗
Using pattern matching, and the fact that true isn't false
, one can write an
algorithm to tell whether or not two booleans are the same:
instance
: Discrete Bool
Discrete-Bool {false} {false} = yes refl
Discrete-Bool {false} {true} = no (λ p → true≠false (sym p))
Discrete-Bool {true} {false} = no true≠false
Discrete-Bool {true} {true} = yes refl Discrete-Bool
opaque: is-set Bool
Bool-is-set = Discrete→is-set Discrete-Bool
Bool-is-set
instance
: ∀ {n} → H-Level Bool (2 + n)
H-Level-Bool = basic-instance 2 Bool-is-set H-Level-Bool
Furthermore, if we know we’re not looking at true, then we must be looking at false, and vice-versa:
: (x : Bool) → ¬ x ≡ true → x ≡ false
x≠true→x≡false = refl
x≠true→x≡false false p = absurd (p refl)
x≠true→x≡false true p
: (x : Bool) → ¬ x ≡ false → x ≡ true
x≠false→x≡true = absurd (p refl)
x≠false→x≡true false p = refl x≠false→x≡true true p
The “not” equivalence🔗
The construction of not
as an
equivalence factors through showing that not
is an
isomorphism. In particular, not
is
its own inverse, so we need a proof that it’s involutive, as is proven
in not-involutive
. With this, we
can get a proof that it’s an equivalence:
: is-equiv not
not-is-equiv = is-involutive→is-equiv not-involutive not-is-equiv
Aut(Bool)🔗
We characterise the type Bool ≡ Bool
. There are exactly
two paths, and we can decide which path we’re looking at by seeing how
it acts on the element true
.
First, two small lemmas: we can tell whether we’re looking at the identity equivalence or the “not” equivalence by seeing how it acts on the constructors.
private
: (p : Bool ≃ Bool)
idLemma → p .fst true ≡ true
→ p .fst false ≡ false
→ p ≡ (_ , id-equiv)
= Σ-path (funext lemma) (is-equiv-is-prop _ _ _) where
idLemma p p1 p2 : (x : Bool) → _
lemma = p2
lemma false = p1 lemma true
If it quacks like the identity equivalence, then it must be.
Otherwise we’re looking at the not
equivalence.
: (p : Bool ≃ Bool)
notLemma → p .fst true ≡ false
→ p .fst false ≡ true
→ p ≡ (not , not-is-equiv)
= Σ-path (funext lemma) (is-equiv-is-prop _ _ _) where
notLemma p p1 p2 : (x : Bool) → _
lemma = p2
lemma false = p1 lemma true
With these two lemmas, we can proceed to classify the automorphisms
of Bool
. For this, we’ll need
another lemma: If a function Bool → Bool
doesn’t
map f x ≡ x
, then it maps f x ≡ not x
.
: (Bool ≡ Bool) ≡ Lift _ Bool
Bool-aut≡2 = Iso→Path the-iso where
Bool-aut≡2 : (f : Bool → Bool) {x : Bool} → ¬ f x ≡ x → f x ≡ not x
lemma {false} x = caseᵈ (f false ≡ true) of λ where
lemma f (yes p) → p
(no ¬p) → absurd (¬p (x≠false→x≡true _ x))
{true} x = caseᵈ (f true ≡ false) of λ where
lemma f (yes p) → p
(no ¬p) → absurd (¬p (x≠true→x≡false _ x))
This lemma is slightly annoying to prove, but it’s not too
complicated. It’s essentially two case splits: first on the boolean, and
second on whether we’re looking at f x ≡ not x
. If we are,
then it’s fine (those are the yes p = p
cases) - otherwise
that contradicts what we’ve been told.
: Iso (Bool ≡ Bool) (Lift _ Bool)
the-iso
= caseᵈ (transport path true ≡ true) of λ where
fst the-iso path (yes path) → lift false
(no ¬path) → lift true
Now we classify the isomorphism by looking at what it does to true
. We arbitrarily map refl
to false
and not
to true
.
.snd .is-iso.inv (lift false) = refl
the-iso .snd .is-iso.inv (lift true) = ua (not , not-is-equiv) the-iso
The inverse is determined by the same rule, but backwards. That’s why
it’s an inverse! Everything computes in a way that lines up to this
function being a right inverse
on the nose.
.snd .is-iso.rinv (lift false) = refl
the-iso .snd .is-iso.rinv (lift true) = refl the-iso
The left inverse is a lot more complicated to prove. We examine how
the path acts on both true
and false
. There
are four cases:
.snd .is-iso.linv path with transport path true ≡? true
the-iso | transport path false ≡? false
... | yes true→true | yes false→false =
(Path≃Equiv .snd .linv _) ⟩
refl ≡⟨ sym (path→equiv refl) ≡⟨ ap ua path→equiv-refl ⟩
ua (_ , id-equiv) ≡⟨ ap ua (sym (idLemma _ true→true false→false)) ⟩
ua (path→equiv path) ≡⟨ Path≃Equiv .snd .linv _ ⟩
ua path ∎
In the case where the path quacks like reflexivity, we use the univalence axiom to show that
we must be looking at the reflexivity path. For this, we use
idLemma
to show that path→equiv path
must be
the identity equivalence.
... | yes true→true | no false→true' =
let
= lemma (transport path) false→true'
false→true = is-contr→is-prop (path→equiv path .snd .is-eqv true)
fibres (true , true→true) (false , false→true)
in absurd (true≠false (ap fst fibres))
The second case is when both booleans map to true
. This is a contradiction - transport
along a path is an equivalence, and equivalences have contractible
fibres; Since we have two fibres over true
, that means we must have
true ≡ false
.
... | no true→false' | yes false→false =
let
= lemma (transport path) true→false'
true→false = is-contr→is-prop (path→equiv path .snd .is-eqv false)
fibres (true , true→false) (false , false→false)
in absurd (true≠false (ap fst fibres))
The other case is analogous.
... | no true→false' | no false→true' =
(not , not-is-equiv)
ua (sym (notLemma _
≡⟨ ap ua (lemma (transport path) true→false')
(lemma (transport path) false→true')))
⟩(path→equiv path) ≡⟨ Path≃Equiv .snd .linv _ ⟩
ua path ∎
The last case is when the path quacks like ua (not, _)
-
in that case, we use the notLemma
to show it must be ua (not, _)
, and the univalence
axiom finishes the job.