Пропозиции как типы на практике: Coq и Lean

От теории к инструментам: что значит «доказать на компьютере» и где это уже применяется всерьёз.

Пруверы на зависимых типах (Coq, Agda, Lean) — системы, где теорема — это тип, доказательство — терм этого типа, а доверять нужно лишь маленькому проверяющему ядру.

Как устроен прувер

В Coq/Lean утверждение записывается как тип, а доказательство строится как терм. Писать терм-доказательство руками тяжело, поэтому используют тактики — команды, пошагово конструирующие доказательство («примени это правило», «разбери случаи», «индукция по n»). Но ключевой принцип — малое доверенное ядро (de Bruijn criterion): какими бы умными ни были тактики, итоговый терм проверяет крошечный, многократно проаудированный type checker. Ошибка в тактике не пропустит ложную теорему — её отвергнет ядро.

Theorem and_comm : forall A B : Prop, A /\ B -> B /\ A.
Proof.
  intros A B H.        (* ввели гипотезы *)
  destruct H as [a b]. (* разобрали конъюнкцию *)
  split.               (* цель B /\ A -- две подцели *)
  - exact b.           (* B *)
  - exact a.           (* A *)
Qed.                   (* ядро проверило терм-доказательство *)

Этот скрипт строит ровно ту функцию-перестановку пары, что мы писали на Python в уроке про Карри — Говарда. Тактики — лишь удобный способ собрать терм; суть та же.

Что реально доказано

Это не упражнения. На пруверах получены результаты промышленного масштаба.

ПроектЧто доказаноИнструмент
CompCertкорректность оптимизирующего компилятора CCoq
seL4корректность микроядра ОС относительно спецификацииIsabelle/HOL
Four Color Theoremмашинная проверка теоремы о четырёх краскахCoq
mathlib (Lean)огромная библиотека формализованной математикиLean

Демонстрация: «доказываем» индукцией на Python

Coq доказывает свойства всех натуральных чисел индукцией — конечным термом, а не перебором. Покажем структуру индуктивного доказательства «сумма 0..n = n(n+1)/2»: база плюс шаг. На Python мы лишь проверяем на примерах, но структура (база/шаг) — ровно та, что формализует прувер.

# свойство P(n): 0+1+...+n == n*(n+1)//2
def lhs(n): return sum(range(n + 1))
def rhs(n): return n * (n + 1) // 2

# база P(0) и шаг: если P(n), то P(n+1)
def base_holds():
    return lhs(0) == rhs(0)
def step_holds(n):
    # P(n+1) сводится к P(n) + (n+1)
    return (rhs(n) + (n + 1)) == rhs(n + 1)

print("база P(0)            :", base_holds())
print("шаг для n=0..4 верен :", all(step_holds(n) for n in range(5)))
print("проверка P(n) n=0..6 :", all(lhs(n) == rhs(n) for n in range(7)))
print("в Coq база+шаг => P(n) для ВСЕХ n (одним термом-доказательством)")

Вывод:

база P(0)            : True
шаг для n=0..4 верен : True
проверка P(n) n=0..6 : True
в Coq база+шаг => P(n) для ВСЕХ n (одним термом-доказательством)

Разница принципиальна: наш Python проверил конечное число n; индуктивное доказательство в Coq покрывает все натуральные числа сразу — это сила типов, а не тестов.

Как работает под капотом

Индукция в пруверах — это рекурсивная функция-доказательство по структуре индуктивного типа Nat = Zero | Succ Nat: база обрабатывает Zero, шаг — Succ n, используя доказательство для n. Чтобы такие рекурсивные доказательства были надёжны, прувер требует завершаемости (структурной убывающей рекурсии) — иначе незавершающийся «доказатель» доказал бы ложь. Это прямое следствие связи завершаемость = состоятельность логики.

Частые ошибки

  • Считать тактики «доказательством». Доказательство — это терм; тактики лишь собирают его, а проверяет ядро.
  • Доверять всему коду прувера. Доверять нужно только ядру; библиотеки тактик могут быть сколь угодно сложны.
  • Путать «доказано» и «протестировано». Индукция покрывает все случаи; тест — конечное число.

Итоги

  • В Coq/Lean теорема — тип, доказательство — терм; доверяем лишь малому ядру.
  • Тактики пошагово собирают терм-доказательство, ядро его перепроверяет.
  • Реальные результаты: CompCert, seL4, теорема о четырёх красках, mathlib.
  • Индукция = рекурсивное доказательство с обязательной завершаемостью.
Проверьте себя
1. Чему доверяет пользователь в пруверах вроде Coq?
AТактикам
BМалому проверяющему ядру (type checker), а не тактикам
CТестам
DДокументации
2. Что является доказательством теоремы в Coq?
AСкрипт тактик
BТерм соответствующего типа
CКомментарий
DТест
3. Почему пруверы требуют завершаемости рекурсивных доказательств?
AДля скорости
BНезавершающийся «доказатель» доказал бы ложь, разрушив состоятельность
CДля экономии памяти
DЭто не требуется