Recursive Data
Inductive types can refer to themselves, enabling powerful data structures like linked lists and trees.
Building a Linked List
Let's implement a generic linked list from scratch to understand how recursion works in data types:
1inductive MyList (α : Type) where2 | nil : MyList α -- Empty list3 | cons : α → MyList α → MyList α -- Element + rest of list4 deriving Repr56-- Create some lists7def empty : MyList Nat := MyList.nil8def oneItem : MyList Nat := MyList.cons 1 MyList.nil9def threeItems : MyList Nat := 10 MyList.cons 1 (MyList.cons 2 (MyList.cons 3 MyList.nil))1112#eval threeItems -- MyList.cons 1 (MyList.cons 2 (MyList.cons 3 MyList.nil))The key insight: cons takes an element and another MyList. This self-reference is what makes the type recursive.
Functions on Recursive Types
Functions on recursive types are typically recursive themselves:
1inductive MyList (α : Type) where2 | nil : MyList α3 | cons : α → MyList α → MyList α4 deriving Repr56-- Length of a list7def MyList.length : MyList α → Nat8 | nil => 09 | cons _ tail => 1 + tail.length1011-- Sum of a list of Nats12def MyList.sum : MyList Nat → Nat13 | nil => 014 | cons head tail => head + tail.sum1516def nums : MyList Nat := MyList.cons 1 (MyList.cons 2 (MyList.cons 3 MyList.nil))1718#eval nums.length -- 319#eval nums.sum -- 6 tail.length + 1 rather than 1 + tail.length. Both compute the same number, but n + 1 is definitionally Nat.succ n, so later proofs by induction, rfl, and simp line up without an extra Nat.add_comm step. The standard library writes it this way throughout, and matching that convention makes your lemmas compose with theirs.Structural Recursion
Lean enforces termination by requiring recursive calls on smaller pieces of data. This is called structural recursion.
1inductive MyList (α : Type) where2 | nil : MyList α3 | cons : α → MyList α → MyList α45def MyList.last? : MyList α → Option α6 | nil => none7 | cons x nil => some x8 | cons _ xs => xs.last?nil) → base result. Recursive case (cons) → recursive call on the tail.Binary Trees
Trees are another classic recursive structure:
1inductive BinaryTree (α : Type) where2 | leaf : BinaryTree α3 | node : α → BinaryTree α → BinaryTree α → BinaryTree α4 deriving Repr56open BinaryTree78-- 29-- / \10-- 1 311def myTree : BinaryTree Nat :=12 node 2 (node 1 leaf leaf) (node 3 leaf leaf)1314-- Count nodes in a tree15def BinaryTree.size : BinaryTree α → Nat16 | leaf => 017 | node _ left right => 1 + left.size + right.size1819-- Calculate depth/height20def BinaryTree.depth : BinaryTree α → Nat21 | leaf => 022 | node _ left right => 1 + max left.depth right.depth2324#eval myTree.size -- 325#eval myTree.depth -- 2Mapping Over Recursive Structures
A common operation is applying a function to every element:
1inductive MyList (α : Type) where2 | nil : MyList α3 | cons : α → MyList α → MyList α4 deriving Repr56def MyList.map (f : α → β) : MyList α → MyList β7 | nil => nil8 | cons head tail => cons (f head) (tail.map f)910def nums := MyList.cons 1 (MyList.cons 2 (MyList.cons 3 MyList.nil))1112#eval nums.map (· * 2) -- MyList.cons 2 (MyList.cons 4 (MyList.cons 6 MyList.nil))13#eval nums.map toString -- MyList.cons "1" (MyList.cons "2" (MyList.cons "3" MyList.nil))Expression Trees
Recursive types are perfect for representing expressions:
1inductive Expr where2 | num : Int → Expr3 | add : Expr → Expr → Expr4 | mul : Expr → Expr → Expr5 | neg : Expr → Expr6 deriving Repr78open Expr910-- Represent: -(2 + 3) * 411def myExpr : Expr := mul (neg (add (num 2) (num 3))) (num 4)1213-- Evaluate the expression14def Expr.eval : Expr → Int15 | num n => n16 | add a b => a.eval + b.eval17 | mul a b => a.eval * b.eval18 | neg e => -e.eval1920#eval myExpr.eval -- -202122-- Pretty print23def Expr.toString : Expr → String24 | num n => s!"{n}"25 | add a b => s!"({a.toString} + {b.toString})"26 | mul a b => s!"({a.toString} * {b.toString})"27 | neg e => s!"-{e.toString}"2829#eval myExpr.toString -- "(-(2 + 3) * 4)"Nested Recursion
A constructor can hold a List of the type being defined. This is a nested inductive type, and it is how you model trees with an arbitrary number of children.
1inductive Rose (α : Type) where2 | node : α → List (Rose α) → Rose α3 deriving Repr45def Rose.size : Rose α → Nat6 | .node _ children => 1 + (children.map Rose.size).sum78#eval (Rose.node 1 [Rose.node 2 [], Rose.node 3 []]).size -- 3Rose.size recurses through List.map — the recursive call is not on a direct sub-argument, but on an element buried inside a list. Lean handles this, but it is doing real work: it must see that List.map only ever applies the function to elements of its argument. If Lean cannot see it, you will get a termination error and need termination_by — covered in the Termination lesson.Deep Dive: Mutual Recursion
Sometimes you need types that refer to each other. Lean supports this withmutual:
1mutual2 inductive Even where3 | zero : Even4 | succOdd : Odd → Even56 inductive Odd where7 | succEven : Even → Odd8endThis is advanced and rarely needed, but it's available when required.
Folding Recursive Structures
fold collapses a recursive structure into a single value:
1inductive MyList (α : Type) where2 | nil : MyList α3 | cons : α → MyList α → MyList α45-- Right fold: process from end to beginning6def MyList.foldr (f : α → β → β) (init : β) : MyList α → β7 | nil => init8 | cons head tail => f head (tail.foldr f init)910def nums := MyList.cons 1 (MyList.cons 2 (MyList.cons 3 MyList.nil))1112-- Sum using fold13#eval nums.foldr (· + ·) 0 -- 61415-- Build a string16#eval nums.foldr (fun n s => toString n ++ s) "" -- "123"The Built-in List Type
Lean's built-in List works exactly like our MyList, but with syntax sugar:
1-- These are equivalent:2def list1 : List Nat := [1, 2, 3]3def list2 : List Nat := 1 :: 2 :: 3 :: []4def list3 : List Nat := List.cons 1 (List.cons 2 (List.cons 3 List.nil))56-- :: is infix for List.cons7-- [] is List.nil89-- Pattern matching works the same way10def head? : List α → Option α11 | [] => none12 | x :: _ => some x1314#eval head? [1, 2, 3] -- some 1Write a function that counts the number of leaves in a binary tree.
1inductive BinaryTree (α : Type) where2 | leaf : BinaryTree α3 | node : α → BinaryTree α → BinaryTree α → BinaryTree α45open BinaryTree67def leafCount : BinaryTree α → Nat8 | leaf => 19 | node _ left right => leafCount left + leafCount right1011#eval leafCount (node 1 leaf (node 2 leaf leaf)) -- 3