Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/leanprover/lean4/llms.txt

Use this file to discover all available pages before exploring further.

Functions and constants are the primary building blocks of any Lean 4 program or proof. Lean 4 uses a unified declaration syntax where def, theorem, lemma, and abbrev all introduce names into the environment — the difference lies in their intended use and how the elaborator and optimizer treat them. This page covers every form of definition you will encounter, from simple constants to well-founded recursive functions.

Basic Declarations

def — Definitions

def introduces a computable definition:
-- Constant
def pi : Float := 3.141592653589793

-- Function (explicit argument)
def square (n : Nat) : Nat := n * n

-- With return type inferred
def greet name := "Hello, " ++ name ++ "!"

#eval square 7    -- 49
#eval greet "Lean"  -- "Hello, Lean!"

theorem and lemma

theorem and lemma are definitionally identical to def but signal to readers (and the compiler) that the term is a proof. The elaborator marks them as opaque by default, so they do not reduce under normal evaluation — this is intentional for proof performance.
theorem add_comm' (a b : Nat) : a + b = b + a := by
  omega

lemma zero_add' (n : Nat) : 0 + n = n := by
  simp

abbrev — Abbreviations

abbrev declares a definition that is always unfolded by the elaborator (at every transparency level). Use it for type aliases or short wrappers where you want the definition to be transparent everywhere:
abbrev Point := Nat × Nat

-- Because Point is an abbrev, the elaborator sees through it
def origin : Point := (0, 0)

noncomputable

Mark a definition noncomputable when it uses classical axioms (Classical.choice) or is otherwise not executable. The compiler will refuse to generate code for it, but it can still be used in proofs:
-- Classical.choice requires a nonemptiness axiom and cannot be executed
noncomputable def someValue : Nat := Classical.choice ⟨0

-- You can still prove things about it in proofs
example : True := trivial

Lambda Expressions

-- Basic lambda
def double : Nat → Nat := fun n => n * 2

-- Multiple arguments
def add : Nat → Nat → Nat := fun a b => a + b

-- Pattern-matching lambda (fun with |)
def describeOpt : Option Nat → String := fun
  | some n => s!"Got {n}"
  | none   => "Nothing"

-- Shorthand anonymous function (placeholder syntax)
#eval [1, 2, 3].map (· * 10)   -- [10, 20, 30]
#eval [1, 2, 3, 4].filter (· % 2 == 0)  -- [2, 4]

Argument Kinds

Explicit Arguments (a : α)

The caller must supply the argument explicitly:
def repeatStr (s : String) (n : Nat) : String :=
  String.join (List.replicate n s)

#eval repeatStr "ha" 3   -- "hahaha"

Implicit Arguments {a : α}

Curly-brace arguments are inferred by the unifier. The caller does not (normally) provide them:
def id' {α : Type*} (a : α) : α := a

#eval id' 42        -- 42  (α is inferred as Nat)
#eval id' "hello"   -- "hello"  (α is inferred as String)

-- Force explicit with @
#eval @id' Nat 42   -- 42

Instance Arguments [inst : Class α]

Square-bracket arguments are filled by typeclass synthesis:
def showTwice [ToString α] (a : α) : String :=
  toString a ++ ", " ++ toString a

#eval showTwice 42        -- "42, 42"
#eval showTwice "hello"   -- "hello, hello"
You can name the instance or leave it anonymous:
def sum' [Add α] [Zero α] (xs : List α) : α :=
  xs.foldl (· + ·) 0

Auto-Bound Implicit Variables

Any lowercase variable used in a type signature but not declared is automatically bound as an implicit argument. This reduces verbosity:
-- α is auto-bound as {α : Type*}
def myLength (xs : List α) : Nat :=
  match xs with
  | []      => 0
  | _ :: t => 1 + myLength t

-- Equivalent to:
-- def myLength {α : Type*} (xs : List α) : Nat := ...
Auto-bound implicit works for lowercase names only. To declare universe variables that should be auto-bound, use universe u at the top of a section.

Optional and Named Arguments

-- Optional argument with default value
def greetFormal (name : String) (greeting : String := "Hello") : String :=
  s!"{greeting}, {name}!"

#eval greetFormal "Alice"              -- "Hello, Alice!"
#eval greetFormal "Alice" "Greetings"  -- "Greetings, Alice!"

-- Named argument (pass out of order)
#eval greetFormal (greeting := "Hi") "Bob"  -- "Hi, Bob!"

Recursive Functions

Structural Recursion

The most common form: recurse on a structurally smaller subterm. Lean’s termination checker accepts this automatically:
def factorial : Nat → Nat
  | 0     => 1
  | n + 1 => (n + 1) * factorial n

def listSum : List Nat → Nat
  | []      => 0
  | x :: xs => x + listSum xs

#eval factorial 10    -- 3628800
#eval listSum [1, 2, 3, 4, 5]  -- 15

Fibonacci with Accumulator

def fib (n : Nat) : Nat :=
  go n 0 1
where
  go : Nat → Nat → Nat → Nat
    | 0,     a, _ => a
    | n + 1, a, b => go n b (a + b)

#eval fib 30    -- 832040

Well-Founded Recursion

When structural recursion does not apply, Lean can use well-founded recursion. Provide a termination_by clause with a decreasing measure:
def collatz (n : Nat) : Nat :=
  if n ≤ 1 then 0
  else if n % 2 == 0 then 1 + collatz (n / 2)
  else 1 + collatz (3 * n + 1)
termination_by n   -- the argument n must decrease; Lean uses a well-founded order

#eval collatz 27    -- 111
For mutual recursion, use mutual ... end:
mutual
  def isEven : Nat → Bool
    | 0     => true
    | n + 1 => isOdd n

  def isOdd : Nat → Bool
    | 0     => false
    | n + 1 => isEven n
end

#eval isEven 42   -- true
#eval isOdd  7    -- true

partial def — Potentially Non-Terminating Functions

When termination cannot be proved (or when you intentionally want a potentially non-terminating function), use partial def. The function is accepted without a termination check, but it cannot be used in proofs:
partial def infiniteLoop : IO Unit := do
  IO.println "tick"
  infiniteLoop

-- Also useful for I/O servers, interpreters, etc.
partial def repl : IO Unit := do
  let line ← (← IO.getStdin).getLine
  if line.trim == "quit" then return
  IO.println s!"Echo: {line}"
  repl
partial functions can loop forever. They are excluded from the kernel, so they cannot appear in proofs or be evaluated during elaboration.

Dot Notation and Projection

Lean 4 supports dot notation for namespace dispatch:
-- Equivalent calls
#eval List.length [1, 2, 3]
#eval [1, 2, 3].length

-- Dot notation finds definitions in the namespace matching the head type
def List.second? (xs : List α) : Option α := xs.tail.head?
#eval [10, 20, 30].second?   -- some 20

Key Attributes on Definitions

Attributes control elaboration, optimization, and the simp set:

@[simp]

Adds the definition or theorem to the global simp set, so simp will use it automatically:
@[simp]
theorem list_length_nil : ([] : List α).length = 0 := rfl

@[simp]
theorem list_length_cons (x : α) (xs : List α) :
    (x :: xs).length = xs.length + 1 := rfl

@[inline]

Instructs the compiler to inline the function at every call site. Use for small, hot-path functions:
@[inline]
def increment (n : Nat) : Nat := n + 1

@[reducible]

Makes the definition visible at reducible transparency. Useful for type aliases that should be seen through by typeclass search:
@[reducible]
def MyList α := List α

-- Typeclass instances for List α now apply to MyList α too

@[extern]

Links the definition to a C function implementation. The Lean declaration provides the type signature; the runtime uses the C function:
@[extern "lean_nat_add"]
protected def Nat.add : Nat → Nat → Nat
  | a, .zero   => a
  | a, .succ b => .succ (Nat.add a b)

#check and #eval for Functions

#check List.map      -- List.map : (α → β) → List α → List β
#check @List.foldl   -- @List.foldl : {α : Type u_1} → {β : Type u_2} → (α → β → α) → α → List β → α
#eval (fun x => x * x) 9   -- 81

Build docs developers (and LLMs) love