Post

The Lean 4 Typeclassopedia

A field guide to Lean 4's typeclasses, in the spirit of the Haskell Typeclassopedia.

The Lean 4 Typeclassopedia

The Lean 4 Typeclassopedia

A field guide to Lean 4’s typeclasses, in the spirit of the Haskell Typeclassopedia.

Lean 4’s typeclass landscape splits cleanly into two layers that are worth keeping mentally separate:

  • Part 1 — Core / std. The typeclasses baked into Init (Lean core) and Std/Batteries. These drive notation elaboration (+, [], , for, <|>, …), decidability, and the handful of functional-programming abstractions (Functor, Monad, …) that Lean needs to make do-notation work. You get these for free in every Lean 4 project, no imports required beyond core.
  • Part 2 — Mathlib. A much larger algebraic/order/category hierarchy (Semigroup, Ring, Lattice, Category, …) built on top of some of the Part 1 classes (Add, Mul, Zero, LE, …). This is a library design, not a language feature — you opt into it by importing Mathlib.

If your proof work stays in core-tactics-only territory (omega, decide, simp, cases, no Mathlib), you’ll live almost entirely in Part 1. Part 2 is included for completeness and because the hierarchy is a well-known reference point even when you’re not importing it.

A note on mechanics before diving in: Lean typeclasses are just structures (or inductives) marked class, resolved by the elaborator via a depth-first search over registered instance declarations, guided by outParam and semiOutParam annotations that tell the search which arguments to treat as outputs rather than inputs. Keep that model in mind — a lot of “why did this instance not fire” debugging comes down to outParam / metavariable ordering.


Part 1 — Lean 4 Core & Std

1. Foundational classes

Inhabited — witnesses that a type is nonempty and hands you a canonical element.

1
2
class Inhabited (α : Sort u) where
  default : α

Used by Classical.arbitrary-free code paths, Array.get!-style panicking accessors (via Inhabited.default as the fallback), and anywhere the elaborator needs some value of a type to make a partial function total in the metaprogramming sense.

Nonempty — the Prop-valued cousin: witnesses existence without handing you a specific element (so it doesn’t leak data in proof-irrelevant contexts).

1
2
class Nonempty (α : Sort u) : Prop where
  intro :: (val : α)

Because it’s a Prop, two proofs of Nonempty α are definitionally equal — you can’t extract val computationally, only reason about it.

Subsingleton — at most one element, up to propositional equality.

1
2
class Subsingleton (α : Sort u) : Prop where
  allEq :  (a b : α), a = b

Every Prop is a subsingleton (proof irrelevance); this class generalizes that fact to arbitrary types (e.g. Unit, or any type with an injection into a subsingleton).

Gotcha: Inhabited, Nonempty, Subsingleton are all separate claims — Nonempty doesn’t give you Inhabited (that requires Classical.choice or Decidable-style constructive evidence), and Inhabited doesn’t give you Subsingleton (a type can have a canonical default and still have many distinct elements).

2. Decidability & equality

Decidable — a Prop that comes with a computable verdict.

1
2
3
class inductive Decidable (p : Prop) where
  | isFalse (h : ¬p) : Decidable p
  | isTrue  (h : p)  : Decidable p

This is what powers if h : p then ... else ... and decide/native_decide. DecidableEq is just sugar over it:

1
abbrev DecidableEq (α : Sort u) := (a b : α)  Decidable (a = b)

BEq — boolean equality, deliberately not required to be lawful.

1
2
class BEq (α : Type u) where
  beq : α  α  Bool

LawfulBEq — the law that ties BEq back to propositional =:

1
2
3
class LawfulBEq (α : Type u) [BEq α] : Prop where
  eq_of_beq : beq a b = true  a = b
  rfl       : beq a a = true

Why the split? Some useful BEq instances are intentionally non-lawful — e.g. NaN != NaN for floats, or approximate/structural equality that ignores metadata fields. Lean makes you opt in to lawfulness rather than assuming it, which matters a lot for your BF16/FPU verification work: BEq Float-style instances are exactly where a LawfulBEq assumption would be silently false.

Ord — a total-order-flavored comparator (three-way, not boolean).

1
2
class Ord (α : Type u) where
  compare : α  α  Ordering  -- Ordering := lt | eq | gt

There’s no single canonical “LawfulOrd” in core the way there is for BEq; laws about compare agreeing with LE/LT are a Mathlib/Std-level concern layered on top when needed.

Hashable

1
2
class Hashable (α : Sort u) where
  hash : α  UInt64

Consumed by Std.HashMap/Std.HashSet. Convention (not enforced by the type system): a == b → hash a = hash b when a LawfulBEq instance exists.

3. Display & debugging

1
2
3
4
5
class Repr (α : Type u) where
  reprPrec : α  Nat  Std.Format

class ToString (α : Type u) where
  toString : α  String

Repr aims for round-trippable, precedence-aware output (what you get from #eval and deriving Repr); ToString is for human-facing display and backs string interpolation s!"...".

4. Coercions

1
2
3
class Coe     (α : Sort u) (β : Sort v)              where coe : α  β
class CoeSort (α : Sort u) (β : Sort v)              where coe : α  β  -- target a Sort, e.g. Type from a structure
class CoeFun  (α : Sort u) (β : outParam (α  Sort v)) where coe : (f : α)  β f

Note the outParam on CoeFun’s second argument — the shape of the function type is computed from α, not searched for independently. There are also CoeHead/CoeTail/CoeHTCT etc. used internally to chain coercions; you rarely write instances of those directly.

5. Numeric literals & operator overloading

The core design principle: every notation is heterogeneous by default, with a homogeneous instance layered on top for the common case.

1
2
3
4
5
6
7
8
9
10
class OfNat (α : Type u) (n : Nat) where
  ofNat : α

class Add (α : Type u) where
  add : α  α  α

class HAdd (α : Type u) (β : Type v) (γ : outParam (Type w)) where
  hAdd : α  β  γ

instance [Add α] : HAdd α α α :=Add.add

a + b elaborates to HAdd.hAdd a b, not Add.add a bAdd only enters the picture via the default instance above. This is why you can add a Nat and a Fin n in some libraries without an explicit cast: someone wrote a bespoke HAdd instance rather than routing through Add. Same pattern for Sub/HSub, Mul/HMul, Div/HDiv, Mod/HMod, Pow/HPow, Append/HAppend, AndThen/HAndThen.

Neg is homogeneous-only (no HNeg):

1
2
class Neg (α : Type u) where
  neg : α  α

Bitwise operators get their own dedicated classes, mostly relevant to your UInt64/bit-vector-flavored ALU work:

1
2
3
4
5
6
class AndOp      (α : Type u) where and : α  α  α         -- &&&
class OrOp       (α : Type u) where or  : α  α  α         -- |||
class Xor        (α : Type u) where xor : α  α  α         -- ^^^
class ShiftLeft  (α : Type u) where shiftLeft  : α  α  α  -- <<<
class ShiftRight (α : Type u) where shiftRight : α  α  α  -- >>>
class Complement (α : Type u) where complement : α  α      -- ~~~

OfScientific handles scientific-notation numeric literals (1.5e10) analogously to OfNat.

6. Collection protocol

1
2
3
4
class GetElem (coll : Type u) (idx : Type v)
              (elem : outParam (Type w))
              (valid : outParam (coll  idx  Prop)) where
  getElem : (c : coll)  (i : idx)  valid c i  elem

This is the class behind xs[i] notation; the valid outParam is what forces you to either discharge an in-bounds proof (xs[i]'h), use the panicking xs[i]!, or the option-returning xs[i]? (the latter two are derived via companion classes/defaults rather than being separate primitive notations). This class has been reworked more than once across Lean versions — if you’re implementing a custom indexed collection, check the current core source rather than assuming the exact shape above hasn’t shifted.

1
2
class Membership (α : outParam (Type u)) (γ : Type v) where
  mem : α  γ  Prop

Backs a ∈ s. (Argument order here has also been a point of churn across versions — worth double-checking against your toolchain.)

1
2
3
4
5
6
7
class EmptyCollection (α : Type u) where emptyCollection : α    -- ∅ / {}
class Insert (α : outParam (Type u)) (γ : Type v) where insert : α  γ  γ
class Singleton (α : outParam (Type u)) (γ : Type v) where singleton : α  γ
class Union (α : Type u) where union : α  α  α                -- ∪
class Inter (α : Type u) where inter : α  α  α                -- ∩
class SDiff (α : Type u) where sdiff : α  α  α                -- \
class Append (α : Type u) where append : α  α  α              -- default HAppend instance, ++

These are what let List, Array, Std.HashSet, Finset (Mathlib), etc. all share {}, , , ++ notation without a shared base type.

7. Functional-programming core

1
2
3
4
5
6
7
8
9
10
11
12
class Functor (f : Type u  Type v) where
  map      : {α β : Type u}  (α  β)  f α  f β
  mapConst : {α β : Type u}  α  f β  f α := fun a => Functor.map (fun _ => a)

class Applicative (f : Type u  Type v) extends Functor f where
  pure : {α : Type u}  α  f α
  seq  : {α β : Type u}  f (α  β)  (Unit  f α)  f β
  seqLeft  : {α β : Type u}  f α  (Unit  f β)  f α := ...
  seqRight : {α β : Type u}  f α  (Unit  f β)  f β := ...

class Monad (m : Type u  Type v) extends Applicative m where
  bind : {α β : Type u}  m α  (α  m β)  m β

Note extends, not independent classes — every Monad is an Applicative is a Functor, with map/pure/seq derivable from bind via default implementations, so writing a Monad instance is usually just supplying pure and bind.

Lawful variants parallel BEq/LawfulBEq:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class LawfulFunctor (f : Type u  Type v) [Functor f] : Prop where
  map_const : (Functor.mapConst : α  f β  f α) = Functor.mapFunction.const β
  id_map    :  (x : f α), id <$> x = x
  comp_map  :  (g : α  β) (h : β  γ) (x : f α), (hg) <$> x = h <$> (g <$> x)

class LawfulApplicative (f : Type u  Type v) [Applicative f] extends LawfulFunctor f : Prop where
  seqLeft_eq  : ...
  seqRight_eq : ...
  pure_seq    :  (g : α  β) (x : f α), pure g <*> x = g <$> x
  map_pure    :  (g : α  β) (x : α), g <$> (pure x : f α) = pure (g x)
  seq_pure    : ...
  seq_assoc   : ...

class LawfulMonad (m : Type u  Type v) [Monad m] extends LawfulApplicative m : Prop where
  bind_pure_comp :  (g : α  β) (x : m α), x >>= (pureg) = g <$> x
  bind_map       : ...
  pure_bind      :  (x : α) (g : α  m β), pure x >>= g = g x
  bind_assoc     :  (x : m α) (g : α  m β) (h : β  m γ),
                     (x >>= g) >>= h = x >>= (fun a => g a >>= h)

The three monad laws (pure_bind, and the two halves folded into bind_assoc / bind_pure_comp) are the ones you’d recognize from Haskell’s left identity / right identity / associativity trio.

1
2
3
class Alternative (f : Type u  Type v) extends Applicative f where
  failure : {α : Type u}  f α
  orElse  : {α : Type u}  f α  (Unit  f α)  f α

Backs <|> and failure; Option and Array/List-as-nondeterminism-monad are the usual examples.

8. Iteration protocol

Lean has no Haskell-style Foldable/Traversable in core. Instead, for x in xs do ... desugars via a dedicated pair of classes:

1
2
3
4
5
class ForIn (m : Type u  Type v) (ρ : Type w) (α : outParam (Type x)) where
  forIn : ρ  β  (α  β  m (ForInStep β))  m β

class ForM (m : Type u  Type v) (ρ : Type w) (α : outParam (Type x)) where
  forM : ρ  (α  m PUnit)  m PUnit

ForInStep β (.yield / .done) is how a for loop body signals early break. Writing a ForIn instance for a custom data structure is what makes it directly loop-able without materializing a List first.

9. Monad transformer plumbing

Relevant if you touch StateT/ReaderT/ExceptT stacks (e.g. structuring a Hypothesis-driven cocotb harness’s internal state):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class MonadLift    (m : Type u  Type v) (n : Type u  Type w) where
  monadLift : {α : Type u}  m α  n α

class MonadFunctor (m : Type u  Type v) (n : Type u  Type w) where
  monadMap : {α : Type u}  ({β : Type u}  m β  m β)  n α  n α

class MonadState  (σ : outParam (Type u)) (m : Type u  Type v) where
  get : m σ
  set : σ  m PUnit
  modifyGet : {β : Type u}  (σ  β × σ)  m β

class MonadExceptOf (ε : Type u) (m : Type v  Type w) where
  throw : {α : Type v}  ε  m α
  tryCatch : {α : Type v}  m α  (ε  m α)  m α

outParam on the state/exception type is what lets get/throw resolve without you annotating the type explicitly at every call site, even threaded through several transformer layers via MonadLift.


Part 2 — Mathlib’s Algebraic Hierarchy

Mathlib does not invent a parallel notion of +/*/ — it reuses the core Add, Mul, Neg, LE, LT classes from Part 1 and layers properties on top as further classes (mostly Prop-valued mixins combined via extends), so the diamond problem is managed by a huge, carefully engineered extends graph rather than duplicated operations.

1. Algebraic hierarchy (multiplicative side; additive side mirrors it)

graph TD
    Mul --> Semigroup
    Semigroup --> CommSemigroup["CommSemigroup (commutative)"]
    Semigroup --> MulOneClass["MulOneClass (adds One + identity laws)"]
    MulOneClass --> Monoid
    Monoid --> CommMonoid
    Monoid --> DivisionMonoid
    DivisionMonoid --> Group
    Group --> CommGroup
The algebraic hierarchy (multiplicative side).

Everything on the additive side has a +-flavored mirror generated automatically via the to_additive attribute: Semigroup → AddSemigroup, Monoid → AddMonoid, Group → AddGroup, etc. — so you almost never write the additive versions by hand.

2. Ring / field hierarchy

graph TD
    Base["AddCommMonoid + Mul + distributivity"] --> NUNA["NonUnitalNonAssocSemiring"]
    NUNA --> Semiring
    Semiring --> CommSemiring
    Semiring --> Ring
    Ring --> CommRing
    Ring --> DivisionRing
    DivisionRing --> Field
The ring and field hierarchy.

Ring = AddCommGroup + Monoid (multiplicative, not necessarily commutative) + distributivity laws. Field additionally requires multiplicative inverses for nonzero elements and commutativity.

3. Order hierarchy

graph TD
    Preorder["Preorder (≤ reflexive + transitive)"] --> PartialOrder["PartialOrder (+ antisymmetry)"]
    PartialOrder --> LinearOrder["LinearOrder (+ totality, decidable ≤)"]
The order hierarchy.

Orthogonal to that, the lattice family:

graph TD
    SL["SemilatticeSup / SemilatticeInf"] --> Lattice["Lattice (has both ⊔ and ⊓)"]
    Lattice --> DistribLattice
    DistribLattice --> CompleteLattice["CompleteLattice (arbitrary sups/infs, not just binary)"]
The lattice family.

Ordered algebraic structures combine both trees, e.g. OrderedSemiring, LinearOrderedField — a LinearOrderedField is simultaneously a Field and a LinearOrder with compatibility axioms (a ≤ b → a + c ≤ b + c, etc.) mixed in.

4. Category theory (Mathlib.CategoryTheory)

1
2
3
4
5
6
class CategoryTheory.Category (obj : Type u) extends Quiver.{v} obj where
  id       : (X : obj)  X  X
  comp     : {X Y Z : obj}  (X  Y)  (Y  Z)  (X  Z)
  id_comp  : ...
  comp_id  : ...
  assoc    : ...

CategoryTheory.Functor (structure-preserving map between categories, not the Part 1 Functor — different namespace, easy to confuse when searching docs) and NatTrans (natural transformations) sit on top, giving you the actual Haskell-Typeclassopedia-flavored Functor/Monad abstractions but generalized to arbitrary categories instead of fixed to Type u → Type v.

5. Analysis (brief pointer only)

TopologicalSpace → UniformSpace → MetricSpace → NormedAddCommGroup → NormedSpace is the rough chain used once you’re doing real/complex analysis in Mathlib; each layer adds structure (open sets → uniformity → distance → norm → scalar-compatible norm) without touching the algebraic hierarchy above it, which is why a NormedField can simultaneously be a Field and a MetricSpace via two independent extends chains meeting at one type.


Quick-reference: where to look when instance search fails

  1. Check outParam/semiOutParam placement — an argument not marked outParam must be fully known before Lean starts the search; if it’s a metavariable, resolution stalls or picks the wrong instance.
  2. extends vs. separate class + [...] argumentextends bakes a parent instance in as a field (one search, one instance found gives you both); separate instance arguments trigger independent searches, which is how diamonds get resolved explicitly rather than silently.
  3. Default methods are instance-local, not global — a Monad instance that only defines bind gets map/seq/pure derived once, at instance-declaration time; if you specialize one of those later for performance, you must redeclare it in the same instance, not add it separately.
  4. deriving hooks into this system directlyderiving Repr, BEq, Hashable, DecidableEq generates real instances of the classes in Part 1, §2–3, using structural recursion over the type’s constructors.

Compiled from Lean 4 core/std and Mathlib conventions as of Lean’s more stable, long-standing naming. A handful of Part 1 signatures (GetElem, Membership argument order in particular) have been reworked across Lean versions — worth a quick diff against your exact toolchain version before relying on the precise field order in proofs.

This post is licensed under CC BY 4.0 by the author.