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:
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:
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:
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-- Each · is a SEPARATE parameter, filled left to right.11-- So (· + ·) is (fun a b => a + b) — a two-argument function.12#eval [1, 2, 3].foldl (· + ·) 0 -- 61314-- Which means this does NOT work on pairs: (·.1 + ·.2) is15-- (fun a b => a.1 + b.2), and map wants a one-argument function.16-- #eval [(1, 2), (3, 4)].map (·.1 + ·.2) -- error1718-- Destructure with fun instead19#eval [(1, 2), (3, 4)].map (fun (a, b) => a + b) -- [3, 7]2021-- ...or use one dot and project inside it22#eval [(1, 2), (3, 4)].map (fun p => p.1 + p.2) -- [3, 7]· 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
The mapfunction applies a transformation to every element in a collection, producing a new collection of the same shape. This is one of the most commonly used higher-order functions because it cleanly separates "what to do" from "how to iterate".
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
The filter function keeps only elements that satisfy a predicate. Unlike map, the output may be shorter than the input. The order of remaining elements is preserved.
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
The foldl (fold-left) function reduces a list to a single value by combining elements from left to right. It takes an initial accumulator value and a combining function that merges each element into the accumulator.
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
The foldr (fold-right) function works like foldlbut processes elements from right to left. For associative operations like addition this gives the same result, but for non-associative operations the order matters.
1-- Different order than foldl2#eval [1, 2, 3].foldl (fun acc x => s!"({acc} + {x})") "0"3-- "(((0 + 1) + 2) + 3)" ← grouped from the LEFT45#eval [1, 2, 3].foldr (fun x acc => s!"({x} + {acc})") "0"6-- "(1 + (2 + (3 + 0)))" ← grouped from the RIGHTfoldl takes (acc, x) and foldr takes (x, acc). In both cases the accumulator sits on the side the fold comes from. Getting this backwards is the usual cause of a baffling type error when switching between the two.foldl for reductions to a scalar (sum, max, count) — it is tail recursive and the left-to-right order matches how you would write a loop. foldr for rebuilding a structure, because it lines up with :: — xs.foldr (· :: ·) [] is the identity on lists, and map and filter are both one-line foldrs.The Pipe Operator
Chain operations with |> for readable data pipelines:
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] -- 12Predicates as Functions
A predicate is just a function returning Bool. This makes it easy to reuse filtering logic.
1def isEven (n : Nat) : Bool := n % 2 == 02def isLarge (n : Nat) : Bool := n >= 1034#eval [1, 2, 10, 12].filter isEven -- [2, 10, 12]5#eval [1, 2, 10, 12].filter isLarge -- [10, 12]Function Composition
Compose functions with ∘ (typed \circ or \o). Unlike Haskell, Lean does not use . for composition — x.f is namespace-resolved method syntax, which is a completely different thing.
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:
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:
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
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 7The Same Functions, Other Containers
map is not a list function — it is a shape-preserving transformation, and it exists for anything with a hole in it. Recognising this saves you writing a lot of pattern matches.
1-- map on Option: apply the function if there is a value2#eval (some 3).map (· * 2) -- some 63#eval (none : Option Nat).map (· * 2) -- none45-- map on Array, on Except, on Prod's second component...6#eval #[1, 2, 3].map (· * 2) -- #[2, 4, 6]7#eval (Except.ok 3 : Except String Nat).map (· * 2) -- Except.ok 689-- filterMap: map and drop the failures in one pass10#eval ["1", "x", "3"].filterMap String.toNat? -- [1, 3]1112-- flatMap: map to lists and concatenate ("for each x, produce several")13#eval [1, 2, 3].flatMap (fun n => [n, n * 10]) -- [1, 10, 2, 20, 3, 30]1415-- any / all: fold to a Bool, short-circuiting16#eval [1, 2, 3].any (· > 2) -- true17#eval [1, 2, 3].all (· > 0) -- truefilterMap is underused and deserves a mention on its own. Whenever you find yourself writing xs.map f |>.filter (· != none) |>.map Option.get!, the answer is xs.filterMap f — one pass, no partial functions, no get!.Effectful Higher-Order Functions
When the function you want to map is itself effectful — it reads a file, or can fail — the plain versions do not typecheck. The M-suffixed variants are what you want:
1-- mapM: map with an effect, collecting the results2#eval do3 let results ← [1, 2, 3].mapM (fun n => do4 IO.println s!"processing {n}"5 return n * 2)6 return results7-- processing 1 / processing 2 / processing 3 / [2, 4, 6]89-- forM: same, but you do not care about the results10#eval [1, 2, 3].forM (fun n => IO.println s!"saw {n}")1112-- foldlM: fold where the step function is effectful13#eval [1, 2, 3].foldlM (fun acc n => do14 IO.println s!"acc={acc}"15 return acc + n) 0 M sibling (mapM, filterM, foldlM, anyM, forM). They work in any monad, not just IO — including Option and Except, where mapMmeans "apply to all, and fail the whole thing if any one fails". Module 5 covers monads properly.Define a function that filters a list using a predicate and then maps a function over the result.
1def filterMap' (p : α → Bool) (f : α → β) (xs : List α) : List β :=2 xs.filter p |>.map f34#eval filterMap' (· > 2) (· * 10) [1, 2, 3, 4] -- [30, 40]