Lecture 2-9: Contractible Maps

In this Lecture we prove three crucial facts about equivalences:

  • Being an equivalence is a proposition about a map, rather than extra structure. That is:

    isProp-isEquiv : (f : A → B) → isProp (isEquiv f)
    
  • The function path→equiv is an equivalence, completing the proof of the univalence principle that we started in Lecture 2-X:

    univalence : (A ≡ B) ≃ (A ≃ B)
    
  • Σ-types respects equivalences. That is, if the inputs to Σ-map are equivalences then the resulting function is an equivalence.

    Σ-map-≃ : (e₁ : A ≃ A')
            → (e₂ : (a : A) → (B a) ≃ (B' (e₁ .map a)))
            → (Σ[ a ∈ A ] B a) ≃ (Σ[ a' ∈ A' ] B' a')
    

    We showed this fact for the non-dependent × way back in ×-map-≃, the dependent version is surprisingly difficult!

To prove each of these facts, it turns out to be easiest to detour through an alternative definition of equivalence: the notion of “contractible map”.

Contractible Maps

In set theory, a bijection between sets and is a function where for every , there is a unique so that . We can translate this definition directly into type theory, using isContr as our interpretation of “unique existence”.

isBijection : {A : Type }  {B : Type ℓ'}  (f : A  B)  Type (ℓ-max  ℓ')
isBijection {A = A} {B = B} f = (b : B)  isContr (Σ[ a  A ] (b  f a))

That type inside isContr comes up a lot, so it is given a name. The fiber of a function over an element is the inverse image of that element, so all elements of whose image under is equal to . In homotopy theory, this would be more accurately called the “homotopy fiber”.

fiber : {A : Type }  {B : Type ℓ'}
   (f : A  B)  (y : B)  Type (ℓ-max  ℓ')
fiber {A = A} f b = Σ[ a  A ] (b  f a)

Then, a bijection is exactly a map that has contractible fibers (just unfolding the definitions).

_ = λ { ℓ' : Level} {A : Type } {B : Type ℓ'} (f : A  B)
   test-identical 
    (isBijection f)
    ((y : B)  isContr (fiber f y))

This shape of definition comes up fairly often. We have a property of types (in this case contractibility), and we use it to define a property of maps by testing that property is satisfied by all the fibers. So, looking forward to other definitions of this sort, we’re going to rename the property of being a bijection to being a contractible map.

-- mvrnote: make record?
isContractibleMap : {A : Type } {B : Type ℓ'} (f : A  B)  Type (ℓ-max  ℓ')
isContractibleMap = isBijection

ContractibleMap : (A : Type ) (B : Type ℓ')  Type (ℓ-max  ℓ')
ContractibleMap A B = Σ[ f  (A  B) ] isContractibleMap f

Now, because a type being contractible is a proposition, a map being contractible is also a proposition.

isProp-isContractibleMap : (f : A  B)  isProp (isContractibleMap f)
-- Exercise:
-- isProp-isContractibleMap f = {!!}

Equivalences are Contractible Maps

It is easy to show that any contractible map is an equivalence. Each fiber is contractible, in particular, each fiber has a point, and we can use these to define an inverse function B → A.

isContractibleMap→isEquiv : {f : A  B}  isContractibleMap f  isEquiv f
isContractibleMap→isEquiv {A = A} {B = B} {f = f} cf = packIsEquiv inv to-fro inv fro-to
  where
    inv : B  A
--  Exercise:
--  inv b = {!!}

    to-fro : isSection f inv
--  Exercise: (Hint: Use the path provided by the fiber.)
--  to-fro = {!!}

    fro-to : isRetract f inv
--  Exercise: (Hint: Use the path provided by isContr.)
--  fro-to = {!!}

We can also show that any equivalence is a contractible map, but the process is more involved.

Here’s the setup: starting with an equivalence e : A ≃ B, we are going to show that the provided section of e is a contractible map, and then use this to show that the original map underlying e is contractible.

Our strategy is to use isProp-with-point→isContr, so our immediate goal is to show that each fiber of e .proof .section .map is a proposition. So fix an element y : A, and two elements of the fiber over it, (x₀, p₀) and (x₁ , p₁):

-- mvrnote: is there a way to split this line?
private module _ (e : A  B) (y : A) (x₀ : B) (p₀ : y  e .proof .section .map x₀) (x₁ : B) (p₁ : y  e .proof .section .map x₁) where

Let’s temporarily give shorter names to the components of the equivalence e.

  private
    f : A  B
    f = e .map

    g : B  A
    g = e .proof .section .map

    s : isSection f g
    s = e .proof .section .proof

    g' : B  A
    g' = e .proof .retract .map

    r : isRetract f g'
    r = e .proof .retract .proof

First we need to produce a path x₀ ≡ x₁ to use in the first component. This is easy enough, using the fact that g is a section of f.

  path₀ : f y  x₀
  -- Exercise:
  -- path₀ = {!!}

  path₁ : f y  x₁
  -- Exercise:
  -- path₁ = {!!}

  path : x₀  x₁
  -- Exercise:
  -- path₂ = {!!} ∙∙ refl ∙∙ {!!}

You’ll see very shortly why defining path in this symmetrical way using ∙∙ is beneficial.

Now, a path between the points x₀ and x₁ in B is not enough, we also need a path-over showing that p₀ and p₁ are equal. That is, we need a square

square : Square refl (ap g path) p₀ p₁

First, we’ll compose the following cube, where the top face is nearly what we want:

                    f p₁
           f y  — — — — — — > f (g x₁)
          / ^                 / ^
        /   |               /   |
      /     | f p₀        /     |
   f y  — — — — — — > f (g x₀)  | s x₁
    ^       |           ^       |                    ^   j
    |       |           |       |                  k | /
    |       |           | s x₀  |                    ∙ — >
    |       |           |       |                      i
    |      f y  — — — — | — — > x₁
    |     /       path₁ |     /
    |   /               |   / path
    | /                 | /
   f y  — — — — — — — > x₀
            path₀
  square-f : Square (ap f refl) (ap f (ap g path)) (ap f p₀) (ap f p₁)
  square-f i j = hcomp ( i   j) faces
    where faces : (k : I)  Partial ( i   j  ~ k) B
          -- Exercise:
          -- faces k = {!!}

We just need to kill the extra ap f on all the sides. For this, we use the fact that g' is a retract of f: we’ll add an extra g', then use r to cancel both g' and f out.

  square : Square refl (ap g path) p₀ p₁
  -- Exercise: (Hint: `homotopy-conjugate-Square` is nice here.)
  -- square = {!!}

So combining path and square, we get the path of between pairs that we wanted.

  lemEquiv : (x₀ , p₀)  (x₁ , p₁)
  -- Exercise:
  -- lemEquiv i = {!!}

The hard work is now done: every fiber of the section map is a proposition. And we can easily find a point of each fiber, so every fiber is contractible.

isEquiv→secIsContractibleMap : (e : A  B)  isContractibleMap (e .proof .section .map)
-- Exercise:
-- isEquiv→secIsContractibleMap e y = isProp-with-point→isContr {!!} {!!}

We usually want to know that the actual map underlying an equivalence is contractible, rather than the section map. To get this, we just invert the equivalence and apply what we’ve just proven, because the section used for the inverse is exactly the original map!

isEquiv→isContractibleMap : {f : A  B}  isEquiv f  isContractibleMap f
isEquiv→isContractibleMap isE = isEquiv→secIsContractibleMap (invEquiv (equiv _ isE))

Equiv→ContractibleMap : A  B  ContractibleMap A B
Equiv→ContractibleMap (equiv f isE) = (f , isEquiv→isContractibleMap isE)

Fact 1: Being an Equivalence is a Proposition

We now turn to the first of the goals that we listed up at the top: showing that isEquiv is always a proposition. We’ll do this by showing that if we do have an element of isEquiv f, then in fact isEquiv f is contractible. Our definition of isEquiv is as the pair of a section and a retract, so the way we’ll do it is showing those two pieces are contractible separately.

First, some generalities about equivalences. These can both be proven by defining an equivalence directly.

isEquiv→isEquiv-postComp : {f : A  B}  isEquiv f  isEquiv  (d : C  A)  f  d)
-- Exercise:
-- isEquiv→isEquiv-postComp e = {!!}

-- mvrnote: exercise?
sectionOf≃fiber : (f : A  B)  SectionOf f  (fiber  (d : B  A)  f  d) idfun)
sectionOf≃fiber {A = A} {B = B} f = inv→equiv fun inv  _  refl)  _  refl)
  where
    fun : SectionOf f  (fiber  (d : B  A)  f  d) idfun)
    fun s .fst = s .map 
    fun s .snd i b = s .proof b (~ i)

    inv : (fiber  (d : B  A)  f  d) idfun)  SectionOf f 
    inv (g , s) .map = g 
    inv (g , s) .proof b i = s (~ i) b

The strategy should be clear: sectionOf f is equivalent to one of the fibers of an equivalence, and because any equivalence is a contractible map, that fiber is contractible. Put it together:

isEquiv→isContrSectionOf : {f : A  B}  isEquiv f  isContr (SectionOf f)
-- Exercise:
-- isEquiv→isContrSectionOf {f = f} isE = {!!}

Caution: We have just shown that if f is an equivalence, its type of sections is contractible. It is definitely not the case in general that the type of sections of a map is contractible.

A symmetrical argument works for the retract, feel free to copy-paste as appropriate.

isEquiv→isEquiv-preComp  : {f : A  B}  isEquiv f  isEquiv  (d : B  C)  d  f)
-- Exercise:
-- isEquiv→isEquiv-preComp e = {!!}

retractOf≃fiber : (f : A  B)  RetractOf f  (fiber  (d : B  A)  d  f) idfun)
-- Exercise:
-- retractOf≃fiber f = {!!}

    inv : (fiber  (d : B  A)  d  f) idfun)  RetractOf f 
    inv (g , s) .map = g
    inv (g , s) .proof b i = s (~ i) b

isEquiv→isContrRetractOf : {f : A  B}  isEquiv f  isContr (RetractOf f)
-- Exercise:
-- isEquiv→isContrRetractOf {f = f} isE = {!!}

Now just glue them together! (Using explode-isEquiv to pull it apart will be helpful here).

isProp-isEquiv : (f : A  B)  isProp (isEquiv f)
-- Exercise:
-- isProp-isEquiv f = with-point-isContr→isProp {!!}

As we showed in ≡-in-subtype at the end of Lecture 2-X, paths in subtypes can be calculated in the underlying type. Since the type A ≃ B of equivalences is a subtype of the type of functions A → B (because we have just shown isEquiv is a proposition), we can compute paths between equivalences on their underlying functions.

equiv≡ : {e f : A  B}  (h : e .map  f .map)  e  f
-- Exercise:
-- equiv≡ {e = e} {f = f} h = {!!}

We knew already that univalence ua has a retract au. But we can now use equiv≡ to show that au is also a section, and so ua is an equivalence.

au-ua : (e : A  B)  au (ua e)  e
-- Exercise: (Hint: ua-comp)
-- au-ua e = {!!}

And prove univalence in all its glory:

univalence : (A  B)  (A  B)
univalence = inv→equiv au ua au-ua ua-au

Equivalence Induction

mvrnote: prose

isContr-singlEquiv : {A : Type }  isContr ( Σ[ T  Type  ] (A  T) )
isContr-singlEquiv {A = A} .center = (A , idEquiv A)
isContr-singlEquiv .contraction (B , e) i .fst = ua e i
isContr-singlEquiv .contraction (B , e) i .snd .map a = Path→ua-PathP e {x = a} refl i
isContr-singlEquiv .contraction (B , e) i .snd .proof = isProp→PathP  j  isProp-isEquiv  a  Path→ua-PathP e refl j)) isEquiv-idfun (e .proof) i

Equiv-J : {B : Type }
   (P : (B : Type )  A  B  Type ℓ')
   P A (idEquiv A)
   (e : A  B)
   P B e
Equiv-J {A = A} {B = B} P p e = subst  t  P (t .fst) (t .snd)) (isContr-singlEquiv .contraction (B , e)) p
ap-isEquiv : {A B : Type } (e : A  B)  {a₁ a₂ : A}  isEquiv (ap {x = a₁} {a₂} (e .map) )
ap-isEquiv e {a₁} {a₂} = Equiv-J  B e  isEquiv (ap {x = a₁} {a₂} (e .map))) isEquiv-idfun e

ap-≃ : {A B : Type } (e : A  B)  {a₁ a₂ : A}  (a₁  a₂)  (e .map a₁  e .map a₂)
ap-≃ e .map = ap (e .map)
ap-≃ e .proof = ap-isEquiv e

Aside: Proving this kind of result using equivalence induction has the same drawback as applying univalence directly (for example in ×-map-≃-again), in that the two input types must lie in the same universe.

It is usually possible to give direct proofs, but for ap-≃ the direct proof is frankly a little fiddly and annoying, so we’ll be satisfied with the version using equivalence induction. mvrnote: reference

Being an Isomorphism is a Not Propositional

isProp-isEquiv justifies our use of “equivalences” over “isomorphisms”. Recall that an isomorphism is a map with a single map going the other way that is both a section and a retract.

record isIso {A : Type } {B : Type ℓ'} (f : A  B) : Type (ℓ-max  ℓ') where
  constructor isIsoData
  field
    inv : B  A
    sectionProof : isSection f inv -- hty?
    retractProof : isRetract f inv

open isIso

Sadly, this type is not always a proposition. This feels strange, because in ordinary set-based mathematics, this defect is impossible to see:

mvrnote: exercises?

iso-inv-unique : {f : A  B}  (i₁ i₂ : isIso f) 
   (b : B)  i₁ .inv b  i₂ .inv b
iso-inv-unique {f = f} i₁ i₂ b
  = i₁ .inv b               ≡⟨ sym (i₂ .retractProof (i₁ .inv b)) 
    i₂ .inv (f (i₁ .inv b)) ≡⟨ ap (i₂ .inv) (i₁ .sectionProof b) 
    i₂ .inv b 

isSet→isProp-isIso : {f : A  B}
   isSet A  isSet B 
   isProp (isIso f)
isSet→isProp-isIso isA isB iso₁ iso₂ i .inv b = iso-inv-unique iso₁ iso₂ b i
isSet→isProp-isIso {f = f} isA isB iso₁ iso₂ i .sectionProof b
  = isB _ _
    (transport-filler   j  f (iso-inv-unique iso₁ iso₂ b j)  b) (iso₁ .sectionProof b) i)
    (transport-filler'  j  f (iso-inv-unique iso₁ iso₂ b j)  b) (iso₂ .sectionProof b) i)
    i
isSet→isProp-isIso {f = f} isA isB iso₁ iso₂ i .retractProof a
  = isA _ _
    (transport-filler   j  iso-inv-unique iso₁ iso₂ (f a) j  a) (iso₁ .retractProof a) i)
    (transport-filler'  j  iso-inv-unique iso₁ iso₂ (f a) j  a) (iso₂ .retractProof a) i)
    i

In the world of homotopy type theory, however, the paths in isSection and isRetract could hold extra data.

Consider the type of ways to show that the identity function X → X is an isomorphism: that is, the type of functions f : X → X such that s : (x : X) → f x ≡ x and r : (x : X) → f x ≡ x. By gluing these together, we can get a path x ≡ x for any x : X.

isIso→center : isIso idfun  (x : A)  (x  x)
isIso→center i x = sym (i .sectionProof x)  i .retractProof x

For sets this poses no problems, but if X is a higher-dimensional type, there may be lots of non-equal elements of (x : A) → (x ≡ x). Indeed, we already encounter the problem when looking at the simplest higher type, .

refl≢rotate-loop : ¬ ((λ _  refl)  rotate-loop)
-- Exercise:
-- refl≢rotate-loop p = {!!}

But now, here are two ways of showing that the identity function on is an isomorphism.

S¹-refl-iso : isIso (idfun {A = })
S¹-refl-iso .inv = idfun
S¹-refl-iso .sectionProof = λ _  refl
S¹-refl-iso .retractProof = λ _  refl

S¹-rotate-loop-iso : isIso (idfun {A = })
S¹-rotate-loop-iso .inv = idfun
S¹-rotate-loop-iso .sectionProof = λ _  refl
S¹-rotate-loop-iso .retractProof = rotate-loop

If isIso idfun were a proposition, then these would have to be equal. This would imply that (λ _ → refl) ≡ rotate-loop, which we’ve just shown cannot be.

¬isProp-isIso : ¬ isProp (isIso (idfun {A = }))
-- Exercise:
-- ¬isProp-isIso p = {!!}

Fact 2: Σ-types Respect Equivalence

The second goal of this Lecture is to prove that an equivalence of the components of a Σ-type extends to an equivalence of the entire Σ-type.

Dealing with the second component is easier and only involves rearranging some data, so let’s do that first.

The claim to prove is that if we have a “fiberwise equivalence”, a map (f₂ : (a : A) → B a → B' a) so that every f₂ a is an equivalence, then the map (Σ[ a ∈ A ] B a) → (Σ[ a ∈ A ] B' a) that applies f₂ to each fiber is also an equivalence.

mvrnote: adjust prose

Σ-map-fst : {B : A'  Type }
   (f₁ : A  A')
   Σ[ a  A ] B (f₁ a)  Σ[ a'  A' ] B a'
Σ-map-fst f₁ = Σ-map f₁  _  idfun)

Σ-map-snd : {B : A  Type } {B' : A  Type ℓ'}
   (f₂ : (a : A)  B a  B' a)
   Σ[ a  A ] B a  Σ[ a  A ] B' a
Σ-map-snd f₂ = Σ-map idfun f₂

Dealing with the second component is easy, in fact, we could have done it way back in Lecture 2-X.

Σ-map-snd-isEquiv : {B : A  Type } {B' : A  Type ℓ'} 
   {f₂ : (a : A)  B a  B' a} 
   ((a : A)  isEquiv (f₂ a)) 
   isEquiv (Σ-map-snd f₂)
-- Exercise:
-- Σ-map-snd-isEquiv isE = {!!}

Σ-map-snd-≃ : {B : A  Type } {B' : A  Type ℓ'}
   (e₂ : (x : A)  B x  B' x)
   (Σ[ a  A ] B a)  (Σ[ a  A ] B' a)
Σ-map-snd-≃ e₂ .map = Σ-map-snd (map  e₂) 
Σ-map-snd-≃ e₂ .proof = Σ-map-snd-isEquiv (proof  e₂)

The first component is the tough one, surprisingly difficult for such a simple statement.

Here’s the key fact, and what gives us the connection to contractible maps. You will have to use the technology from Lecture 2-X on transport and transport-filler.

Σ-map-fst-fib-≃ : {B : A'  Type }
   (f₁ : A  A')
   (t : Σ[ a'  A' ] B a') 
   fiber (Σ-map-fst f₁) t  fiber f₁ (t .fst)
Σ-map-fst-fib-≃ {B = B} f₁ (a' , b') = inv→equiv to fro to-fro fro-to
  where
    to : fiber (Σ-map-fst f₁) (a' , b')  fiber f₁ a'
    -- Exercise:
    to ((a , b) , p) .fst = a

    fro : fiber f₁ a'  fiber (Σ-map-fst f₁) (a' , b')
    -- Exercise:
    fro (a , p) .fst .fst = a

    to-fro : isSection to fro
    to-fro (a , p) = refl

    fro-to : isRetract to fro
    -- Exercise:
    fro-to ((a , b) , p) i .fst .fst = a

mvrnote: this could alternatively be done by using J everywhere

Now, we know that fiber (Σ-map-fst f₁) t is contractible whenever fiber f₁ (t .fst) is. Use isContractibleMap→isEquiv and isEquiv→isContractibleMap to complete the proof.

Σ-map-fst-isEquiv : {B : A'  Type }
   (f₁ : A  A')
   isEquiv f₁ 
   isEquiv (Σ-map-fst {B = B} f₁)
-- Exercise:
Σ-map-fst-isEquiv f₁ e = isContractibleMap→isEquiv  t  isContr-equiv (Σ-map-fst-fib-≃ f₁ t) (isEquiv→isContractibleMap e (t .fst)))

Finally, combine Σ-map-fst-≃ with Σ-map-snd-≃ to prove the original result were looking for.

Σ-map-≃ : {B : A  Type ℓ'} {B' : A'  Type ℓ'}
         (e₁ : A  A')
         (e₂ : (a : A)  (B a)  (B' (e₁ .map a)))
         (Σ[ a  A ] B a)  (Σ[ a'  A' ] B' a')
Σ-map-≃ {A = A} {A' = A'} {B = B} {B' = B'} e₁ e₂ =
  Σ[ a   A  ] B  a           ≃⟨ Σ-map-snd-≃ e₂ 
  Σ[ a   A  ] B' (e₁ .map a) ≃⟨ Σ-map-fst-≃ e₁ 
  Σ[ a'  A' ] B' a'          ∎e

References and Further Reading

https://cj-xu.github.io/faum/escardo.pdf