Interactive Theorem Proving in Lean

Lean logo QR code link to these slides

18th CICM, Brasilia. 6-10 October 2025.
Florent Schaffhauser, Heidelberg University.

Session 1: Lean's syntax and pattern matching

Organisation of the tutorial

  • Duration: 3h.
  • Each session: 1/3 Presentation, 2/3 Hands-on practice.
  • Practice files are provided.
  • No installation of Lean required.
Session 1: Lean's syntax and pattern matching

Outline

  • Goal: to learn how to use a programming language to represent mathematical objects and proofs.
  • Intended audience: mathematics educators and students who are not familiar with programming.
  • Format: 3h tutorial for complete beginners.
    • Session 1: Lean's syntax and pattern-matching (1h).
    • Session 2: Dependent type theory and applications to mathematics (2h).
  • Your indispensable companion: The Mathlib 4 documentation webpage, hosted on the Leanprover community website. Just google Mathlib GitHub!
Session 1: Lean's syntax and pattern matching

The Lean 4 web server

  • If you would rather dive straight into the practice files, here are the first two 😊 . There will be one more on the last slide!

    Lean syntax Basic tactics

  • The first file is about the Lean syntax (it is very basic, but might be useful if you have little to no experience in programming). The second file is about basic tactics.

  • These files open in the Lean 4 web server, from which you can download the edited files to save your work! Just look for the Save File button in the menu on the top right.

Session 1: Lean's syntax and pattern matching

The Lean syntax

  • Terms:

    def aListOfNat     : List Nat    := [1, 2, 3]
    def aListOfStrings : List String := ["Hello, ", "world!"]
    
  • Introduction of functions:

    def successor    : NatNat           := fun n ↦ 1 + n
    def prepend_zero : List NatList Nat := fun L ↦ (0 :: L)
    
  • Evaluation of functions:

    #eval successor 41             -- 42
    #eval prepend_zero aListOfNat  -- [0, 1, 2, 3]
    
Session 1: Lean's syntax and pattern matching

Inductive types

  • You can think of a type as a label attached to a term. Thanks to that label, you can quantify over terms of that type.

    theorem EuclideanDivision : Prop := 
      ∀ (a b : Nat), b ≠ 0 → ∃ (q r : Nat), (a = q * b + r) ∧ (r < b)
    
  • Types can be inductively defined, by specifying special functions called constructors.

    inductive Bool : Type where
    | true  : Bool
    | false : Bool
    
    #check Bool.true  -- Bool.true : Bool
    
Session 1: Lean's syntax and pattern matching

Constructors

  • Constructors for an inductively-defined type can take arguments from that type itself.

    inductive Nat : Type where
    | zero           : Nat
    | succ (n : Nat) : Nat  -- also written succ : Nat → Nat
    
    #check Nat.succ (Nat.succ Nat.zero)  -- Nat.zero.succ.succ : Nat
    
  • Constructors can take more than one argument. The return type is always the type under construction (for instance Nat or List X).

    inductive {X : Type} List X : Type where
    | nil                       : List X
    | cons (x : X) (L : List X) : List X  -- cons : X → List X → List X
    
Session 1: Lean's syntax and pattern matching

Constructing new types from old ones

  • Types are themselves terms and we can define functions on types.

  • Prod X Y can also be denoted by X × Y. It has just one constructor, called Prod.mk. The term Prod.mk x y can also be denoted (x, y).

    inductive Prod (X : Type) (Y : Type) : Type where
    | mk (x : X) (y : Y) : Prod X Y
    
  • Sum X Y can also be denoted by X ⊕ Y. It has two constructors, called Sum.inl and Sum.inr.

    inductive Sum (X : Type) (Y : Type) : Type where
    | inl (x : X) : Sum X Y
    | inr (y : Y) : Sum X Y
    
Session 1: Lean's syntax and pattern matching

Pattern-matching

  • To construct a function out of an inductively-defined type, it suffices to specify the value of the function on the so-called canonical terms (i.e. values of constructors).

  • For instance, we can define negation on Booleans in the following way:

    def Bool.neg : BoolBool
    | .true  => .false
    | .false => .true
    
    #eval Bool.neg Bool.true  -- Bool.false
    
  • Or the projection to the first factor of a product in the following way:

    def pr1 {X Y : Type} : X × YX 
    | (x, y) => x
    
Session 1: Lean's syntax and pattern matching

Recursion

  • Recursive definitions using only the constructors of an inductive type are handled automatically (no termination proof required). This feature is called structural recursion.

  • For instance, the factorial function on natural numbers is defined as follows (in the second case, the function fact calls on itself).

    def fact : NatNat
    | .zero   => .succ .zero
    | .succ n => .succ n * fact n
    
  • Similarly, the length of a list is defined as follows.

    def List.length {X : Type} : List XNat
    | .nil      => .zero
    | .cons a L => .succ (length L)
    
Session 1: Lean's syntax and pattern matching

Equivalent syntax

  • To define functions by pattern-matching, you can also use the match keyword.

    def Bool.neg : BoolBool :=
      fun b ↦ match b with
      | .true  => .false
      | .false => .true
    
    def Nat.fact : NatNat :=
      fun n ↦ match n with
      | .zero   => .zero
      | .succ n => .succ n * fact n
    
  • Exercise. Write a function that, given a list of strings, returns the concatenated string:

    #eval List.concatenate ["Hello, ", "world"]  -- "Hello, world!" (hint: use ++)
    
Session 1: Lean's syntax and pattern matching

Curried functions

  • Functions of two variables can be introduced using product types, as we usually do in mathematics (uncurried notation).

    def addᵤ : Nat × NatNat
    | (n, m) => n + m         -- this works because + is already known :-)
    
    #eval addᵤ (1, 1)  -- 2
    
  • Or as a function that sends a term to a function (curried notation). All brackets below are optional (try it!). Curried functions are in fact a primitive construct (see Prod.mk).

    def add : Nat → (NatNat)
    | n ↦ (fun m ↦ n + m)
    
    #eval (add 1) 1  -- 2
    
Session 1: Lean's syntax and pattern matching

Higher-order functions

  • Functions that take a function as argument are usually called higher-order functions.

    def F : (NatNat) → NatNat :=
      fun (g : NatNat) (n : Nat) => 2 * (g n)
    

    Note that the term g does not have to be previously defined: the first argument of the function F can be passed as an anonymous function (introduced by the keyword fun).

    #eval F (fun (n : Nat) => n + 1) 2  -- 6
    
  • Bracketing: by definition, the arrow type Nat → Nat → Nat → Nat is Nat → (Nat → (Nat → Nat)), which is different from (Nat → Nat) → Nat → Nat above. So the message is: if you want to take a function as an argument, you need to use brackets.

Session 1: Lean's syntax and pattern matching

Propositions-as-types

  • Mathematical propositions are represented as types, which may or may not be inhabited (= have terms). This is often referred to as the Curry-Howard correspondence.

    #check 1 + 1 = 2  -- Prop
    #check 2 + 2 = 5  -- Prop
    
  • The point is that the syntax above can be used to define logical connectives and construct new propositions. For instance, given two propositions P and Q, the implication P ⇒ Q is usually represented by the type of functions P → Q.

  • This follows the Brouwer-Heyting-Kolmogorov interpretation of logical connectives: To prove that P implies Q means to construct a function from P to Q, which in general is stronger than ¬(P ∧ ¬Q) and weaker than ¬P ∨ Q.

Session 1: Lean's syntax and pattern matching

Conjunctions, disjunctions

  • Conjunctions and disjunctions are defined inductively (note the analogy with the product and the sum of types).

  • And P Q can also be denoted by P ∧ Q. It has one constructor, called And.intro.

    inductive {P Q : Prop} And P Q : Prop where
    | intro (p : P) (q : Q) : And P Q  -- intro : P → Q → And P Q 
    
  • Or P Q can also be denoted by P ∨ Q. It has two constructors, called Or.inl and Or.inr.

    inductive {P Q : Prop} Or P Q : Prop where
    | inl (p : Q) : Or P Q  -- inl : P → Or P Q
    | inr (q : Q) : Or P Q  -- inr : Q → Or P Q
    
Session 1: Lean's syntax and pattern matching

Falsity and negation

  • Negation is defined as implying an absurdity. Absurdity is itself defined inductively, as a type with no constructors (yes, this is valid syntax).

    inductive False : Prop where
    
  • Negation is now defined as function on propositions. Not P can also be denoted by ¬P.

    def Not : PropProp :=
      fun P ↦ (PFalse)
    

    ⚠️ With this definition, we can prove P → ¬¬P, but not ¬¬P → P.

  • It is a good exercise to think about what it means to have a proof of False → P.

Session 1: Lean's syntax and pattern matching

Logical equivalences

  • Logical equivalence is denoted by P ↔︎ Q in Lean. It is defined as double implication, either by a formula or directly as an inductive type.

    def Iff₁ : PropProp :=
      fun P Q ↦ (PQ) ∧ (QP)
    
    inductive Iff₂ : Prop where
    | intro (f : PQ) (g : QP) : IffP Q
    
  • A good way to manipulate these concepts is to try and prove De Morgan's laws:

    You will notice that the implication of the second law requires an application of for a proposition .

Session 1: Lean's syntax and pattern matching

How to actually interact with Lean?

  • Lean’s tactic language can assist us in writing a program (= a proof). To enter tactic mode, one simply puts the keyword by after the := sign.
  • This will be reflected in the infoview (to the right), which should display the goal of
    the program. This goal is what appears after the turnstile symbol .
  • You can also see the goal in term mode, using the underscore symbol _
  • Note that the goal of a program is always a type (or a proposition).
  • To close a goal in tactic mode, we need to construct a term of the correct type. To do so, we can use so-called tactics, which range from basic to sophisticated.
  • The elementary tactics are presented in the practice file Basic tactics. Lean users can create their own tactics via meta-programming. This is done in the Lean language itself.
Session 1: Lean's syntax and pattern matching

The modus ponens rule

  • In a typing system such as Lean's, the natural deduction rule known as modus ponens is derived, not postulated. Namely, it is a consequence of the fact that functions can be evaluated.

    theorem mp {P Q : Prop} : (PQ) ∧ PQ :=
      fun t ↦ match t with | .intro f p => f p
    
  • Or, using the intro, rcases and exact tactics (curly brackets are optional):

    theorem mp {P Q : Prop} : (PQ) ∧ PQ :=
      by {                      --                     ⊢ (P → Q) ∧ P → Q
        intro t                 -- (t : (P → Q) ∧ P)   ⊢ Q
        rcases t with ⟨f, p⟩    -- (f : P → Q) (p : P) ⊢ Q
        exact f p               -- No goals
      }
    
Session 1: Lean's syntax and pattern matching

Practice time

Here are two practice files that you can work on for the rest of this session. I am happy to answer any questions you may have 😊 .

  1. On basic tactics.
  2. On logic.

QR code link to the Basic Tactics file QR code link to the Logic file

Session 1: Lean's syntax and pattern matching

**+ link to these slides?**

Languages such as Lean are based on [typed $\lambda$-calculus](https://en.wikipedia.org/wiki/Typed_lambda_calculus), which has three basic constructs: