У цьому прикладі, ми побудуємо сертифікований перевіряльник типів для простої мови виразів яка має лише 2 типа натуральні числа і логічні значення, і лише дві операції додавання і логічне І.

Зауваження: цей приклад базується на прикладі із книги Certified Programming with Dependent Types Адама Чіліпала і прикладу із репозіторію Lean4.

Створимо індуктивний тип який описує 4 типа виразів - літерал цілого числа, літерал логічного типа, операція додавання та операція логічного І.

inductive Expr where
  | nat  : Nat  Expr
  | plus : Expr  Expr  Expr
  | bool : Bool  Expr
  | and  : Expr  Expr  Expr

Ми визначаємо просту мову типів використовуючи індуктивний тип даних Ty. Тип цілого числа буде визначено як випадок nat, тип логічних значень буде визначений як bool. Правила типізації, будуть визначені використовуючи індуктивний предікат HasType. Цей тип визначає стверждення про наявність типа у вираза. У нас будуть 4 типа правил - правила типізації цілого числа, правила типізації логічних типів, правила типізації додавання і правила типізації логічного І. Таким чином ми маємо по одному правилу на кожен тип виразу із Expr.

inductive Ty where
  | nat
  | bool
  deriving DecidableEq

inductive HasType : Expr  Ty  Prop
  | nat  : HasType (.nat v) .nat
  | plus : HasType a .nat  HasType b .nat  HasType (.plus a b) .nat
  | bool : HasType (.bool v) .bool
  | and  : HasType a .bool  HasType b .bool  HasType (.and a b) .bool

Ми легко можемо показати що якщо e має тип t₁ і тип t₂, тоді t₁ і t₂ повинні бути однакові використовуючи тактику cases. Ця тактика створює нову підціль для кожного конструктора, і автоматично відкидає недоступні випадки. Таким чином cases h₁ створить 4 підцілі, по одній на кожне правило типізації. Комбінатор тактик tac₁ <;> tac₂ застосовує tac₂ до кожної підцілі створеною tac₁. Післі cases h₁ <;> cases h₂ у нас залишаться лише 4 підцілі а не 16, тому що правила типізації для HasType e _ у h₁ і тип h₂ повинні співпадати, через те що e це той самий вираз і у обоїх випадках. Потім, тактика rfl використовується для закриття усіх створених цілей використовуючи рефлексівність.

theorem HasType.det (h : HasType e t) (h : HasType e t) : t = t := by
  cases h <;> cases h <;> rfl

Індуктивний тип Maybe p має два конструктора: found a h та unknown. Перший конструктор містить елемент a : α і доказ що a задовольняє предікат p. Конструктор unknown використовується для кодування “помилки”. У нашому прикладі, це буде помилка типізації.

inductive Maybe (p : α  Prop) where
  | found : (a : α)  p a  Maybe p
  | unknown

Ми визначаємо нотацію для Maybe яка схожа до вбудованої нотації для вбудованого типу Lean Subtype.

notation " x " => Maybe (fun x => p)

Створимо функцію Expr.typeCheck e яка повертає тип ty і доказ того що e має тип ty, або unknown. Зауважте що, def Expr.typeCheck ... у Lean це нотація до namespace Expr def typeCheck ... end Expr. Терм .found .nat .nat це сахар до Maybe.found Ty.nat HasType.nat. Lean може вивести використовуємий простір імен для очікуваних типів. Нотація e.typeCheck це також сахар для виклику Expr.typeCheck e.

def Expr.typeCheck (e : Expr) :  :=
  match e with
  | nat ..   => .found .nat .nat
  | bool ..  => .found .bool .bool
  | plus a b =>
    match a.typeCheck, b.typeCheck with
    | .found .nat h, .found .nat h => .found .nat (.plus h h)
    | _, _ => .unknown
  | and a b =>
    match a.typeCheck, b.typeCheck with
    | .found .bool h, .found .bool h => .found .bool (.and h h)
    | _, _ => .unknown

theorem Expr.typeCheck_correct (h : HasType e ty) (h : e.typeCheck  .unknown)
        : e.typeCheck = .found ty h := by
  revert h
  cases typeCheck e with
  | found ty' h' =>
    intro;
    have := HasType.det h h';
    subst this;
    rfl
  | unknown =>
    intro;
    contradiction

Тепер ми докажемо що якщо Expr.typeCheck e повертає Maybe.unknown, тоді для усіх ty, HasType e ty не виконується. Lean може це вивести тому що ми явно сказали що e має тип Expr. Доказ буде по індукції по e і аналізу випадків. Ми кажемо що змінна недоступна якщо вона введена тактикою (наприклад, cases) або була затінена іншою змінною variable введеною користувачем. Зверніть увагу що тактика simp [typeCheck] застосовується для усіх цілей згенерованих тактикою induction, і закриває випадки відповідні до конструкторів Expr.nat та Expr.bool. Тактика split розбиває вираз match у цілі на нові підцілі, і ми доводимо їх ітеруя одну за одною за допомогою ‘next`.

theorem Expr.typeCheck_complete {e : Expr} : e.typeCheck = .unknown  ¬ HasType e ty := by
  induction e with simp [typeCheck]
  | plus a b iha ihb =>
    split
    next => intro; contradiction
    next ra rb hnp =>
      -- Зверніть увагу що `hnp` це гіпотеза згенерована тактикою `split`
      -- яка стверждує що попередній випадок не був використаний
      intro h ht
      cases ht with
      | plus h h =>
        exact hnp h h (typeCheck_correct h (iha · h)) (typeCheck_correct h (ihb · h))
  | and a b iha ihb =>
    split
    next => intro; contradiction
    next ra rb hnp =>
      intro h ht
      cases ht with
      | and h h =>
        exact hnp h h (typeCheck_correct h (iha · h)) (typeCheck_correct h (ihb · h))

Нарешті, ми показуємо що перевірка типа для e може бути вирішена використовуючи Expr.typeCheck. Для цього ми робимо показуємо екземпляр типу Decidable (HasType e t) і за допомогою раніше доведених функцій Expr.typeCheck_complete та HasType.det показуємо як доводити вирішеність.

instance (e : Expr) (t : Ty) : Decidable (HasType e t) :=
  match h' : e.typeCheck with
  | .found t' ht' =>
    if heq : t = t' then
      isTrue (heq  ht')
    else
      isFalse fun ht => heq (HasType.det ht ht')
  | .unknown => isFalse (Expr.typeCheck_complete h')

Файл який можна завантажити на Гісті.