Functions & Definitions
Functions are the heart of Lean programming. Learn to define them with explicit types, type inference, named arguments, and default values.
Basic Function Definitions
Use the def keyword to define functions. The syntax is clean and expressive:
1-- A function with explicit types2def add (x : Nat) (y : Nat) : Nat := x + y34-- Using the function5#eval add 3 5 -- 867-- A function that takes a String8def greet (name : String) : String := "Hello, " ++ name ++ "!"910#eval greet "Lean" -- "Hello, Lean!"The pattern is: def name (params) : ReturnType := body. Each parameter has its own parentheses and type annotation.
Type Inference
Lean's type inference is powerful. You can often omit types and let the compiler figure them out:
1-- Type inferred for simple values2def x := 42 -- Inferred: Nat3def name := "Alice" -- Inferred: String45-- The RETURN type can be inferred from the body6def double (x : Nat) := x + x -- : Nat, inferred from x + x78-- But a bare parameter cannot be inferred:9-- def double' x := x + x10-- error: typeclass instance problem is stuck, HAdd ?m ?m ?m11--12-- Lean has nothing to go on: x + x works for Nat, Int, Float, ...13-- so it cannot pick one. Unlike Hindley-Milner languages, Lean does NOT14-- generalise over unknown types here, because + is resolved by type class15-- lookup and the class needs a concrete type first.1617-- Fix: annotate the parameter18def double'' (x : Nat) : Nat := x + xCommon Pitfalls
Beginners often run into small syntax gotchas. Keep these in mind as you practice.
- Each parameter usually gets its own parentheses unless you group same-typed parameters.
- Lean is expression-based—every branch must return the same type.
- Use
:=for definitions and=for equations.
Multiple Parameters
Functions can take multiple parameters. Each parameter is in its own parentheses, or you can group parameters of the same type:
1-- Each parameter separate2def add1 (x : Nat) (y : Nat) : Nat := x + y34-- Grouped parameters of same type 5def add2 (x y : Nat) : Nat := x + y67-- Mixed types8def describe (name : String) (age : Nat) : String :=9 s!"{name} is {age} years old"1011#eval describe "Alice" 30 -- "Alice is 30 years old"Named Arguments
You can call functions with named arguments for clarity:
1def greet (name : String) (formal : Bool) : String :=2 if formal then s!"Good day, {name}." else s!"Hey {name}!"34-- Positional arguments5#eval greet "Alice" true -- "Good day, Alice."67-- Named arguments (order doesn't matter)8#eval greet (name := "Bob") (formal := false) -- "Hey Bob!"9#eval greet (formal := true) (name := "Carol") -- "Good day, Carol."Default Values
Parameters can have default values, making them optional:
1def greet (name : String) (formal : Bool := false) : String :=2 if formal then s!"Good day, {name}." else s!"Hey {name}!"34-- Using default5#eval greet "Alice" -- "Hey Alice!" (formal defaults to false)67-- Override default8#eval greet "Bob" true -- "Good day, Bob."910-- Named argument to override11#eval greet "Carol" (formal := true) -- "Good day, Carol.":= value are optional. This pattern is common for configuration options and flags.Anonymous Functions (Lambdas)
Create inline functions with fun (or the Unicode λ):
1-- Lambda syntax2#eval (fun x => x + 1) 5 -- 63#eval (fun x y => x * y) 3 4 -- 1245-- With type annotations6#eval (fun (x : Nat) => x * 2) 5 -- 1078-- Unicode lambda (optional but pretty)9#eval (λ x => x + 1) 5 -- 61011-- Lambdas are often used with higher-order functions12#eval [1, 2, 3].map (fun x => x * 2) -- [2, 4, 6]1314-- A lambda can pattern match directly, with no "match" keyword15#eval [0, 1, 5].map (fun | 0 => "zero" | _ => "nonzero")16-- ["zero", "nonzero", "nonzero"]Equation-Style Definitions
When a function is defined entirely by cases on its argument, you can skip the match and write the equations directly after the type. This is the style you will see throughout the standard library.
1-- Give the TYPE, then the equations — no parameter names, no :=2def isZero : Nat → Bool3 | 0 => true4 | _ => false56#eval isZero 0 -- true7#eval isZero 3 -- false89-- Equivalent long form10def isZero' (n : Nat) : Bool :=11 match n with12 | 0 => true13 | _ => false1415-- Multiple arguments: one pattern per argument, separated by commas16def bothZero : Nat → Nat → Bool17 | 0, 0 => true18 | _, _ => false1920-- You can mix: name some parameters, match on the rest21def replicate (n : Nat) : α → List α22 | a => List.replicate n aisZero.eq_1, isZero.eq_2, …) which simp and rw can use when you later prove things about the function. You get the same lemmas from a match, but the equation form makes the case split visible in the signature.def, abbrev, example, and theorem
def is one of a family of declaration keywords. They differ in how eagerly Lean unfolds them and in what they are for:
1-- def: the normal case. Semi-reducible — unfolds when asked (simp [f], unfold f)2def Celsius := Float34-- abbrev: fully reducible. Behaves as a transparent alias everywhere,5-- including for type class search and definitional equality6abbrev Name := String7def hi (n : Name) : String := "hi " ++ n8#eval hi "Ada" -- works: Name IS String as far as Lean is concerned910-- example: an anonymous declaration, checked and then discarded.11-- Perfect for the code samples on this page.12example : 1 + 1 = 2 := rfl1314-- theorem: like def, but the result is a Prop and the body is a proof.15-- Proofs are irrelevant, so theorem bodies are never unfolded during evaluation.16theorem two_eq : 1 + 1 = 2 := rflabbrev for type synonyms you want to be transparent, and def when you want a genuinely distinct name. Choosing def Celsius := Float above means Lean will notautomatically accept a Float where a Celsius is expected in type class search — often exactly what you want, occasionally a source of confusing errors.partial def
Every Lean function must terminate, and Lean must be able to see why. When it cannot — or when the recursion genuinely may not terminate — mark the definition partial.
1-- Lean cannot prove this terminates (n * 2 is not "smaller" than n)2partial def doubleUntilBig (n : Nat) : Nat :=3 if n > 100 then n else doubleUntilBig (n * 2)45#eval doubleUntilBig 1 -- 128partial is a real trade-off, not a magic escape hatch. The function still compiles and runs, but Lean treats its body as opaque: you cannot unfold it in a proof, and rfl/decide will not evaluate it. Use it for I/O loops and interpreters; avoid it for anything you intend to reason about. The Termination lesson shows how to discharge the obligation properly with termination_by instead.variable: Shared Parameters
When several declarations take the same parameter, variabledeclares it once. Lean adds it to any declaration that mentions it — and only to those.
1variable (k : Nat)23def addK (n : Nat) : Nat := n + k4#check addK -- addK (k n : Nat) : Nat — k was inserted automatically56def noK (n : Nat) : Nat := n + 17#check noK -- noK (n : Nat) : Nat — k was NOT insertedk is inserted before n, because variables come first. This trips people up when they add a variable to an existing file and every call site silently changes meaning.Implicit Arguments
Some arguments can be inferred from context. These are marked with curly braces:
1-- The type α is implicit - Lean infers it2def identity {α : Type} (x : α) : α := x34-- Lean infers α = Nat from the argument5#eval identity 42 -- 426#eval identity "hello" -- "hello" (α = String)78-- You can provide implicit args explicitly with @9#eval @identity Nat 42 -- 42Deep Dive: Why Implicit Arguments?
Implicit arguments reduce verbosity. Without them, you'd have to writeidentity Nat 42 every time, even though Lean can clearly see 42 is a Nat.
The @ prefix makes all arguments explicit, which is useful when type inference fails or you want to be specific.
Instance-Implicit Arguments
Square brackets [...] denote instance-implicit arguments. Lean automatically finds type class instances:
1-- [BEq α] means "α must have equality defined"2def contains [BEq α] (xs : List α) (target : α) : Bool :=3 xs.any (· == target)45-- Lean finds the BEq instance automatically6#eval contains [1, 2, 3] 2 -- true7#eval contains ["a", "b"] "c" -- false89-- Multiple instance arguments: require everything the body actually uses10def summarise [Ord α] [ToString α] (xs : List α) : String :=11 let sorted := xs.mergeSort (fun a b => compare a b != .gt)12 ", ".intercalate (sorted.map toString)1314#eval summarise [3, 1, 2] -- "1, 2, 3"1516-- Named instance argument: gives you a handle on the instance itself17def customCompare [inst : Ord α] (x y : α) : Ordering :=18 inst.compare x y1920#eval customCompare 1 2 -- Ordering.lt[BEq α], Lean finds the appropriate equality implementation at compile time.Autobound Implicit Arguments
When you use an undeclared lowercase identifier as a type, Lean automatically makes it an implicit argument:
1-- These are equivalent:2def first1 (xs : List α) : Option α := xs.head?3def first2 {α : Type} (xs : List α) : Option α := xs.head?45-- α is automatically bound as {α : Type}67-- Works with multiple type variables8def zip (xs : List α) (ys : List β) : List (α × β) := 9 xs.zip ys10-- Both α and β are autobound1112-- Disable with set_option13set_option autoImplicit false in14def explicit {α : Type} (x : α) : α := xFunction Types
Functions have types expressed with arrows. Understanding these helps you read library documentation:
1-- Single argument function2#check (fun x => x + 1 : Nat → Nat)34-- Multiple arguments (curried)5#check (fun x y => x + y : Nat → Nat → Nat)6-- Read as: takes a Nat, returns a function Nat → Nat78-- With implicit arguments9#check @List.map10-- {α : Type} → {β : Type} → (α → β) → List α → List βPartial Application
Because multi-argument functions are curried, you can partially apply them:
1def add (x y : Nat) : Nat := x + y23-- Partially apply: fix the first argument4def add5 := add 5 -- add5 : Nat → Nat56#eval add5 3 -- 87#eval add5 10 -- 1589-- Useful with higher-order functions10#eval [1, 2, 3].map (add 10) -- [11, 12, 13]Function Composition
Compose small functions into larger ones using · withFunction.comp or the ∘ operator.
1def double (n : Nat) : Nat := n * 22def inc (n : Nat) : Nat := n + 134def doubleThenInc : Nat → Nat := inc ∘ double5def incThenDouble : Nat → Nat := double ∘ inc67#eval doubleThenInc 3 -- 78#eval incThenDouble 3 -- 8Define a function that squares a number, then converts it to a string.
1def square (n : Nat) : Nat := n * n2def toText (n : Nat) : String := toString n34def squareToString : Nat → String :=5 toText ∘ square67#eval squareToString 12 -- "144"Where Clauses
For complex functions, use where to define helpers:
1def quadratic (a b c x : Float) : Float := 2 a * x^2 + b * x + c34-- Cleaner with where5def quadratic' (a b c x : Float) : Float := 6 term1 + term2 + term37where8 term1 := a * x^29 term2 := b * x10 term3 := c1112#eval quadratic' 1 2 1 3 -- 16.000000where helpers can take arguments and can be recursive, which let cannot. That makes where the standard place to put an accumulator loop:1def sumTo (n : Nat) : Nat := go n 02where3 go : Nat → Nat → Nat4 | 0, acc => acc5 | k + 1, acc => go k (acc + k + 1)67#eval sumTo 10 -- 55