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:
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:
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"Using Type Classes in Functions
Functions can require types to have certain instances:
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}"Implicit Instance Arguments
Instance arguments are implicit. You can pass them explicitly with @when debugging instance resolution.
1def showTwice [Show α] (x : α) : String :=2 s!"{Show.show x}, {Show.show x}"34#eval showTwice 7Standard Type Classes
ToString
The ToString type class powers string interpolation. Any type with a ToString instance can be embedded ins!"..." strings.
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)
The BEq type class enables the ==and != operators. It defines a single methodbeq that returns a Bool.
1class BEq (α : Type) where2 beq : α → α → Bool34-- Enables == operator5#eval 5 == 5 -- true6#eval "a" == "b" -- falseOrd (Ordering)
The Ord type class provides comparison operators. Itscompare method returns an Orderingvalue: .lt, .eq, or .gt.
1class Ord (α : Type) where2 compare : α → α → Ordering34-- Ordering is: .lt, .eq, or .gt5-- Enables <, >, <=, >= operators6#eval compare 5 3 -- .gt7#eval 5 < 3 -- falseHashable
The Hashable type class is required for using a type as keys in HashMap or elements in HashSet.
1class Hashable (α : Type) where2 hash : α → UInt6434-- Used by HashMap, HashSet5#eval hash "hello" -- some UInt64Functor
Functor provides map for transforming values inside a container:
1class Functor (f : Type → Type) where2 map : (α → β) → f α → f β34-- map transforms the value inside5#eval Functor.map (· + 1) (some 5) -- some 66#eval Functor.map (· + 1) [1, 2, 3] -- [2, 3, 4]78-- Infix operator: <$>9#eval (· * 2) <$> some 10 -- some 2010#eval (· * 2) <$> [1, 2, 3] -- [2, 4, 6]Applicative
Applicative extends Functor with pure and sequential application:
1class Applicative (f : Type → Type) extends Functor f where2 pure : α → f α3 seq : f (α → β) → (Unit → f α) → f β45-- pure wraps a value6#eval (pure 5 : Option Nat) -- some 578-- Apply functions inside containers: <*>9#eval some (· + 1) <*> some 5 -- some 610#eval [(*2), (*3)] <*> [1, 2] -- [2, 4, 3, 6]Instance Resolution
Lean automatically finds the right instance based on types:
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 ++ ", " ++ sDeriving Instances
Many instances can be auto-generated:
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
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)Write a function that logs any value that has a ToString instance.
1def log [ToString α] (x : α) : String :=2 s!"value = {x}"34#eval log 1235#eval log true