Functions
Functions are the heart of Lean. This level takes you from a one-line def to the pipeline style used throughout real Lean codebases.
Learning Goals
- Define functions with
def, with and without explicit types. - Write anonymous functions three different ways and know when each reads best.
- Explain what
Nat → Nat → Natreally means (it is not what most people assume). - Use
map,filter, andfoldlinstead of writing loops. - Structure larger definitions with
letandwhere.
Definitions with def
The full form names each argument with its type, then the return type, then the body. When several arguments share a type you can group them.
1def add (x : Nat) (y : Nat) : Nat := x + y2#eval add 3 5 -- 834-- Same thing, grouped arguments5def add' (x y : Nat) : Nat := x + y6#eval add' 3 5 -- 878def greet (name : String) : String := "Hello, " ++ name9#eval greet "Lean" -- "Hello, Lean"Letting Lean infer the types
Lean can often work out the types itself. This is convenient for scratch work, but in code you intend to keep, writing the type is worth it: it documents your intent and it makes error messages point at the real mistake instead of somewhere three functions downstream.
1def double x := x * 223#check double -- double (x : Nat) : Nat4#eval double 21 -- 42Nat here because numeric literals default to Nat. If you wanted double to work on Int, inference would quietly give you the wrong thing — another reason to annotate.Anonymous Functions
Not every function deserves a name. Lean gives you three ways to write one inline, in decreasing order of verbosity.
1-- 1. Full lambda syntax2#eval (fun x => x * 2) 21 -- 423#eval (fun x y => x + y) 3 4 -- 745-- 2. With an explicit type when Lean needs help6#check fun (x : Nat) => x * 2 -- fun x => x * 2 : Nat → Nat78-- 3. Centred-dot shorthand: · becomes the argument9#eval (· * 2) 21 -- 4210#eval [1, 2, 3].map (· * 2) -- [2, 4, 6]· is a middle dot, not a period. In VS Code type \. followed by a space. The shorthand expands to a lambda over the nearest enclosing parentheses, which is why (· * 2) means fun x => x * 2.Currying: Every Function Takes One Argument
This is the idea that unlocks a lot of Lean code. When you write add (x y : Nat) : Nat, you have not defined a function of two arguments. You have defined a function of one argument that returns another function. Ask Lean and it will tell you so.
1def addC (x : Nat) (y : Nat) : Nat := x + y23#check addC -- addC (x y : Nat) : Nat4#check addC 3 -- addC 3 : Nat → Nat ← still a function!56-- So you can supply arguments one at a time7def addFive : Nat → Nat := addC 58#eval addFive 10 -- 15Supplying fewer arguments than the full count is called partial application, and it is how you build small specialised functions out of general ones without writing a lambda.
Deep Dive: Why the arrow associates to the right
The type of addC is written Nat → Nat → Nat. The arrow groups to the right, so this reads as Nat → (Nat → Nat): “give me a Nat, and I will give you back a function from Nat to Nat”.
Meanwhile application groups to the left, so addC 3 5 means (addC 3) 5. The two conventions are designed to fit together: you can write and read multi-argument calls normally, while the underlying machinery only ever deals with one argument at a time. That uniformity is what makes partial application work everywhere with no special cases.
Pattern Matching in Definitions
Instead of a single body, a function can branch on the shape of its input. Both forms below define the same function; the second drops the match boilerplate by listing the cases directly after the type.
1def isZero (n : Nat) : Bool :=2 match n with3 | 0 => true4 | _ => false56-- Equation style: same function, less ceremony7def isZero' : Nat → Bool8 | 0 => true9 | _ => false1011#eval isZero 0 -- true12#eval isZero 4 -- falseLevel 6 covers pattern matching properly. For now just note that the equation style is extremely common in Lean source, so it is worth recognising.
Structuring Bigger Definitions
Use let to name an intermediate value inside a body, and where to attach helper definitions after it. where reads well when the helper is a detail you want out of the way of the main idea.
1def sumOfSquares (a b : Nat) : Nat :=2 let sq := a * a + b * b3 sq + 145#eval sumOfSquares 3 4 -- 2667def area (w h : Nat) : String :=8 s!"area is {a}"9where10 a : Nat := w * h1112#eval area 3 4 -- "area is 12"Higher-Order Functions
A function that takes another function as an argument is called higher-order. Lean has no forloop in the imperative sense — the three functions below do most of the work loops do in other languages.
1def applyTwice (f : Nat → Nat) (x : Nat) : Nat := f (f x)2#eval applyTwice (· + 3) 10 -- 1634-- map: transform every element5#eval [1, 2, 3].map (· * 2) -- [2, 4, 6]67-- filter: keep the elements that pass a test8#eval [1, 2, 3, 4, 5].filter (· % 2 == 0) -- [2, 4]910-- foldl: collapse a list to a single value11#eval [1, 2, 3, 4, 5].foldl (· + ·) 0 -- 15Reading foldl
foldl f init xs starts with init and folds each element in, left to right. With (· + ·) and 0 it computes ((((0+1)+2)+3)+4)+5. Swap in (· * ·) and 1and you have a product instead. Most “loop over a list and accumulate” tasks are a fold in disguise.
Pipelines
Chaining operations with nested parentheses gets unreadable fast. The |> operator feeds the value on the left into the call on the right, letting you read a transformation in the order it happens.
1-- Nested: read inside-out2#eval ([1, 2, 3, 4, 5].filter (· % 2 == 0)).map (· * 10)34-- Pipeline: read left-to-right5#eval [1, 2, 3, 4, 5] |>.filter (· % 2 == 0) |>.map (· * 10)67-- Both print [20, 40]There is also function composition with ∘, which builds a new function rather than immediately applying one. Order matters: the right-hand function runs first.
1def inc (n : Nat) : Nat := n + 12def dbl (n : Nat) : Nat := n * 234#eval (inc ∘ dbl) 5 -- 11 doubles first, then increments5#eval (dbl ∘ inc) 5 -- 12 increments first, then doublesImplicit Arguments
Braces instead of parentheses mark an argument Lean should figure out on its own. The identity function works at any type, but you never have to say which type you are using — Lean reads it off the value you pass.
1def myId {α : Type} (x : α) : α := x23#eval myId 5 -- 5 α inferred as Nat4#eval myId "hi" -- "hi" α inferred as String56-- The @ prefix shows the function with implicits made visible7#check @myId -- @myId : {α : Type} → α → αα or ?m.1234, it usually means Lean could not infer an implicit argument. Supplying it explicitly with @ is a good debugging move.Practice
Write getOrZero which extracts the value from an Option Nat, returning 0 when there is nothing there.
Show solution
1def getOrZero (value : Option Nat) : Nat :=2 match value with3 | some n => n4 | none => 056#eval getOrZero (some 7) -- 77#eval getOrZero none -- 0The standard library already has this as Option.getD, but writing it yourself is the point: the match is what forces you to handle the missing case.
Generalise applyTwice. Write applyN f n x which applies f to x exactly n times. Applying it zero times should return x unchanged.
Show solution
1def applyN (f : Nat → Nat) : Nat → Nat → Nat2 | 0, x => x3 | n + 1, x => applyN f n (f x)45#eval applyN (· + 3) 4 0 -- 12Two things to notice: you can match on several arguments at once by separating the patterns with commas, and the n + 1 pattern lets you name the predecessor. That pattern is the workhorse of Level 7.
Using a pipeline, write sumEvens which keeps only the even elements of a list and adds them up. Try to do it without recursion.
Show solution
1def sumEvens (xs : List Nat) : Nat :=2 xs.filter (· % 2 == 0) |>.foldl (· + ·) 034#eval sumEvens [1, 2, 3, 4, 5, 6] -- 12Common Mistakes
- Using a period instead of the centred dot.
(. * 2)is not(· * 2). - Expecting an arity error from partial application.Passing too few arguments is legal and gives you a function back — the error usually surfaces later, somewhere confusing.
- Reaching for a loop. If you catch yourself wanting
for, the answer is almost alwaysmap,filter,foldl, or recursion. - Getting composition backwards.
f ∘ grunsgfirst.
map, filter, and pipelines replace loops so cleanly.Check Yourself
- What is the type of
add 3ifadd : Nat → Nat → Nat? - Rewrite
fun x => x + 1using the dot shorthand. - What does
[1,2,3].foldl (· * ·) 1evaluate to? - Which runs first in
(inc ∘ dbl) 5?