Глава 2 . Система модулей
В этой главе содержится введение в систему модулей Objective Caml.
2 . 1 Структура
Главная цель модулей - сгруппировать связанные определения
(например, определения типа данных и операций, допустимых для
него) и обеспечить разумную схему именования. Подобная группа
называется структурой и задается блоком
struct...end
, внутри которого может
содержаться произвольная последовательность
определений. Обычно струкуре дается имя с помощью оператора
module
. Ниже приведен пример структуры,
включающей тип очереди по приоритету и операции для этого
типа:
# module PrioQueue = struct type priority = int type 'a queue = Empty | Node of priority * 'a * 'a queue * 'a queue let empty = Empty let rec insert queue prio elt = match queue with Empty -> Node(prio, elt, Empty, Empty) | Node(p, e, left, right) -> if prio <= p then Node(prio, elt, insert right p e, left) else Node(p, e, insert right prio elt, left) exception Queue_is_empty let rec remove_top = function Empty -> raise Queue_is_empty | Node(prio, elt, left, Empty) -> left | Node(prio, elt, Empty, right) -> right | Node(prio, elt, (Node(lprio, lelt, _, _) as left), (Node(rprio, relt, _, _) as right)) -> if lprio <= rprio then Node(lprio, lelt, remove_top left, right) else Node(rprio, relt, left, remove_top right) let extract = function Empty -> raise Queue_is_empty | Node(prio, elt, _, _) as queue -> (prio, elt, remove_top queue) end;;
module PrioQueue : sig type priority = int and 'a queue = Empty | Node of priority * 'a * 'a queue * 'a queue val empty : 'a queue val insert : 'a queue -> priority -> 'a -> 'a queue exception Queue_is_empty val remove_top : 'a queue -> 'a queue val extract : 'a queue -> priority * 'a * 'a queue end
Вне структуры на ее компоненты можно ссылаться, предваряя имя
компонента именем структуры и точкой. Например,
PrioQueue.insert
в контексте значения
является функцией insert
, определенной в
структуре PrioQueue
. Аналогично,
PrioQueue.queue
в контексте типа является
типом queue
, определенным в
PrioQueue
.
2 . 2 Сигнатуры
Сигнатуры - это интерфейсы для структур. Они указывают, какие
элементы структуры с какими типами доступны извне. Поэтому
сигнатуры могут использоваться для скрытия некоторых
компонентов структуры (например, локальных функций) или для
ограничения типов экспортируемых компонентов. Сигнатура ниже
включает три оператора очереди по приоритету
empty
, insert
и
extract
, но скрывает вспомогательную
функцию remove_top
. Кроме того, тип
queue
становится абстрактным (конкретный
тип не указывается):
# module type PRIOQUEUE = sig type priority = int (* конкретный *) type 'a queue (* абстрактный *) val empty : 'a queue val insert : 'a queue -> int -> 'a -> 'a queue val extract : 'a queue -> int * 'a * 'a queue exception Queue_is_empty end;;
module type PRIOQUEUE = sig type priority = int and 'a queue val empty : 'a queue val insert : 'a queue -> int -> 'a -> 'a queue val extract : 'a queue -> int * 'a * 'a queue exception Queue_is_empty end
В результате мы получаем иное представление структуры
PrioQueue
: функция
remove_top
недоступна, а конкретный тип
очередей по приоритету скрыт.
#module AbstractPrioQueue = (PrioQueue : PRIOQUEUE);;
module AbstractPrioQueue : PRIOQUEUE
#AbstractPrioQueue.remove_top;;
Unbound value AbstractPrioQueue.remove_top
#AbstractPrioQueue.insert AbstractPrioQueue.empty 1 "hello";;
- : string AbstractPrioQueue.queue = <abstr>
Подобные ограничения можно наложить и при определении структуры:
module PrioQueue = (struct ... end : PRIOQUEUE);;
Альтернативный синтаксис таков:
module PrioQueue : PRIOQUEUE = struct ... end;;
2 . 3 Функторы
Функторы - это "функции" для структур. Они используются для параметризации структур: структура A, параметризованная структурой B является функтором F с формальным аргументом B (соответвующим сигнатуре B), возвращающим непосредственно структуру А. Функтор F может быть применен к различным реализациям B типа B1...Bn, вернув в результате структуры А1...Аn, соответственно.
Ниже приведен пример структуры, реализующей наборы как упорядоченные списки, и параметризованной структурой, предоставляющей тип элементов набора и функцию сортировки для этого типа (используется, чтобы сохранять списки отсортированными).
#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 уже помещен в s *) | Less -> x :: s (* x меньше любого элемента 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 находится в s *) | Less -> false (* x меньше любого элемента s *) | Greater -> member x tl end;;
module Set : functor (Elt : ORDERED_TYPE) -> sig type element = Elt.t and 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 and 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
2 . 4 Функторы и абстрагирование от типа
Как и в примере с PrioQueue
хорошим стилем
будет скрыть реализацию типа set
, чтобы
пользователи структуры не основывались на том, что она
представлена как список, а мы могли бы впоследствие
использовать более эффективную реализацию набора, не заставляя
их переписывать код. Достаточно ограничить
Set
подходящей сигнатурой функтора.
#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 type SETFUNCTOR = functor (Elt : ORDERED_TYPE) -> sig type element = Elt.t and 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 and 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 = <abstr>
Может показаться, что ограничение типа получится элегантнее, если именовать сигнатуру структуры, возвращаемой функтором, а затем использовать эту сигнатуру в ограничении:
#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 and 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 and set = WrongSet(OrderedString).set val empty : set val add : element -> set -> set val member : element -> set -> bool end
#WrongStringSet.add "gee" WrongStringSet.empty;;
This expression has type string but is here used with type WrongStringSet.element = WrongSet(OrderedString).element
Дело в том, что в модуле SET
тип
element
определен как абстрактный, поэтому
равенство типов между element
в результате
функтора и t
не соблюдается. Следовательно,
WrongStringSet.element
и
string
имеют разные типы, а потому операции
WrongStringSet
не применимы к
строкам. Поэтому тип element
в сигнатуре
SET
должен быть объявлен как
Elt.t
, что, увы, невозможно, поскольку
SET
объявлен в контексте, где
Elt
не существует. Подобные проблемы в
Objective Caml обходятся с помощью конструкции with
type
, позволяющей добавить в сигнатуру
дополнительные условия равенства типов.
#module AbstractSet = (Set : functor(Elt: ORDERED_TYPE) -> (SET with type element = Elt.t));;
module AbstractSet : functor (Elt : ORDERED_TYPE) -> sig type element = Elt.t and set val empty : set val add : element -> set -> set val member : element -> set -> bool end
Как и в случае простых структур, для определения функторов и ограничения их результатов существует альтернативный синтаксис:
module AbstractSet(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 s1) (String.lowercase 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 and set = AbstractSet(NoCaseString).set val empty : set val add : element -> set -> set val member : element -> set -> bool end
#NoCaseStringSet.add "FOO" AbstractStringSet.empty;;
This expression has type AbstractStringSet.set = AbstractSet(OrderedString).set but is here used with type NoCaseStringSet.set = AbstractSet(NoCaseString).set
Типы AbstractStringSet.set
и
NoCaseStringSet.set
не совместимы, и их
значения не равны. Это правильное поведение: несмотря на то,
что оба содержат элементы одного типа (строки), в них
используются разные функции сортировки, соотвественно,
операции должны поддерживать разные инварианты. Использование
операций AbstractStringSet
для значений
типа NoCaseStringSet.set
приведет к
неправильным результатам и созданию списков, нарушающих
инварианты NoCaseStringSet
.
2 . 5 Модули и раздельная компиляция
До сих пор о модулях рассказывалось в контексте интерактивной системы. Однако особенно полезны они в больших программах, предназначенных для пакетной компиляции. В подобных случаях практически необходимо разделять исходный текст на несколько файлов (единиц компиляции), и компилировать их раздельно, чтобы после внесения изменений повторно компилировать минимум текстов.
В Objective Caml единицы компиляции - это особый случай
структур и сигнатур, причем отношения между этими единицами
легко описываются в терминах системы модулей. Единица
компиляции a
включает в себя два файла:
-
Реализация
a.ml
, включающая определения, как в блокеstruct...end
. -
Интерфейс
a.mli
, содержащий спецификации, как в блокеsig...end
.
Оба эти файла описывают структуру А
(то же
имя, что и a
, но с первой заглавной буквой,
как если бы было введено следующее определение:
module A: sig (* содержимое файла a.mli *) end = struct (* содержимое файла a.ml *) end;;
Файлы, составляющие единицу, компилируются отдельно, командой
ocamlc -c
(ключ -c
означает "только компиляция без компоновки"), что дает файлы с
расширениями .cmi
для интерфейса и
.cmo
для объектного кода. По окончании
единиц объектные файлы компонуются командой
ocamlc
. Следующте команды компилирую и
компонуют программу из единиц aux
и
main
.
$ ocamlc -c aux.mli # создает aux.cmi $ ocamlc -c aux.ml # создает aux.cmo $ ocamlc -c main.mli # создает main.cmi $ ocamlc -c main.ml # создает 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
могут использовать определения и
объявления из aux.ml
c помощью нотации
Aux.ident
, если соответствующие определения
экспортируются в файле aux.mli
.
Объектные файлы передаются компилятору в порядке следования
определений модуля. В примере выше Aux
идет первым, поэтому Main
может ссылаться
на него, но Aux
не может ссылаться на
Main
.
На раздельно компилируемые файлы отображаются только структуры верхнего уровня, но никак не функторы или типы модулей. Впрочем, подобные объекты могут входить в структуру, а те отображаются на файлы.