Pavel Panchekha

By

Share under CC-BY-SA.

Why Equality?

When I started studying dependent types a few years ago, one question I never got a good answer to was: why is equality such a big deal?

Equality types are, of course, practically important. They're practically important because we want to prove stuff equal to other stuff a lot! But why does adding equality types necessitate so much deep theory about how a dependent type system works?1 [1 After all, integers, also a complicated concept, can be added cleanly and independently, even though inductive types are pretty complex.]

At first, I came up with an answer based on the importance of identity, but it was unsatisfying. I now think about equality as being about computation. I'd like to explain both views.

Assume for this post a dependently-typed language with equality but without inductive types.2 [2 Despite their initial simplicity, I think inductive types are confusing and I do not understand them.]

Identity

My first explanation of the importance of equality is that equality is the only built-in function of type A → Prop, and therefore is necessary to give meaning to values.

For any formal system, it is important to ask, what constrains the system to make it correspond to the real world? For dependent type systems, all differences stem from differences between types.

Imagine for a moment a “hostile” dependent type system. How do you know that what it calls the type of natural numbers is the type of natural numbers? Possibly what it calls the type of natural numbers has extra members; or possibly extra properties; or possibly two different numbers are actually equal. How can you tell otherwise?

I discussed this a bit in my post on function extensionality, where I pointed out that we prove things unequal (a value-level difference) by mapping one of the values to and the other to (a type-level difference).

Mapping one value to and one to proves they are different because those two types are different, and those differences stem from the fact that one can write x : ⊤ but one can never write x : ⊥. Note that a : A, the basic judgment in dependent type theory, is controlled by the inference rules we write down. So it makes sense to say that the fact that those rules treat and differently creates type-level differences, and those type-level differences create value-level differences.3 [3 Those value-level differences also rely on the right elimination rule for values, of course. Different elimination rules define different types with values that are different in different ways.]

This viewpoint goes some way toward explaining why, in theorem proving, functions A → Prop are seen as more fundamentally meaningful than functions A → bool. We write down and prove propositions in Prop; we don't compute bool values.

So how does equality enter into this?

Equality is important is because it is the type constructor that has value-level arguments and thus allows raising value-level reasoning to the type level. Raising value-level reasoning to the type level allows differences in values to turn into differences in types, and differences in types are the fundamental differences, so equality is what creates differences at the value level. In short form, you would say that the equality type defines value identity.

The equality type is an especially good way to raise value-level reasoning to type-level reasoning: it is a function A → Prop for all A simultaneously, and it is also very simple, just comparing two objects.

Without an equality type, the only functions A → Prop would apply only to particular A and would be user-defined functions like:

(x ↦ if x then ⊤ else ⊥) : bool → Prop

This would be inconvenient, because there would be no generic equality function and no general notion of value identity (just type-specific notions).

This was my first answer, but it wasn't satisfying. It explains why we need some type from A → Prop to come built-in, but not the peculiar A → A → Prop signature of equality.4 [4 At best, I can see an argument that equality is the simplest, or at least, a very simple such function.] And why define a uniform proposition? I can think of lots of nat → Prop functions, why aren't those enough?5 [5 Type-specific functions wouldn't establish a type-generic notion of value identity, but why is that so important? Perhaps one can argue that if you want type abstraction and identity, you need an generic A → Prop function.] And if we are going to have two arguments, why equality?6 [6 We could order every type (arbitrarily) and use an ordering relation, for example!] Even if we think equality is just practically important, why not add define equality differently, maybe like the following inductive type:

type (=) {A} (a b : A)  inductive
| refl A a : a = a
| funext (A  B) f g : ( x : A, f x = g x)  f = g

If we use this definition, we get an ugly induction principle (because A is an index, not a parameter) but why does that matter in a fundamental sense if all we really want is a function A → Prop?

Computation

I have a new answer to why equality is important: equality internalizes substitution, which is the core principle of computation in a dependent type system.

Computation in a dependent type system happens by reducing eliminators applied to known constructors, until no eliminators are left. For example, if you define add like so:

def add (x : ) :    
  induct x
  | 0  (y  y)
  | S x'  (y  S (add x' y))

and then run add 3 2, we end up applying the eliminator induct for natural numbers to a constructor 3 for natural numbers. All computation happens by rewriting combinations of eliminators and constructors.

As we know, the most important data type in a dependently typed language is functions, and they have a constructor, λ, and an eliminator, application, (? ?). So when I write add 3 2, actually what I have is the eliminator for functions, application, applied to a constructor for functions, add. This eliminator requires a special sauce: substitution. Since functions are the most important data type, their computation rule, substitution, is the most important computation rule.

In most presentations of dependent type systems, substitution is presented with a relation a ≡ b on terms, which means "intersubstitutible" and which includes at least all of the eliminator-constructor reductions. It participates in a bunch of important rules in the dependent type system, such as the rule that if a : A and A ≡ B then a : B. This rule is a consequence of the fact that if A ≡ B, then they can be substituted for one another in the judgment a : ?.

So far I've described a particular way to look at how a dependent type system is built. Now let's look at equality.

My view is that the equality type is an internalization of substitution, and therefore an internalization of the main form of computation. In this view, equality is the combination of a constructor, where if a ≡ b then you can write a = b, and an eliminator, where if a = b then you can substitute them in an arbitrary context.7 [7 At this point, details might require separate type-level and value-level equality functions, and separate type-level and value-level eliminators, depending on how your dependent type system is set up.]

In this view, the fact that the constructor can be written in the cutesy form (a : A) → a = a it a confusing shorthand, and the fact that the elimination principle is exactly the induction principle for an inductive type with that one constructor is a strange coincidence.8 [8 The coincidence must be related to the fact that if we have only this one constructor, we have no values that are equal except for the syntactically equal ones. It's still a little magical that the constructor gives us the right induction principle, and this all likely has something to do with parametricity.] So some mysteries remain, but I think linking equality to substitution and thus to computation is crucial here. It suggests that an equality is a natural consequence of having functions, the product of a drive to internalize the meta-theory.

Future directions

To summarize, my current view of a dependent type system is something like this:

  • We start by defining constructors and eliminators; computation proceeds by recognizing eliminator-constructor pairs and rewriting them according to various rules, which we choose to correspond to the real world.
  • We then add functions because we want to add hypothetical reasoning.9 [9 Dependent types become possible here, and in fact there's no reason not to add them.] We pretend that functions work using a similar eliminator-constructor mechanism, but this mechanism includes the new substitution operation, which, because functions are so important, becomes the most important form of computation.
  • To internalize this form of computation, which unlike the previous forms is tricky enough to warrant internalization, we add equality.

There's a few questions which right now I can't answer:

  • Why do the constructor and eliminator for equality types match so well? The work on HoTT and cubical type theory suggest that this is not an easy question, since they involve adding new constructors without changing the eliminator. I suspect the coincidence between constructor and eliminator is an illusion, and derives from the fact that pure substitution never "does anything" in some vague sense; after all, without adding new equality constructors, the only values that can be proven equal are identical values.10 [10 Non-identical terms can be proven equal, that's different.]
  • What could it mean to internalize the eliminator-constructor structure of computation that existed before we added functions? Is it trivial? Is it hiding somewhere in the structure of a dependent type theory, such as the fact that eliminator-constructor pairs are considered intersubstitutible? If that's it, how would we internalize eliminator-constructor pairs if we didn't have functions and thus didn't have substitution?
  • Are there other ways to define function elimination that don't involve substitution and thus do not require equality? I wrote up one attempt in my Fine language, where functions are given multiple constructors and where elimination does not require substitution.

The framework I presented here for understanding equality and its place is, I think, one good way to structure your understanding of dependent type systems. Let me know if you like it and have any thoughts.

Footnotes:

1

After all, integers, also a complicated concept, can be added cleanly and independently, even though inductive types are pretty complex.

2

Despite their initial simplicity, I think inductive types are confusing and I do not understand them.

3

Those value-level differences also rely on the right elimination rule for values, of course. Different elimination rules define different types with values that are different in different ways.

4

At best, I can see an argument that equality is the simplest, or at least, a very simple such function.

5

Type-specific functions wouldn't establish a type-generic notion of value identity, but why is that so important? Perhaps one can argue that if you want type abstraction and identity, you need an generic A → Prop function.

6

We could order every type (arbitrarily) and use an ordering relation, for example!

7

At this point, details might require separate type-level and value-level equality functions, and separate type-level and value-level eliminators, depending on how your dependent type system is set up.

8

The coincidence must be related to the fact that if we have only this one constructor, we have no values that are equal except for the syntactically equal ones. It's still a little magical that the constructor gives us the right induction principle, and this all likely has something to do with parametricity.

9

Dependent types become possible here, and in fact there's no reason not to add them.

10

Non-identical terms can be proven equal, that's different.