Standard ML
Standard ML (SML) — компилируемый язык программирования общего назначения высшего порядка[англ.], основанный на системе типов Хиндли — Милнера. Является «в основном функциональным» языком[7][8], то есть поддерживает большинство технических свойств функциональных языков, но также предоставляет развитые возможности императивного программирования при необходимости. Сочетает устойчивость программ, гибкость на уровне динамически типизируемых языков и быстродействие на уровне языка Си; обеспечивает превосходную поддержку как быстрого прототипирования, так и модульности и крупномасштабного программирования[англ.][9][10]. SML был первым самостоятельным компилируемым языком в семействе ML и до сих пор служит опорным языком в сообществе по развитию ML (successor ML)[11]. В SML впервые была реализована уникальная аппликативная система модулей — язык модулей ML. Общие сведенияЯзык изначально ориентирован на крупномасштабное программирование[англ.] программных систем: он предоставляет эффективные средства абстракции и модульности, обеспечивая высокий коэффициент повторного использования кода, и это делает его подходящим также для быстрого прототипирования программ, в том числе и крупномасштабных[англ.]. Например, в процессе разработки (тогда ещё экспериментального) компилятора SML/NJ[англ.] (60 тысяч строк на SML), порой приходилось вносить радикальные изменения в реализации ключевых структур данных, влияющих на десятки модулей — и новая версия компилятора была готова в течение дня.[9] (см. также ICFP Programming Contest за 2008, 2009.) При этом, в отличие многих других языков, подходящих для быстрого прототипирования, SML может очень эффективно компилироваться . SML известен своим относительно низким порогом вхождения и служит языком обучения программированию во многих университетах мира[12]. Обширно документирован в рабочем виде, активно используется учёными в качестве базы для исследования новых элементов языков программирования и идиом (см., например, полиморфизм структурных типов). К настоящему времени все реализации языка (включая устаревшие) стали открытыми и свободными. Отличительные особенностиЯзык имеет математически точное (англ. rigorous) формальное определение, называемое «Определением» (англ. The Definition). Для Определения построено доказательство полной типобезопасности, что гарантирует устойчивость программ и предсказуемое поведение даже при некорректных входных данных и возможных ошибках программистов. Даже содержащая ошибку программа на SML всегда продолжает вести себя как ML-программа: она может навечно уйти в расчёты или породить исключение, но она не может обрушиться[13]. SML является в основном функциональным (mostly functional или primarily functional) языком[7][8], то есть поддерживает большинство технических свойств функциональных языков, но также предоставляет возможности императивного программирования. Его чаще называют «языком высшего порядка[англ.]», чтобы подчеркнуть поддержку первоклассных функций и при этом отличить его от ссылочно-прозрачных языков. SML реализует выдающиеся средства поддержки крупномасштабного программирования[англ.] за счёт наиболее мощной и выразительной системы модулей среди известных (язык модулей ML). В SML реализована ранняя версия языка модулей, являющаяся отдельным слоем языка: модули могут содержать объекты ядра языка, но не наоборот[14]. В отличие многих других языков семейства ML (OCaml, Haskell, F#, Felix, Opa, Nemerle и других), SML весьма минималистичен: он не имеет изначально встроенных средств объектно-ориентированного программирования, средств конкурентности, ad-hoc-полиморфизма, динамической типизации, генераторов списков и многих других возможностей. Однако, SML отличается ортогональностью[15] (то есть реализует минимально необходимый, но полный набор максимально различных элементов), что позволяет сравнительно легко эмулировать прочие возможности, и необходимые для этого приёмы широко освещены в литературе . Фактически, SML позволяет использовать сколь угодно высокоуровневую функциональность в качестве примитивной для реализации функциональности ещё более высокого уровня[16]. В частности, построены модели реализации классов типов и монад с использованием только стандартных конструкций SML, а также средств объектно-ориентированного программирования[17]. Более того, SML является одним из немногих языков, в котором непосредственно реализованы продолжения первого класса. Возможности
Система типов Хиндли — Милнера (Х-М) является отличительной особенностью ML и его потомков. Она обеспечивает надёжность программ за счёт раннего выявления ошибок, высокий коэффициент повторного использования кода, высокий потенциал к оптимизации, сочетая эти качества с немногословностью и выразительностью на уровне динамически типизируемых языков. Наиболее характерными чертами, присущими Х-М, являются полиморфизм типов, а также алгебраические типы данных и сопоставление с образцом на них. Реализация Х-М в SML имеет следующие особенности:
Способы использованияВ отличие от многих языков, SML предоставляет большое многообразие способов своего использования[21]:
При этом в определённых режимах возможны самые разные целевые платформы и стратегии компиляции :
Сами стратегии компиляции также существенно различаются:
ЯзыкБазовая семантика
Объявления, выражения, блоки, функцииПримитивные типыСоставные и определяемые типыМутабельные значенияОграничение на значенияОграничение на значения (англ. value restriction) Управляющие конструкцииКрупномасштабное программирование
Модульность
Система модулей SML является наиболее развитой системой модулей в языках программирования. Она повторяет семантику ядра ML (англ. Core ML), так что зависимости между крупными компонентами программ строятся подобно зависимостям мелкого уровня. Эта система модулей состоит из трёх видов модулей:
Структуры похожи на модули в большинстве языков программирования. Сигнатуры служат интерфейсами структур, но не привязываются жёстко к определённым структурам, а выстраивают отношения по схеме «многие-ко-многим», позволяя гибко управлять видимостью компонентов структур в зависимости от нужд контекста программы. Функторы представляют собой «функции над структурами», позволяя разрывать зависимости времени компиляции и описывать параметризованные модули. Они дают возможность типобезопасным образом описывать вычисления над компонентами программ, которые в других языках могут реализовываться только посредством метапрограммирования[23] — как шаблоны C++, только без боли и страданий[24], или макроязык Лиспа, только со статическим контролем безопасности порождаемого кода[23]. Большинство языков вовсе не имеют ничего, сравнимого с функторами[25]. Принципиальным отличием языка модулей ML является то, что результат функтора может включать не только значения, но и типы, причём они могут зависеть от типов, входящих в состав параметра функтора. Этим модули ML оказываются наиболее близки в своей выразительности к системам с зависимыми типами, но, в отличие от последних, модули ML могут быть редуцированы в плоскую Систему Fω[англ.] (см. Язык модулей ML#F-преобразование Россберга — Руссо — Дрейера). Синтаксис и синтаксический сахарСинтаксис языка очень краток, по количеству зарезервированных слов занимает промежуточную позицию между Haskell и Pascal[26]. SML имеет контекстно-свободную грамматику, хотя в ней отмечены некоторые неоднозначности. SML/NJ[англ.] использует LALR(1), но в одном месте присутствует LALR(2). Список ключевых слов языка (совпадающие с ними идентификаторы не допускаются)[27]: abstype and andalso as case datatype do
else end eqtype exception fn fun functor
handle if in include infix infixr let local
nonfix of op open orelse raise rec
sharing sig signature struct structure
then type val where while with withtype
Допустимы также символьные идентификаторы — то есть имена типов, данных и функций могут состоять из следующих небуквенных символов: ! % & $ # + - * / : < = > ? @ \ ~ ' ^ | Имена из этих символов могут быть любой длины[27]: val ----> = 5
fun !!?©**??!! x = x - 1
infix 5 $^$^$^$ fun a $^$^$^$ b = a + b
val :-|==>-># = List.foldr
Разумеется, использование таких имён на практике не желательно, но если предыдущий автор поддерживаемого кода их интенсивно применял, то благодаря формальному определению, становится возможным (и SML сам позволяет довольно легко эту задачу решить) написание препроцессора для исправления мнемоники. Исключаются лишь следующие цепочки символов: : | = => -> # :> Причина этого ограничения заключена в их особой роли в синтаксисе языка:
В SML не предусмотрено встроенного синтаксиса для массивов и векторов (константных массивов). Некоторые реализации в той или иной мере поддерживают синтаксис для массивов ( Операция присваивания записывается как в языках семейства Паскаль: Экосистема языкаСтандартная библиотекаСтандартная библиотека SML носит название Базиса (англ. Basis). Она формировалась много лет, пройдя тщательное тестирование на реальных задачах на базе SML/NJ[англ.] , её черновик был опубликован в 1996 году[28], и затем её спецификация была официально издана в 2004 году[29]. В этот период уже появлялись руководства по её использованию[30]. Базисная библиотека реализует лишь необходимый минимум модулей: тривиальные типы данных, арифметика над ними, ввод-вывод, платформенно-независимый интерфейс к операционной системе, и т. п., но не реализует более сложную функциональность (например, многопоточность). Многие компиляторы дополнительно представляют различные экспериментальные библиотеки. Компиляторы могут использовать знания о Базисе для применения заранее оптимизированных алгоритмов и специализированных техник оптимизации: например, MLton использует нативное представление типов Базиса (в точности соответствующее примитивным типам языка Си), а также простейших агрегатных типов, составленных из них. Как и в большинстве языков, в Базисе SML действует ряд определённых архитектурных и синтаксических соглашений. Прежде всего это тривиальные составляющие стандартных структур, таких как сходные по названию и сигнатурам комбинаторы (такие как Конверторы и сканерыСтандартная схема преобразования в строковый тип и обратно инкапсулирована в структуру structure StringCvt :
sig
datatype radix = BIN | OCT | DEC | HEX
datatype realfmt
= SCI of int option
| FIX of int option
| GEN of int option
| EXACT
type ('a, 'b) reader = 'b -> ('a * 'b) option
val padLeft : char -> int -> string -> string
val padRight : char -> int -> string -> string
val splitl : (char -> bool) -> (char, 'a) reader -> 'a -> (string * 'a)
val takel : (char -> bool) -> (char, 'a) reader -> 'a -> string
val dropl : (char -> bool) -> (char, 'a) reader -> 'a -> 'a
val skipWS : (char, 'a) reader -> 'a -> 'a
type cs
val scanString : ((char, cs) reader -> ('a, cs) reader) -> string -> 'a option
end
Схема преобразования не ограничивается перечислением обоснований систем счисления, как в Си ( Ключевым ингредиентом этой схемы являются ридеры, то есть значения типа (char,'b) reader -> (T,'b) reader
— то есть представляет собой конвертор из ридера символов в ридер данного типа. Сканеры входят в состав многих стандартных модулей, например, сигнатура signature INTEGER = sig
eqtype int
...
val scan : StringCvt.radix -> (char, 'a) StringCvt.reader -> 'a -> (int * 'a) option
end
Числа считываются атомарным образом, но ридеры могут считывать из потоков и цепочки поэлементно, например, посимвольно строку из строки: fun stringGetc (s) =
let
val ss = Substring.full (s)
in
case Substring.getc (ss)
of NONE => NONE
| SOME (c,ss') => SOME (c,Substring.string (ss'))
end;
stringGetc ("hello");
(* val it = SOME (#"h","ello") : (char * string) option *)
stringGetc ( #2(valOf it) );
(* val it = SOME (#"e","llo") : (char * string) option *)
stringGetc ( #2(valOf it) );
(* val it = SOME (#"l","lo") : (char * string) option *)
stringGetc ( #2(valOf it) );
(* val it = SOME (#"l","o") : (char * string) option *)
stringGetc ( #2(valOf it) );
(* val it = SOME (#"o","") : (char * string) option *)
stringGetc ( #2(valOf it) );
(* val it = NONE : (char * string) option *)
Сканеры позволяют создавать ридеры из имеющихся ридеров, например: val stringGetInt = Int.scan StringCvt.DEC stringGetc
Структура Следует отметить, что не символьные ридеры являются особым случаем ридеров вообще, а наоборот[32]. Причина этого в том, что извлечение подпоследовательности из последовательности представляет собой обобщение извлечения подстроки из строки. ПортированиеБольшинство реализаций языка достаточно строго соответствуют Определениюввода-вывода и т. д.). Однако, Определение предъявляет лишь минимальные требования к составу начального базиса, так что единственный обозримый результат корректной программы согласно Определению состоит в том, что программа завершается либо порождает исключение, и большинство реализаций совместимы на этом уровне[33]. . Различия заключаются в технических деталях, таких как бинарный формат раздельно компилируемых модулей, реализация FFI и др. На практике, реальная программа должна отталкиваться от некоего базиса (минимального набора типов, средствОднако, даже в стандартном Базисе[33], константа С определёнными усилиями возможна разработка программ, свободно портируемых между всеми актуальными реализациями языка. Примером такой программы является HaMLet .Инструментарий разработки
К настоящему времени Standard ML полностью стал достоянием общественности: все реализации являются бесплатными и открытыми и распространяются под самыми лояльными лицензиями (BSD-style, MIT); тексты Определения языка (как в версии 1990 года, так и в ревизированной версии 1997 года) и спецификации Базиса также доступны бесплатно . SML имеет большое число реализаций. Значительная их часть написана на самом SML; исключение составляют рантаймы некоторых компиляторов, написанные на Си и Ассемблере, а также система Poplog[англ.] . Компиляторы в нативный код
Верифицирующие компиляторы
Реализации более высокого уровня
Неактуальные реализации
Диалекты, расширения
SML#SML#[56] консервативно расширяет SML полиморфизмом записей в модели Ацуси Охори, который в SML# используется для бесшовного встраивания SQL в код на SML для интенсивного database-программирования. Символ решётки ( AliceAlice ML консервативно расширяет SML примитивами для конкурентного программирования на основе экзотичной стратегии вычисления «call by future (вызов по будущности)», решателем в ограничениях, а также всеми согласованными элементами проекта successor ML. В частности, Alice поддерживает модули первого класса в форме пакетов с динамической загрузкой и динамической типизацией, что позволяет реализовывать распределённые вычисления. Alice также наделяет будущности первоклассными свойствами, в том числе, предоставляя будущности уровня модулей (будущные структуры и будущные сигнатуры). Одноимённый компилятор использует виртуальную машину. Разработан и развивается в Саарландском университете под руководством Андреаса Россберга. Concurrent MLConcurrent ML (CML) — библиотека, воплощающая встраиваемый язык, который расширяет SML конструкциями конкурентного программирования высшего порядка[англ.] на основе модели синхронной передачи первоклассных сообщений. Входит в стандартную поставку компиляторов SML/NJ[англ.] и MLton. Ключевые идеи CML лежат в основе проекта Manticore и включены в проект successor ML[11]. ManticoreManticore[40] реализует всестороннюю поддержку конкурентного и параллельного программирования, от логической декомпозиции системы на процессы до тонкого контроля за максимально эффективным использованием многоядерных систем. Manticore основан на подмножестве SML, исключая мутабельные массивы и ссылки, то есть является чистым языком, сохраняя строгий порядок вычисления. Механизмы явной конкурентности и грубого параллелизма (потоки) основаны на CML , а механизмы тонкого параллелизма уровня данных (параллельные массивы) — аналогичны NESL[англ.]. Одноимённый компилятор порождает нативный код. MLPolyRMLPolyR — игрушечный язык, основанный на простейшем подмножестве SML и дополняющий его несколькими уровнями типобезопасности. Целью проекта является глубокое исследование полиморфизма записей для нужд проекта successor ML. Инновационная система типов MLPolyR решает проблему выражения[англ.] и гарантирует отсутствие необработанных исключений в программах. Разработан под руководством Матиаса Блюма (автора NLFFIТехнологическом Институте Тойота в Чикаго[англ.], США. ) вMythrylMythryl[57] — синтаксический вариант SML, нацеленный на повышение скорости разработки под POSIX. Новый синтаксис во многом заимствован из Си; терминология также пересмотрена под более традиционную (например, функторы переименованы в дженерики). При этом авторы подчёркивают, что не намерены создать «очередную свалку языковых возможностей», а придерживаются минималистичной природы SML и опираются на его Определение . Реализация является форком SML/NJ[англ.] . Прочие
Утилиты
Межъязыковое взаимодействие
Идеоматика, соглашенияК оформлению программ на SML не предъявляется никаких требований, поскольку грамматика языка полностью контекстно-свободна и не содержит явных неоднозначностей. Тем не менее, в ней отмечаются частные проблемы, например, при передаче оператора умножения Однако, всё же существуют определённые рекомендации, нацеленные на улучшение читабельности, модульности и повторного использования кода, а также раннего обнаружения ошибок и повышения модифицируемости (но не для внесения информации о типах в идентификаторы, как это делается, например, в венгерской нотации)[64]. В частности, в SML рекомендуется правило именования идентификаторов уровня ядра языка, аналогичное тому, что требуется в Haskell: Наиболее непривычными и неожиданными могут быть:
ПроцедурыДля процедур принята та же идиома, что и в Си: процедуры представляются функциями, возвращающими значение единичного типа[англ.]: fun p s = print s
(* val p = fn : sting -> unit *)
Последовательные вычисления
let D in E end
fun foo ... =
let
val _ = ...
in
...
end
ПриёмыЭта-расширениеЭта-расширение (англ. eta-expansion) выражения Значения, индексированные типамиЗначения, индексированные типами (англ. type-indexed values) — это техника, позволяющая ввести в SML поддержку ad-hoc-полиморфизма (которая в нём изначально отсутствует)[69]. Существует целый ряд её вариантов, в том числе нацеленные на поддержку полноценного объектно-ориентированного программирования[17]. Fold«Fold»[70] — это техника, позволяющая ввести в SML ряд распространённых идиом, включая функции с переменным числом аргументов, именованные параметры функций, значения параметров по умолчанию, синтаксическую поддержку массивов в коде, функциональное обновление записей и косметическое изображение зависимой типизации для обеспечения типобезопасности функций вроде Принцип Необходимо определить три функции — fold (a, f) (step0 h1) (step0 h2) ... (step0 hn) $
= f (hn (... (h2 (h1 a))))
Их минимальное определение немногословно: fun $ (a, f) = f a
structure Fold =
struct
fun fold (a, f) g = g (a, f)
fun step0 h (a, f) = fold (h a, f)
end
Более развитая реализация позволяет контролировать типы выражений с использованием Fold. Пример: переменное число аргументов функции val sum = fn z => Fold.fold (0, fn s => s) z
fun a i = Fold.step0 (fn s => i + s)
...
sum (a 1) (a 2) (a 3) $
(* val it : int = 6 *)
Пример: списковые литералы val list = fn z => Fold.fold ([], rev) z
val ‘ = fn z => Fold.step1 (op ::) z
...
list ‘w ‘x ‘y ‘z $
Пример: (косметически) зависимые типы val f = fn z => Fold.fold ((), id) z
val a = fn z => Fold.step0 (fn () => "hello") z
val b = fn z => Fold.step0 (fn () => 13) z
val c = fn z => Fold.step0 (fn () => (1, 2)) z
...
f a $ = "hello": string
f b $ = 13: int
f c $ = (1, 2): int * int
Примеры программ
Hello World!Простейшая программа на SML может быть записана в одну строку: print "Hello World!\n"
Однако, учитывая ориентированность языка на крупномасштабное программирование[англ.], минимальной всё же следует считать её обёртку в язык модулей (некоторые компиляторы работают только с программами уровня модулей). Подробности signature HELLO_WORLD =
sig
val helloworld : unit -> unit
end
structure HelloWorld : HELLO_WORLD =
struct
fun helloworld () = print "Hello World!\n"
end
Точкой старта программы, вообще говоря, может быть выбрана любая функция, но на практике имеет смысл соблюдать общепринятые соглашения, поэтому стоит добавить такой код: structure Main =
struct
fun main (name: string, args: string list) : OS.Process.status =
let
val _ = HelloWorld.helloworld ()
in
OS.Process.success
end
end
Для компилятора SML/NJ[англ.] в структуру val _ = SMLofNJ.exportFn ("project1", main);
Для многомодульных программ требуется также создать файл проекта для отслеживания зависимостей в менеджере компилятора (некоторые компиляторы делают это автоматически). Например, для SML/NJ следует создать файл с именем Group
signature HELLO_WORLD
structure HelloWorld
is
helloworld-sig.sml
helloworld.sml
end Более универсальным с точки зрения поддержки различными компиляторами (но несколько более ограниченным по возможностям) вариантом может быть создание обычного файла исходного кода на SML с линейным перечислением подключаемых файлов: use "helloworld-sig.sml";
use "helloworld.sml";
Выходной машинный код для минимальной программы также получается относительно крупным (в сравнении с реализаций Hello World на Си), поскольку даже самая маленькая программа на SML обязана включать в себя рантайм-систему языка, бо́льшую часть которой составляет сборщик мусора. Однако, не следует воспринимать размер исходного и машинного кодов на начальном этапе как тяжеловесность SML: их причиной является интенсивная ориентированность языка на разработку крупных и сложных систем. Дальнейшее наращивание программ происходит по существенно более пологой кривой, чем в большинстве других статически типизируемых языков, и оверхед становится едва заметным при разработке серьёзных программ[71]. Автоматическая вёрсткаfun firstLine s =
let
val (name, rest) =
Substring.splitl (fn c => c <> #".") (Substring.full s)
in
"\n<P><EM>" ^ Substring.string name ^ "</EM>" ^ Substring.string rest
end
fun htmlCvt fileName =
let
val is = TextIO.openIn fileName
and os = TextIO.openOut (fileName ^ ".html")
fun cvt _ NONE = ()
| cvt _ (SOME "\n") = cvt true (TextIO.inputLine is)
| cvt first (SOME s) =
( TextIO.output (os,
if first then firstLine s
else "<br>" ^ s);
cvt false (TextIO.inputLine is)
)
in
cvt true (SOME "\n"); TextIO.closeIn is; TextIO.closeOut os
end
Этот код преобразует по простейшим правилам плоский текст в HTML, оформляя диалог по ролям[72]. Демонстрация работы Допустим, есть следующий текстовый файл с именем Westmoreland. Of fighting men they have full three score thousand.
Exeter. There's five to one; besides, they all are fresh.
Westmoreland. 0 that we now had here
But one ten thousand of those men in England
That do no work to-day!
King Henry V. What's he that wishes so?
My cousin Westmoreland? No, my fair cousin:
If we are marked to die, we are enough
To do our country loss; and if to live,
The fewer men, the greater share of honour. Тогда вызов программы следующей строкой: val _ = htmlCvt "Henry.txt"
Создаст файл с именем <P><EM>Westmoreland</EM>. Of fighting men they have full three score thousand.
<P><EM>Exeter</EM>. There's five to one; besides, they all are fresh.
<P><EM>Westmoreland</EM>. 0 that we now had here
<br>But one ten thousand of those men in England
<br>That do no work to-day!
<P><EM>King Henry V</EM>. What's he that wishes so?
<br>My cousin Westmoreland? No, my fair cousin:
<br>If we are marked to die, we are enough
<br>To do our country loss; and if to live,
<br>The fewer men, the greater share of honour. Этот файл можно открыть в браузере, увидев следующее:
Троичные деревьяДля задачи поиска строки в словаре троичные деревья[англ.] сочетают молниеносную скорость префиксных деревьев с экономичностью двоичных деревьев в отношении памяти. type key = Key.ord_key
type item = Key.ord_key list
datatype set = LEAF | NODE of { key: key, lt: set, eq: set, gt: set }
val empty = LEAF
exception AlreadyPresent
fun member (_, LEAF) = false
| member (h::t, NODE {key,lt,eq,gt}) =
(case Key.compare (h, key) of
EQUAL => member(t, eq)
| LESS => member(h::t, lt)
| GREATER => member(h::t, gt) )
| member ([], NODE {key,lt,eq,gt}) =
(case Key.compare (Key.sentinel, key) of
EQUAL => true
| LESS => member([], lt)
| GREATER => member([], gt) )
fun insert(h::t, LEAF) = NODE { key=h, eq = insert(t, LEAF), lt=LEAF, gt=LEAF }
| insert([], LEAF) = NODE { key=Key.sentinel, eq=LEAF, lt=LEAF, gt=LEAF }
| insert(h::t, NODE {key,lt,eq,gt}) =
(case Key.compare (h, key) of
EQUAL => NODE {key=key, lt=lt, gt=gt, eq=insert(t, eq)}
| LESS => NODE {key=key, lt=insert(h::t, lt), gt=gt, eq=eq}
| GREATER => NODE {key=key, lt=lt, gt=insert(h::t, gt), eq=eq} )
| insert([], NODE {key,lt,eq,gt}) =
(case Key.compare (Key.sentinel, key) of
EQUAL => raise AlreadyPresent
| LESS => NODE {key=key, lt=insert([], lt), gt=gt, eq=eq}
| GREATER => NODE {key=key, lt=lt, gt=insert([], gt), eq=eq} )
fun add(l, n) = insert(l, n) handle AlreadyPresent => n
Этот код использует Базисную структуру datatype order = LESS | EQUAL | GREATER
О языкеБыстродействиеТипичные преимущества функционального программирования (типобезопасность, автоматическое управление памятью, высокий уровень абстракции и др.) проявляются в обеспечении надёжности и вообще работоспособности программ, а в ответственных, особенно крупномасштабных[англ.] задачах быстродействие часто играет второстепенную роль. Упор на эти свойства исторически привёл к тому, что программистам на функциональных языках зачастую оказываются недоступны многие эффективные структуры данных (массивы, строки, битовые цепочки), поэтому функциональные программы обычно заметно менее эффективны, чем эквивалентные программы на Си.[73] ML изначально предоставляет весьма неплохие средства для тонкого контроля за быстродействием , однако, исторически реализации ML были крайне медлительными. Тем не менее, ещё в начале 1990-х Эндрю Аппель[англ.] прочил[74] языку SML скорость исполнения выше скорости языка Си, по крайней мере при интенсивной работе со сложноструктурированными данными (но SML не претендует на то, чтобы быть заменой Си в задачах системного программирования). За следующие несколько лет напряжённая работа над развитием компиляторов привела к тому, что скорость исполнения программ на SML повысилась в 20-40 раз[75]. В конце 1990-х Стивен Уикс задался целью добиться от программ на SML максимально возможной производительности и написал дефункторизатор для SML/NJ[англ.] , сразу показавший прирост скорости ещё в 2-3 раза. Дальнейшая работа в этом направлении привела к созданию компилятора MLton , который к середине нулевых годов XXI века показывал прирост скорости перед другими компиляторами в среднем на два порядка[45], конкурируя с Си (подробнее см. MLton). Стратегия автоматического управление памятью на основе вывода регионов позволяет устранять затраты на инициализацию и высвобождение памяти из исполнения программы (то есть реализует сборку мусора на этапе компиляции). Компилятор ML Kit использует эту стратегию, позволяя решать задачи реального времени, хотя он уступает MLton по возможностям оптимизации. На основе фронт-енда SML/NJ[англ.] был разработан компилятор в исходный код на Си — sml2c . Он порождает код хорошего качества, но примечательно, что схема компиляции «сначала в Си, затем в машинный код» до двух раз снижает быстродействие по сравнению с прямой компиляцией SML в машинный код из-за семантических различий между SML и Си[5]. Некоторые компиляторы SML предоставляют возможность профилирования кода с целью определения функций, занимающих наибольшее процессорное время (причём результат всегда неожиданный)[73], после чего можно сосредоточиться на их оптимизации средствами SML, либо вынести их в код на Си через FFI . Обоснование семантикиОбщие сведенияТеоретической основой языка является полиморфно типизированное лямбда-исчисление (Система F), ограниченное Let-полиморфизмом. «Определение» (The Definition)Официальным «стандартом» языка является изданное в виде книги «Определение» (англ. The Definition). Определение сформулировано в строгих математических понятиях и имеет доказанную надёжность. Непротиворечивость Определения позволяет человеку без запуска конкретного компилятора проверить программу на корректность и вычислить её результат; но, с другой стороны, Определение требует высокой квалификации для понимания и не может служить учебником по языку[74]. Доказуемость надёжности не далась сама по себе — Определение было неоднократно пересмотрено прежде, чем увидело свет. Многие языки опираются на общие теории, но при разработке они почти никогда не проверяются на безопасность совместного использования конкретных языковых элементов, являющихся частными приложениями этих теорий, что неизбежно приводит к несовместимости между реализациями языка. Эти проблемы либо игнорируются, либо начинают преподноситься как естественное явление (англ. «not a bug, but a feature»), но в действительности их причиной является то, что язык не был подвергнут математическому анализу[76]. Подробности Первоначальное Определение, «The Definition of Standard ML», было издано в 1990 году[2]. Спустя год были изданы «Комментарии к Определению» («Commentary on Standard ML»), объясняющие применённые подходы и нотации[77]. Вместе они составляют спецификацию языка, ныне известного как «SML'90». В течение следующих лет появился ряд критических замечаний и предложений по улучшению (одним из наиболее известных среди которых являются просвечивающие суммы Харпера — Лилибриджа), и в 1997 году многие из них были собраны в ревизированной версии Определения, «The Definition of Standard ML: Revised»[3], определив версию языка «SML'97», сохраняющего обратную совместимость с прежним. Ревизированное Определение использует принципы, описанные в Комментариях 1991 года, так что намеревающимся досконально изучить Определение SML рекомендуется сначала изучать SML'90, и лишь затем SML'97.[78] Со временем в тексте Определения был обнаружен ряд неоднозначностей и упущений[79][80][81]. Однако, они не умаляют строгости Определения по сути — доказательство его надёжности было механизировано в Twelf[англ.][82]. Большинство реализаций достаточно строго соответствуют Определению, отклоняясь в технических особенностях — бинарных форматах, FFI и пр., а также в особенностях трактовки неоднозначных мест в Определении — всё это приводит к необходимости приложения некоторых дополнительных усилий (существенно меньших, чем для большинства других языков) для обеспечения безупречной портируемости реальных программ на SML между реализациями (небольшие программы в большинстве случаев не имеют проблем портирования). Определение SML является примером структурной операционной семантики[англ.]!!; оно является не первым формальным определением языка, но первым, однозначно понятным разработчикам компиляторов[83]. Определение оперирует семантическими объектами (semantic objects), описывая их смысл (meaning). Во введении авторы делают акцент на том, что именно семантические объекты (к числу коих, в зависимости от конкретного языка, могут относиться такие понятия как пакет, модуль, структура, исключение, канал, тип, процедура, ссылка, соиспользование и пр.), а не синтаксис, определяют концептуальное представление о языке программирования, и именно на них должно строиться определение всякого языка[84]. Содержание Согласно Определению, SML разделяется на три языка, надстроенные один над другим: нижний слой, называемый «Ядром языка» (Core language), средний слой, называемый «Модулями» (Modules) и небольшой верхний слой, называемый «Программами» (Programs), представляющий собой совокупность определений верхнего уровня (top-level declarations). Определение включает около 200 правил вывода (inferrence), записываемых в виде обыкновенной дроби, где в позиции числителя стоит формализованная фраза ML, а в позиции знаменателя — следствие, которое можно заключить, если фраза корректна. Определение различает три основные фазы в языке[85][86]: разбор (parsing), выработка (elaboration) и вычисление (evaluation). Выработка относится к статической семантике; вычисление — к динамической. Но вычисление здесь не следует путать с исполнением (execution): SML является языком, основанным на выражениях (expression-based language), и получение результата от применения функции ко всем аргументам называется исполнением (execution), а «вычисление функции» означает построение определения её самой. Следует также обратить внимание, что поддержка каррирования в языке означает представление всех функций замыканиями, а это, в свою очередь, означает, что использовать термин «вызов функции» некорректно. Вместо вызова следует говорить о применении функции (function application) — функция просто не может быть вызвана, пока не получит все аргументы; частичное применение функции означает вычисление новой функции (нового замыкания). Для каждого из слоёв языка (Ядро, Модули, Программы) отдельно описывается статическая и динамическая семантика, то есть этапы разбора, выработки и вычисления. Конкретная реализация языка не обязана проводить все эти различия, они несут лишь формальный характер[86]. Фактически, единственная реализация, которая стремится строго их соблюдать — это HaMLet . В частности, выработка без вычисления означает традиционное понятие о компиляции. Вычисление каждого определения по ходу программы меняет состояние глобального окружения (top-level environment), называемого базисом. Формально, исполнение программы представляет собой вычисление нового базиса как суммы начального базиса и определений программы. Стандартная библиотека в SML представляет собой «базис по умолчанию», доступный всякой программе от её начала, и потому называется просто Базисом. Само Определение содержит только начальный базис (initial basis), содержащий минимально необходимые определения; более обширный Базис был стандартизирован много позже, пройдя длительную отработку на практике . Семантика Харпера — СтоунаСемантика Харпера — Стоуна (сокращённо «семантика Х-С», англ. HS semantics) представляет собой интерпретацию SML в типизированной системе (typed framework). Семантика SML по Х-С определяется через выработку внешнего языка SML во внутренний язык, представляющий собой явно типизированное лямбда-исчисление, и, таким образом, служит теоретико-типовым обоснованием языка. Эта интерпретация может рассматриваться как альтернатива Определению , формализующая «статические семантические объекты» через выражения типизированного лямбда-исчисления; а также как декларативное описание правил выработки для компиляторов, управляемых типами (англ. type-directed compilers), таких как TILT или SML/NJ . Фактически, фронт-енд компилятора TILT воплощает эту семантику, хотя он был разработан на несколько лет раньше.[87][88][89] Внутренний язык основан на языке XML Харпера — Митчела, но имеет более обширный набор примитивов и более выразительную систему модулей, основанную на просвечивающих суммах Харпера — Лилибриджа. Этот язык пригоден для выработки многих других языков, семантика которых основана на лямбда-исчислении, таких как Haskell и Scheme. Применение этого подхода заложено в проект successor ML. При этом, изменения в языке, не влияющие на внутренний язык, рассматриваются как кратковременная перспектива (англ. short-term), а требующие его изменения — как долговременная перспектива (англ. long-term). Критика и сравнение с альтернативами
Разработчики SML изначально установили самую высокую планку требований качества для языка, так что и порог критики располагается намного выше, чем у большинства промышленных языков. Упоминания о недостатках языка SML встречаются в официальной печати так же часто, как и языка C++, и много чаще большинства других языков, но причина вовсе не в негативном отношении к SML — напротив, всякая критика SML производится с очень тёплым отношением к нему. Даже педантичный разбор недостатков SML обычно сопровождается его описанием как «изумительного языка, единственного серьёзного из существующих»[90]. Иначе говоря, исследователи досконально копаются в недостатках, подразумевая, что даже с их учётом SML оказывается более предпочтителен для применения в гигантских наукоёмких проектах, чем многие более популярные языки, и желая довести SML до совершенства.
Недостатки Основной проблемой для разработчика на SML на сегодняшний день является скудный уровень развития окружения (особенно IDE) и библиотечных наработок. Безопасность SML означает оверхед на арифметику: из-за требования, что всякая операция должна иметь идентичное поведение на любой платформе, контроль переполнения, деления на ноль и т. п. являются неотъемлемыми компонентами всякой арифметической операции. Это делает язык неэффективным выбором для задач числодробилки, особенно для конвейерных архитектур[91]. Сравнение с OCaml: OCaml является ближайшим родственником SML, отделившимся от него ещё до стандартизации. OCaml настолько шире развит, что его иногда шутливо называют «SML++». В массовом программировании OCaml существенно опережает SML по популярности; в академических кругах SML значительно чаще является объектом научных изысканий. Ведущий разработчик OCaml Ксавье Леруа является членом совета по successor ML. OCaml имеет единственную реализацию, включающую два компилятора (в байт-код и в натив), совместимых почти идентично, которая непрерывно эволюционирует, предлагая не только всё лучшее окружение, но и всё более мощные семантические возможности. SML имеет множество реализаций различной направленности, следующих единому Определению языка и Базисной библиотеки, и иногда предлагающих дополнительные возможности. Важнейшие отличия состоят в семантике эквивалентности типов. Во-первых, в SML функторы являются порождающими, а в OCaml — аппликативными (см. эквивалентность типов в языке модулей ML). Во-вторых, OCaml не поддерживает переменные типа, допускающего проверку на равенство (equality type variable): операция проверки на равенство принимает объекты любых типов, но порождает исключение, если они несовместимы. OCaml современных версий включает семантические возможности, доступные лишь по отдельности в некоторых расширениях SML, например:
Сравнение с Haskell: Haskell является наследником ML/SML (в этом смысле обычно не проводится принципиальных различий между ML и SML). Оба языка основаны на системе типов Хиндли — Милнера, включая выведение типов, из чего следует масса общих черт[95] (первоклассные функции, типобезопасный параметрический полиморфизм, алгебраические типы данных и сопоставление с образцом на них). Среди ключевых отличий можно назвать[95][96][97][98][99][68][100]:
История, философия, терминология
Формальная семантика SML ориентирована на интерпретацию, однако большинство его реализаций является компиляторами (в том числе интерактивными компиляторами), некоторые из которых уверенно соперничают по эффективности с языком Си, так как язык хорошо поддаётся глобальному анализу. По той же причине SML может компилироваться в исходный код на других языках высокого или среднего уровня — например, существуют компиляторы из SML в Си и Ada. В основе языка лежит сильная статическая полиморфная типизация, которая не только обеспечивает верификацию программ на этапе компиляции, но и жёстко обособляет мутабельность, что само по себе повышает потенциал к автоматической оптимизации программ — в частности, упрощает реализацию сборщика мусора[104]. Первая версия ML была представлена миру в 1974 году в роли мета-языка для построения интерактивных доказательств в составе системы Edinburgh LCF (Logic for Computable Functions)[англ.][2]. Её реализовали Малькольм Ньюи, Локвуд Моррис и Робин Милнер на платформе DEC10. Первая реализация была крайне неэффективной, так как конструкции ML транслировались в Lisp, который затем интерпретировался[105]. Первое полное описание ML как компонента LCF издано в 1979 году[2]. Около 1980 года Лука Карделли[англ.] реализовал первый компилятор Vax ML, дополнив ML некоторыми своими идеями. Вскоре Карделли портировал Vax ML на Unix, используя Berkley Pascal. Среда выполнения была переписана на Си, но большая часть компилятора оставалась на Паскале. Работа Карделли вдохновила Милнера на создание SML как самостоятельного языка общего назначения, и они начали совместную работу в Эдинбурге, результатом чего стал компилятор Edinburgh ML , выпущенный в 1984 году. В процессе этой работы Майк Гордон придумал ссылочные типы и предложил их Луи Дамасу, который позже защитил на них диссертацию[106]. Одновременно велось сотрудничество Кэмбриджа с INRIA. Жерар Хью из INRIA портировал ML на Maclisp под Multics. INRIA разработали собственный диалект ML, названный Caml, и впоследствии развившийся в OCaml . Лоуренс Полсон[англ.] оптимизировал Edinburgh ML , так что код на ML стал работать в 4-5 раз быстрее. Вскоре после этого Дэвид Мэтьюз разработал язык Poly на основе ML. Дальнейшая работа в этом направлении привела к созданию среды Poly/ML . В 1986 Дэвид МакКуин сформулировал язык модулей ML, и к работе подключился Эндрю Аппель[англ.]. Совместно они начали вести работу над компилятором SML/NJ[англ.], который служил одновременно исследовательской платформой для развития языка и первым промышленным оптимизирующим компилятором. Многие из реализаций языка были первоначально разработаны с использованием SML/NJ[англ.] и затем раскручены. С опытом крупномасштабных разработок был обнаружен ряд недочётов в Определении языка от 1990 года.[3], но рамки ревизии исключают потерю обратной совместимости (коды адаптируются косметически, без необходимости переписывания с нуля). В 2004 году была опубликована спецификация на состав Базисной библиотеки (черновик спецификации датируется 1996 годом). Другие недостатки были устранены в других языках: ML породил целое семейство Х-М-типизированных языков. Эти языки завоевали популярность на задаче разработке языков и часто определяются как «DSL для денотационной семантики[англ.]». Исследователи, занимавшиеся развитием и использованием SML на протяжении почти трёх десятилетий, к концу XX века сформировали сообщество по созданию нового языка — successor ML. Часть недостатков была устранена в Ревизии Определения от 1997 годаФактически, SML не был первым в семействе после собственно LCF/ML — ему предшествовали такие языки как Cardelli ML и Hope[9]. Французы поддерживают собственный диалект — Caml/OCaml[12]. Тем не менее, говоря «ML», многие подразумевают «SML»[107], и даже пишут через дробь: «ML/SML»[82]. ИзучениеНаиболее рекомендуемым[108][109][110][111][112][113] учебником по SML является книга Лоуренса Полсона[англ.] (автора системы HOL[англ.]) «ML for the Working Programmer»[107]. Для первоначального ознакомления с языком может быть полезен краткий (несколько десятков страниц) курс Роберта Харпера[англ.] «Introduction to Standard ML» (доступный в русском переводе[114]), который он использовал для преподавания языка и за следующие два десятилетия расширил до более крупного учебника[115]. Учебным руководством по использованию стандартной библиотеки языка, предполагающим его базовое знание, служит книга Рикардо Пуцеллы[30]. В числе других учебников можно назвать книги Гилмора[116], Ульмана[117], Шипмана[118], онлайн-книгу Камминга[119]. Среди руководств по профессиональному использованию языка можно выделить книгу Эндрю Аппеля[англ.] (ведущего разработчика SML/NJ[англ.]) «Modern compiler implementation in ML»[120] (у этой книги есть две сестры-близняшки: «Modern compiler implementation in Java» и «Modern compiler implementation in C», эквивалентные по структуре, но использующие другие языки для воплощения излагаемых методов). Имеется также масса статей, издаваемых в таких журналах, как JFP, «ML workshop» и др.[121][122] ПрименениеSML, наряду с OCaml, служит первым языком преподавания обучения программированию во многих университетах мира. Среди аппликативных языков они имеют, вероятно, самый низкий собственный порог вхождения. Значительная часть существующих кодов на SML представляет собой либо реализацию его же компиляторов, либо системы автоматического доказательства, такие как HOL[англ.], Twelf[англ.] и Isabelle (система автоматического доказательства теорем). Все они свободны и открыты. Тем не менее, существуют и более «приземлённые», в том числе проприетарные продукты[123]. Примечания
ЛитератураСтандарты
Учебники, руководства, справочники, использование
История, анализ, критика
Прочее
Ссылки
|