1. Введение в Coq: функциональное программирование на языке Gallina и базовые типы
Введение в Coq: функциональное программирование на языке Gallina и базовые типы
Добро пожаловать в курс «Основы формальной верификации в Coq». Это первая статья, в которой мы познакомимся с инструментом Coq, его внутренним языком Gallina и начнем писать первые функциональные программы. Если вы привыкли к императивным языкам вроде Python или C++, подход Coq может показаться вам необычным, но именно он лежит в основе создания математически надежного программного обеспечения.
Что такое Coq и зачем он нужен?
Coq — это интерактивная система доказательства теорем (proof assistant). В отличие от обычных компиляторов, которые просто превращают код в инструкции для процессора, Coq позволяет вам формулировать математические утверждения и писать формальные доказательства того, что эти утверждения верны.
В основе Coq лежит язык Gallina. На этом языке мы делаем две вещи:
Это возможно благодаря мощной теоретической базе, называемой исчислением конструкций (Calculus of Constructions). В этой системе «программы» и «доказательства» — это, по сути, одно и то же. Но пока мы не будем углубляться в теорию типов, а сосредоточимся на программировании.
Первые шаги: Определение типов данных
В функциональном программировании мы начинаем с описания данных. В Coq создание нового типа данных осуществляется с помощью ключевого слова Inductive. Давайте определим простой тип, представляющий дни недели.
Разберем этот код:
* Inductive означает, что мы определяем индуктивный тип данных.
* day — это имя нашего нового типа.
* : Type указывает, что day сам по себе является типом.
* После := мы перечисляем конструкторы (monday, tuesday и т.д.). Это единственные возможные значения для типа day.
Теперь, когда у нас есть тип, мы можем написать функцию, которая работает с ним. Например, функцию, вычисляющую следующий рабочий день.
Функции и сопоставление с образцом
Для определения функций в Coq используется ключевое слово Definition. Язык Gallina является строго типизированным, поэтому мы должны указывать типы аргументов и возвращаемого значения.
Здесь мы видим конструкцию match ... with ... end. Это сопоставление с образцом (pattern matching). Это аналог switch или case в других языках, но гораздо более мощный. Coq строго следит за тем, чтобы вы рассмотрели все возможные варианты значений d. Если вы забудете, например, sunday, Coq выдаст ошибку.
Проверка работы кода
В Coq есть команды для взаимодействия с окружением. Они называются «Vernacular commands» и часто начинаются с заглавной буквы. Три самые важные:
Check — показывает тип выражения.Compute — вычисляет значение выражения.Print — показывает определение функции или типа.Пример использования:
Булев тип (Booleans)
В стандартной библиотеке Coq булев тип определен точно так же, как мы определили дни недели — через перечисление:
Функции над булевыми значениями также определяются через сопоставление с образцом. Напишем функцию отрицания negb (логическое НЕ):
И функцию конъюнкции andb (логическое И):
Обратите внимание на лаконичность andb. Если b1 ложно, нам не важно, чему равно b2 — результат все равно будет false. Если b1 истинно, то результат полностью зависит от b2.
Натуральные числа: арифметика Пеано
Самый интересный базовый тип в Coq — это натуральные числа (nat). В обычных языках программирования числа — это 32 или 64 бита в памяти процессора. В Coq числа — это математическая абстракция, основанная на аксиомах Пеано.
Определение выглядит так:
Это рекурсивное определение:
O (заглавная буква O, не ноль) — это конструктор, представляющий ноль.S — это конструктор, который принимает натуральное число и возвращает «следующее» за ним число (Successor).Математически это записывается так:
Где — это базовый элемент, обозначающий ноль.
Где — это функция следования, примененная к нулю, что дает единицу.
Где — это функция следования, примененная к единице, что дает двойку.
И так далее для любого числа :
Где — это число, следующее за .
!Визуализация структуры натуральных чисел в Coq как вложенных конструкторов.
Такое представление кажется неэффективным для вычислений (и это правда для больших чисел), но оно идеально подходит для доказательств по индукции, чем мы займемся в следующих статьях.
Рекурсивные функции
Поскольку числа определены рекурсивно, функции для работы с ними тоже должны быть рекурсивными. В Coq для определения рекурсивной функции используется ключевое слово Fixpoint вместо Definition.
Давайте определим функцию проверки на четность evenb:
Разбор логики:
* Если число — это O (0), то оно четное (true).
* Если число — это S O (1), то оно нечетное (false).
* Если число имеет вид S (S n') (то есть ), то его четность совпадает с четностью . Мы рекурсивно вызываем evenb для .
Сложение чисел
Теперь определим сложение. Вспомним математическое определение сложения через инкремент:
Где — любое число, а прибавление не меняет его значение.
Где — следующее число за , а означает, что мы переносим операцию инкремента наружу суммы.
В Coq мы обычно делаем рекурсию по первому аргументу:
Как это работает для plus 2 3 (то есть plus (S (S O)) (S (S (S O))))?
n не O, это S (S O). n' становится S O. Результат: S (plus (S O) 3).n теперь S O. n' становится O. Результат: S (S (plus O 3)).n теперь O. Срабатывает первая ветка match. Возвращаем m (то есть 3).S (S 3) -> S (S (S (S (S O)))) -> 5.Важное правило завершения (Termination)
Coq требует, чтобы все функции завершались. Вы не можете написать бесконечный цикл while(true). Это необходимо для логической непротиворечивости системы.
При использовании Fixpoint Coq автоматически проверяет, что рекурсивный вызов происходит над «меньшим» аргументом (structural recursion). В примере с plus, на каждом шаге n уменьшается (от S n' к n'), поэтому Coq гарантирует, что вычисление когда-нибудь достигнет O и остановится.
Заключение
Мы познакомились с основами языка Gallina:
* Определили перечислимые типы (day, bool).
* Узнали, как устроены натуральные числа Пеано (nat).
* Научились писать простые (Definition) и рекурсивные (Fixpoint) функции.
* Использовали сопоставление с образцом (match).
Это фундамент, на котором строится вся формальная верификация. В следующей статье мы перейдем от написания программ к написанию доказательств их свойств, используя тактики Coq.