Equality in Dependent Type Theories
A common way to define equality in type theories,
attributed to Per Martin-Löf,
is as follows:
a == b is a type with two parameters a and b,
and it has a single constructor refl x with type x == x.
You might find this weird: the two parameters feel a bit useless since they’ll always be the same.
And you’d be right: them being the same is the whole point of this being an equality.
The way this works is that if you need a proof that two types are equal then you just take a a ==
b a argument.
So what can you do with an a == b?
My favorite definition is the one from the HoTT book:
from the definition of the type,
we automatically get a “destructor” function1 :
transport: (a: Type) -> (b: Type) -> (f: Type -> Type) -> a == b -> f a -> f b
This function says: if a == b, then I can turn a f a into a f b for any function f.
What I find insanely cute, and what compelled me to write this, is that this single function is
enough to define a reasonable notion of equality.
Here is a proof of symmetry, in a suspiciously Rust-looking dependent lambda-calculus:
let symmetry(a: Type, b: Type, ab: a == b) -> b == a =
transport a b (|x: Type| x == a) ab (refl a)
Here’s how it works: the function being transported is |x| x == a.
So given a == b, transport will turn a == a into b == a.
We can build a a == a using refl, so we win!
Here is transitivity:
let transitivity(a: Type, b: Type, c: Type, ab: a == b, bc: b == c) -> a == c =
(transport b c (|x: Type| a == x) bc) ab
The idea is similar: we transport |x| a == x from b to c.
That’s all I had to say, I don’t know why this feels so good to my brain but now you can taste it too! If you enjoyed this, go read the HoTT book.