Глава 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: This expression 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 :
functor (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 =
functor (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 =
functor (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 : functor(Elt: ORDERED_TYPE) -> SET);; module WrongSet : functor (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 expression 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 : functor(Elt: ORDERED_TYPE) -> (SET with type element = Elt.t));;
module AbstractSet2 :
functor (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: This expression 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-2024 INRIA.
https://ocaml.org/manual/5.2/moduleexamples.html