Глава 2. Модульная система
В этой главе рассматривается модульная система OCaml.
1 Структуры
Основное назначение модулей — объединять связанные определения (например, определения типа данных и связанных с ним операций) и обеспечивать согласованную систему имён для этих определений. Это позволяет избежать нехватки имён и случайной путаницы. Такой пакет называется структурой и вводится конструкцией struct…end, содержащей произвольную последовательность определений. Структуре обычно присваивают имя с помощью связывания module. Например, следующая структура объединяет тип очередей FIFO и операции над ними:
# module Fifo =
struct
type 'a queue = { front: 'a list; rear: 'a list }
let make front rear =
match front with
| [] -> { front = List.rev rear; rear = [] }
| _ -> { front; rear }
let empty = { front = []; rear = [] }
let is_empty = function { front = []; _ } -> true | _ -> false
let add x q = make q.front (x :: q.rear)
exception Empty
let top = function
| { front = []; _ } -> raise Empty
| { front = x :: _; _ } -> x
let pop = function
| { front = []; _ } -> raise Empty
| { front = _ :: f; rear = r } -> make f r
end;;
module Fifo :
sig
type 'a queue = { front : 'a list; rear : 'a list; }
val make : 'a list -> 'a list -> 'a queue
val empty : 'a queue
val is_empty : 'a queue -> bool
val add : 'a -> 'a queue -> 'a queue
exception Empty
val top : 'a queue -> 'a
val pop : 'a queue -> 'a queue
end Вне структуры к её компонентам можно обращаться с помощью «точечной нотации», то есть указывать идентификаторы с именем структуры в качестве квалификатора. Например, Fifo.add — это функция add, определённая внутри структуры Fifo, а Fifo.queue — тип queue, определённый в Fifo.
# Fifo.add "hello" Fifo.empty;;
- : string Fifo.queue = {Fifo.front = ["hello"]; rear = []} Другой способ — открыть модуль, включив все определённые в нём идентификаторы в область видимости текущей структуры.
# open Fifo;;
# add "hello" empty;;
- : string Fifo.queue = {front = ["hello"]; rear = []} Открытие модуля упрощает доступ к его компонентам, но затрудняет определение того, в каком модуле задан идентификатор. В частности, открытые модули могут затенять идентификаторы, доступные в текущей области видимости, что потенциально приводит к запутанным ошибкам:
# let empty = [] open Fifo;; val empty : 'a list = []
# let x = 1 :: empty ;;
Error: The value empty has type 'a Fifo.queue
but an expression was expected of type int list Частично решить эту проблему можно, открывая модули локально, чтобы их компоненты были доступны только в соответствующем выражении. Это также упрощает чтение кода (поскольку инструкция open находится ближе к месту использования) и его рефакторинг (поскольку фрагмент кода становится более самодостаточным). Для этого предусмотрены две конструкции:
# let open Fifo in
add "hello" empty;;
- : string Fifo.queue = {front = ["hello"]; rear = []} и
# Fifo.(add "hello" empty);;
- : string Fifo.queue = {front = ["hello"]; rear = []} Во второй форме, если тело локального открытия само ограничено круглыми, фигурными или квадратными скобками, скобки локального открытия можно опустить. Например:
# Fifo.[empty] = Fifo.([empty]);; - : bool = true
# Fifo.[|empty|] = Fifo.([|empty|]);; - : bool = true
# Fifo.{ contents = empty } = Fifo.({ contents = empty });;
- : bool = true Эта вторая форма также работает с шаблонами:
# let at_most_one_element x = match x with
| Fifo.{ front = ([] | [_]); rear = [] } -> true
| _ -> false ;;
val at_most_one_element : 'a Fifo.queue -> bool = Компоненты модуля также можно скопировать в другой модуль с помощью инструкции include. Это может быть особенно полезно для расширения существующих модулей. Например, можно добавить функции, которые возвращают необязательное значение, а не вызывают исключение, если очередь пуста.
# module FifoOpt =
struct
include Fifo
let top_opt q = if is_empty q then None else Some(top q)
let pop_opt q = if is_empty q then None else Some(pop q)
end;;
module FifoOpt :
sig
type 'a queue = 'a Fifo.queue = { front : 'a list; rear : 'a list; }
val make : 'a list -> 'a list -> 'a queue
val empty : 'a queue
val is_empty : 'a queue -> bool
val add : 'a -> 'a queue -> 'a queue
exception Empty
val top : 'a queue -> 'a
val pop : 'a queue -> 'a queue
val top_opt : 'a queue -> 'a option
val pop_opt : 'a queue -> 'a queue option
end 2 Сигнатуры
Сигнатуры — это интерфейсы структур. Сигнатура определяет, какие компоненты структуры доступны извне и какой у них тип. Её можно использовать, чтобы скрыть некоторые компоненты структуры (например, локальные определения функций) или экспортировать компоненты с ограниченным типом. Например, приведённая ниже сигнатура задаёт операции очереди empty, add, top и pop, но не вспомогательную функцию make. Аналогично, она делает тип queue абстрактным (не указывая его фактическое представление в виде конкретного типа). Это гарантирует, что пользователи модуля Fifo не смогут нарушить инварианты структуры данных, на которые опираются операции, например: «если передний список пуст, задний список тоже должен быть пуст».
# module type FIFO =
sig
type 'a queue (* now an abstract type *)
val empty : 'a queue
val add : 'a -> 'a queue -> 'a queue
val top : 'a queue -> 'a
val pop : 'a queue -> 'a queue
exception Empty
end;;
module type FIFO =
sig
type 'a queue
val empty : 'a queue
val add : 'a -> 'a queue -> 'a queue
val top : 'a queue -> 'a
val pop : 'a queue -> 'a queue
exception Empty
end Ограничение структуры Fifo этой сигнатурой даёт другое представление структуры Fifo, в котором функция make недоступна, а фактическое представление очередей скрыто:
# module AbstractQueue = (Fifo : FIFO);; module AbstractQueue : FIFO
# AbstractQueue.make [1] [2;3] ;; Error: Unbound value AbstractQueue.make
# AbstractQueue.add "hello" AbstractQueue.empty;; - : string AbstractQueue.queue =
Ограничение можно также выполнить при определении структуры, как показано ниже:
module Fifo = (struct ... end : FIFO);;
Для этого предусмотрен альтернативный синтаксис:
module Fifo : FIFO = struct ... end;;
Как и для модулей, сигнатуру можно включить, чтобы скопировать её компоненты в текущую сигнатуру. Например, можно расширить сигнатуру FIFO функциями top_opt и pop_opt:
# module type FIFO_WITH_OPT =
sig
include FIFO
val top_opt: 'a queue -> 'a option
val pop_opt: 'a queue -> 'a queue option
end;;
module type FIFO_WITH_OPT =
sig
type 'a queue
val empty : 'a queue
val add : 'a -> 'a queue -> 'a queue
val top : 'a queue -> 'a
val pop : 'a queue -> 'a queue
exception Empty
val top_opt : 'a queue -> 'a option
val pop_opt : 'a queue -> 'a queue option
end 3 Функторы
Функторы — это «функции», преобразующие модули в модули. Они позволяют создавать параметризованные модули, а затем передавать другие модули в качестве параметров, чтобы получить конкретную реализацию. Например, модуль Set, реализующий множества в виде отсортированных списков, можно параметризовать так, чтобы он работал с любым модулем, предоставляющим тип элементов и функцию сравнения compare (например, OrderedString):
# type comparison = Less | Equal | Greater;; type comparison = Less | Equal | Greater
# module type ORDERED_TYPE =
sig
type t
val compare: t -> t -> comparison
end;;
module type ORDERED_TYPE = sig type t val compare : t -> t -> comparison end # module Set =
functor (Elt: ORDERED_TYPE) ->
struct
type element = Elt.t
type set = element list
let empty = []
let rec add x s =
match s with
[] -> [x]
| hd::tl ->
match Elt.compare x hd with
Equal -> s (* x is already in s *)
| Less -> x :: s (* x is smaller than all elements of s *)
| Greater -> hd :: add x tl
let rec member x s =
match s with
[] -> false
| hd::tl ->
match Elt.compare x hd with
Equal -> true (* x belongs to s *)
| Less -> false (* x is smaller than all elements of s *)
| Greater -> member x tl
end;;
module Set :
(Elt : ORDERED_TYPE) ->
sig
type element = Elt.t
type set = element list
val empty : 'a list
val add : Elt.t -> Elt.t list -> Elt.t list
val member : Elt.t -> Elt.t list -> bool
end Применив функтор Set к структуре, реализующей упорядоченный тип, мы получаем операции над множествами этого типа:
# module OrderedString =
struct
type t = string
let compare x y = if x = y then Equal else if x < y then Less else Greater
end;;
module OrderedString :
sig type t = string val compare : 'a -> 'a -> comparison end # module StringSet = Set(OrderedString);;
module StringSet :
sig
type element = OrderedString.t
type set = element list
val empty : 'a list
val add : OrderedString.t -> OrderedString.t list -> OrderedString.t list
val member : OrderedString.t -> OrderedString.t list -> bool
end # StringSet.member "bar" (StringSet.add "foo" StringSet.empty);; - : bool = false
4 Функторы и абстракция типов
Как и в примере с Fifo, хорошим стилем будет скрыть фактическую реализацию типа set, чтобы пользователи структуры не полагались на то, что множества представлены списками, и чтобы впоследствии можно было перейти к другому, более эффективному представлению множеств, не нарушая их код. Этого можно добиться, ограничив Set подходящей сигнатурой функтора:
# module type SETFUNCTOR =
(Elt: ORDERED_TYPE) ->
sig
type element = Elt.t (* concrete *)
type set (* abstract *)
val empty : set
val add : element -> set -> set
val member : element -> set -> bool
end;;
module type SETFUNCTOR =
(Elt : ORDERED_TYPE) ->
sig
type element = Elt.t
type set
val empty : set
val add : element -> set -> set
val member : element -> set -> bool
end # module AbstractSet = (Set : SETFUNCTOR);; module AbstractSet : SETFUNCTOR
# module AbstractStringSet = AbstractSet(OrderedString);;
module AbstractStringSet :
sig
type element = OrderedString.t
type set = AbstractSet(OrderedString).set
val empty : set
val add : element -> set -> set
val member : element -> set -> bool
end # AbstractStringSet.add "gee" AbstractStringSet.empty;; - : AbstractStringSet.set =
Пытаясь более элегантно записать приведённое выше ограничение типа, можно захотеть дать имя сигнатуре структуры, возвращаемой функтором, а затем использовать эту сигнатуру в ограничении:
# module type SET =
sig
type element
type set
val empty : set
val add : element -> set -> set
val member : element -> set -> bool
end;;
module type SET =
sig
type element
type set
val empty : set
val add : element -> set -> set
val member : element -> set -> bool
end # module WrongSet = (Set : (Elt: ORDERED_TYPE) -> SET);; module WrongSet : (Elt : ORDERED_TYPE) -> SET
# module WrongStringSet = WrongSet(OrderedString);;
module WrongStringSet :
sig
type element = WrongSet(OrderedString).element
type set = WrongSet(OrderedString).set
val empty : set
val add : element -> set -> set
val member : element -> set -> bool
end # WrongStringSet.add "gee" WrongStringSet.empty ;;
Error: This constant has type string but an expression was expected of type
WrongStringSet.element = WrongSet(OrderedString).element Проблема здесь в том, что SET задаёт тип element как абстрактный, поэтому равенство типов между element в результате функтора и t в его аргументе теряется. В результате WrongStringSet.element — это не тот же тип, что и string, и операции WrongStringSet нельзя применять к строкам. Как показано выше, важно, чтобы тип element в сигнатуре SET был объявлен равным Elt.t; к сожалению, в данном случае это невозможно, поскольку SET определена в контексте, где Elt не существует. Чтобы преодолеть эту трудность, в OCaml предусмотрена конструкция with type для сигнатур, позволяющая дополнить сигнатуру дополнительными равенствами типов:
# module AbstractSet2 =
(Set : (Elt: ORDERED_TYPE) -> (SET with type element = Elt.t));;
module AbstractSet2 :
(Elt : ORDERED_TYPE) ->
sig
type element = Elt.t
type set
val empty : set
val add : element -> set -> set
val member : element -> set -> bool
end Как и для простых структур, предусмотрен альтернативный синтаксис для определения функторов и ограничения их результата:
module AbstractSet2(Elt: ORDERED_TYPE) : (SET with type element = Elt.t) = struct ... end;;
Абстрагирование компонента типа в результате функтора — мощный приём, обеспечивающий высокий уровень типобезопасности, как мы сейчас покажем. Рассмотрим порядок строк, отличающийся от стандартного порядка, реализованного в структуре OrderedString. Например, будем сравнивать строки без учёта регистра.
# module NoCaseString =
struct
type t = string
let compare s1 s2 =
OrderedString.compare (String.lowercase_ascii s1) (String.lowercase_ascii s2)
end;;
module NoCaseString :
sig type t = string val compare : string -> string -> comparison end # module NoCaseStringSet = AbstractSet(NoCaseString);;
module NoCaseStringSet :
sig
type element = NoCaseString.t
type set = AbstractSet(NoCaseString).set
val empty : set
val add : element -> set -> set
val member : element -> set -> bool
end # NoCaseStringSet.add "FOO" AbstractStringSet.empty ;;
Error: The value AbstractStringSet.empty has type
AbstractStringSet.set = AbstractSet(OrderedString).set
but an expression was expected of type
NoCaseStringSet.set = AbstractSet(NoCaseString).set Обратите внимание, что типы AbstractStringSet.set и NoCaseStringSet.set несовместимы, и значения этих двух типов не совпадают. Это правильное поведение: хотя оба типа множеств содержат элементы одного типа (строки), они построены на основе разных порядков этого типа, и операции должны поддерживать разные инварианты (строгое возрастание при стандартном порядке и при порядке без учёта регистра). Применение операций из AbstractStringSet к значениям типа NoCaseStringSet.set может привести к неверным результатам или создать списки, нарушающие инварианты NoCaseStringSet.
5 Модули и раздельная компиляция
До сих пор все примеры модулей приводились в контексте интерактивной системы. Однако модули наиболее полезны в больших программах, компилируемых пакетно. Для таких программ необходимо разделять исходный код на несколько файлов, называемых единицами компиляции, которые можно компилировать отдельно, сводя к минимуму повторную компиляцию после внесения изменений.
В OCaml единицы компиляции представляют собой частные случаи структур и сигнатур, а отношения между ними легко объяснить с помощью модульной системы. Единица компиляции A состоит из двух файлов:
- файла реализации A.ml, содержащего последовательность определений, аналогичную содержимому конструкции struct…end;
- файла интерфейса A.mli, содержащего последовательность спецификаций, аналогичную содержимому конструкции sig…end.
Вместе эти два файла определяют структуру с именем A, как если бы на верхнем уровне было введено следующее определение:
module A: sig (* contents of file A.mli *) end
= struct (* contents of file A.ml *) end;;
Файлы, определяющие единицы компиляции, можно компилировать отдельно с помощью команды ocamlc -c (параметр -c означает «только компилировать, не пытаться выполнить компоновку»); при этом создаются скомпилированные файлы интерфейсов (с расширением .cmi) и файлы объектного кода (с расширением .cmo). После компиляции всех единиц их файлы .cmo связываются с помощью команды ocamlc. Например, следующие команды компилируют и связывают программу, состоящую из двух единиц компиляции Aux и Main:
$ ocamlc -c Aux.mli # produces aux.cmi $ ocamlc -c Aux.ml # produces aux.cmo $ ocamlc -c Main.mli # produces main.cmi $ ocamlc -c Main.ml # produces main.cmo $ ocamlc -o theprogram Aux.cmo Main.cmo
Программа ведёт себя точно так же, как если бы на верхнем уровне были введены следующие выражения:
module Aux: sig (* contents of Aux.mli *) end
= struct (* contents of Aux.ml *) end;;
module Main: sig (* contents of Main.mli *) end
= struct (* contents of Main.ml *) end;;
В частности, Main может ссылаться на Aux: определения и объявления в Main.ml и Main.mli могут ссылаться на определение в Aux.ml с помощью записи Aux.ident, если это определение экспортируется в Aux.mli.
Порядок, в котором файлы .cmo передаются команде ocamlc на этапе компоновки, определяет порядок появления определений модулей. Поэтому в приведённом выше примере сначала появляется Aux, и Main может на него ссылаться, но Aux не может ссылаться на Main.
Обратите внимание, что отдельным файлам могут соответствовать только структуры верхнего уровня, но не функторы и не типы модулей. Однако объекты всех классов модулей могут выступать компонентами структуры, поэтому функтор или тип модуля можно поместить в структуру, которой затем будет соответствовать файл.
© 1995-2026 INRIA.
https://ocaml.org/manual/5.5/moduleexamples.html