1. Введение в Coq: функциональное программирование на языке Gallina и базовые типы
Введение в Coq: функциональное программирование на языке Gallina и базовые типы
Добро пожаловать в курс «Основы формальной верификации в системе Coq». Это первая статья, в которой мы заложим фундамент для всего дальнейшего обучения. Мы привыкли, что программы пишутся, тестируются и отправляются в продакшн. Но что, если цена ошибки слишком высока? В авиации, медицине, криптографии или финансовых системах обычного тестирования недостаточно. Здесь на сцену выходит формальная верификация — математическое доказательство того, что программа работает в строгом соответствии со своей спецификацией.
Инструмент, который мы будем изучать — Coq. Это интерактивная система доказательства теорем, которая позволяет не только формулировать математические утверждения и доказывать их, но и писать исполняемые программы. В основе Coq лежит язык функционального программирования Gallina.
Архитектура системы Coq
Прежде чем писать код, важно понять, с чем мы имеем дело. Coq — это не просто компилятор, это среда, объединяющая три сущности:
Check, Compute, Print).Сегодня мы сосредоточимся исключительно на Gallina. Если вы знакомы с Haskell, OCaml или F#, многое покажется вам знакомым. Если нет — не переживайте, мы начнем с основ.
Объявление типов и значений
В Coq, как и в любом типизированном языке, каждое выражение имеет тип. Чтобы проверить тип выражения, используется команда Check.
Здесь nat обозначает натуральные числа, а bool — логический тип. Обратите внимание: каждая команда в Coq должна заканчиваться точкой.
Чтобы дать имя значению, мы используем ключевое слово Definition:
Это создает глобальное определение answer с типом nat (натуральное число) и значением 42.
Пользовательские типы данных
Мощь Gallina заключается в возможности создавать собственные типы. Самый базовый способ — это индуктивные типы (Inductive types). Давайте определим тип, представляющий дни недели.
Разберем этот код:
* Inductive — ключевое слово для объявления нового типа.
* day — имя нашего нового типа.
* Type — это «тип типов» (сорт), указывающий, что day является типом данных.
* monday, tuesday и так далее — это конструкторы типа day.
Теперь мы можем написать функцию, которая работает с этим типом. В функциональном программировании основным инструментом управления потоком является сопоставление с образцом (pattern matching).
Функции и сопоставление с образцом
Напишем функцию, которая определяет следующий рабочий день.
Здесь:
* Definition объявляет функцию next_weekday.
* (d : day) — аргумент функции d типа day.
* : day после скобки указывает тип возвращаемого значения.
* Конструкция match d with ... end проверяет значение d и возвращает соответствующий результат.
Мы можем проверить работу функции с помощью команды Compute:
Булев тип (Booleans)
В стандартной библиотеке Coq булев тип определен точно так же, как мы определили дни недели — через перечисление конструкторов:
Функции над булевыми значениями также определяются через сопоставление с образцом. Например, логическое отрицание:
И логическое «И» (конъюнкция):
Обратите внимание на лаконичность andb. Если b1 истинно, то результат зависит только от b2. Если b1 ложно, то результат всегда false, независимо от b2.
Натуральные числа: взгляд изнутри
Самый интересный момент для новичков в Coq — это представление чисел. В обычных языках программирования числа — это примитивы процессора (32 или 64 бита). В Coq натуральные числа (nat) определены математически, используя аксиоматику Пеано.
Определение выглядит так:
Это рекурсивное определение:
O (заглавная буква O) — это конструктор, представляющий ноль.S — это конструктор, который принимает одно натуральное число и возвращает «следующее» за ним (Successor).!Визуализация структуры натуральных чисел Пеано, где каждое число является оболочкой над предыдущим.
Таким образом:
* представляется как O.
* представляется как S O.
* представляется как S (S O).
* представляется как S (S (S O)).
Математически это записывается так:
Где — множество натуральных чисел, — начальный элемент, а — функция следования.
Рекурсивные функции
Поскольку числа определены рекурсивно, функции для работы с ними тоже должны быть рекурсивными. В Gallina для объявления рекурсивной функции используется ключевое слово Fixpoint вместо Definition.
Давайте определим функцию проверки на четность evenb:
Логика работы:
O (ноль), то оно четное (true).S O (единица), то оно нечетное (false).S (S n') (то есть ), то его четность совпадает с четностью . Мы рекурсивно вызываем evenb для .Здесь — это переменная, которая связывается с «пред-предыдущим» числом.
Сложение чисел
Определение сложения также базируется на структуре чисел Пеано. Математически сложение можно определить так:
Где первая строка — это базовый случай (если первое слагаемое ноль), а вторая — рекурсивный шаг (мы выносим за скобки).
В коде Gallina это выглядит следующим образом:
Когда Coq вычисляет plus (S (S O)) (S O) (то есть ), он делает следующие шаги редукции:
plus (S (S O)) (S O)S (plus (S O) (S O)) (сработала вторая ветка match)S (S (plus O (S O))) (снова вторая ветка)S (S (S O)) (сработала первая ветка, так как первый аргумент стал O)Результат — S (S (S O)), что соответствует числу 3.
Строгая типизация и завершимость
Важно отметить одну особенность Gallina: все функции должны завершаться. В Coq нельзя написать бесконечный цикл while(true). Это необходимо для того, чтобы логика доказательств оставалась непротиворечивой.
Когда вы используете Fixpoint, Coq автоматически проверяет, что рекурсия «убывает». В примере с plus аргумент n уменьшается на каждом шаге (от S n' к n'), поэтому Coq разрешает такое определение. Если бы Coq не смог убедиться в завершении функции, он бы выдал ошибку компиляции.
Заключение
Мы познакомились с языком Gallina, научились определять простые типы данных, использовать сопоставление с образцом и писать рекурсивные функции для натуральных чисел. Это — алфавит, на котором мы будем писать не только программы, но и доказательства их корректности.
В следующей статье мы перейдем от простого программирования к логике и узнаем, как формулировать утверждения и доказывать их, используя тактики.