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 -- 6nil) → 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)"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 1