Lean code is built from expressions. An expression may be a value, a function, a type, a proposition, or a proof. The same syntactic system is used for computation and reasoning, so learning the basic expression language is the first step toward writing both programs and theorems.
Problem
You want to read and write small Lean expressions, understand their types, and recognize how Lean parses ordinary definitions and proof terms.
Solution
Use #check to inspect the type of an expression and #eval to compute expressions that have executable content.
#check 5
#check Nat
#check String
#check true
#check fun x : Nat => x + 1
#eval 5 + 7
#eval "Lean" ++ " " ++ "Cookbook"
#eval (fun x : Nat => x + 1) 10Typical output includes types such as:
5 : Nat
Nat : Type
String : Type
true : Bool
fun x => x + 1 : Nat -> Nat
12
"Lean Cookbook"
11The command #check asks Lean to infer a type. The command #eval asks Lean to compute a value.
Expressions
An expression is any Lean term that can be checked by the type system.
Examples:
42
"hello"
true
Nat
Nat -> Nat
fun x : Nat => x + 1Each expression has a type.
#check 42 -- 42 : Nat
#check "hello" -- "hello" : String
#check true -- true : Bool
#check Nat -- Nat : Type
#check Nat -> Nat -- Nat -> Nat : TypeTypes themselves are expressions. For example, Nat has type Type.
Function Application
Function application is written by placing the argument after the function.
#eval Nat.succ 4The result is:
5Multiple arguments are written by placing them in sequence:
#eval Nat.add 3 4This means:
(Nat.add 3) 4Function application associates to the left. Therefore:
f x y zmeans:
(((f x) y) z)Function Types
The type:
Nat -> Natmeans a function from natural numbers to natural numbers.
A function with two arguments has a curried type:
Nat -> Nat -> NatThis means:
Nat -> (Nat -> Nat)A function takes one argument and returns another function. This convention makes partial application natural.
def addThree : Nat -> Nat -> Nat :=
fun x y => x + y + 3
#check addThree
#check addThree 4
#eval addThree 4 5The expression addThree 4 is itself a function.
Lambda Expressions
A lambda expression creates an anonymous function.
fun x : Nat => x + 1This expression has type:
Nat -> NatSeveral arguments can be written together:
fun x y : Nat => x + yThis is shorthand for:
fun x : Nat => fun y : Nat => x + yLambda expressions are useful when a small function is needed only once.
#eval List.map (fun x : Nat => x + 1) [1, 2, 3]Expected result:
[2, 3, 4]Let Expressions
A let expression gives a local name to a value.
#eval
let x := 10
let y := 20
x + yExpected result:
30A let expression is still an expression. It computes by replacing the local name with its value.
You can also annotate the type:
#eval
let x : Nat := 10
x + 5Annotations are useful when Lean cannot infer the intended type.
Parentheses
Parentheses control parsing.
#eval (fun x : Nat => x + 1) 5Without parentheses, Lean would try to parse the expression differently.
Parentheses are also useful in nested applications:
#eval Nat.succ (Nat.succ 3)Expected result:
5Infix Operators
Many functions have infix notation.
#eval 2 + 3
#eval 10 - 4
#eval 3 * 5These are notation for ordinary functions. For example:
2 + 3is notation for addition on natural numbers.
Precedence determines how expressions are grouped.
#eval 2 + 3 * 4
#eval (2 + 3) * 4Expected results:
14
20Type Ascription
A type ascription tells Lean the intended type of an expression.
#check (5 : Nat)
#check ("hello" : String)This is useful when literals are overloaded.
#check (0 : Nat)
#check (0 : Int)The same printed literal may be elaborated at different types, depending on context.
Propositions as Expressions
Propositions are also expressions.
#check True
#check False
#check 1 = 1
#check NatTypical output:
True : Prop
False : Prop
1 = 1 : Prop
Nat : TypeA proof is an expression whose type is a proposition.
#check True.intro
#check rflThe term True.intro proves True. The term rfl proves equalities that hold by reflexivity or definitional computation.
Definitions
A definition binds a name to an expression.
def one : Nat :=
1
def two : Nat :=
one + oneAfter these declarations, the names can be checked and evaluated.
#check one
#eval twoExpected result:
2The general shape is:
def name : Type :=
expressionLean checks that expression has the declared type.
Theorems as Expressions
A theorem binds a name to a proof expression.
theorem true_example : True :=
True.introThe general shape is:
theorem name : proposition :=
proofA theorem is checked like any other declaration. The difference is that its declared type lies in Prop.
A tactic proof produces the same kind of proof term:
theorem implication_example (P : Prop) : P -> P := by
intro h
exact hThis theorem says that every proposition implies itself.
Holes
A hole asks Lean to report what it needs at that point.
def addOne (n : Nat) : Nat :=
_Lean reports that the hole has expected type Nat.
Holes are useful during development because they reveal the local context and the target type.
A named hole can appear more than once:
def pairSame (n : Nat) : Nat × Nat :=
(?x, ?x)Lean reports a goal named ?x.
Commands vs Expressions
Some Lean syntax is a command, not an expression.
Commands include:
def
theorem
#check
#eval
import
namespace
endExpressions include:
5
Nat
fun x : Nat => x + 1
x + y
TrueCommands modify or inspect the environment. Expressions are the terms that Lean checks.
Recipe: Inspect Before You Prove
When a proof or definition fails, inspect smaller pieces.
#check Nat.succ
#check Nat.succ 3
#eval Nat.succ 3Then build the final declaration:
def next (n : Nat) : Nat :=
Nat.succ nThis style avoids guessing. Lean becomes easier to use when each subexpression has a known type.
Common Pitfalls
If Lean reports a type mismatch, insert a type ascription to make the intended type explicit.
If function application parses incorrectly, add parentheses.
If a literal has the wrong type, annotate it.
If a name is unknown, check imports and namespaces.
If a theorem looks like a definition, remember that the difference is its type: propositions live in Prop.
Takeaway
Lean programs and proofs are built from expressions. Every expression has a type, and types themselves are expressions. Function application, lambda abstraction, let binding, type ascription, and proposition formation are the basic forms you need before definitions, theorems, tactics, and larger modules become predictable.