Sums (Inductive Types)
Inductive types let you define data that can be one of several variants—like enums with data. This is the foundation of functional domain modeling.
Basic Inductive Types
An inductive type lists all possible forms a value can take:
1inductive Weekday where2 | sunday3 | monday4 | tuesday5 | wednesday6 | thursday7 | friday8 | saturday9 deriving Repr1011def today : Weekday := Weekday.wednesday1213#eval today -- Weekday.wednesdayEach variant (called a "constructor") is a possible value of the type. This is similar to enums in other languages.
Inductive Types with Data
Unlike simple enums, constructors can carry data:
1inductive Shape where2 | circle (radius : Float)3 | rectangle (width : Float) (height : Float)4 | triangle (base : Float) (height : Float)5 deriving Repr67def myCircle : Shape := Shape.circle 5.08def myRect : Shape := Shape.rectangle 3.0 4.0910#eval myCircle -- Shape.circle 5.000000A Shapecan be a circle with a radius, a rectangle with dimensions, or a triangle. This is called a "sum type" because a value is one of the options.
Pattern Matching on Inductives
To use inductive types, you pattern match on their constructors:
1inductive Shape where2 | circle (radius : Float)3 | rectangle (width : Float) (height : Float)4 | triangle (base : Float) (height : Float)56def area : Shape → Float7 | Shape.circle r => 3.14159 * r * r8 | Shape.rectangle w h => w * h9 | Shape.triangle b h => 0.5 * b * h1011#eval area (Shape.circle 5.0) -- 78.53975012#eval area (Shape.rectangle 3.0 4.0) -- 12.00000013#eval area (Shape.triangle 6.0 4.0) -- 12.000000The Option Type
Option is a built-in inductive type for values that might not exist:
1-- Option is defined roughly as:2-- inductive Option (α : Type) where3-- | none : Option α4-- | some : α → Option α56def safeDivide (x y : Nat) : Option Nat :=7 if y == 0 then none else some (x / y)89#eval safeDivide 10 2 -- some 510#eval safeDivide 10 0 -- none1112-- Pattern match to extract the value13def divideOrZero (x y : Nat) : Nat :=14 match safeDivide x y with15 | none => 016 | some result => resultModeling Real Domains
Inductive types excel at representing complex domain logic:
1inductive PaymentMethod where2 | cash3 | creditCard (number : String) (expiry : String)4 | bankTransfer (accountId : String)5 deriving Repr67inductive OrderStatus where8 | pending9 | paid (method : PaymentMethod)10 | shipped (trackingId : String)11 | delivered12 | cancelled (reason : String)13 deriving Repr1415-- Create realistic orders16def order1 : OrderStatus := OrderStatus.pending17def order2 : OrderStatus := OrderStatus.paid (PaymentMethod.creditCard "1234..." "12/25")18def order3 : OrderStatus := OrderStatus.cancelled "Customer request"Notice how the type system prevents invalid states: you can't have a tracking ID without the order being shipped, and you can't have a payment method without the order being paid.
Three Ways to Shorten Constructor Names
Writing Shape.rectangle everywhere gets old. There are three ways out, in increasing order of preference:
1inductive Color where2 | red | green | blue3 deriving Repr45-- 1. Fully qualified — always works, always unambiguous6def c0 : Color := Color.red78-- 2. open, for one declaration or for the rest of the section9open Color in10def c1 : Color := red1112open Color13def c2 : Color := blue1415-- 3. Leading-dot notation: Lean fills in the namespace from the EXPECTED type.16-- No import, no open, no ambiguity — this is the idiomatic choice.17def c3 : Color := .green1819def isWarm : Color → Bool20 | .red => true21 | .green => false22 | .blue => false2324#eval isWarm .red -- trueopen Color pulls red, green, and blue into scope for the whole file, which collides the moment you define a second colour-like type. .red is resolved from the type Lean already expects, so it can never be ambiguous.Deep Dive: Inductive vs Structure
Structures are for products: combining multiple pieces of data together (AND). A Point has an x AND a y.
Inductive types are for sums: representing alternatives (OR). A Shape is a circle OR a rectangle.
Both can be combined: a constructor in an inductive type can hold multiple fields (product inside sum), and a structure field can be an inductive type (sum inside product).
Generic Inductive Types
Inductive types can be parameterized over other types. This lets you build reusable containers and wrappers that work with any element type.
1-- A result that's either a success value or an error message2inductive Result (α : Type) where3 | ok (value : α)4 | error (message : String)5 deriving Repr67def parseInt (s : String) : Result Nat :=8 match s.toNat? with9 | some n => Result.ok n10 | none => Result.error s!"'{s}' is not a valid number"1112#eval parseInt "42" -- Result.ok 4213#eval parseInt "hello" -- Result.error "'hello' is not a valid number"Boolean Logic with Inductives
Even Bool is an inductive type:
1-- Bool is defined as:2-- inductive Bool where3-- | false : Bool4-- | true : Bool56-- Implement our own not7def myNot : Bool → Bool8 | true => false9 | false => true1011-- Implement and12def myAnd : Bool → Bool → Bool13 | true, true => true14 | _, _ => false1516#eval myNot true -- false17#eval myAnd true false -- falseWhat Lean Generates For You
An inductive declaration is not just a type — it silently adds a family of definitions and theorems to the environment. Knowing they exist explains a lot of what tactics do later.
1inductive Shape where2 | circle (radius : Float)3 | rectangle (width : Float) (height : Float)45-- The recursor: "to build something for every Shape, handle every constructor"6#check @Shape.rec7-- {motive : Shape → Sort u} →8-- ((radius : Float) → motive (Shape.circle radius)) →9-- ((width height : Float) → motive (Shape.rectangle width height)) →10-- (t : Shape) → motive t1112-- Injectivity: constructors never lose information13#check Shape.circle.injEq14-- (Shape.circle r₁ = Shape.circle r₂) = (r₁ = r₂)1516-- Disjointness: different constructors are never equal17#check Shape.noConfusion cases, injection, and simp tactics use under the hood. When simp reduces circle r = rectangle w hto False, it is using noConfusion. Every match you write is compiled down to Shape.rec.Useful deriving Clauses
1inductive Color where2 | red | green | blue3 deriving Repr, DecidableEq, Inhabited, BEq, Hashable, Ord45#eval Color.red -- Color.red (Repr)6#eval Color.red == Color.blue -- false (BEq)7#eval (default : Color) -- Color.red (Inhabited: the FIRST constructor)8#eval compare Color.red Color.blue -- Ordering.lt (Ord: declaration order)910-- DecidableEq is stronger than BEq: it lets you decide equality PROPOSITIONS11example : Color.red ≠ Color.blue := by decidederiving Inhabited picks the first constructor that Lean can build, and deriving Ord orders by declaration order. Both are silent decisions that your code may come to depend on — if the order matters, write the instance by hand rather than relying on where you happened to put a constructor.The Built-in Sum Type
When you want "an A or a B" without inventing names, Lean has Sum, written ⊕. It is the generic version of every two-constructor type you would otherwise write by hand.
1-- inductive Sum (α β : Type) where2-- | inl (a : α) : Sum α β3-- | inr (b : β) : Sum α β45#check (Nat ⊕ String) -- Type6#eval (Sum.inl 3 : Nat ⊕ String) -- Sum.inl 378def render : Nat ⊕ String → String9 | .inl n => s!"number {n}"10 | .inr s => s!"text {s}"1112#eval render (.inr "hi") -- "text hi"⊕ in your own domain code. Result with ok/error reads far better than Nat ⊕ String, and the constructor names document which side means what. ⊕ earns its place in generic library code, where there is nothing meaningful to name.Write a function that turns a Shape into a short description.
1inductive Shape where2 | circle (radius : Float)3 | rectangle (width : Float) (height : Float)4 | triangle (base : Float) (height : Float)56def describe : Shape → String7 | Shape.circle r => s!"circle r={r}"8 | Shape.rectangle w h => s!"rectangle {w}x{h}"9 | Shape.triangle b h => s!"triangle {b}x{h}"1011#eval describe (Shape.circle 2.5)