| ← Everything has a type | Index |
Consider three short definitions. Every token in them is doing something specific; each is examined in full below.
def double (n : Nat) : Nat := n * 2
def double (n : Nat) : Nat := n * 2
def— the keyword that introduces a new global definition into the environment: a name, permanently available afterward, standing for a fixed term. This is different from a hypothesis or a local variable. Once elaborated,doubleis a completely ordinary constant, referable anywhere later in the file (or, once the file is imported, anywhere else). Compare this withexamplefrom Chapter 3, which elaborates a term but does not bind it to a name.defis for the case where the definition is meant to be reused.double— the name being bound. Lean enforces no special naming convention, but lowerCamelCase fordefs and UpperCamelCase for types is the near-universal community style, which this book follows.(n : Nat)— an explicit argument namedn, of typeNat. “Explicit” means a caller must supply it positionally:double 5provides5forndirectly. This is what makesdoublea function rather than a plain value. Everything afterdef doubleup to the first bare:=(if there are argument binders) is the function’s parameter list, anddouble’s actual type ends up beingNat → Nat, exactly as if it had been writtendef double : Nat → Nat := fun n => n * 2instead. The two forms elaborate to the same term. The(n : Nat)binder form is simply the standard, more readable surface syntax for “a function with named parameters.”: Nat(the second one, right before:=) — the declared return type. This is not optional filler. Lean uses it to check the body against a known expected type while elaborating, rather than only inferring a type afterward. If the body’s type did not match, an error would occur at this point, not somewhere downstream.:=— separates the declaration’s signature (name, arguments, return type) from its definition (the actual term). Read it as “is defined to be.”n * 2— the body. At this point in elaboration,nis in scope with typeNat(introduced by the(n : Nat)binder above), son * 2isNatmultiplication applied tonand the numeral2. That numeral itself elaborates to aNat, because that is whatNat.mul’s second argument’s type forces it to be. Numeral elaboration is guided by the expected type — another case of Lean checking against context instead of guessing.
Mathematical reading. def double (n : Nat) : Nat := n * 2 is nothing
more than the ordinary mathematical definition
with the signature $\mathrm{double} : \mathbb{N} \to \mathbb{N}$ split
across def double (n : Nat) : Nat (domain and codomain, spelled out
argument-by-argument rather than as a single arrow), and the equation
$\mathrm{double}(n) = 2n$ becomes := n * 2. There is no real difference
between writing the domain as one arrow Nat → Nat or as a named binder
(n : Nat) : Nat, exactly as $f : A \to B,\ f(a) = \ldots$ and
$f = (a \mapsto \ldots) : A \to B$ describe the same function.
def average (a b : Nat) : Nat :=
let sum := a + b
sum / 2
def average (a b : Nat) : Nat := let sum := a + b; sum / 2
(a b : Nat)— two explicit arguments, both of typeNat, written with a single shared type annotation. This is pure surface-syntax sugar for(a : Nat) (b : Nat); Lean expands it identically either way. This shorthand is standard style whenever several consecutive parameters share a type, and costs nothing.let sum := a + b— introduces a local definition, visible only in the rest of this particular body, as opposed todef’s global one.letdoes not need (though it can take) a type annotation, since the type ofa + bis already fully determined byaandb’s types, so Lean infers it. Operationally,let sum := a + b; sum / 2means exactly the same thing as substitutinga + bfor every occurrence ofsuminsum / 2. Aletis definitionally transparent, soaverage a band(a + b) / 2elaborate to the same normal form. The reason to write it as aletanyway, rather than inlining(a + b) / 2directly, is purely for the human reader: naming an intermediate quantity documents what it means, and in a longer proof or definition, it prevents repeating a nontrivial subexpression (and therefore repeating a mistake in it) in several places.- The line break between
let sum := a + bandsum / 2is whitespace, not two separate statements needing a semicolon. Lean’s parser uses indentation-sensitive layout forlet-chains, the same way it does forby-blocks in tactic mode.sum / 2is thelet’s body: the whole two-line constructlet sum := a + b; sum / 2is itself one term, which is then whataverage’s:=binds to. sum / 2—Natdivision, which in Lean is truncating.average 4 10computessum = 14, then14 / 2 = 7exactly. Butaverage 1 2would computesum = 3, then3 / 2 = 1(rounded down, sinceNathas no fractions). This should be noted before relying on thisaveragefor anything where the rounding matters.
Mathematical reading. The let is exactly a “let $s := a + b$ in
$\ldots$” clause of the kind used constantly in written proofs to name an
intermediate quantity:
One caveat: this average computes $\lfloor s/2 \rfloor$ (floor division)
rather than the true rational average $s/2$, since Nat’s division is
truncating. This gap is easy to overlook in ordinary mathematical prose,
but Lean forces it to be confronted explicitly: there is no coercion to
$\mathbb{Q}$ happening for free.
def identity {α : Type} (x : α) : α := x
def identity {α : Type} (x : α) : α := x
{α : Type}— an implicit argument, marked by curly braces instead of parentheses. The nameα(conventionally a Greek letter for a type variable — again pure convention,TorAwould work just as well) has typeType, meaning this argument is itself a type, not a value of some fixed type.identityis thus polymorphic: it works uniformly for every choice ofα.- The crucial difference from
(n : Nat)above:{α : Type}is not supplied positionally at call sites. Writingidentity 5does not mean “pass5asα”. Lean instead elaborates (infers)αby unification, working backward from the type of the explicit argument actually supplied. Inidentity 5, Lean sees that5 : Natis being passed where anx : αis expected, unifiesα := Nat, and only then checks the rest. An implicit argument can still be supplied explicitly when overriding inference is necessary, with@identity Nat 5. The@prefix means “no more auto-inference; every argument, implicit or not, is given by hand.” This escape hatch matters mainly for debugging elaboration failures, not everyday use. - Why implicit rather than explicit here at all:
αis determined byx’s type at every call site, so requiring the caller to type it out (as inidentity Nat 5) would be pure noise. Lean already has enough information without being told. The general rule of thumb, used throughout Mathlib and this book: mark an argument implicit exactly when its value is always recoverable from the other arguments or from the expected return type; keep it explicit when it genuinely varies independently, and a reader benefits from seeing it written at the call site. : α := x— the return type isαitself (the same type variable bound above, now in scope for the rest of the signature and body), and the body is simplyx, the argument unchanged. This is the identity function at every type simultaneously: onedef, rather than one per type, which is exactly what the{α : Type}parameter provides.
Mathematical reading. identity {α : Type} (x : α) : α := x is the
family ${\,\mathrm{id}A : A \to A\,}{A \in \mathbf{Type}}$ indexed over
every type $A$ at once. It is precisely the assignment sending each
object $A$ of the category to its identity morphism $\mathrm{id}A$,
packaged as a single polymorphic definition rather than one definition per
$A$. The implicit argument {α : Type} is what makes this a *statement
about all $A$ simultaneously* rather than a single fixed function. In
category theory one would never write “$\mathrm{id}{\mathbb{Z}}$,
$\mathrm{id}_{\mathbb{R}}$, …” one at a time either, but would instead
say “for every object $A$, there is an identity morphism $\mathrm{id}_A$,” which is
exactly what the universally-quantified, implicitly-inferred {α : Type}
expresses.
| ← Everything has a type | Index | Next: Dependent types, with examples → |