Congruence and Subtyping
Every type system makes tradeoffs between precision and convenience. A function that takes Nat will accept any natural number, including zero, even when zero would cause a division error three stack frames later. A function that takes Int cannot directly accept a Nat without explicit conversion, even though every natural number is an integer. The constraints are either too loose or the syntax is too verbose. Pick your frustration.
Lean provides tools to fix both problems. Subtypes let you carve out precisely the values you want: positive numbers, non-empty lists, valid indices. The constraint travels with the value, enforced by the type system. Coercions let the compiler insert safe conversions automatically, so you can pass a Nat where an Int is expected without ceremony. These mechanisms together give you precise types with ergonomic syntax.
Types are sets with attitude. A Nat is not just the natural numbers but the natural numbers with all their operations and laws. A subtype narrows this: the positive natural numbers are the naturals with an extra constraint, a proof obligation that travels with every value. This is refinement: taking a broad type and carving out the subset you actually need.
The other direction is coercion. When Lean expects an Int but you give it a Nat, something must convert between them. Explicit casts are tedious. Coercions make the compiler do the work, inserting conversions automatically where safe. The result is code that looks like it mixes types freely but maintains type safety underneath.
Congruence handles the third concern: propagating equality. If a = b, then f a = f b. This seems obvious, but the compiler needs to be told. The congr tactic applies this principle systematically, breaking equality goals into their components.
Subtypes
A subtype refines an existing type with a predicate. Values of a subtype carry both the data and a proof that the predicate holds.
def Positive := { n : Nat // n > 0 }
def five : Positive := ⟨5, by decide⟩
#eval five.val -- 5
def NonEmpty (α : Type) := { xs : List α // xs ≠ [] }
def singletonList : NonEmpty Nat := ⟨[42], by decide⟩
Working with Subtypes
Functions on subtypes can access the underlying value and use the proof to ensure operations are safe.
def doublePositive (p : Positive) : Positive :=
⟨p.val * 2, Nat.mul_pos p.property (by decide)⟩
#eval (doublePositive five).val -- 10
def addPositive (a b : Positive) : Positive :=
⟨a.val + b.val, Nat.add_pos_left a.property b.val⟩
def safeHead {α : Type} (xs : NonEmpty α) : α :=
match h : xs.val with
| x :: _ => x
| [] => absurd h xs.property
Refinement Types
Subtypes let you express precise invariants. Common patterns include bounded numbers, non-zero values, and values satisfying specific properties.
def Even := { n : Nat // n % 2 = 0 }
def Odd := { n : Nat // n % 2 = 1 }
def zero' : Even := ⟨0, rfl⟩
def two' : Even := ⟨2, rfl⟩
def one' : Odd := ⟨1, rfl⟩
def three' : Odd := ⟨3, rfl⟩
def BoundedNat (lo hi : Nat) := { n : Nat // lo ≤ n ∧ n < hi }
def inRange : BoundedNat 0 10 := ⟨5, by omega⟩
Basic Coercions
Coercions allow automatic type conversion. When a value of type A is expected but you provide type B, Lean looks for a coercion from B to A.
instance : Coe Positive Nat where
coe p := p.val
def useAsNat (p : Positive) : Nat :=
p + 10
#eval useAsNat five -- 15
instance {α : Type} : Coe (NonEmpty α) (List α) where
coe xs := xs.val
def listLength (xs : NonEmpty Nat) : Nat :=
xs.val.length
#eval listLength singletonList -- 1
Coercion Chains
Coercions can chain together. If there is a coercion from A to B and from B to C, Lean can automatically convert from A to C.
structure Meters where
val : Float
deriving Repr
structure Kilometers where
val : Float
deriving Repr
instance : Coe Meters Float where
coe m := m.val
instance : Coe Kilometers Meters where
coe km := ⟨km.val * 1000⟩
def addDistances (a : Meters) (b : Kilometers) : Meters :=
⟨a.val + (b : Meters).val⟩
#eval addDistances ⟨500⟩ ⟨1.5⟩ -- Meters.mk 2000.0
Function Coercions
CoeFun allows values to be used as functions. This is useful for callable objects and function-like structures.
instance : CoeFun Positive (fun _ => Nat → Nat) where
coe p := fun n => p.val + n
#eval five 10 -- 15
structure Adder where
amount : Nat
instance : CoeFun Adder (fun _ => Nat → Nat) where
coe a := fun n => n + a.amount
def addFive : Adder := ⟨5⟩
#eval addFive 10 -- 15
Sort Coercions
CoeFun coerces values to functions, allowing structures to behave like callable objects.
structure Predicate' (α : Type) where
test : α → Bool
instance {α : Type} : CoeFun (Predicate' α) (fun _ => α → Bool) where
coe p := p.test
def isEven' : Predicate' Nat := ⟨fun n => n % 2 == 0⟩
#eval isEven' 4 -- true
#eval isEven' 5 -- false
Congruence
The congr tactic applies congruence reasoning: if you need to prove f a = f b and you know a = b, congruence can close the goal.
example (a b : Nat) (h : a = b) : a + 1 = b + 1 := by
congr
example (f : Nat → Nat) (a b : Nat) (h : a = b) : f a = f b := by
congr
example (a b c d : Nat) (h1 : a = b) (h2 : c = d) : a + c = b + d := by
congr <;> assumption
Congruence with Multiple Arguments
Congruence works with functions of multiple arguments, generating subgoals for each argument that differs.
example (f : Nat → Nat → Nat) (a b c d : Nat)
(h1 : a = c) (h2 : b = d) : f a b = f c d := by
rw [h1, h2]
example (xs ys : List Nat) (h : xs = ys) : xs.length = ys.length := by
rw [h]
Substitution and Rewriting
The subst tactic substitutes equal terms, and rw rewrites using equalities. These are fundamental tactics for equality reasoning.
example (a b : Nat) (h : a = b) : a * a = b * b := by
subst h
rfl
example (a b c : Nat) (h1 : a = b) (h2 : b = c) : a = c := by
rw [h1, h2]
example (a b : Nat) (h : a = b) (f : Nat → Nat) : f a = f b := by
rw [h]
Type Conversion
Lean provides automatic coercion between numeric types and explicit conversion functions.
example (n : Nat) : Int := n
def natToInt (n : Nat) : Int := n
#eval natToInt 42 -- 42
def stringToNat? (s : String) : Option Nat :=
s.toNat?
#eval stringToNat? "123" -- some 123
#eval stringToNat? "abc" -- none
Decidable Propositions
A proposition is decidable if there is an algorithm to determine its truth. This enables using propositions in if-expressions.
def isPositive (n : Int) : Decidable (n > 0) :=
if h : n > 0 then isTrue h else isFalse h
def checkPositive (n : Int) : String :=
if n > 0 then "positive" else "not positive"
#eval checkPositive 5 -- "positive"
#eval checkPositive (-3) -- "not positive"
def decideEqual (a b : Nat) : Decidable (a = b) :=
if h : a = b then isTrue h else isFalse h
Type Class Inheritance
Type classes can extend other classes, creating inheritance hierarchies. A type implementing a subclass automatically implements its parent classes.
class Animal (α : Type) where
speak : α → String
class Dog (α : Type) extends Animal α where
fetch : α → String
structure Labrador where
name : String
instance : Animal Labrador where
speak lab := s!"{lab.name} says woof!"
instance : Dog Labrador where
speak lab := s!"{lab.name} says woof!"
fetch lab := s!"{lab.name} fetches the ball!"
def makeSpeak {α : Type} [Animal α] (a : α) : String :=
Animal.speak a
def rex : Labrador := ⟨"Rex"⟩
#eval makeSpeak rex -- "Rex says woof!"
#eval Dog.fetch rex -- "Rex fetches the ball!"
Universe Polymorphism
Types live in a hierarchy of universes. Type is shorthand for Type 0, and each type has a type in the next universe.
def useType (α : Type) : Type := List α
def useType' (α : Type 1) : Type 1 := List α
universe u
def polyType (α : Type u) : Type u := List α
#check (Nat : Type)
#check (Type : Type 1)
#check (Type 1 : Type 2)
Structure Extension
Structures can extend other structures, inheriting their fields while adding new ones.
structure Shape where
name : String
structure Circle extends Shape where
radius : Float
structure Rectangle extends Shape where
width : Float
height : Float
def myCircle : Circle := { name := "unit circle", radius := 1.0 }
def myRect : Rectangle := { name := "square", width := 2.0, height := 2.0 }
#eval myCircle.name -- "unit circle"
#eval myCircle.radius -- 1.0
Nominal vs Structural Typing
Lean uses nominal typing: two types with identical structures are still distinct types. This prevents accidental mixing of values with different semantics. A UserId and a ProductId might both be integers underneath, but you cannot accidentally pass one where the other is expected. The bug where you deleted user 47 because product 47 was out of stock becomes a compile error. Nominal typing is the formal version of “label your variables.”
structure Meters' where
val : Float
structure Seconds where
val : Float
def distance : Meters' := ⟨100.0⟩
def time : Seconds := ⟨10.0⟩
abbrev Speed := Float
def calcSpeed (d : Meters') (t : Seconds) : Speed :=
d.val / t.val
#eval calcSpeed distance time -- 10.0