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

Является наиболее развитой системой модулей среди встречающихся в языках программирования.

Общие сведения В простейшей форме язык модулей состоит из трёх видов модулей: структура сигнатура функтор

Сигнатуру можно рассматривать как описание структуры, а структуру, соответственно, как реализацию сигнатуры. Многие языки предоставляют похожие конструкции, обычно под другими названиями: сигнатуры часто называются интерфейсами или спецификациями пакетов, а структуры часто называются реализациями () или пакетами. Вне зависимости от терминологии, идея состоит в назначении типа целому фрагменту кода. В отличие от многих языков, в ML отношения между сигнатурами и структурами выстраиваются по схеме , а не или . Сигнатура может описывать множество разных структур, а структура может соответствовать многим разным сигнатурам. Большинство других языков связаны более сильными ограничениями, требуя, чтобы данная структура имела единственную сигнатуру, или чтобы данная сигнатура выводилась из единственной структуры. Это не так в случае ML.

В мейнстримных объектно-ориентированных языках вроде C++ или Java, абстракция обеспечивается посредством классов, которые совмещают в одной концепции сразу ряд возможностей (наследование, и ), что затрудняет их формализацию и может приводить к нежелательным последствиям при неосторожном использовании. В отличие от классов, язык модулей ML полностью фокусируется на абстракции, предоставляя обширный спектр её форм и обеспечивая надёжную формальную базу для их исследования. Он предоставляет возможности управления иерархией пространств имён, тончайшей настройки интерфейсов, абстракцией на стороне реализатора и на стороне клиента.

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

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

Язык

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

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

Введение понятия структуры требует пересмотра определения окружения в ядре языка. Отныне окружение представляет собой коллекцию типов, значений и структур. Соответственно, структура представляет собой коллекцию типов, значений и других структур. Рекурсивное вложение структур не допускается, хотя некоторые реализации их поддерживают.

Основным средством определения структур являются инкапсулированные объявления, то есть объявления, заключённые в синтаксические скобки struct...end. Например, следующая структура реализует стек, определяя внутреннюю организацию объектов алгебраического типа «стек» и минимально необходимый набор функций над ним: struct type 'a t = 'a list val empty = [] val isEmpty = null val push = op :: val pop = List.getItem end «Значением» этого инкапсулированного объявления является структура. Чтобы это значение использовать, необходимо назначить ему идентификатор: structure Stack = struct type 'a t = 'a list val empty = [] val isEmpty = null val push = op :: val pop = List.getItem end Теперь доступ к компонентам структуры может осуществляться посредством составных (или квалифицированных) имён, например, Stack.push, .

Сигнатура (signature) представляет собой перечисление описаний элементов структуры, то есть интерфейс структуры. Каждый элемент этого перечисления называется спецификацией. Если в сигнатуре для значения x специфицирован тип t, то в структуре идентификатор x должен к значению типа t. Можно понимать сигнатуру как своего рода «тип» структуры, но сигнатура не является типом в строгом понимании, так как тип представляет собой множество значений, а «значение сигнатуры» может содержать типы (которые в ML значениями не являются). Каждый идентификатор в сигнатуре должен быть единственен. Правило лексического затенения имён в сигнатурах не соблюдается, поэтому порядок их перечисления значения не имеет, но типы должны быть объявлены до их использования, поэтому традиционно их помещают в начало сигнатуры.

Определение сигнатуры записывается в синтаксических скобках . Например, структура Stack имеет следующую сигнатуру (компилятор выводит её автоматически):

structure Stack : sig type 'a t = 'a list val empty: 'a t val isEmpty: 'a t -> bool val push: 'a * 'a t -> 'a t val pop: 'a t -> ('a * 'a t) option end

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

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

Например, разработчик может определить сигнатуру, описывающую разнообразные потоки данных (структуры данных с последовательным доступом), и назначить ей идентификатор:

signature STREAM = sig type 'a t val empty : 'a t val isEmpty: 'a t -> bool val push: 'a * 'a t -> 'a t val pop: 'a t -> ('a * 'a t) option end

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

В данном случае можно записать: .

Прозрачное сопоставление традиционно имеет синтаксис «», а тёмное — «». Однако, создатели OCaml отказались от поддержки прозрачного сопоставления полностью, то есть все сопоставления в OCaml являются тёмными, но для простоты используется синтаксис «».

В простейших случаях можно подписывать сигнатуру немедленно, не назначая ей отдельного идентификатора:

structure Stack :> sig type 'a t val isEmpty: 'a t -> bool val push: 'a * 'a t -> 'a t val pop: 'a t -> ('a * 'a t) option end = struct type 'a t = 'a list val empty = [] val isEmpty = null val push = op :: val pop = List.getItem end

Однако, на практике безымянные сигнатуры довольно редки, поскольку применение сигнатур не ограничивается лишь сокрытием.

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

Это можно рассматривать как простейший аналог абстрактных классов (в терминах объектно-ориентированного программирования) в том смысле, что сигнатура описывает общий интерфейс, а сопоставимые с ней структуры реализуют этот интерфейс различным образом. Однако, в ML связь «родитель-потомок» явно не объявляется, поскольку система типов ML имеет семантику, то есть сопоставление структуры с сигнатурой осуществляется тем же механизмом, что и сопоставление значения 5 с типом int.

Например, можно определить структуру, которая реализует очередь, но которая также сопоставима с сигнатурой STREAM:

structure Queue = struct datatype 'a t = T of 'a list * 'a list val empty = T ([], []) val isEmpty = fn T ([], _) => true | _ => false val normalize = fn ([], ys) => (rev ys, []) | q => q fun push (y, T (xs, ys)) = T (normalize (xs, y::ys)) val pop = fn (T (x::xs, ys)) => SOME (x, T (normalize (xs, ys))) | _ => NONE end

Поскольку структура Stack не была явным образом подписана более бедной сигнатурой, то внешняя программа «знает» о том, что тип t тождественен типу list и может использовать это знание, обрабатывая объекты этого типа методами стандартного модуля List. Если впоследствии потребуется изменить реализацию структуры Stack (например, представив стек посредством заранее выделенного массива), то это потребует переписывания всего кода, который использовал это знание. То же верно и для структуры Queue. Более того, если некий модуль был параметризован собственной сигнатурой структуры Stack, то ему будет невозможно передать в качестве параметра структуру Queue.

Таким образом, экспортирование лишней информации из структур существенно ухудшает модифицируемость программ. Для повышения уровня абстракции следует подписывать структуры более бедными сигнатурами, например:

structure Stack :> STREAM = struct type 'a t = 'a list val empty = [] val isEmpty = null val push = op :: val pop = List.getItem end

Структура Stack сопоставлена с сигнатурой STREAM тёмным образом, поэтому внешняя программа будет иметь возможность в полной мере оперировать значениями типа Stack.t, но не будет иметь доступа к его реализации, и из всех возможных значений этого типа она будет иметь возможность использовать лишь значение Stack.empty (опять же, «не зная» о том, что оно равно nil). Любая обработка данных этого типа будет осуществляться абстрактно, без учёта его реализации, и осуществлять её можно лишь посредством функций Stack.push и Stack.pop.

Но нигде сигнатуры так ни важны и ни полезны, как при использовании функторов.

Наследование Структуры могут вкладываться друг в друга: structure E = struct structure A structure B ... end Естественно, сигнатуры позволяют описывать вложенные структуры. При этом, как и в остальных случаях, вложение структур контролируется на основании сигнатур, а не по тождественному совпадению: signature D = sig structure A : C structure B : C end

Сигнатуры можно включать (синтаксис ) друг в друга, последовательно обогащая интерфейс: signature POOR = sig type 'a t val isEmpty: 'a t -> bool val push: 'a * 'a t -> 'a t val pop: 'a t -> ('a * 'a t) option end

signature RICH = sig include POOR val empty : 'a t end

Согласно описанной семантике, подписывание сигнатуры не обязательно осуществлять немедленно. Если требуется разработать некий набор тесно взаимосвязанных модулей, которые более «дружны» между собой, чем с остальной программой, то после окончания его разработки можно подписать структуры более бедными сигнатурами: