Как модуль OCaml может экспортировать поле, определенное в зависимом модуле? - PullRequest
4 голосов
/ 05 декабря 2011

У меня есть декомпозиция, где модуль A определяет тип структуры и экспортирует поле этого типа, которое определяется как значение в модуле B:

a.ml:

type t = {
  x : int
}

let b = B.a

b.ml:

open A (* to avoid fully qualifying fields of a *)
let a : t = {
  x = 1;
}

Круговая зависимость исключается, поскольку B зависит только от объявлений типов (не значений) в A.

a.mli:

type t = {
  x : int
}

val b : t

Насколько я знаю, это должно быть кошерным.Но компилятор ошибается с этим:

File "a.ml", line 1, characters 0-1:
Error: The implementation a.ml does not match the interface a.cmi:
       Values do not match: val b : A.t is not included in val b : t

Это, конечно, особенно глупо, потому что неясно, какой val b интерпретируется как имеющий тип t, а какой имеет тип A.t (и к которому A - определение интерфейса или определение модуля - это относится).

Я предполагаю, что есть какое-то загадочное правило (по аналогии с «полями структуры должны ссылаться полностьюквалифицированное имя модуля, когда модуль не открыт (семантика, которая кусает каждого неофита OCaml в какой-то момент), но я пока в растерянности.

Ответы [ 2 ]

5 голосов
/ 06 декабря 2011

Модули в микроскопе более тонкие, чем кажется

(Если ваши глаза в какой-то момент застекляются, переходите ко второму разделу.)

Давайте посмотримчто произойдет, если вы положите все в один файл.Это должно быть возможным, поскольку отдельные вычислительные блоки не увеличивают мощность системы типов. (Примечание: используйте отдельные каталоги для этого и для любого теста с файлами a.* и b.*, в противном случае компилятор увидит единицы компиляции A и B, что может сбить с толку.)

module A = (struct
    type t = { x : int }
    let b = B.a
  end : sig
    type t = { x : int }
    val b : t
  end)
module B = (struct
    let a : A.t = { A.x = 1 }
  end : sig
    val a : A.t
  end)

О, ну, это не может работать.Очевидно, что B здесь не определено.Нам нужно быть более точным в отношении цепочки зависимостей: сначала определим интерфейс A, затем интерфейс B, затем реализации B и A.

module type Asig = sig
    type t = { x : int }
    type u = int
    val b : t
  end
module B = (struct
    let a : Asig.t = { Asig.x = 1 }
  end : sig
    val a : Asig.t
  end)
module A = (struct
    type t = { x : int }
    let b = B.a
  end : Asig)

Хорошо., нет.

File "d.ml", line 7, characters 12-18:
Error: Unbound type constructor Asig.t

Видите ли, Asig это подпись.Подпись - это спецификация модуля, и не более;в Окамле нет исчисления подписей.Вы не можете ссылаться на поля подписи.Вы можете ссылаться только на поля модуля.Когда вы пишете A.t, это относится к полю типа t модуля A.

В Ocaml эта тонкость возникает довольно редко.Но вы пытались заглянуть в угол языка, и вот что там скрывается.

Так что же происходит, когда есть два модуля компиляции?Более близкая модель - видеть A как функтор, который принимает модуль B в качестве аргумента.Требуемая подпись для B - это подпись, описанная в файле интерфейса b.mli.Аналогично, B - это функция, которая принимает в качестве аргумента модуль A, сигнатура которого указана в a.mli.О, подождите, это немного сложнее: A появляется в сигнатуре B, так что интерфейс B действительно определяет функтор, который принимает A и производит, так сказать, B.

module type Asig = sig
    type t = { x : int }
    type u = int
    val b : t
  end
module type Bsig = functor(A : Asig) -> sig
    val a : A.t
  end
module B = (functor(A : Asig) -> (struct
    let a : A.t = { A.x = 1 }
  end) : Bsig)
module A = functor(B : Bsig) -> (struct
    type t = { x : int }
    let b = B.a
  end : Asig)

И здесь, при определении A, мы сталкиваемся с проблемой: у нас еще нет A для передачи в качестве аргумента B.(Конечно, за исключением рекурсивных модулей, но здесь мы пытаемся понять, почему мы не можем обойтись без них.)

Определение порождающего типа является побочным эффектом

Фундаментальное прилипаниеДело в том, что type t = {x : int} является генеративным определением типа.Если этот фрагмент появляется дважды в программе, определяются два разных типа.(Ocaml предпринимает шаги и запрещает вам определять два типа с одинаковыми именами в одном модуле, за исключением верхнего уровня.)

Фактически, как мы видели выше, type t = {x : int} вМодуль реализации является генеративным определением типа.Это означает «определить новый тип с именем d, который представляет собой тип записи с полями…».Тот же синтаксис может присутствовать в интерфейсе модуля, но там он имеет другое значение: там он означает «модуль определяет тип t, который является типом записи…».

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

Когда вы приступаете к этому, определяя порождающий тип, этоэто форма побочного эффекта.Этот побочный эффект возникает во время компиляции или во время инициализации программы (различие между этими двумя появляется только тогда, когда вы начинаете смотреть на функторы, что я не буду здесь делать). Поэтому важно отслеживать, когда этот побочный эффект происходит:происходит, когда модуль A определен (скомпилирован или загружен).

Итак, чтобывыразите это более конкретно: определение типа type t = {x : int} в модуле A скомпилировано в «пусть t будет типом # 1729, новым типом, который является типом записи с полем…».(Тип fresh означает тип, который отличается от любого типа, который когда-либо был определен ранее.).Определение B определяет a для типа # 1729.

Поскольку модуль B зависит от модуля A, A должен быть загружен до B.Но реализация A явно использует реализацию B.Эти два являются взаимно рекурсивными.Сообщение об ошибке Окамла немного сбивает с толку, но вы действительно выходите за рамки языка.

2 голосов
/ 05 декабря 2011

(и к которому относится A - определение интерфейса или определение модуля - это относится).

A относится ко всему модулю A. При обычной процедуре сборки это будет относиться к реализации в .ml, ограниченной подписью в a.mli. Но если вы играете в трюки, перемещая cmi и все такое - вы сами:)

Насколько я знаю, это должно быть кошерно.

Я лично квалифицирую эту проблему как циклическую зависимость и буду категорически против структурирования кода таким способом. ИМХО это вызывает больше проблем и головокружений, чем решение реальных проблем. Например. Перемещение определений общих типов в type.ml, и покончим с этим - вот что приходит на ум. Какая ваша изначальная проблема приводит к такому структурированию?

...