13.4. Propositional Equality🔗

Propositional equality is the operator that allows the equality of two terms to be stated as a proposition. Definitional equality is checked automatically where necessary. As a result, its expressive power is limited in order to keep the algorithm that checks it fast and understandable. Propositional equality, on the other hand, must be explicitly proved and explicitly used—Lean checks the validity of the proofs, rather than determining whether the statement is true. In exchange, it is much more expressive: many terms are propositionally equal without being definitionally equal.

Propositional equality is defined as an inductive type. Its sole constructor Eq.refl requires that both of the equated values be the same; this is implicitly an appeal to definitional equality. Propositional equality can also be thought of as the least reflexive relation modulo definitional equality. In addition to Eq.refl, equality proofs are generated by the propext and Quot.sound axioms.

🔗inductive predicate
Eq.{u_1} {α : Sort u_1} : ααProp

The equality relation. It has one introduction rule, Eq.refl. We use a = b as notation for Eq a b. A fundamental property of equality is that it is an equivalence relation.

variable (α : Type) (a b c d : α) variable (hab : a = b) (hcb : c = b) (hcd : c = d) example : a = d := Eq.trans (Eq.trans hab (Eq.symm hcb)) hcd

Equality is much more than an equivalence relation, however. It has the important property that every assertion respects the equivalence, in the sense that we can substitute equal expressions without changing the truth value. That is, given h1 : a = b and h2 : p a, we can construct a proof for p b using substitution: Eq.subst h1 h2. Example:

example (α : Type) (a b : α) (p : α Prop) (h1 : a = b) (h2 : p a) : p b := Eq.subst h1 h2 example (α : Type) (a b : α) (p : α Prop) (h1 : a = b) (h2 : p a) : p b := h1 h2

The triangle in the second presentation is a macro built on top of Eq.subst and Eq.symm, and you can enter it by typing \t. For more information: Equality

Conventions for notations in identifiers:

  • The recommended spelling of = in identifiers is eq.

Constructors

refl.{u_1} {α : Sort u_1} (a : α) : a = a

Eq.refl a : a = a is reflexivity, the unique constructor of the equality type. See also rfl, which is usually used instead.

syntaxPropositional Equality
term ::= ...
    | The equality relation. It has one introduction rule, `Eq.refl`.
We use `a = b` as notation for `Eq a b`.
A fundamental property of equality is that it is an equivalence relation.
```
variable (α : Type) (a b c d : α)
variable (hab : a = b) (hcb : c = b) (hcd : c = d)

example : a = d :=
  Eq.trans (Eq.trans hab (Eq.symm hcb)) hcd
```
Equality is much more than an equivalence relation, however. It has the important property that every assertion
respects the equivalence, in the sense that we can substitute equal expressions without changing the truth value.
That is, given `h1 : a = b` and `h2 : p a`, we can construct a proof for `p b` using substitution: `Eq.subst h1 h2`.
Example:
```
example (α : Type) (a b : α) (p : α → Prop)
        (h1 : a = b) (h2 : p a) : p b :=
  Eq.subst h1 h2

example (α : Type) (a b : α) (p : α → Prop)
    (h1 : a = b) (h2 : p a) : p b :=
  h1 ▸ h2
```
The triangle in the second presentation is a macro built on top of `Eq.subst` and `Eq.symm`, and you can enter it by typing `\t`.
For more information: [Equality](https://lean-lang.org/theorem_proving_in_lean4/quantifiers_and_equality.html#equality)


Conventions for notations in identifiers:

 * The recommended spelling of `=` in identifiers is `eq`.term = term

Propositional equality is typically denoted by the infix = operator.

🔗def
rfl.{u} {α : Sort u} {a : α} : a = a

rfl : a = a is the unique constructor of the equality type. This is the same as Eq.refl except that it takes a implicitly instead of explicitly.

This is a more powerful theorem than it may appear at first, because although the statement of the theorem is a = a, Lean will allow anything that is definitionally equal to that type. So, for instance, 2 + 2 = 4 is proven in Lean by rfl, because both sides are the same up to definitional equality.

🔗
Eq.symm.{u} {α : Sort u} {a b : α} (h : a = b) :
  b = a

Equality is symmetric: if a = b then b = a.

Because this is in the Eq namespace, if you have a variable h : a = b, h.symm can be used as shorthand for Eq.symm h as a proof of b = a.

For more information: Equality

🔗
Eq.trans.{u} {α : Sort u} {a b c : α}
  (h₁ : a = b) (h₂ : b = c) : a = c

Equality is transitive: if a = b and b = c then a = c.

Because this is in the Eq namespace, if you have variables or expressions h₁ : a = b and h₂ : b = c, you can use h₁.trans h₂ : a = c as shorthand for Eq.trans h₁ h₂.

For more information: Equality

🔗
Eq.subst.{u} {α : Sort u} {motive : αProp}
  {a b : α} (h₁ : a = b) (h₂ : motive a) :
  motive b

The substitution principle for equality. If a = b and P a holds, then P b also holds. We conventionally use the name motive for P here, so that you can specify it explicitly using e.g. Eq.subst (motive := fun x => x < 5) if it is not otherwise inferred correctly.

This theorem is the underlying mechanism behind the rw tactic, which is essentially a fancy algorithm for finding good motive arguments to usefully apply this theorem to replace occurrences of a with b in the goal or hypotheses.

For more information: Equality

🔗def
cast.{u} {α β : Sort u} (h : α = β) (a : α) : β

Cast across a type equality. If h : α = β is an equality of types, and a : α, then a : β will usually not typecheck directly, but this function will allow you to work around this and embed a in type β as cast h a : β.

It is best to avoid this function if you can, because it is more complicated to reason about terms containing casts, but if the types don't match up definitionally sometimes there isn't anything better you can do.

For more information: Equality

🔗
congr.{u, v} {α : Sort u} {β : Sort v}
  {f₁ f₂ : αβ} {a₁ a₂ : α} (h₁ : f₁ = f₂)
  (h₂ : a₁ = a₂) : f₁ a₁ = f₂ a₂

Congruence in both function and argument. If f₁ = f₂ and a₁ = a₂ then f₁ a₁ = f₂ a₂. This only works for nondependent functions; the theorem statement is more complex in the dependent case.

For more information: Equality

🔗
congrFun.{u, v} {α : Sort u} {β : αSort v}
  {f g : (x : α) → β x} (h : f = g) (a : α) :
  f a = g a

Congruence in the function part of an application: If f = g then f a = g a.

🔗
congrArg.{u, v} {α : Sort u} {β : Sort v}
  {a₁ a₂ : α} (f : αβ) (h : a₁ = a₂) :
  f a₁ = f a₂

Congruence in the function argument: if a₁ = a₂ then f a₁ = f a₂ for any (nondependent) function f. This is more powerful than it might look at first, because you can also use a lambda expression for f to prove that <something containing a₁> = <something containing a₂>. This function is used internally by tactics like congr and simp to apply equalities inside subterms.

For more information: Equality

🔗def
Eq.mp.{u} {α β : Sort u} (h : α = β) (a : α) : β

If h : α = β is a proof of type equality, then h.mp : α β is the induced "cast" operation, mapping elements of α to elements of β.

You can prove theorems about the resulting element by induction on h, since rfl.mp is definitionally the identity function.

🔗def
Eq.mpr.{u} {α β : Sort u} (h : α = β) (b : β) :
  α

If h : α = β is a proof of type equality, then h.mpr : β α is the induced "cast" operation in the reverse direction, mapping elements of β to elements of α.

You can prove theorems about the resulting element by induction on h, since rfl.mpr is definitionally the identity function.

syntaxCasting
term ::= ...
    | `h ▸ e` is a macro built on top of `Eq.rec` and `Eq.symm` definitions.
Given `h : a = b` and `e : p a`, the term `h ▸ e` has type `p b`.
You can also view `h ▸ e` as a "type casting" operation
where you change the type of `e` by using `h`.

The macro tries both orientations of `h`. If the context provides an
expected type, it rewrites the expected type, else it rewrites the type of e`.

See the Chapter "Quantifiers and Equality" in the manual
"Theorem Proving in Lean" for additional information.
term  term

When a term's type includes one side of an equality as a sub-term, it can be rewritten using the operator. If the both sides of the equality occur in the term's type, then the left side is rewritten to the right.

13.4.1. Uniqueness of Equality Proofs🔗

Because of definitional proof irrelevance, propositional equality proofs are unique: two mathematical objects cannot be equal in different ways.

theorem Eq.unique {α : Sort u} (x y : α) (p1 p2 : x = y) : p1 = p2 := α:Sort ux:αy:αp1:x = yp2:x = yp1 = p2 All goals completed! 🐙

Streicher's axiom K (Streicher, 1993)Thomas Streicher, 1993. Investigations into Intensional Type Theory. Habilitation, Ludwig-Maximilians-Universität München is also a consequence of definitional proof irrelevance, as is its computation rule. Axiom K is a principle that's logically equivalent to Eq.unique, implemented as an alternative recursor for propositional equality.

def K {α : Sort u} {motive : {x : α} x = x Sort v} (d : {x : α} motive (Eq.refl x)) (x : α) (z : x = x) : motive z := d example {α : Sort u} {a : α} {motive : {x : α} x = x Sort u} {d : {x : α} motive (Eq.refl x)} {v : motive (Eq.refl a)} : K (motive := motive) d a rfl = d := α:Sort ua:αmotive:{x : α} → x = xSort ud:{x : α} → motivev:motiveK (fun {x} => d) a = d All goals completed! 🐙

13.4.2. Heterogeneous Equality🔗

Heterogeneous equality is a version of propositional equality that does not require that the two equated terms have the same type. However, proving that the terms are equal using its version of rfl requires that both the types and the terms are definitionally equal. In other words, it allows more statements to be formulated.

Heterogeneous equality is typically less convenient in practice than ordinary propositional equality. The greater flexibility afforded by not requiring both sides of the equality to have the same type means that it has fewer useful properties. It is often encountered as a result of dependent pattern matching: the split tactic and functional induction add heterogeneous equality assumptions to the context when the ordinary equality assumptions that are needed to accurate reflect the corresponding control flow would not be type correct. In these cases, the built-in automation has no choice but to use heterogeneous equality.

🔗inductive predicate
HEq.{u} {α : Sort u} :
  α → {β : Sort u} → βProp

Heterogeneous equality. HEq a b asserts that a and b have the same type, and casting a across the equality yields b, and vice versa.

You should avoid using this type if you can. Heterogeneous equality does not have all the same properties as Eq, because the assumption that the types of a and b are equal is often too weak to prove theorems of interest. One important non-theorem is the analogue of congr: If HEq f g and HEq x y and f x and g y are well typed it does not follow that HEq (f x) (g y). (This does follow if you have f = g instead.) However if a and b have the same type then a = b and HEq a b are equivalent.

Constructors

refl.{u} {α : Sort u} (a : α) : HEq a a

Reflexivity of heterogeneous equality.

🔗def
HEq.rfl.{u} {α : Sort u} {a : α} : HEq a a

A version of HEq.refl with an implicit argument.

Heterogeneous Equality

The type Vector α n is wrapper around an Array α that includes a proof that the array has size n. Appending Vectors is associative, but this fact cannot be straightforwardly stated using ordinary propositional equality:

variable {xs : Vector α l₁} {ys : Vector α l₂} {zs : Vector α l₃} theorem Vector.append_assoc : xs ++ (ys ++ zs) = type mismatch xs ++ ys ++ zs has type Vector α (l₁ + l₂ + l₃) : outParam (Type u) but is expected to have type Vector α (l₁ + (l₂ + l₃)) : outParam (Type u)(xs ++ ys) ++ zs := sorry `«Manual.BasicProps:418:4» All goals completed! 🐙

The problem is that the associativity of addition of natural numbers holds propositionally, but not definitionally:

type mismatch
  xs ++ ys ++ zs
has type
  Vector α (l₁ + l₂ + l₃) : outParam (Type u)
but is expected to have type
  Vector α (l₁ + (l₂ + l₃)) : outParam (Type u)

One solution to this problem is to use the associativity of natural number addition in the statement:

theorem declaration uses 'sorry'Vector.append_assoc' : xs ++ (ys ++ zs) = Nat.add_assoc _ _ _ ((xs ++ ys) ++ zs) := α:Type ul₁:Natl₂:Natl₃:Natxs:Vector α l₁ys:Vector α l₂zs:Vector α l₃xs ++ (ys ++ zs) = (xs ++ ys ++ zs) All goals completed! 🐙

However, such proof statements can be difficult to work with in certain circumstances.

Another is to use heterogeneous equality:

theorem declaration uses 'sorry'Vector.append_assoc : HEq (xs ++ (ys ++ zs)) ((xs ++ ys) ++ zs) := α:Type ul₁:Natl₂:Natl₃:Natxs:Vector α l₁ys:Vector α l₂zs:Vector α l₃HEq (xs ++ (ys ++ zs)) (xs ++ ys ++ zs) All goals completed! 🐙

In this case, the simplifier can rewrite both sides of the equation without having to preserve their types. However, proving the theorem does require eventually proving that the lengths nonetheless match.

theorem Vector.append_assoc : HEq (xs ++ (ys ++ zs)) ((xs ++ ys) ++ zs) := α:Type ul₁:Natl₂:Natl₃:Natxs:Vector α l₁ys:Vector α l₂zs:Vector α l₃HEq (xs ++ (ys ++ zs)) (xs ++ ys ++ zs) α:Type ul₁:Natl₂:Natl₃:Natys:Vector α l₂zs:Vector α l₃toArray✝:Array αsize_toArray✝:toArray✝.size = l₁HEq ({ toArray := toArray✝, size_toArray := size_toArray✝ } ++ (ys ++ zs)) ({ toArray := toArray✝, size_toArray := size_toArray✝ } ++ ys ++ zs); α:Type ul₁:Natl₂:Natl₃:Natzs:Vector α l₃toArray✝¹:Array αsize_toArray✝¹:toArray✝¹.size = l₁toArray✝:Array αsize_toArray✝:toArray✝.size = l₂HEq ({ toArray := toArray✝¹, size_toArray := size_toArray✝¹ } ++ ({ toArray := toArray✝, size_toArray := size_toArray✝ } ++ zs)) ({ toArray := toArray✝¹, size_toArray := size_toArray✝¹ } ++ { toArray := toArray✝, size_toArray := size_toArray✝ } ++ zs); α:Type ul₁:Natl₂:Natl₃:NattoArray✝²:Array αsize_toArray✝²:toArray✝².size = l₁toArray✝¹:Array αsize_toArray✝¹:toArray✝¹.size = l₂toArray✝:Array αsize_toArray✝:toArray✝.size = l₃HEq ({ toArray := toArray✝², size_toArray := size_toArray✝² } ++ ({ toArray := toArray✝¹, size_toArray := size_toArray✝¹ } ++ { toArray := toArray✝, size_toArray := size_toArray✝ })) ({ toArray := toArray✝², size_toArray := size_toArray✝² } ++ { toArray := toArray✝¹, size_toArray := size_toArray✝¹ } ++ { toArray := toArray✝, size_toArray := size_toArray✝ }) α:Type ul₁:Natl₂:Natl₃:NattoArray✝²:Array αsize_toArray✝²:toArray✝².size = l₁toArray✝¹:Array αsize_toArray✝¹:toArray✝¹.size = l₂toArray✝:Array αsize_toArray✝:toArray✝.size = l₃HEq { toArray := toArray✝² ++ (toArray✝¹ ++ toArray✝), size_toArray := ⋯ } { toArray := toArray✝² ++ (toArray✝¹ ++ toArray✝), size_toArray := ⋯ } α:Type ul₁:Natl₂:Natl₃:NattoArray✝²:Array αsize_toArray✝²:toArray✝².size = l₁toArray✝¹:Array αsize_toArray✝¹:toArray✝¹.size = l₂toArray✝:Array αsize_toArray✝:toArray✝.size = l₃l₁ + (l₂ + l₃) = l₁ + l₂ + l₃α:Type ul₁:Natl₂:Natl₃:NattoArray✝²:Array αsize_toArray✝²:toArray✝².size = l₁toArray✝¹:Array αsize_toArray✝¹:toArray✝¹.size = l₂toArray✝:Array αsize_toArray✝:toArray✝.size = l₃HEq ⋯ ⋯ α:Type ul₁:Natl₂:Natl₃:NattoArray✝²:Array αsize_toArray✝²:toArray✝².size = l₁toArray✝¹:Array αsize_toArray✝¹:toArray✝¹.size = l₂toArray✝:Array αsize_toArray✝:toArray✝.size = l₃l₁ + (l₂ + l₃) = l₁ + l₂ + l₃ All goals completed! 🐙 α:Type ul₁:Natl₂:Natl₃:NattoArray✝²:Array αsize_toArray✝²:toArray✝².size = l₁toArray✝¹:Array αsize_toArray✝¹:toArray✝¹.size = l₂toArray✝:Array αsize_toArray✝:toArray✝.size = l₃HEq ⋯ ⋯ α:Type ul₁:Natl₂:Natl₃:NattoArray✝²:Array αsize_toArray✝²:toArray✝².size = l₁toArray✝¹:Array αsize_toArray✝¹:toArray✝¹.size = l₂toArray✝:Array αsize_toArray✝:toArray✝.size = l₃ = α:Type ul₁:Natl₂:Natl₃:NattoArray✝²:Array αsize_toArray✝²:toArray✝².size = l₁toArray✝¹:Array αsize_toArray✝¹:toArray✝¹.size = l₂toArray✝:Array αsize_toArray✝:toArray✝.size = l₃((toArray✝² ++ (toArray✝¹ ++ toArray✝)).size = l₁ + (l₂ + l₃)) = ((toArray✝² ++ (toArray✝¹ ++ toArray✝)).size = l₁ + l₂ + l₃) α:Type ul₁:Natl₂:Natl₃:NattoArray✝²:Array αsize_toArray✝²:toArray✝².size = l₁toArray✝¹:Array αsize_toArray✝¹:toArray✝¹.size = l₂toArray✝:Array αsize_toArray✝:toArray✝.size = l₃ = All goals completed! 🐙 α:Type ul₁:Natl₂:Natl₃:NattoArray✝²:Array αsize_toArray✝²:toArray✝².size = l₁toArray✝¹:Array αsize_toArray✝¹:toArray✝¹.size = l₂toArray✝:Array αsize_toArray✝:toArray✝.size = l₃((toArray✝² ++ (toArray✝¹ ++ toArray✝)).size = l₁ + (l₂ + l₃)) = ((toArray✝² ++ (toArray✝¹ ++ toArray✝)).size = l₁ + l₂ + l₃) α:Type ul₁:Natl₂:Natl₃:NattoArray✝²:Array αsize_toArray✝²:toArray✝².size = l₁toArray✝¹:Array αsize_toArray✝¹:toArray✝¹.size = l₂toArray✝:Array αsize_toArray✝:toArray✝.size = l₃(toArray✝² ++ (toArray✝¹ ++ toArray✝)).size = l₁ + (l₂ + l₃)(toArray✝² ++ (toArray✝¹ ++ toArray✝)).size = l₁ + l₂ + l₃ α:Type ul₁:Natl₂:Natl₃:NattoArray✝²:Array αsize_toArray✝²:toArray✝².size = l₁toArray✝¹:Array αsize_toArray✝¹:toArray✝¹.size = l₂toArray✝:Array αsize_toArray✝:toArray✝.size = l₃(toArray✝² ++ (toArray✝¹ ++ toArray✝)).size = l₁ + (l₂ + l₃)(toArray✝² ++ (toArray✝¹ ++ toArray✝)).size = l₁ + l₂ + l₃α:Type ul₁:Natl₂:Natl₃:NattoArray✝²:Array αsize_toArray✝²:toArray✝².size = l₁toArray✝¹:Array αsize_toArray✝¹:toArray✝¹.size = l₂toArray✝:Array αsize_toArray✝:toArray✝.size = l₃(toArray✝² ++ (toArray✝¹ ++ toArray✝)).size = l₁ + l₂ + l₃(toArray✝² ++ (toArray✝¹ ++ toArray✝)).size = l₁ + (l₂ + l₃) α:Type ul₁:Natl₂:Natl₃:NattoArray✝²:Array αsize_toArray✝²:toArray✝².size = l₁toArray✝¹:Array αsize_toArray✝¹:toArray✝¹.size = l₂toArray✝:Array αsize_toArray✝:toArray✝.size = l₃(toArray✝² ++ (toArray✝¹ ++ toArray✝)).size = l₁ + (l₂ + l₃)(toArray✝² ++ (toArray✝¹ ++ toArray✝)).size = l₁ + l₂ + l₃α:Type ul₁:Natl₂:Natl₃:NattoArray✝²:Array αsize_toArray✝²:toArray✝².size = l₁toArray✝¹:Array αsize_toArray✝¹:toArray✝¹.size = l₂toArray✝:Array αsize_toArray✝:toArray✝.size = l₃(toArray✝² ++ (toArray✝¹ ++ toArray✝)).size = l₁ + l₂ + l₃(toArray✝² ++ (toArray✝¹ ++ toArray✝)).size = l₁ + (l₂ + l₃) α:Type ul₁:Natl₂:Natl₃:NattoArray✝²:Array αsize_toArray✝²:toArray✝².size = l₁toArray✝¹:Array αsize_toArray✝¹:toArray✝¹.size = l₂toArray✝:Array αsize_toArray✝:toArray✝.size = l₃h:(toArray✝² ++ (toArray✝¹ ++ toArray✝)).size = l₁ + l₂ + l₃(toArray✝² ++ (toArray✝¹ ++ toArray✝)).size = l₁ + (l₂ + l₃) α:Type ul₁:Natl₂:Natl₃:NattoArray✝²:Array αsize_toArray✝²:toArray✝².size = l₁toArray✝¹:Array αsize_toArray✝¹:toArray✝¹.size = l₂toArray✝:Array αsize_toArray✝:toArray✝.size = l₃h:(toArray✝² ++ (toArray✝¹ ++ toArray✝)).size = l₁ + (l₂ + l₃)(toArray✝² ++ (toArray✝¹ ++ toArray✝)).size = l₁ + l₂ + l₃α:Type ul₁:Natl₂:Natl₃:NattoArray✝²:Array αsize_toArray✝²:toArray✝².size = l₁toArray✝¹:Array αsize_toArray✝¹:toArray✝¹.size = l₂toArray✝:Array αsize_toArray✝:toArray✝.size = l₃h:(toArray✝² ++ (toArray✝¹ ++ toArray✝)).size = l₁ + l₂ + l₃(toArray✝² ++ (toArray✝¹ ++ toArray✝)).size = l₁ + (l₂ + l₃) All goals completed! 🐙
🔗def
HEq.elim.{u, v} {α : Sort u} {a : α}
  {p : αSort v} {b : α} (h₁ : HEq a b)
  (h₂ : p a) : p b

HEq.ndrec variant

🔗def
HEq.ndrec.{u1, u2} {α : Sort u2} {a : α}
  {motive : {β : Sort u2} → βSort u1}
  (m : motive a) {β : Sort u2} {b : β}
  (h : HEq a b) : motive b

Non-dependent recursor for HEq

🔗def
HEq.ndrecOn.{u1, u2} {α : Sort u2} {a : α}
  {motive : {β : Sort u2} → βSort u1}
  {β : Sort u2} {b : β} (h : HEq a b)
  (m : motive a) : motive b

HEq.ndrec variant

🔗
HEq.subst.{u} {α β : Sort u} {a : α} {b : β}
  {p : (T : Sort u) → TProp} (h₁ : HEq a b)
  (h₂ : p α a) : p β b
🔗
eq_of_heq.{u} {α : Sort u} {a a' : α}
  (h : HEq a a') : a = a'
🔗
heq_of_eq.{u} {α : Sort u} {a a' : α}
  (h : a = a') : HEq a a'
🔗
heq_of_eqRec_eq.{u} {α β : Sort u} {a : α}
  {b : β} (h₁ : α = β) (h₂ : h₁a = b) :
  HEq a b
🔗
eqRec_heq.{u, v} {α : Sort u} {φ : αSort v}
  {a a' : α} (h : a = a') (p : φ a) :
  HEq (Eq.recOn h p) p
🔗
cast_heq.{u} {α β : Sort u} (h : α = β)
  (a : α) : HEq (cast h a) a
🔗
heq_of_heq_of_eq.{u} {α β : Sort u} {a : α}
  {b b' : β} (h₁ : HEq a b) (h₂ : b = b') :
  HEq a b'
🔗
type_eq_of_heq.{u} {α β : Sort u} {a : α}
  {b : β} (h : HEq a b) : α = β