Модули и сигнатуры
Знакомимся с модульной системой OCaml — одной из мощнейших в индустрии — и её основой: модулями и сигнатурами.
Модуль группирует типы и значения в единое пространство имён; сигнатура задаёт его публичный интерфейс, скрывая детали реализации.
Модульная система — то, чем OCaml особенно гордится. Если в большинстве языков модуль — это просто файл с функциями, то в OCaml модули сами по себе — мощный язык: их можно параметризовать, ограничивать интерфейсами и комбинировать.
Модуль как пространство имён
Каждый файл foo.ml автоматически становится модулем Foo. Можно определять модули и явно:
module Stack = struct
type 'a t = 'a list
let empty = []
let push x s = x :: s
let pop = function
| [] -> None
| x :: rest -> Some (x, rest)
end
let s = Stack.push 1 (Stack.push 2 Stack.empty)
Снаружи к определениям обращаются через точку: Stack.push. Принято называть основной тип модуля просто t (тогда снаружи он читается как Stack.t).
Сигнатура: интерфейс модуля
module type STACK = sig
type 'a t
val empty : 'a t
val push : 'a -> 'a t -> 'a t
val pop : 'a t -> ('a * 'a t) option
end
Здесь type 'a t объявлен абстрактно — без правой части. Снаружи никто не знает, что Stack.t — это список. Можно поменять реализацию на массив или дерево, и код-пользователь не сломается.
Инкапсуляция через ограничение
module Stack : STACK = struct
type 'a t = 'a list
let empty = []
let push x s = x :: s
let pop = function [] -> None | x :: r -> Some (x, r)
end
(* Теперь это ОШИБКА — тип t абстрактен снаружи: *)
(* let bad = List.length (Stack.push 1 Stack.empty) *)
Поскольку t абстрактен, нельзя обращаться со стеком как со списком — только через push/pop. Это инкапсуляция, гарантированная типами.
Как работает под капотом
Абстрактные типы существуют только на этапе компиляции. В рантайме Stack.t — это всё тот же список без обёртки, поэтому абстракция бесплатна. Сигнатура — это контракт, который проверяет тайпчекер: он сопоставляет реализацию с интерфейсом и стирает всё, что не входит в сигнатуру, из видимости. Файлы .mli играют ту же роль: stack.mli задаёт публичный интерфейс для stack.ml.
Частые ошибки
- Раскрывать тип
tв сигнатуре без нужды. Если написатьtype 'a t = 'a listвsig, инкапсуляция теряется. - Путать
struct/endиsig/end.struct— реализация,sig— интерфейс. - Дублировать сигнатуру и в
.mli, и инлайн. Обычно достаточно одного места.
Итоги
- Модуль (
struct ... endили файл) группирует типы и значения в пространство имён. - Сигнатура (
sig ... endили.mli) задаёт публичный интерфейс. - Абстрактный тип
type tбез определения скрывает реализацию без накладных расходов.