Организация проекта и экосистема dune

Собираем знания о модулях в практику: как структурировать настоящий проект с библиотеками, интерфейсами и зависимостями.

Файл .ml — реализация модуля, парный файл .mli — его публичный интерфейс; dune связывает их в библиотеки и исполняемые файлы.

Теперь, когда модули понятны, посмотрим, как из них складывается реальный проект. На практике вы редко пишете module ... = struct вручную — вместо этого раскладываете код по файлам, а интерфейсы выносите в .mli.

Файлы .ml и .mli

Файл geometry.ml становится модулем Geometry. Если рядом положить geometry.mli, он сыграет роль сигнатуры.

(* geometry.mli — публичный интерфейс *)
type shape
val circle : float -> shape
val area : shape -> float
(* geometry.ml — реализация *)
type shape = Circle of float | Rect of float * float
let circle r = Circle r
let area = function
  | Circle r -> 3.14159 *. r *. r
  | Rect (w, h) -> w *. h
(* конструкторы скрыты: тип shape абстрактен *)

Пользователь создаёт фигуры только через circle, а внутреннее устройство shape можно менять, не ломая чужой код.

Структура проекта

myproject/
  dune-project
  lib/
    dune
    geometry.ml
    geometry.mli
  bin/
    dune
    main.ml
  test/
    dune
    test_geometry.ml

Принято разделять: lib/ — переиспользуемая библиотека, bin/ — точка входа, test/ — тесты.

Файлы dune

# lib/dune
(library
 (name geometry))
# bin/dune
(executable
 (name main)
 (libraries geometry str))

В main.ml модуль из библиотеки доступен по имени: Geometry.area (Geometry.circle 2.0).

Зависимости через opam

ПакетНазначение
Base / Coreальтернативная стандартная библиотека (Jane Street)
Lwt / Asyncасинхронность и конкурентность
Dreamвеб-фреймворк
Alcotest / QCheckтестирование

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

dune анализирует обращения к модулям, строит граф зависимостей между файлами и компилирует их в правильном порядке, кешируя результаты. Когда есть .mli, dune сперва компилирует интерфейс, затем проверяет реализацию против него — поэтому несоответствие сигнатуре всплывёт при сборке. Команда dune build @runtest прогоняет тесты, dune fmt форматирует код, dune utop lib запускает REPL с загруженной библиотекой. Вся инфраструктура (opam + dune + LSP) делает разработку на OCaml комфортной.

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

  • Забыть добавить библиотеку в (libraries ...). Тогда модуль «не найден» при сборке.
  • Несоответствие .ml и .mli. Если реализация не предоставляет что-то из интерфейса — ошибка компиляции.
  • Класть всё в один гигантский файл. Разбивайте на модули по ответственности.

Итоги

  • .ml — реализация модуля, парный .mli — публичный интерфейс с возможной абстракцией типов.
  • dune описывает библиотеки, исполняемые файлы и тесты; зависимости — в (libraries ...).
  • Экосистема (opam, Base/Core, Lwt, Dream, Alcotest) покрывает асинхронность, веб и тестирование.
Проверьте себя
1. Какую роль играет файл geometry.mli рядом с geometry.ml?
AЭто копия реализации
BПубличный интерфейс (сигнатура), ограничивающий, что видно снаружи
CФайл тестов
DКонфигурация dune
2. Где в файле dune перечисляются зависимости исполняемого файла?
AВ dune-project
BВ поле (libraries ...)
CВ .mli
DВ opam-файле автоматически
3. Что произойдёт, если реализация .ml не предоставит значение, объявленное в .mli?
AНичего, оно станет приватным
BОшибка компиляции: реализация не соответствует интерфейсу
CЗначение создастся автоматически
DПредупреждение в рантайме