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: 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.

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

« Язык основного уровняОбъекты в OCaml »
Авторские права © 2024 Institut National de Recherche en Informatique et en Automatique

© 1995-2024 INRIA.
https://ocaml.org/manual/5.2/moduleexamples.html

Spec-Zone.ru

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