Зависимые типы звучат как академическая экзотика, но в Idris 2 они решают прикладную задачу: не дать программе выйти за границы массива, перепутать состояния протокола или собрать конфигурацию с несовместимыми полями. Компилятор проверяет не только типы значений, но и их связь с длиной, индексом и структурой данных. Ниже — практический маршрут: установка, векторы фиксированной длины, Fin, доказательства, сборка проекта и честные ограничения подхода.

Что дают зависимые типы

В обычном языке head возвращает значение или бросает исключение. В Idris тип формулируют так, что пустой список станет ошибкой компиляции. Vect n a содержит длину на уровне типа, а Fin n — число, заведомо меньшее n. Вместе они превращают проверки в доказательства, которые нельзя забыть.

Выгода не в математике ради математики, а в переносе части тестов в компилятор. Парсер CSV, возвращающий строку ровно из n колонок; протокол, где нельзя отправить данные до рукопожатия; доступ к буферу без проверки индекса — всё это описывается типами. Цена — сложные сигнатуры и время на доказательства.

Установка

Проще всего поставить Idris 2 через pack: скрипт приносит компилятор в домашний каталог и не трогает системные пакеты. Понадобятся Chez Scheme, Git и компилятор C. Пакеты и LSP ставятся отдельно.

bash -c "$(curl -fsSL https://raw.githubusercontent.com/stefan-hoeck/idris2-pack/main/install.bash)"
pack update
pack install-app idris2-lsp
idris2 --version

После установки проверьте, что pack виден в новом shell. Если вы предпочитаете системный пакет, в некоторых дистрибутивах есть готовые пакеты idris2, но версия может отставать. Для редактора поставьте плагин, который умеет запускать idris2-lsp: без него подсказки типов и интерактивные дыры становятся неудобными.

Векторы фиксированной длины

Начнём с функции, которая берёт первый элемент и не падает на пустом векторе. Компилятор видит, что образец x :: _ покрывает вектор длины хотя бы один.

import Data.Vect

safeHead : Vect (S n) a -> a
safeHead (x :: _) = x

safeTail : Vect (S n) a -> Vect n a
safeTail (_ :: xs) = xs

Сигнатура Vect (S n) a означает «вектор, длина которого равна следующему за n», то есть минимум один. Вызвать safeHead [] нельзя: типы не сойдутся. Функция safeTail возвращает вектор длины n. Обе функции тотальны: конструкторы вектора покрывают все случаи.

Длина может быть результатом вычисления. replicate принимает n и значение, а возвращает Vect n a. Складывать можно только векторы одинаковой длины, а конкатенация возвращает сумму длин. За каждой операцией стоит индуктивное определение.

Fin и безопасная индексация

Fin n — тип чисел от нуля до n-1. Он строится конструкторами FZ (ноль) и FS (следующий). Индекс нельзя создать из воздуха: значение типа Fin 3 равно 0, 1 или 2.

import Data.Fin
import Data.Vect

safeIndex : (i : Fin n) -> Vect n a -> a
safeIndex FZ (x :: _) = x
safeIndex (FS k) (_ :: xs) = safeIndex k xs

third : Vect 3 String -> String
third v = safeIndex 2 v

Литерал 2 приводится к Fin 3 автоматически. Если написать safeIndex 3 v, программа не соберётся: тройка не помещается в Fin 3. В императивном коде это было бы исключением в рантайме или чтение чужой памяти, а здесь ошибка видна до запуска.

Тип Fin удобен для разбора бинарных форматов и таблиц переходов. Состояние описывают как индекс, а таблицу переходов — как вектор той же длины. Добавили состояние — компилятор потребует обновить таблицу.

Доказательства и тотальность

Простые равенства доказываются конструктивно. Чтобы убедить компилятор в n + 0 = n, достаточно индукции: база — Refl, шаг — cong S к индукционному предположению.

%default total

plusZero : (n : Nat) -> n + 0 = n
plusZero Z = Refl
plusZero (S k) = cong S (plusZero k)

%default total требует, чтобы функции завершались и покрывали все случаи. Это дисциплина, которая ловит забытые ветки. Иногда компилятор не может доказать завершение — тогда помогает assert_total, но это осознанное обещание, а не способ заткнуть ошибку. Ещё один инструмент — rewrite: он заменяет часть типа по доказанному равенству.

Для конфигурации удобен тип-индекс: data Setting : String -> Type, где Setting "port" хранит число, а Setting "host" — строку. Разбор возвращает Maybe (Setting key), и вызывающий код не перепутает поля. Это заменяет десяток проверок вроде «а точно ли здесь число».

Проект и сборка

Проект описывается файлом ipkg. В нём перечислены модули, зависимости и главный файл. Сборка через pack подтягивает зависимости и кладёт исполняемый файл в build/exec.

package configcheck
version = 0.1.0
authors = "Team"
main = Main
depends = base
modules = Config, Main

pack build configcheck.ipkg
./build/exec/configcheck

Для быстрой проверки одного файла хватает idris2 Main.idr -o app. Команда pack repl открывает REPL с зависимостями проекта, а pack run test.ipkg запускает тестовый модуль. Держите доказательства рядом с кодом: если тип изменился, компилятор укажет все места, где доказательство перестало работать.

FFI и бэкенды

Idris 2 по умолчанию компилирует через Chez Scheme, но есть бэкенды Racket, Node.js и C. Для вызова C используется директива %foreign; возвращаемое значение оборачивают в PrimIO, чтобы сохранить чистоту.

%foreign "C:puts,libc"
puts : String -> PrimIO Int

main : IO ()
main = do
  _ <- primIO (puts "hello from Idris")
  pure ()

Для C-бэкенда сборка выглядит как idris2 --cg refc -o app Main.idr. Проверяйте, какой бэкенд поддерживает нужные библиотеки: экосистема Idris 2 меньше, чем у Python или Go, и не каждая задача имеет готовую обёртку. FFI стоит изолировать в отдельном модуле, чтобы основная логика оставалась чистой и проверяемой.

Когда не стоит

Idris 2 не заменит Python для скриптов и не подходит команде без готовности разбираться с доказательствами:

  • компиляция медленнее;
  • вакансий мало;
  • библиотек немного.

Но там, где ошибка стоит дорого — разбор протоколов, расчёты с инвариантами, конфигурация сервисов — зависимые типы окупаются. Разумный сценарий: небольшой проверенный модуль рядом с основным сервисом, а не переписывание системы.

Итог

Idris 2 превращает часть проверок в типы: Vect не даёт выйти за длину, Fin гарантирует допустимый индекс, а тотальные функции исключают необработанные ветки. Начните с установки через pack, напишите безопасные safeHead и safeIndex, затем добавьте доказательство вроде plusZero. Проект собирается через ipkg и pack, а FFI позволяет обращаться к C. Главное — не гнаться за тотальностью всего кода, а выбрать участки, где цена ошибки выше цены доказательства.

Насколько публикация полезна?

Нажмите на звезду, чтобы оценить!

Средняя оценка 0 / 5. Количество оценок: 0

Оценок пока нет. Поставьте оценку первым.

Поделиться: Telegram ВКонтакте