Higher-Order Functions
Functions that take or return other functions are called higher-order functions. They're the backbone of functional programming.
Functions as Values
In Lean, functions are first-class citizens—they can be stored, passed, and returned like any other value:
lean
1-- Store a function in a variable2def addOne : Nat → Nat := fun x => x + 134-- Pass a function to another function5#eval [1, 2, 3].map addOne -- [2, 3, 4]67-- Return a function from a function8def makeAdder (n : Nat) : Nat → Nat :=9 fun x => x + n1011def addFive := makeAdder 512#eval addFive 10 -- 15Lambda Expressions
Create inline anonymous functions with fun:
lean
1-- Full syntax2#eval [1, 2, 3].map (fun x => x * 2) -- [2, 4, 6]34-- With type annotation5#eval [1, 2, 3].map (fun (x : Nat) => x * 2)67-- Multiple parameters8#eval [(1, 2), (3, 4)].map (fun (a, b) => a + b) -- [3, 7]910-- Unicode lambda (stylistic choice)11#eval [1, 2, 3].map (λ x => x * 2) -- [2, 4, 6]Shorthand Syntax with ·
The dot · creates concise lambdas:
lean
1-- (· + 1) means (fun x => x + 1)2#eval [1, 2, 3].map (· + 1) -- [2, 3, 4]34-- (· * 2) means (fun x => x * 2)5#eval [1, 2, 3].map (· * 2) -- [2, 4, 6]67-- (· > 2) means (fun x => x > 2)8#eval [1, 2, 3, 4].filter (· > 2) -- [3, 4]910-- Multiple dots for multiple arguments11#eval [(1, 2), (3, 4)].map (·.1 + ·.2) -- [3, 7]Key Takeaway
The
· syntax is perfect for simple transformations. Use fullfun syntax for complex logic or when you need to name parameters.Standard Higher-Order Functions
map: Transform Each Element
lean
1#eval [1, 2, 3].map (· * 10) -- [10, 20, 30]2#eval ["a", "b"].map String.toUpper -- ["A", "B"]3#eval [1, 2, 3].map toString -- ["1", "2", "3"]filter: Keep Matching Elements
lean
1#eval [1, 2, 3, 4, 5].filter (· > 3) -- [4, 5]2#eval [1, 2, 3, 4, 5].filter (· % 2 == 0) -- [2, 4]3#eval ["", "a", "", "b"].filter (· != "") -- ["a", "b"]foldl: Accumulate Left to Right
lean
1-- Sum: start with 0, add each element2#eval [1, 2, 3, 4].foldl (· + ·) 0 -- 1034-- Product: start with 1, multiply each5#eval [1, 2, 3, 4].foldl (· * ·) 1 -- 2467-- Build a string: start with "", append each8#eval [1, 2, 3].foldl (fun s n => s ++ toString n) "" -- "123"910-- Find maximum: start with 0, take max11#eval [3, 1, 4, 1, 5].foldl max 0 -- 5foldr: Accumulate Right to Left
lean
1-- Different order than foldl2#eval [1, 2, 3].foldl (fun acc x => s!"({acc} + {x})") "0"3-- "((0 + 1) + 2) + 3)"45#eval [1, 2, 3].foldr (fun x acc => s!"({x} + {acc})") "0"6-- "(1 + (2 + (3 + 0)))"The Pipe Operator
Chain operations with |> for readable data pipelines:
lean
1-- Without pipes (nested, hard to read)2#eval String.length (String.toUpper "hello")34-- With pipes (flows left to right)5#eval "hello" |> String.toUpper |> String.length -- 567-- Complex pipeline8def processNumbers (nums : List Nat) : Nat :=9 nums10 |> List.filter (· > 0) -- Keep positives11 |> List.map (· * 2) -- Double them12 |> List.foldl (· + ·) 0 -- Sum them1314#eval processNumbers [0, 1, 2, 3] -- 12💡
Pipes make data transformation pipelines read like English: "Take nums, filter positives, map double, fold sum."
Function Composition
Compose functions with ∘ (or . in some contexts):
lean
1def addOne (x : Nat) : Nat := x + 12def double (x : Nat) : Nat := x * 234-- Compose: (double ∘ addOne) means "first addOne, then double"5def addOneThenDouble := double ∘ addOne67#eval addOneThenDouble 5 -- 12 (5 + 1 = 6, 6 * 2 = 12)89-- Use in pipelines10#eval [1, 2, 3].map (double ∘ addOne) -- [4, 6, 8]Partial Application
Fix some arguments to create a new function:
lean
1def add (x y : Nat) : Nat := x + y23-- Partially apply: fix first argument4def add10 := add 1056#eval add10 5 -- 157#eval add10 20 -- 3089-- Useful with higher-order functions10#eval [1, 2, 3].map (add 100) -- [101, 102, 103]1112-- Works with operators too13#eval [1, 2, 3].map (· + 100) -- Same thingDeep Dive: Currying
All multi-parameter functions in Lean are "curried"—they take one argument at a time and return a function for the rest:
lean
1-- These are equivalent2def add (x y : Nat) : Nat := x + y3def add' (x : Nat) : Nat → Nat := fun y => x + y4def add'' : Nat → Nat → Nat := fun x y => x + y56-- add 1 returns a function Nat → Nat7#check add 1 -- Nat → NatThis is why partial application works so naturally in Lean.
Building Custom Higher-Order Functions
lean
1-- Apply a function n times2def iterate (n : Nat) (f : α → α) (x : α) : α :=3 match n with4 | 0 => x5 | n + 1 => iterate n f (f x)67#eval iterate 3 (· + 1) 0 -- 38#eval iterate 5 (· * 2) 1 -- 32910-- Find first element matching predicate11def findFirst (p : α → Bool) : List α → Option α12 | [] => none13 | x :: xs => if p x then some x else findFirst p xs1415#eval findFirst (· > 5) [1, 3, 7, 2, 8] -- some 7