The Concept of Type Classes
A type class names a set of operations. A function written against the class works for every type that has an instance of it.
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 := toString xs78-- Tedious! We want:9-- display 42 → "42"10-- display true → "true"11-- display [1,2,3] → "[1, 2, 3]"The issue: we want display 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"2-- (the method is called display: show is a Lean keyword)3class Show (α : Type) where4 display : α → String56-- Implement for specific types7instance : Show Nat where8 display n := toString n910instance : Show Bool where11 display b := if b then "true" else "false"1213-- Now we can use show generically14#eval Show.display 42 -- "42"15#eval Show.display 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.display 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.display x} < {Show.display y}"12 | .eq => s!"{Show.display x} = {Show.display y}"13 | .gt => s!"{Show.display x} > {Show.display 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.display x}, {Show.display 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 -- Ordering.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" -- 9821865621596011261 : 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 61011-- Note: Lean core does NOT give List an Applicative instance, so12-- [(· * 2), (· * 3)] <*> [1, 2]13-- fails with "failed to synthesize Seq List". Use Option, Except, or IO —14-- or flatMap, which is what the list version would mean anyway:15#eval [(· * 2), (· * 3)].flatMap (fun f => [1, 2].map f) -- [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.display 42 -- Uses Show Nat3#eval Show.display "hi" -- Uses Show String45-- This happens at compile time6def printTwice [Show α] (x : α) : String :=7 let s := Show.display x -- Lean picks the instance8 s ++ ", " ++ sInstances Can Depend on Instances
This is where type classes go beyond interfaces. An instance can itself require instances, so Lean builds the one you need by composing smaller ones.
1-- "A list is showable IF its elements are showable"2instance [ToString α] : ToString (List α) where3 toString xs := "[" ++ ", ".intercalate (xs.map toString) ++ "]"45-- Now Lean can build ToString for arbitrary nesting, with no extra work:6#eval toString [[1, 2], [3]] -- "[[1, 2], [3]]"7#eval toString [some 1, none] -- needs ToString (Option Nat) tooToString (List MyType). Read the type in the error carefully; it tells you exactly which instance to write.Scoping Instances
Instances are global by default: writing one anywhere makes it visible everywhere that imports the file. That is usually what you want, and occasionally a disaster — two libraries both defining ToString Nat differently would silently change behaviour. Lean offers narrower scopes for that reason.
1namespace Verbose23-- scoped: only active when someone writes "open Verbose"4scoped instance : ToString Bool where5 toString b := if b then "affirmative" else "negative"67end Verbose89#eval toString true -- "true" (the default instance)1011open Verbose in12#eval toString true -- "affirmative" (the scoped one wins here)1314-- local instance: active only for the rest of the current section/file15section16local instance : ToString Unit where17 toString _ := "<unit>"1819#eval toString () -- "<unit>"20endscoped instance for anything opinionated — a formatting choice, a non-obvious ordering, a numeric coercion. A global instance for a type you do not own is a promise that your interpretation is the only reasonable one, and it is very hard for a downstream user to undo.Deriving 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 resemble interfaces in object-oriented languages, with some important differences:
- 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