Products (Structures)
Structures bundle multiple values together into a single type. They're similar to structs in Rust/C or records in other functional languages.
Defining Structures
Use the structure keyword to define a new data type with named fields:
1structure Point where2 x : Float3 y : Float45-- Create instances6def origin : Point := { x := 0.0, y := 0.0 }7def p1 : Point := { x := 3.0, y := 4.0 }89-- Access fields with dot notation10#eval p1.x -- 3.011#eval p1.y -- 4.0The where keyword introduces the field list. Each field has a name and a type.
Anonymous Constructor Syntax
For concise construction, use angle brackets ⟨...⟩, typed\< and \> (the longer abbreviations \langle and \rangle work too). There is no ASCII spelling of the brackets themselves — if you want to avoid Unicode entirely, name the constructor:
1structure Point where2 x : Float3 y : Float45-- Anonymous constructor (order matters!)6def p1 : Point := ⟨3.0, 4.0⟩78-- Every structure gets a constructor named .mk — this is the ASCII form9def p2 : Point := Point.mk 3.0 4.01011-- Named fields: order does NOT matter, and it documents itself12def p3 : Point := { x := 3.0, y := 4.0 }13def p4 : Point := { y := 4.0, x := 3.0 } -- same value as p31415-- There is also a "where" form, handy for long field lists16def p5 : Point where17 x := 3.018 y := 4.0⟨...⟩ only works when Lean already knows the expected type. def p := ⟨3.0, 4.0⟩ fails — there is nothing to tell Lean you meant a Point rather than a pair. Annotate the definition, or use Point.mk.{ x := ..., y := ... } is clearer for structures with many fields.Default Field Values
Fields can have default values, making them optional during construction:
1structure Config where2 host : String := "localhost"3 port : Nat := 80804 debug : Bool := false56-- Use all defaults7def defaultConfig : Config := {}89-- Override some fields10def prodConfig : Config := { port := 443, debug := false }1112-- Override all13def customConfig : Config := { host := "api.example.com", port := 9000, debug := true }1415#eval defaultConfig.host -- "localhost"16#eval prodConfig.port -- 443Deriving Common Functionality
Use deriving to automatically generate useful instances:
1structure Person where2 name : String3 age : Nat4 deriving Repr, DecidableEq, Inhabited56def alice : Person := { name := "Alice", age := 30 }7def bob : Person := { name := "Bob", age := 25 }89-- Repr allows #eval to print the structure10#eval alice -- { name := "Alice", age := 30 }1112-- DecidableEq allows equality comparison13#eval alice == bob -- false14#eval alice == alice -- true1516-- Inhabited provides a default value17#eval (default : Person) -- { name := "", age := 0 }deriving Repr is essential for debugging—it makes your structures printable with #eval.Updating Structures
Structures are immutable, but you can create modified copies with "update" syntax:
1structure Person where2 name : String3 age : Nat4 deriving Repr56def alice : Person := { name := "Alice", age := 30 }78-- Create a new Person with updated age9def olderAlice : Person := { alice with age := 31 }1011-- Original is unchanged12#eval alice.age -- 3013#eval olderAlice.age -- 31Accessor Functions
You can create small helper functions to keep structure access clean and reusable.
1structure User where2 name : String3 score : Nat4 deriving Repr56def isTopUser (u : User) : Bool := u.score >= 1007def displayName (u : User) : String := s!"@{u.name}"89#eval isTopUser { name := "alice", score := 120 } -- true10#eval displayName { name := "bob", score := 10 }Functions on Structures
Define functions that work with your structures:
1structure Point where2 x : Float3 y : Float4 deriving Repr56def Point.distance (p1 p2 : Point) : Float :=7 let dx := p2.x - p1.x8 let dy := p2.y - p1.y9 Float.sqrt (dx * dx + dy * dy)1011def origin : Point := ⟨0.0, 0.0⟩12def p : Point := ⟨3.0, 4.0⟩1314#eval Point.distance origin p -- 5.00000015#eval origin.distance p -- 5.000000 (method syntax!)Notice the method syntax: origin.distance p. When a function is in the Point namespace and takes a Point as its first argument, you can call it with dot notation.
Point, wherever it appears in the signature. So if you write def Point.scaleBy (k : Float) (p : Point) : Point, then p.scaleBy 2.0 still works — pslots into the Point position, not position one.Field accessors are ordinary functions, which means you can pass them around:
1#check Point.x -- Point.x (self : Point) : Float23-- So p.x is literally Point.x p, and the accessor composes like any function4#eval [⟨1.0, 2.0⟩, ⟨3.0, 4.0⟩].map Point.x -- [1.000000, 3.000000]Nested Structures
Structures can contain other structures:
1structure Address where2 street : String3 city : String4 deriving Repr56structure Person where7 name : String8 address : Address9 deriving Repr1011def alice : Person := {12 name := "Alice"13 address := { street := "123 Main St", city := "Boston" }14}1516#eval alice.address.city -- "Boston"1718-- Nested update19def movedAlice : Person := {20 alice with21 address := { alice.address with city := "Cambridge" }22}Structure Inheritance with extends
A structure can extendsone or more others. The parent's fields become part of the child, and — crucially — the child is usable wherever the parent is expected.
1structure Animal where2 name : String3 deriving Repr45structure Dog extends Animal where6 breed : String7 deriving Repr89-- Construct it in one go: parent fields are just more fields10def rex : Dog := { name := "Rex", breed := "Labrador" }1112#eval rex -- { toAnimal := { name := "Rex" }, breed := "Labrador" }13#eval rex.name -- "Rex" — parent fields are accessed directly14#eval rex.toAnimal -- { name := "Rex" } — the parent value, if you need it1516-- A Dog can be used where an Animal is expected17def describe (a : Animal) : String := s!"This is {a.name}"18#eval describe rex.toAnimal -- "This is Rex"Dog is actually represented: it has a single field toAnimal : Animal plus its own breed. Lean generates the flattened accessors (rex.name) on top of that. This is why the Repr output shows the nesting: inheritance here is composition with sugar, not a vtable.1-- Multiple inheritance is allowed, and shared parents are merged2structure HasName where3 name : String45structure HasAge where6 age : Nat78structure Employee extends HasName, HasAge where9 salary : Nat1011def e : Employee := { name := "Ada", age := 36, salary := 100 }12#eval e.name ++ toString e.age -- "Ada36"Deep Dive: Structures vs Classes
Lean also has a class keyword. A class is a structure — the difference is that its values (instances) are found automatically by the elaborator rather than passed by hand. Use structure for data, and class for an interface you want Lean to resolve for you. Module 4 covers this.
What Lean does not have is dynamic dispatch: there is no runtime polymorphism where an Animal-typed value might secretly be a Dogand call an overridden method. If you need "one of several shapes", that is a sum type — the next lesson.
Create a function that increments a person's age and returns the updated record.
1structure Person where2 name : String3 age : Nat45def birthday (p : Person) : Person :=6 { p with age := p.age + 1 }78#eval birthday { name := "Eve", age := 29 }Pattern Matching on Structures
You can destructure structures in function parameters:
1structure Point where2 x : Float3 y : Float45-- Destructure in the parameter list6def magnitude : Point → Float7 | ⟨x, y⟩ => Float.sqrt (x * x + y * y)89-- Or use a match expression10def magnitude' (p : Point) : Float :=11 match p with12 | ⟨x, y⟩ => Float.sqrt (x * x + y * y)1314-- Or just use field access15def magnitude'' (p : Point) : Float :=16 Float.sqrt (p.x * p.x + p.y * p.y)1718#eval magnitude ⟨3.0, 4.0⟩ -- 5.0