In Lean, you can find out the type of an expression by using the #check command:
#check 42 -- 42 : Nat
#check "42" -- "42" : String
#check 1 + 1 -- 1 + 1 : Nat
#check Nat.mul -- Nat.mul : Nat → Nat → Nat
#check 1 + 1 = 2 -- 1 + 1 = 2 : Prop
#check 1 + 1 > 2 -- 1 + 1 > 2 : Prop
Note that a well-typed equality can be recognised as a well-typed proposition regardless of whether it is "mathematically correct". The expression 2 + 2 = 5, for instance, is well-typed (since it is an equality between two natural numbers). In contrast, certain routinely seen expressions such as 2 * (3 + 1) = 2 * 3 + 2 * 1 = 8 are not well-typed.
It is common sense not to mix quantities that are not related to one another. Here is an example of an ill-typed expression.
Image credits: MikeGogulski, CC BY-SA 3.0.
The architecture of an interactive theorem prover can be represented as follows.
Image credits: Assia Mahboubi.
The following is Lean code for the function sending a natural number
def square : Nat → Nat :=
fun (n : Nat) ↦ n * n
def is the keyword to declare a function, square is our identifier for this function.Nat → Nat is the type signature of the function square.fun (n : Nat) ↦ n * n is the body of the declaration (appearing after :=).This is accepted by the type-checker because n * n is recognised as a term of type Nat, where * has been previously defined. The type-annotated form of the expression n * n is (Nat.mul : Nat → Nat → Nat) (n : Nat) (n : Nat), which is inferred by the type-checker.
Simply-typed functions f : X → Y are the basic objects of a language like Lean.
This includes basic recursive functions, whose implementation can look similar to the usual mathematical definition:
def fact : Nat → Nat :=
fun (k : Nat) ↦ match k with
| 0 => 1
| k + 1 => (k + 1) * fact k
#check fact -- fact : Nat → Nat
#check fact 5 -- fact 5 : Nat
#eval fact 5 -- 120
The natural way to define a function of two variables in Lean is to use curried notation.
A function of two variables
In functional programming languages, when we write f : A → B → C, then, in the expression f a b, the term f a is a function from B to C, and it is applied to b.
def sum : Nat → Nat → Nat := fun x y ↦ x + y
#check sum 3 -- sum 3 : Nat → Nat
#eval sum 3 5 -- 8
If we set instead def sum₁ : Nat → Nat → Nat := fun x ↦ (fun y ↦ x + y), then we get sum = sum₁, by reflexivity.
We can write curried functions with an arbitrary number of variables: if f : A → B → C → D → E, then for all a : A, b : B, c : C, d : D, we have f a b c d : E.
This is not the same as f : A → (B → C) → D → E, which takes as arguments a term a : A, a function u : B → C and a term d : D, returning a term of type E.
A → B → C → D → E is the same as A → (B → (C → (D → E))).
def f : Nat → (Nat → ℝ) → ℝ → ℝ :=
fun (n : Nat) (u : Nat → ℝ) (x : ℝ) ↦ 2 ^ n * u n + x
def v : Nat → ℝ := fun n ↦ 2 * n
#eval f 3 v (-6) -- 42
The most famous inductive type in mathematics is probably the type Nat, whose definition goes back to Giuseppe Peano in 1889. In Lean, it is implemented as follows.
inductive Nat : Type
| zero : Nat
| succ : Nat → Nat
Inductive declarations produce special functions called constructors. In the present case, there are two of them (two introduction rules for terms of type Nat):
Nat.zero : Nat (a function whose value is its own name is called an atom, you can view it as a function from the Unit type to Nat, if you prefer).Nat.succ : Nat → Nat (the successor function), saying that for every n : Nat there is a term n.succ : Nat (dot notation for Nat.succ (n : Nat)).The fact that product types are defined inductively may seem less familiar to mathematicians.
inductive Prod (X Y : Type) : Type
| mk : X → Y → Prod X Y
Prod X Y are introduced via the constructor Prod.mk : X → Y → Prod X Y.x : X and all y : Y, the term Prod.mk x y is of type Prod X Y and this is the only introduction rule for terms of type Prod X Y.Prod X Y can be denoted by X × Y and its terms by ⟨x, y⟩ (angle brackets or usual brackets).We have already seen examples of a function f : Nat → Nat, namely the factorial function. It was defined via primitive recursion. Note that, to define f (n + 1), we may use n (and f n), as was the case in the definition of fact (n + 1) := (n + 1) * fact n.
The point here is that recursion is not limited to Nat: every inductive type has an associated recursion principle. For the product, it implies in particular that, in order to define a function f : X × Y → Z, it suffices to define it on the canonical terms Prod.mk x y. In practice, this is done via pattern matching.
def proj₁ {X Y : Type} : X × Y → X
fun t ↦ match t with
| Prod.mk x y => x
The sum
The inductive type
To eliminate terms of type
inductive Sum (X Y : Type) : Type
| inl : X → Sum X Y
| inr : Y → Sum X Y
def characteristic_function_of_second_summand {X Y : Type} : X ⊕ Y → Bool
| Sum.inl x => (false : Bool)
| Sum.inr y => (true : Bool)
Here is the practice file on Lean's syntax again:
I am happy to answer any questions you may have 😊. Thank you for your attention!
As a matter of fact, the type of booleans is also an inductive type, with two constructors.