Lecture 2-7: Propositions

In Lecture 1-X, we saw how to use types to represent propositions. But not all types have a sensible interpretation as propositions: an element of in some sense contains more information than the mere fact that a proposition being true. How can we characterise which types should be thought of as propositions?

Contractible Types

Before we get to defining propositions properly, we’ll start with a class of types that is even simpler.

A singleton is a type consisting of exactly one element. In ordinary set theory, if is an element of a set , then the singleton subset of containing is which, to make crystal clear that we are dealing with a subset of , we could write more pedantically as the set . This is the notion of singleton we are going to use as our definition in type theory. For an element of a type a : A, the singleton type at a is:

singl : {A : Type }  (a : A)  Type 
singl {A = A} a = Σ[ x  A ] a  x

There is a unique element of singl a, namely the pair (a, refl).

singl-center : (a : A)  singl a
singl-center a = (a , refl)

We would like to say in type theory that singl a has a unique element, so we need a way of expressing “has a unique element” type-theoretically. For this, we use:

∃! : Type   Type 
∃! A = Σ[ x  A ] ((y : A)  y  x)

An element of ∃! A demonstrates existence and uniqueness of elements of A: an element x : A exists, and the function assigning to every y : A a path x ≡ y means that it is unique. Homotopically speaking, this means that the type A contracts onto the point x. If we have an element of ∃! A we say that A is contractible.

This concept comes up often enough will define a record to capture it so that we can name the two components.

record isContr (A : Type ) : Type  where
  constructor isContrData
  field
    center : A
    contraction : (a : A)  center  a

open isContr public

The type was defined to have only the element tt, so it is easy to show it is contractible.

isContr-⊤ : isContr 
-- Exercise:
-- isContr-⊤ = {!!}

Singletons should also have a unique element.

isContr-singl : (a : A)  isContr (singl a)
-- Exercise: (Hint: You will need to use `J` or a connection.)
-- isContr-singl a = {!!}

There are inductive types other than that we can show are contractible. As a somewhat contrived example, consider the “fake” interval type defined by

data Interval : Type where
  zero : Interval
  one  : Interval
  seg  : zero  one

This interval contains no interesting information at all:

isContr-Interval : isContr Interval
-- Exercise: (Hint: You will need to use a connection square.)
-- isContr-Interval = {!!}

Aside: This Interval is an ordinary type, in contrast to the built-in interval I. We can therefore use it like any other type; form functions into it, look at paths in it, and so on. It does not contain any of the magic that I does, however, so we can’t make a corresponding Path or hcomp.

mvrnote: needed later

Σ-fst-≃ : {B : A  Type }  ((a : A)  isContr (B a))  (Σ[ a  A ] B a)  A
Σ-fst-≃ c = inv→equiv fst  a  a , c a .center)  _  refl)  (a , b)  ap (a ,_) (c a .contraction b))

The empty type is not contractible: it doesn’t have any elements at all.

¬isContr-∅ : ¬ isContr 
-- Exercise:
-- ¬isContr-∅ = {!!}

And neither is Bool. Although elements of Bool certainly exist, they’re not unique.

¬isContr-Bool : ¬ isContr Bool
-- Exercise: (Hint: `true≢false`.)
-- ¬isContr-Bool (isContrData b p) = {!!}

Propositions

Let’s recall from Lecture 1-X that when considering a type A as a proposition, we think of an element a : A as a witness to the fact that the proposition A is true. Once we’ve constructed at least one witness like this, we don’t particularly care about the details of the construction: we don’t want there to more than one way that the proposition 1 + 1 ≡ 2 can be true.

We turn this observation into a definition: propositions are types which have at most one element. A type is a proposition when, for any two elements, there is a path between them.

isProp : Type   Type 
isProp A = (x y : A)  x  y

The type is a proposition that has a proof tt — truth. Because its induction principle lets us assume any element of is exactly tt, it’s easy to show this is a proposition in this new sense by pattern-matching.

isProp-⊤ : isProp 
-- Exercise:
-- isProp-⊤ = {!!}

The definition of isProp doesn’t actually promise that there are any elements of A, just that if we did have two elements, they would be equal. This is just as well, because not every proposition has a proof. As we saw in Lecture 1-X, represents a proposition with no proof — falsity. If a type has no elements at all then it certainly has at most one element:

isProp-∅ : isProp 
-- Exercise:
-- isProp-∅ = {!!}

Using these two facts, we can show that the observational equality types defined in Lecture 1-X are all propositions.

isProp-≡Bool : (a b : Bool)  isProp (a ≡Bool b)
isProp-≡Bool true true = isProp-⊤
isProp-≡Bool true false = isProp-∅
isProp-≡Bool false true = isProp-∅
isProp-≡Bool false false = isProp-⊤

isProp-≡ℕ : (n m : )  isProp (n ≡ℕ m)
-- Exercise:
-- isProp-≡ℕ n m = {!!}

Contractible types may be thought of as types with a unique element. Of course, a type with exactly one element also has at most one element, so contractible types are also propositions.

isContr→isProp : isContr A  isProp A
-- Exercise:
-- isContr→isProp c a b = {!!}

The same is not true the other way, of course. We saw that is a proposition but isn’t contractible. Not being inhabited is the only thing missing for a proposition to be contractible: as soon as a proposition has an element, that element is unique.

isProp-with-point→isContr : isProp A  A  isContr A
-- Exercise:
-- isProp-with-point→isContr p = {!!}

Conversely, if assuming that a type is inhabited causes it to be contractible, then that type must be a proposition to being with:

with-point-isContr→isProp : (A  isContr A)  isProp A
-- Exercise:
-- with-point-isContr→isProp c = {!!}

Of course, the point of having a definition like isProp is that not all types are propositions.

¬isProp-Bool : ¬ isProp Bool
-- Exercise:
-- ¬isProp-Bool pBool = {!!}

¬isProp-ℕ : ¬ isProp 
-- Exercise:
-- ¬isProp-ℕ pℕ = {!!}

Propositions, Equivalences and Retracts

Any function between contractible types is an equivalence.

contrEnds→isEquiv : isContr A  isContr B  (f : A  B)  isEquiv f
-- Exercise:
-- contrEnds→isEquiv c c' f = {!!}

contrExt : isContr A  isContr B  A  B
-- Exercise:
-- contrExt c c' = {!!}

So, in fact, every contractible type is equivalent to .

isContr→≃⊤ : isContr A  A  
isContr→≃⊤ c = contrExt c isContr-⊤

A map between propositions is not enough for them to be equivalent, consider the unique map ∅ → ⊤. But if there are maps both ways, then that’s enough. This is known as “proposition extensionality”.

propEnds→isEquiv : isProp A  isProp B
   (f : A  B)
   (g : B  A)
   isEquiv f
-- Exercise:
propEnds→isEquiv pA pB f g .section .map = g

propExt : isProp A  isProp B
   (f : A  B)
   (g : B  A)
   A  B
-- Exercise:
-- propExt pA pB f g = equiv f g {!!} {!!}

The converse of isContr→≃⊤ is true: if A is equivalent to , then A is contractible. To prove this, we will use an argument that we will repeat, with variations, in a couple of other proofs.

Many properties of types are preserved by equivalences, but it turns out that often just half of the data of an equivalence is enough. Recall that a map is a retract when it has a section.

record _RetractOnto_ (A : Type ) (B : Type ℓ') : Type (ℓ-max  ℓ') where
  constructor retractOntoData
  field
    map : A  B
    section : SectionOf map

open _RetractOnto_ public

If A is equivalent to B then certainly B retracts onto A, forgetting the other part of the equivalence.

equiv→retract : {A : Type }  {B : Type ℓ'}  (A  B)  B RetractOnto A
equiv→retract e .map = e .proof .retract .map
equiv→retract e .section .map = e .map
equiv→retract e .section .proof = e .proof .retract .proof

If B' retracts onto A, then in some sense Ais a continuous shrinking ofB. And so if Bis a proposition thenA` must be too. Here is the key lemma, with a pre-drawn cube for your convenience:

            y — — — — — — — — > y
          / ^                 / ^
  .map  /   |            p  /   |
      /     |             /     |
    x — — — — — — — — > x       |
    ^       |           ^       |                    ^   j
    |       |           |       |                  k | /
    |       |           |       |                    ∙ — >
    |       |           |       |                      i
    |    r (s y)  — — — | — — > y
    |     /             |     /
    |   /               |   /
    | /                 | /
 r (s x) — — — — — — —  x
retract-≡ : (r : B RetractOnto A)
   {x y : A}
   (r .section .map x  r .section .map y) RetractOnto (x  y)
-- Exercise: (Hint: Using `∙∙` gives the cleanest argument!)
-- retract-≡ r {x} {y} = {!!}

Then the fact about isProp follows easily.

isProp-retract : B RetractOnto A  isProp B  isProp A
-- Exercise:
-- isProp-retract r pB x y = {!!}

In particular, any type equivalent to a proposition is also a proposition.

isProp-equiv : A  B  isProp B  isProp A
-- Exercise:
-- isProp-equiv = {!!}

And if some contractible type B retracts onto A, then A is also contractible.

isContr-retract : B RetractOnto A  isContr B  isContr A
-- Exercise:
-- isContr-retract r c = {!!}

isContr-equiv : A  B  isContr B  isContr A
-- Exercise:
-- isContr-equiv = {!!}

So we have a converse to isContr→≃⊤, and being contractible is the same as being equivalent to .

≃⊤→isContr : (A  )  isContr A
-- Exercise:
-- ≃⊤→isContr = {!!}

mvrnote:sort

¬→≃∅ : ¬ A  (A  )
¬→≃∅ p .map = p
¬→≃∅ p .proof .section .map ()
¬→≃∅ p .proof .section .proof ()
¬→≃∅ p .proof .retract .map ()
¬→≃∅ p .proof .retract .proof a = ∅-rec (p a)

Closure Properties of Propositions

Back in Lecture 1-X, we used ordinary type constructors to represent logical operations on propositions. We had better make sure that our new notion of proposition is preserved by these type constructors!

First, we have implication. If A and B are propositions then the type of functions A → B is a proposition — namely, the proposition that A implies B.

isProp-→ : isProp B  isProp (A  B)
-- Exercise:
-- isProp-→ p f g i a = {!!}

As a special case of implication, we find that type negation ¬ A is always a proposition even when A isn’t.

isProp-¬ : isProp (¬ A)
isProp-¬ = isProp-→ isProp-∅

If B is true, then A → B is always true. Using isContr as our interpretation for “true proposition”, this means that A → B is contractible as soon as B is contractible.

isContr→ : isContr B  isContr (A  B)
-- Exercise:
-- isContr→ c = {!!}

The “and” of two propositions A and B is the type of pairs A × B.

isProp-× : isProp A  isProp B  isProp (A × B)
-- Exercise:
-- isProp-× pA pB = {!!}

And if A and B are both true, then A × B should also be true.

isContr-× : isContr A  isContr B  isContr (A × B)
-- Exercise:
-- isContr-× cA cB = {!!}

For contractibility, the converse of isContr-× holds: if the product is contractible then the inputs must have been.

isContr-×-conv : isContr (A × B)  isContr A × isContr B
-- Exercise:
-- isContr-×-conv cAB = {!!}

The same is not true for propositions: a product of types being a proposition does not imply that the two components are.

¬isProp-×-conv : ¬ ((A B : Type)  isProp (A × B)  isProp A × isProp B)
-- Exercise: (Hint: ∅×≃∅)
-- ¬isProp-×-conv = {!!}

Propositions and contractible types are also closed under forming path types. There’s another way interpret this fact. The hcomp operation lets us fill in open boxes in any type. If the type is a proposition, then we can fill any shape at all.

Filling Shapes in Propositions

mvrnote: this section needs rationalising

If a type is a proposition we can use the element of isProp to find a path between any two points. Not only that, but this path we are given is unique; all path between those points are equal.

This is a priori surprising: the definition of isProp gives us paths between points, but says nothing about cubes of higher dimension.

mvrnote: draw cube

mvrnote: out of date:

isProp→Square : isProp A
   {a b c d : A}
   (r : a  c) (s : b  d)
   (t : a  b) (u : c  d)
   Square t u r s
-- Exercise:
-- isProp→Square pA {a = a} r s t u i j = {!!}

A special case of isProp→Square is when we fix two sides of the square to be refl, resulting in an ordinary path between paths. There’s another way to read this: for x and y elements of a proposition, x ≡ y is also a proposition.

isProp→isProp≡ : isProp A  (x y : A)  isProp (x  y)
-- Exercise:
-- isProp→isProp≡ = {!!}

And from this we get that the types of paths in any proposition are always contractible.

isProp→isContr≡ : isProp A  (x y : A)  isContr (x  y)
-- Exercise: 
-- isProp→isContr≡ p x y = {!!}

We could have used this as an alternative definition of what it means to be a proposition, because it’s trivial to go the other way:

isContr≡→isProp : ((x y : A)  isContr (x  y))  isProp A
-- Exercise: 
-- isContr≡→isProp f x y = {!!}

There’s a another way we can generalise isProp. If we have a path of types which are all propositions, we can produce a path-over that path between any endpoints.

isProp→PathP : {A : I  Type } 
   ((i : I)  isProp (A i))
   (a0 : A i0) (a1 : A i1)
   PathP A a0 a1
-- Exercise: (Hint: `toPathP`)
-- isProp→PathP {A = A} hB a0 a1 = {!!}

isProp→isProp-PathP : {A : I  Type }
   ((i : I)  isProp (A i))
   (a0 : A i0) (a1 : A i1)
   isProp (PathP A a0 a1)
-- Exercise: (Hint: Piggyback on `isProp→isProp≡`)
-- isProp→isProp-PathP pA x y = isProp-equiv {!!} {!1}

We can use what we’ve proven so far to bootstrap the process of filling more interesting shapes. For example, any SquareP can be filled. We prove this in full, painful generality, because we will need to use it shortly.

mvrnote: draw square

isProp→SquareP : {A : I  I  Type } 
   ((i j : I)  isProp (A i j))
  
   {a : A i0 i0} {b : A i0 i1} {c : A i1 i0} {d : A i1 i1}

   (r : PathP  j  A j i0) a c) (s : PathP  j  A j i1) b d)
   (t : PathP  j  A i0 j) a b) (u : PathP  j  A i1 j) c d)

   SquareP A t u r s
-- Exercise:
-- isProp→SquareP pA r s t u = {!!}

We can prove similar facts for contractibility. These can be done entirely by gluing together results we’ve already seen.

isContr→isContr≡ : isContr A  (a b : A)  isContr (a  b)
-- Exercise: (Hint: `isProp-with-point→isContr`)
-- isContr→isContr≡ c a b = {!!}

isContr→isContr-PathP : {A : I  Type } (c : isContr (A i1))  (a : A i0)  (b : A i1)  isContr (PathP A a b)
isContr→isContr-PathP {A = A} isc a b = isContr-equiv (PathP≃Path A) (isContr→isContr≡ isc _ _)

Let’s give some more explicit examples of propositions. The first is a little self-referential: for any type A, there is the proposition that A… is a proposition.

isProp-isProp : isProp (isProp A)
-- Exercise:
-- isProp-isProp pA₀ pA₁ i a b j = {!!}

And isContr A is always a proposition; the proposition that A has a unique element.

isProp-isContr : isProp (isContr A)
-- Exercise:
-- isProp-isContr cA₀ cA₁ i .center = {!!}
-- isProp-isContr cA₀ cA₁ i .contraction x j = {!!}

There’s another important type that is a proposition: the fact that a map is an equivalence. We will prove this a little later in Lecture 2-X.

Subtypes

Our definition of proposition leads to a good notion of “subtype”. If P : A → Type is a family of propositions depending on a type A, then the subtype of A carved out by P is simply the type of pairs Σ[ a ∈ A ] P a. So, an element of the subtype is pair (a , p) of an a : A and a witness p : P a that P is true about a.

mvrnote: examples, isEven etc mvrnote: union/intersection etc

The main fact to prove about subtypes is that they have the same paths as the types they came from. That is, (a1 , b1) ≡ (a2 , b2) is equivalent to a1 ≡ a2 whenever B is a family of propositions.

≡-in-subtype : {A : Type } {B : A  Type ℓ'}
   (p : (a : A)  isProp (B a))
   (x y : Σ[ a  A ] B a)
   (x .fst  y .fst)  (x  y)
≡-in-subtype pB x y = inv→equiv to (ap fst) to-fro fro-to
  where
    to : x .fst  y .fst  x  y
    -- Exercise: (Hint: `isProp→PathP`)
    -- to e i .fst = {!!}
    -- to e i .snd = {!!}

    to-fro : isSection to (ap fst)
    -- Exercise: (Hint: `isProp→SquareP`)
    -- to-fro e i j .fst = {!!}
    -- to-fro e i j .snd = {!!}

    fro-to : isRetract to (ap fst)
    fro-to p i j = p j

To foreshadow Lecture 3-X, this is extremely useful when we start looking at algebraic structures such as groups, rings, and so on. These come with some data, like addition and multiplication operators, together with a bunch of axioms, like associativity, commutativity, and so on. What we’ve just proven tells us that to build a path between two groups, it’s enough to build a path just between the underlying data, ignoring all the axioms.

Dependent Closure Properties

The isProp-× operation has an upgraded, dependent version. This states that if A is a proposition and P : A → Type is a family of propositions depending on a : A then the type of pairs Σ[ a ∈ A ] B a is also a proposition. Really, Σ[ a ∈ A ] P a still represents the proposition “A and B” — the difference is that we can use it in situations where the proposition B only makes sense when A is known to hold.

isProp-Σ : {A : Type } {P : A  Type ℓ'}
   isProp A
   ((a : A)  isProp (P a))
   isProp (Σ[ a  A ] P a)
-- Exercise: (Hint: use isProp→PathP.)
-- isProp-Σ pA pP (a₀ , b₀) (a₁ , b₁) i = {!!}

And similarly for contractibility. If A is contractible and P : A → Type is a family of contractible types, then the entire Σ-type is contractible. This is similar to the isContr-× case, but will require isProp→PathP or transport-fixing in the second component.

isContr-Σ : {P : A  Type }  isContr A  ((x : A)  isContr (P x))  isContr (Σ[ a  A ] P a)
isContr-Σ p q .center .fst = p .center 
isContr-Σ p q .center .snd = q (p .center) .center
isContr-Σ p q .contraction (a , b) i .fst = p .contraction a i 
isContr-Σ {P = P} p q .contraction (a , b) i .snd = q (p .contraction a i) .contraction (transport-fixing  j  P (p .contraction a (i  ~ j))) i b) i

And isProp-→ can be extended to dependent functions. If P : A → Type is a family of propositions depending on A, then the type of functions (a : A) → P a is a proposition; the proposition that “for all a : A, the proposition P a holds”.

isProp-Π : {A : Type }  {P : A  Type ℓ'}
   (p : (a : A)  isProp (P a))
   isProp ((a : A)  P a)
-- Exercise:
-- isProp-Π p f g = {!!}

And if in fact every P a does hold, then the “for all” proposition holds too.

isContr-Π : {A : Type }  {P : A  Type ℓ'}
   ((a : A)  isContr (P a))
   isContr ((a : A)  P a)
-- Exercise:
-- isContr-Π c = {!!}

Propositional Truncation

We are still missing two important logical operations, the same two that we had trouble with back in Lecture 1-X: “or” and “exists”.

Our guess for “or” was disjoint union , but the disjoint union of two propositions is not necessarily a proposition. We checked in ¬isProp-Bool that Bool is not a proposition, and we know from Bool≃⊤⊎⊤ that Bool is the disjoint union ⊤ ⊎ ⊤.

¬isProp-⊤⊎⊤ : ¬ isProp (  )
-- Exercise:
-- ¬isProp-⊤⊎⊤ = {!!}

Aside: But! If we know that the two propositions are mutually exclusive, then their disjoint union is still a proposition.

isPropExclusive⊎ : isProp A  isProp B  ¬ (A × B)  isProp (A  B)
-- Exercise:
-- isPropExclusive⊎ pA pB dis x y = {!!}

For “or” and “exists”, we introduce another inductive type: the propositional truncation. This accepts any type A as a parameter and forms a proposition ∃ A — the proposition that “there exists some element of A”. An element of ∃ A will be a proof that A has some element, but crucially, knowing ∃ A won’t actually provide us with a specific element of A, just the fuzzy knowledge that there exists one.

data ∃_ (A : Type ) : Type  where
  in-∃ : A   A
  squash : (x y :  A)  x  y

infix 3 ∃_

The first constructor, written in-∃, says that to prove that there exists an element in A, it suffices to have an actual element of A. The second constructor, squash, is exactly the claim that ∃ A to be a proposition. This is a recursive constructor (like suc is for ).

isProp-∃ : isProp ( A)
isProp-∃ = squash

Aside: In fact, Agda would even let us declare the squash constructor to have type isProp (∃ A), and realise by unfolding the definition that this is asking for a path constructor.

Warning: The usual terminology for propositional truncation in Homotopy Type Theory is ∥ A ∥, but this can get confusing if we are doing mathematics where the same double-bars denote the norm of a vector or operator.

The recursion principle for ∃ A says that to prove that ∃ A implies some proposition P, it suffices to assume we have an actual element a : A and then prove P. That is, given a function A → P, we can get an implication ∃ A → P whenever P is a proposition.

∃-rec : (isProp P)
       (A  P)
       ( A  P)
-- Exercise:
-- ∃-rec pP f (in-∃ x) = {!!}
-- ∃-rec pP f (squash x y i) = pP {!!} {!!} {!!}

Aside: This definition is recursive — we use ∃-rec in its own definition. It’s tempting to give the squash constructor the non-recursive type (x y : A) → (in-∃ x) ≡ (in-∃ y). It turns out this is not enough: it really is necessary to equate all elements of ∃ A, not just those coming from A. With the non-recursive type, it’s not possible to prove that ∃ A is a proposition.

As usual, there is a dependently-typed upgrade for this recursion principle. The individual proposition P is replaced by a family of types, each of which is a proposition.

∃-ind : {P :  A  Type }
       ((e :  A)  isProp (P e))
       ((a : A)  P (in-∃ a))
       ((e :  A)  P e)
-- Exercise:
-- ∃-ind pP f (in-∃ x) = {!!}
-- ∃-ind pP f (squash x y i) = isProp→PathP {!!} {!!} {!!} {!!}

In fact, all maps into a proposition are of this form, that is, ∃-ind is an equivalence.

∃-ump-≃ : {P :  A  Type }
       ((e :  A)  isProp (P e))
       ((a : A)  P (in-∃ a))
       ((e :  A)  P e)
-- Exercise:
-- ∃-ump-≃ pP = propExt {!!} {!!} {!!} {!!}

is functorial, that is, if we have a function from A to B then A having an element implies B has an element.

∃-map : (A  B)  ( A   B)
-- Exercise:
-- ∃-map f = {!!}

If P is already a proposition, truncating it should do nothing:

isProp→≃∃ : isProp P  P  ( P)
-- Exercise: (Hint: use propExt)
-- isProp→≃∃ isPropP = {!!}

In particular, truncating twice is the same as truncating once.

∃≃∃∃ : ( P)  (  P)
∃≃∃∃ = isProp→≃∃ isProp-∃

With propositional truncation, we can finally define the proposition representing “or” which has eluded us. The type A ⊎ B has some element exactly when A has some element or B has some element. Therefore, we can define A orP B as the proposition that there exists an element in A ⊎ B.

_orP_ : Type   Type ℓ'  Type (ℓ-max  ℓ')
A orP B =  (A  B)

Here’s how we can justify that this is the correct definition. First of all, clearly A orP B is always a proposition, via isProp-∃. And, it has the correct universal mapping property with respect to other propositions: P orP Q → R exactly when P → R and Q → R.

orP-ump-≃ : {P Q R : Type }
   isProp P  isProp Q  isProp R
   (P  R) × (Q  R)  (P orP Q  R)
-- Exercise:
-- orP-ump-≃ pP pQ pR = {!!}

References and Further Reading

mvrnote:

  • The General Universal Property of the Propositional Truncation, Nicolai Kraus: https://arxiv.org/abs/1411.2682