The Concept of Type Classes
Type classes enable ad-hoc polymorphism—writing functions that work with any type that provides certain operations.
The Problem: Ad-Hoc Polymorphism
Consider printing values. Different types need different printing logic:
lean
1-- We want one "show" function that works for many types2-- But each type has different logic:34def showNat (n : Nat) : String := toString n5def showBool (b : Bool) : String := if b then "true" else "false"6def showList (xs : List Nat) : String := s!"[{xs}]"78-- Tedious! We want:9-- show 42 → "42"10-- show true → "true"11-- show [1,2,3] → "[1, 2, 3]"The issue: we want show to work on any type, but with type-specific behavior. This is called ad-hoc polymorphism.
Type Classes: The Solution
A type class declares an interface. Types implement that interface via instances:
lean
1-- Define what it means to be "showable"2class Show (α : Type) where3 show : α → String45-- Implement for specific types6instance : Show Nat where7 show n := toString n89instance : Show Bool where10 show b := if b then "true" else "false"1112-- Now we can use show generically13#eval Show.show 42 -- "42"14#eval Show.show true -- "true"Key Takeaway
Type classes separate what operations exist (the class) from howthey're implemented (the instances). This enables generic programming.
Using Type Classes in Functions
Functions can require types to have certain instances:
lean
1-- [Show α] means "α must have a Show instance"2def greet [Show α] (x : α) : String :=3 s!"Hello, {Show.show x}!"45#eval greet 42 -- "Hello, 42!"6#eval greet true -- "Hello, true!"78-- Multiple constraints9def compare [Ord α] [Show α] (x y : α) : String :=10 match Ord.compare x y with11 | .lt => s!"{Show.show x} < {Show.show y}"12 | .eq => s!"{Show.show x} = {Show.show y}"13 | .gt => s!"{Show.show x} > {Show.show y}"Standard Type Classes
ToString
lean
1-- Built-in, used by string interpolation2class ToString (α : Type) where3 toString : α → String45-- This is why s!"x = {x}" works6#eval s!"value: {42}" -- "value: 42"7#eval s!"flag: {true}" -- "flag: true"BEq (Boolean Equality)
lean
1class BEq (α : Type) where2 beq : α → α → Bool34-- Enables == operator5#eval 5 == 5 -- true6#eval "a" == "b" -- falseOrd (Ordering)
lean
1class Ord (α : Type) where2 compare : α → α → Ordering34-- Ordering is: .lt, .eq, or .gt5-- Enables <, >, <=, >= operators6#eval compare 5 3 -- .gt7#eval 5 < 3 -- falseHashable
lean
1class Hashable (α : Type) where2 hash : α → UInt6434-- Used by HashMap, HashSet5#eval hash "hello" -- some UInt64Instance Resolution
Lean automatically finds the right instance based on types:
lean
1-- Lean figures out which Show instance to use2#eval Show.show 42 -- Uses Show Nat3#eval Show.show "hi" -- Uses Show String45-- This happens at compile time6def printTwice [Show α] (x : α) : String :=7 let s := Show.show x -- Lean picks the instance8 s ++ ", " ++ sℹ
Instance resolution is automatic. You don't pass instances explicitly—Lean finds them based on the types involved.
Deriving Instances
Many instances can be auto-generated:
lean
1-- deriving generates instances automatically2structure Point where3 x : Int4 y : Int5 deriving Repr, BEq, Hashable67-- Now these work:8#eval repr (Point.mk 1 2) -- Shows the structure9#eval Point.mk 1 2 == Point.mk 1 2 -- trueDeep Dive: Type Classes vs Interfaces
Type classes are similar to interfaces in OOP but more powerful:
- Retroactive conformance: You can add instances for types you don't own
- Multi-parameter: Classes can involve multiple types
- Associated types: Classes can have type members
- Default implementations: Classes can provide default methods
Where Instances Come From
lean
1-- 1. Built-in instances (Nat, Bool, String, etc.)2#eval (5 : Nat) == 5 -- BEq Nat exists34-- 2. Derived instances5structure RGB where6 r : UInt87 g : UInt88 b : UInt89 deriving BEq1011-- 3. Your own instances12instance : ToString RGB where13 toString c := s!"rgb({c.r}, {c.g}, {c.b})"1415-- 4. Instances from libraries16-- (Importing a library may bring instances into scope)