Spec-Zone.ru › OCaml
☰Введение в OCaml
  • Основной язык
  • Модульная система
  • Объекты в OCaml
  • Именованные аргументы
  • Полиморфные варианты
  • Полиморфизм и его ограничения
  • Обобщённые алгебраические типы данных
  • Продвинутые примеры с классами и модулями
  • Параллельное программирование
  • Модель памяти: сложные аспекты

Глава 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.

Обратите внимание, что отдельным файлам могут соответствовать только структуры верхнего уровня, но не функторы и не типы модулей. Однако объекты всех классов модулей могут выступать компонентами структуры, поэтому функтор или тип модуля можно поместить в структуру, которой затем будет соответствовать файл.

« Основной языкОбъекты в OCaml »
Copyright © 2026 Institut National de Recherche en Informatique et en Automatique

© 1995-2026 INRIA.
https://ocaml.org/manual/5.5/moduleexamples.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API