1
Fork 0
mirror of https://github.com/thegeneralist01/fphistoryru synced 2026-01-10 14:10:24 +01:00
fphistoryru/compilers.md
2024-03-31 21:48:32 +05:00

104 KiB
Raw Blame History

История применения и оценки функционального программирования.

Часть 1: Первые компиляторы.

Как мы выяснили в предыдущей - нулевой - части истории, к началу 80-х уже появился функциональный язык, похожий на современные - HOPE. Но имплементаторы ФЯ не все и не сразу захотели имплементировать языки, похожие на HOPE. Были желание и, главное, возможность имплементировать более необычные для нас функциональные языки.
Поэтому в первые годы десятилетия появилось несколько имплементаций ФЯ, которые только позднее стали имплементациями ФЯ, похожих на HOPE. Или были использованы как части таких имплементаций. Первая часть истории функционального программирования будет о них.
Наконец-то прошли времена, когда никакие идеи по имплементации функциональных языков не работали. С появлением относительно распространенных компьютеров с большой памятью заработали все. Конечно, не все одинаково быстро и одинаково хорошо.
На смену этим имплементациям придет новое поколение компиляторов, многие из которых будут написаны на языках, имплементированных с помощью первых компиляторов функциональных языков, использовать их в качестве бэкендов. Будут использовать первые имплементации для бутстрапа или, хотя-бы, прототипирования. Таким образом сформируются главные единицы нашей истории ФП - семейства имплементаций. В отличие от имплементаций ФЯ, большинство из которых давно заброшены, большинство семейств имплементаций живы и сегодня, хотя и могут влачить довольно жалкое существование.
Мы начинаем историю функционального программирования с компилятора, разработка которого началась в одном из важнейших мест, в которых разворачивалась предыстория.

От Луки

Прежде всего, следует понять, что, по крайней мере, потенциально, ML может быть скомпилирован очень эффективно.
Л. Карделли, Компиляция функционального языка [Card84].

В то время, когда авторы HOPE позиционировали свой язык как едва исполняемую спецификацию, в том же Эдинбурге более известный другими своими работами Лука Карделли начал работу на более амбициозным проектом.

Лука Карделли

Лука Карделли (Luca Andrea Cardelli) изучал компьютерные науки в Пизанском Университете (University of Pisa) в 73-78 годах. В том же городе, где шли работы над одним из первых языков уравнений TEL. Там Карделли получил доступ к IBM 370. Карделли хотел писать на Algol 68 [Card07], но на машине не было такого компилятора. Уже знакомый нам Algol68C, хоть и заработал на IBM 370 в конце 73-го [Birr77], но зарелизили его только в начале 80-го [ALGOL80]. Первый компилятор Algol 68 для IBM 370 со сборщиком мусора - FLACC - анонсировали в 78-ом году [Thom78]. Единственный компилятор, имплементировавший окончательный Algol 68 полностью, коммерциализированная версия компилятора FLASC, бывшего магистерской диссертацией [Thom76] как и RABBIT, генерирующий не такой быстрый код, как у компиляторов из Кембриджа и Молверна из-за большого числа проверок времени исполнения.
Все слишком поздно для Карделли. Как он говорит в 2007-ом году - "к счастью" [Card07]. Но в середине 70-х Карделли, как и Уоррену, Алгол 68 нравился. Большинство прочих доступных языков: Fortran, APL, Lisp 1.5, PL/1 Карделли не нравились вовсе. Оставался только один язык, которому, как вспоминает Карделли не было никакой альтернативы - SIMULA 67. На IBM 370 компилятор Симулы был доступен с мая 72-го [Holm98]. Как раз вовремя! Не смотря на то, какими работами Карделли наиболее известен, ООП ему не особенно понравилось. Понравилась ему сборка мусора, типизация, поддержка строк в языке. Классы он использовал для кодирования индуктивных типов и не считал особенно удобными для этого.
Карделли вспоминает, что в студенческие годы познакомился с функциональным программированием и Схемой, но не упоминает Схему как язык-кандидат, рассматриваемый им для какого-то практического использования. Как мы писали в главе про уравнения, в Пизе работали люди, которые интересовались результатами Эдинбургской исследовательской программы, но Карделли, по видимому, ознакомился с Эдинбургскими работами только в Эдинбурге.
Карделли прибыл в Эдинбург и стал работать над диссертацией в ноябре 78-го [Card21] [MacQ15] под руководством Плоткина. Это плохо согласуется с тем, что остатки бывшей Группы Экспериментального Программирования влились в Департамент Компьютерных Наук Эдинбургского Университета через год после этого, в конце 79. Может быть, Плоткин сделал это раньше Бурсталла, а может быть это событие датировано неправильно. Если это слияние двух ветвей Эдинбургской программы произошло раньше - некоторые странности и несоответствия истории HOPE исправляются, но слабое влияние HOPE на ранние работы Карделли становится еще более странным. Это слабое влияние, впрочем, делает сам вопрос не очень важным.
Также Карделли изучил Паскаль и посчитал его "приятным языком для имплементации". И это было не очень популярное мнение в Эдинбурге в частности и среди имплементаторов функциональных языков вообще. Судя по тому, что долгое время не было много желающих работать над программой, которую он писал на Паскале. Что он писал на Паскале?
Диссертация Карделли была вовсе не об имплементации компилятора языка общего назначения, как у Уоррена или Стила, а об описании и верификации железа [Card82d]. Какое он имеет отношение к нашей истории? Карделли писал программы для форматирования текста своей диссертации [MacQ14]. И для новой машины Эдинбургского Университета, VAX-11/780, нет компилятора Симулы, которой Карделли привык пользоваться, пока был студентом в Италии. Не беда. Карделли вспоминает [Card07], что в Эдинбурге "наконец-то" узнал о первом языке программирования, который он посчитал "приятным". Об ML.
К сожалению, ML - это не язык на котором можно писать какие-то программы для обработки текстов. И первоначальные авторы не собираются его таким делать [Miln93] [Miln2003]. Но Карделли решил: Почему нет? Почему бы мне не имплементировать первый компилятор для функционального языка, на котором я хочу писать код для форматирования текста моей диссертации? На Паскале - языке, приятном языке для имплементации ФЯ. И имплементировал. Потому что мог.

Пари PASCAL

На самом-то деле - поясняет Карделли - ML можно компилировать в быстрый код [Card84]. У функционального языка Эдинбургской программы ряд важных свойств, облегчающих его эффективную имплементацию:

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

Как и Уоррен, Карделли посчитал решениями многое из того, что его предшественники считали проблемами.
Но не все так радужно, для эффективной компиляции ФЯ есть и препятствия, основное из которых - полиморфизм. Полиморфизм требует универсального представления, боксинга (почти) всех данных, чтоб они были одинаково представлены - как указатели, что не полезно для производительности и потребления памяти.
Типы данных, которые можно описывать в LCF/ML тоже имеют плохое представление в памяти. Потому, что собираются из пар как придумал МакКарти и типизировал Моррис.
Та же проблема и у структур в памяти, которые создают имплементации ФЯ. Все эти стеки-списки, окружения-списки, замыкания-списки желательно заменить на плоские структуры.
Наконец, использовать абстрактные типы данных для представления изменяемых структур данных в LCF/ML нельзя из-за ограничений letref.
Нужно решить массу проблем.

FAM

К октябрю 1980 Карделли описал виртуальную машину для имплементации ML и прочих ФЯ на машинах с большой памятью. Машину он называл сначала AM [Card80], а позднее FAM (Functional Abstract Machine). Это SECD, в которой используемые там для всего списки заменены на массивы и сделаны некоторые изменения для более быстрой имплементации функций. Эти изменения:

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

Замыкание представлено в памяти массивом ссылок. То, что собирался делать Стил в RABBIT, да так и не собрался. Замыкания должны разделять между собой мутабельные ячейки и замыкания из списков (как в SECD и в RABBIT) позволяют осуществить такое. Но цена не устраивала Карделли. Поскольку он предполагал, что использоваться будут, в основном, неизменяемые переменные он выбрал имплементацию, которая делает дороже использование изменяемых переменных: значения изменяются не в массиве, представляющем замыкание, а в ячейке кучи, на которую есть указатель в этом массиве. Замыкания "плоские", но только для неизменяемых переменных. Имплементация Схемы или языков вроде обоекембриджских ISWIM-ов PAL и McG потребовала бы для этого нелокального анализа, но в LCF/ML мутабельность явная. Удобно!
Массивы доступны и для пользователя виртуальной машины. И массивы обычных объектов и специальные строки.
(F)AM использовала более компактное представление данных [Card80] [Card83], чем LCF/ML.
В слово на VAX-11 помещается только один указатель, так что, анбоксинг не требует ухищрений, которые были необходимы на PDP-10. Вместо указателя на объект в каждое слово можно поместить целое число довольно полезного размера, хотя полным 32-битным диапазоном придется пожертвовать. Разумеется, один бит нужно потратить на то, чтоб отличать указатель от числа или еще какого-нибудь значения, которое можно закодировать как целое число.
Один бит используется для этого сегодня в OCaml, но Карделли различает указатели и целые числа не так. Карделли умещает на место указателя существенно менее полезное число - только 16-бит. Все, что больше 65536 - указатель.
Дополнять эти некрупные целые должны были сначала 32-битные числа с плавающей точкой в куче [Card80]. Позднее Карделли передумал и решил имплементировать длинные целые [Card83]. Предполагалось, что значения одного и того же типа могут быть представлены как 32-бит слово с 16-бит. целым числом или как указатель на массив в куче, представляющий настолько длинное число, насколько длинным позволяет быть ограничение на размер массива. Массивы, разумеется, разрешены не очень большие - до 64Кб.
Но поддержка длинных целых не будет имплементирована до 84-го года [Card84b]. Все это время ML Карделли будет поддерживать только 16-бит целые числа и никакие другие.
Как небольшие целые числа кодируются и некоторые другие значения: пустой список, true и false, аналог хаскельного типа () [Card80] [Card83].
Также машина поддерживает рекорды со многими полями, они не ограничены парами. Но представление для вариантов все еще как у МакКарти, требует лишний уровень косвенности, как и изменяемая ссылка. Это ссылка на пару из ссылки на объект кучи и тега. Почему-то именно в таком порядке [Card83].
Описывая виртуальную машину, Карделли отмечает, что сборщик мусора надо использовать с поколениями, как у Хьюита и Либермана. И ссылается на более позднюю версию их отчета [Lieb81]. Но сначала сборщик мусора - это только планы, что уже не должно удивлять читателя. Когда же планы воплощены в реальность - сборщик все равно был не сборщиком Хьюита и Либермана. Карделли имплементировал копирующий сборщик [Card82a]. Рекурсивный, даже не алгоритм Чейни, так что сборщик падал, обходя списки в тысячи элементов [Card84b]. Сначала размер кучи не менялся. Рантайм Карделли занимал как минимум 1Мб памяти. 512Кб доступно для программы на ML, еще 512Кб резервировалось для копирования. Пользователь системы не мог установить этот размер ни при запуске, ни при компиляции программы на ML. Размер кучи - константа в рантайме на Паскале, который нужно пересобирать, чтоб изменить значение. Карделли пишет что минимальная куча, которая позволяет коду на ML работать ("но не долго") - 8Кб [Card82a]. Позднее размер кучи стал изменяться автоматически [Card83b], но рекурсивность алгоритма Карделли так и не исправил [Card84b].
Фиксированная ML-куча была только минимальной оценкой требуемой памяти потому, что помимо ML-кучи, была еще и куча без сборки мусора, которая использовалась компилятором и постоянно росла. Почему компилятор занимает какую-то память во время исполнения? Это необычный компилятор.

Диалоги

Но необычный не по тем причинам, по которым был необычным RABBIT. Компилятор Карделли совсем не похож на RABBIT, на который Карделли ссылается [Card84]. Вместо десятка проходов только четыре: парсинг, проверка типов, компиляция в стековую виртуальную машину и генерация VAX машкода. Вывод и проверка типов совмещались с вычислением свободных переменных и смещения стека.
В компиляторе Карделли вместо сложных нелокальных оптимизаций RABBIT, трансформирующих абстрактные синтаксические деревья, оптимизации напоминающие скорее компилятор Уоррена. На который Карделли не ссылается. Оптимизатор работает с последовательностью команд виртуальной машины и заглядывает в этой последовательности не очень далеко. Например, поддержка быстрого карринга реализуется как выбрасывание из последовательности операций виртуальной машины для применения функции f x y

сохранить фрейм, применить, восстановить фрейм, сохранить фрейм, применить, восстановить фрейм

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

сохранить фрейм, применить, восстановить фрейм, возврат

на специализированную операцию хвостовое применение. Типичный подход в компиляторе Уоррена.
Как в компиляторах Пролога Уоррена, BCPL Ричардса, как в компиляторе Algol 68C, эти операции виртуальной машины разворачиваются в код по шаблону. Это довольно распространенный способ имплементации. В отличие, например, от того, который использовал Стил в RABBIT. Получающийся в результате код не особенно хороший. Например, нет хорошего распределения регистров. Карделли пишет, что в это время считается, что пользы от хорошего использования регистров для ФЯ нет вовсе. Сам Карделли несколько дистанцируется от этого мнения и не говорит, что так считает он сам. Но он считает, что пока сойдет и так.
Но то, что компилятор Карделли использует виртуальную машину, как многие другие, еще не означает, что это обычный компилятор. Есть то, что делает компилятор карделли непохожим на другие.
В отличие от компилятора Уоррена, Карделли не использует системный микро-ассемблер. Почему? Компилятор Карделли интерактивный. Для каждого вводимого пользователем или загружаемого из файла выражения производится компиляция. Компиляция целиком происходит в памяти и результат подлинковывается к коду, полученному на предыдущих шагах REPL. Каждое выражение компилируется в окружении, сформированном на предыдущих шагах - статическая видимость даже на топлевеле.
Карделли пишет, что компилятор выглядит необычно потому, что он не был знаком с тем как пишут обычные компиляторы. В качестве примера таких знаний, которые ему удавалось избегать до поры - книга зеленого дракона Ульмана и Ахо.
Но Карделли, в отличие от имплементаторов LCF/ML и HOPE, умел парсить менее экзотическим способом. Карделли написал парсер рекурсивным спуском вручную.
Зато Карделли, по его заверениям, знаком с тем как имплементировались Лиспы и Схемы. Отсюда, объясняет он [Card84] и все странности. Мы уже отмечали, что сходство с RABBIT мало. Но давайте посмотрим, что думают о компиляторе Карделли лисперы.
Имплементатор Схем Кент Дибвиг (Kent Dybvig) в 84-ом году познакомился [Dybv06] с Карделли и тот показал ему свой интерактивный компилятор ML.
Он понравился Дибвигу и тот имплементировал Chez Scheme как интерактивный компилятор. Хотя первоначально планировал другой подход, который считал обычным для Лиспов - как пару из интерпретатора и компилятора. Интерактивный компилятор он, наоборот, считает совсем необычным для Лиспа.
Также как и Карделли, Дибвиг выбрал для своей Схемы и "плоское" представление замыканий, но еще до того, как познакомился с Карделли. Этот способ организации замыканий он тоже не считает лисповым, считает схожим с алголовыми "дисплеями" и почерпнул его из книги Рассела об имплементации Алгола.
В докладе [Card07] о SIMULA карделли говорит, что ML интерактивный и потому более объектно-ориентированный, чем SIMULA. Что бы не значило это загадочное утверждение, но наверное не значит то, что Карделли позаимствовал идею об интерактивном компиляторе у симулистов.
Куча, которую использует интерактивный компилятор только растет [Card82a]. Раз компилятор интерактивный - компиляция раздельная с очень небольшой минимальной единицей компиляции, как и у Уоррена. Но функциональные программисты, как мы видели в коде на HOPE, не всегда охотно делят код на такие небольшие единицы. Карделли рекомендует писать "небольшие" функции в 10-100 строк [Card84b].
Фичи виртуальной машины Карделли позволяют компилировать обычный код на LCF/ML, но, чтоб использовать их полностью, в ML нужно добавить некоторые фичи, доступные непосредственно программисту. Что Карделли и сделал.

Сумма против Милнера

Виртуальная машина Карделли поддерживает рекорды, в которых больше двух полей. И варианты, объединяющие более двух видов объектов кучи. Для того, чтоб программист на ML мог объявлять эти новые, менее ветвистые структуры данных, Карделли добавил новые конструкторы типов и значений в ML [Card82b].
Не смотря на то, что представление в памяти у них скорее как у типов данных, которые придумывал и пытался продвигать Хоар - наборы полей со ссылками на другие наборы полей со ссылками - как языковая фича они ближе к типам данных в Algol 68.
Как типы данных в Algol 68, но не как типы данных Хоара или алгебраические типы данных, рекорды и варианты Карделли не требуют деклараций, создающих новые типы. Они все уже существуют как наборы пар имен и типов, порядок которых не имеет значения:

- let r = (|a=3; c="1"; b=true|);
> r = (|a=3; b=true; c="1"|) : (|a:int; b:bool; c:tok list|)

Рекорды-произведения у Карделли дополняют варианты-суммы. Это существенное отличие от типов-объединений Algol 68. С этим взглядом на структуры данных как на сочетание дополняющих друг друга произведений и сумм Карделли познакомил его научрук Плоткин [Card07]. В языке Карделли можно объединять объекты одного типа и строить сложные структуры, которые не коллапсируют в алголовское одноуровневое объединение. Программисту нужно придумывать названия для каждого тега. Но для имплементатора должно быть удобно, что не нужно получать теги из типов. Тем более в языке с параметрическим полиморфизмом и стиранием типов. В первом же примере Карделли особо не раздумывает и делает названия тегов именами типов: [|int: int; bool: bool|].
Попробуем сконструировать значение такого типа.

- [|int=3|];
Unresolvable Variant Type: [|int:int|]

Да, вторая версия системы МакКарти еще хуже для вывода типов, чем первая. Естественно, такой конструктор - не конструктор значения единственного типа, а конструктор значения любого из бесчисленного числа уже имеющихся типов, даже если они вам все и не нужны. Какого именно - придется часто указывать:

- [|int=3|]:[|int: int; bool: bool|];

Поскольку каждый раз записывать структурный тип может быть хлопотно, как и в Algol 68 можно объявить короткое имя для них:

let type R = (|a:int; b:bool; c:tok list|);
let type OneOfTwo = [|int: int; bool: bool|];

И, скорее, даже нужно.
Как и синонимы типов в Algol 68, синонимы типов в LCF/ML и в ML Карделли не параметризованы. Как и в Algol 68, хотя и по другой причине, в ML Карделли нельзя просто так взять и написать рекурсивный тип. Рекурсию нужно разбивать "прокладкой". Если в Algol 68 рекурсия не работает из-за плоского представления, то у Карделли из-за вывода типов. К счастью, в ML, в отличие от Algol 68, есть способ объявлять новые типы и решить проблемы с рекурсией и параметризацией - абстрактные типы данных:

let type * list <=> [| cons: (| head: *; tail: * list |);
                       nil |]

Разумеется, представление в памяти у такого списка, даже во второй версии системы МакКарти, хуже, чем у "непосредственной имплементации" с суммами произведений. Значение abslist [| cons = (| head = 1; tail = abslist [|nil|] |) |] представляется в памяти так:

                                    
  ┌───┐   ┌───┬───┐                
  │   ├──►│   │ 0 │                
  └───┘   └─┬─┴───┘                
            │                      
            ▼                      
      ┌───┬───┬───┐   ┌───┬───┐    
      │ 2 │ 1 │   ├──►│ 0 │ 1 │    
      └───┴───┴───┘   └───┴───┘    
                                    

Тег варианта в отдельном объекте от объекта-рекорда. Ячейка с числом 2, перед той, на которую указывает ссылка - это размер рекорда, информация для сборщика мусора.
Поэтому для конструкторов списков специальные объекты в FAM, и список - все еще встроенный тип.
Для конструкторов вариантов типа . (в Хаскеле это ()) есть специальные сокращенные имена и конструкторы. Например, тут можно писать просто nil вместо nil: . и конструировать [|nil|] вместо [|nil=()|]. Что удобно для определения перечислений:

let type color = [|red; orange; yellow|];

Не смотря на то, что перечисление можно представлять числом, в FAM значения такого типа - объекты в куче, и занимают там больше одного слова. Первое слово - 0 соответствующий конструктору (), а сразу за ним число - тег.
Обращение к полям рекордов неожиданно современное и обычное:

- r.a;
  3 : int

Если у сумм проблемы с выводом типов при конструировании, то у произведений, понятное дело, проблемы с выводом типов при разборе:

- \r. r.a;
Unresolvable Record Selection of type: (|a:*a|)
Field selector is: a

У таких функций нужно или аннотировать тип или употреблять их тут же, в однозначном контексте:

- (\r. r.a) r;
  3 : int

Карделли пытался решить проблему с выводом типов подтипированием рекордов и вариантов, но, пока что, безуспешно [Card07].
Для разбора сумм-вариантов используется конструкция case почти как у Бурсталла:

let rec map f xs = case replist xs of
    [| cons = (| head; tail |) . abslist [| 
        cons = (| head = f head; tail = map f tail |)|];
       nil . abslist [| nil |]
    |]

Для ПМ рекордов тоже есть сокращенный синтаксис. Можно писать (| head; tail |) вместо (| head=head; tail=tail |).
Интересно, что тут, в отличие от Algol 68 и идей Хоара, паттерн-матчинг хотя-бы вложенный. Эта новая для ML разновидность паттерн-матчинга не очень хорошо сочетается со старой. Суммы и произведения можно матчить в let паттерне и даже как-то сочетать с разновидностями паттернов из LCF/ML. Но не всегда. case на первом уровне работает только для сумм, на что недвусмысленно указывает и его синтаксис, с такими же квадратными скобками.
В месте разбора суммы тип выводится, но потому, что этот case обязан перечислить все теги. И только один раз, так что в этом он ближе к case в core, чем к case в Haskell. Так что, скорее всего, не получится использовать старую разновидность ПМ в case с помощью такого вот трюка:

case [| l = xs |]:[| l: * list |] of
    [| l = h :: t . ...;
       l = [] . ...
    |]

Но, надо полагать, что если бы пользователи ML Карделли хотели что-то такое использовать, то язык бы выглядел несколько иначе. В примерах работа со списками происходит по старому:

let rec map f l =
    if null l then []
              else f(hd l)::map f (tl l);

дополним этот пример до нашего традиционного примера:

map (\x. x + y) [1; 2; 3] where y = 2;

Добавив в ML эти типы данных МакКарти второго поколения, Карделли оставил и систему МакКарти первого поколения из LCF/ML, в которой все собирается из пар. Со многими другими пережитками первого ML он поступил иначе.

Ссылки больше не против

Как мы помним, letref не особенно часто использовался в коде на LCF/ML и мы рассмотрели одну из причин - большая часть кода и практически весь мутабельный код был написан на Лиспе и только вызывался из ML. Разумеется, в имплементированном не через компиляцию в Лисп языке не так хорошо обстоят дела с вызовом лисповых функций.
Отсутствие в LCF/ML первоклассных изменяемых ссылок - другая причина. Мутабельность в ML сделали ради изменяемых деревьев. Но делать их без первоклассных ссылок не особенно удобно. Поэтому Гордону хотелось [Gord80] добавить мутабельные структуры, такие как массивы или мутабельные списки в ML. При этом Гордон сохраняет скепсис в отношении изменяемых ссылок. Считает, что идея совсем не очевидно хорошая и может быть "тем же по отношению к letref, чем является goto по отношению к while". Критика, похожая на критику изменяемых ссылок Хоаром. Это довольно сложное отношение Гордона к первоклассным изменяемым ссылкам еще больше осложнялось тем, что они с Милнером просто не знали как их типизировать так, чтоб сохранить безопасность и не получить средства приведения любого типа к любому другому.
Так что Гордон поручил разобраться как типизировать конструктор типов ref Луишу Дамашу, пишущему в это время в Эдинбурге диссертацию под руководством Милнера.
Луиш Мануэл Мартинш Дамаш (Luís Manuel Martins Damas) придумал [Dama84] даже два способа: доработал первоначальную идею Гордона о "слабых" переменных и собственный. И оба позаимствовал Карделли для своей версии ML [MacQ15]. Не одновременно, а последовательно.
Читатель может заподозрить, что если придумано два способа сделать что-то и не так очевидно какой лучше - вопрос все еще не закрыт. Да, так и есть, эмелистам предстоит работать над этой проблемой еще много лет. Но первые практические результаты уже получены. Сам Дамаш имплементировал типо-безопасные первоклассные изменяемые ссылки для LCF/ML.
Итак, вместо letref как в первоначальном LCF/ML у Карделли первоклассные ссылки. Было letref a = 3 in a := a + 1, а стало let a = ref 3 in a := @a + 1. Можно писать let a = ref[] на топлевеле без аннотации типа. Можно использовать для определения абстрактных типов let type * array <=> * ref list [Card82b].
И хотя вполне можно было бы поддержать старую letref аннотацию, в ML Карделли она не попала.

Детали

Некоторые отличия были продиктованы ограничениями виртуальной машины [Card82b] [MacQ15]. Например: размеры токенов, целых чисел. Отсутствие информации о типах во время выполнения изменило поведение операции структурного сравнения.
Новые фичи языка перехватили некоторые старые операторы. Конструктор списка теперь _. ., которая была в LCF/ML, в Лиспе и, первоначально, в Прологе теперь занята селекторами рекордов. Конкатенация двух списков, которая в LCF/ML была @ у Карделли ::. @ - теперь операция получения значения по ссылке.
Виртуальная машина позволяет делать хвостовую оптимизацию, и циклов if then loop пока нет, но планируются. Полностью Карделли от них не отказался. Вот циклов, управляемых выбросами исключений, как в LCF/ML у Карделли нет и не планируется.
У абстрактных типов новый синтаксис: вместо abstype TA = TC у Карделли let type TA <=> TC. Отличаются наборы символов из которых можно составлять имена. Вместо -3 у Карделли ~3.
Карделли заменил длинные ключевые слова LCF/ML вроде letrec на составные вроде let rec.
Карделли не только просто менял одни токены в декларациях на другие. Он ввел в ML конструкцию decl1 ins decl2. Раннюю версию современной конструкции local decl1 in decl2.
Одних только косметических изменений синтаксиса хватило бы для того, чтоб при переносе кода с LCF/ML на ML Карделли пришлось бы редактировать почти каждую строчку. В лучших традициях диалектов NPL и языков Тернера. Не то чтобы, правда, было много кода на LCF/ML, было что переносить и было желание переносить. Все-таки в 70-е функционального программирования не было.
Получившийся язык Карделли известен под множеством имен. В описании [Card82a] отличий от LCF/ML он называется Edinburgh ML. Карделли также называет его ML under VMS, ML under Unix, "моя имплементация для VAX". Вадсворт называет его Cardelli ML, Гордон - Luca ML. В редакционной колонке первого номера самиздат-журнала "Полиморфизм" [Card82c] Карделли и МакКвин называют язык VAX ML. Так же называет язык описывающий его историю [MacQ14] МакКвин и в наши дни.
Появление нового диалекта ML вызвало некоторое желание у эмелистов и не только стандартизировать ML. Это желание приведет к появлению языка, который мы считаем диалектом NPL. Но в названии этого диалекта NPL есть ML, так что мнение о том, что это еще один диалект ML не совсем лишены основания. Это, впрочем, уже другая история, к которой мы еще вернемся.

Без сборщика и со сборщиком.

Милнер вспоминает [Miln93] [Miln2003], что Карделли имплементировал компилятор за несколько недель. Но между первым описанием виртуальной машины в октябре 80-го и первой версией компилятора прошли месяцы.
Первый релиз готов 12 июня 81-го года [Card82c]. В этой версии не полностью имплементированы абстрактные типы, но самое главное - нет сборщика мусора.
Все это было исправлено 13 октября 1981 во второй и последней версия компилятора Карделли для VMS.
Карделли не ограничивал использование в университетах и распространение в другие университеты, но использование в исследовательских лабораториях и в индустрии требовало его согласия в каждом отдельном случае [Card82a]. И некоторые университеты заинтересовались компилятором ML. Как и у Тернера, у Карделли был список [Card82c] известных пользователей VAX ML. Не очень впечатляющий список, умещающийся на одну страничку и включающий самого Карделли. Но само наличие списка уже означает выдающуюся популярность имплементации ФЯ в те времена.
Список пользователей говорит скорее о том, когда пользователь перестал получать новые версии, а не когда начал. И многие не обновляли компилятор никогда, первая версия без сборщика мусора была для них и последней.
С компилятором Карделли ознакомился Милнер, Гордон и Вадсворт. Ознакомились в важных центрах разработки ФЯ в Гётеборге, Оксфорде и в INRIA, рассказ о которых еще впереди.
Как мы выяснили в предыдущей части, VMS - стандартная ОС VAX-11 - не особенно хорошо подходила для использования требующих большой памяти программ вроде компиляторов ФЯ. По крайней мере, компилирующих что-то побольше однострочников. BSD подходила лучше и была популярна в академии. По каким-то причинам Карделли не начал имплементацию на этой системе, так что за исправление этой ошибки взялись другие.
Не позднее марта 1982 [Sait82] в Университете Карнеги — Меллона Сайто Нобуо (Saito Nobuo) [MacQ14] портировал 13-10-81 версию [Card82c] компилятора Карделли на Unix с помощью Berkeley Pascal. Того самого компилятора Паскаля, который писали в Беркли Кен Томпсон и другие [McKus]. Долгое время Berkeley Pascal был только интерпретатором байт-кода, но в 1980-ом появился компилятор [Joy86].
Известным Карделли пользователем этой версии был Ханс Боэм (Hans Boehm), который еще поучаствует в нашей истории.
Иэн Коттам (Ian Cottam) планировал [Card82c] портировать эту версию на одну из первых рабочих станций с Motorola 68K. Рабочую станцию продавала компания Apollo, написавшая Multics/Unix-подобную ОС Aegis на Паскале. Можно предположить, что их компилятор был получше. Если б эти планы закончились успехом - название VAX ML стало бы не таким подходящим, но пока что смена названия откладывается.

Разъединение

В апреле 1982 Лука Карделли покинул Эдинбург и отправился в Нью-Джерси. Но история VAX ML в Эдинбурге на этом не заканчивается. Кто-то (Милнер не помнит кто) предложил использовать компилятор Карделли для обучения второкурсников [Miln93] [Miln2003]. Вскоре для компилятора Карделли нашли в Эдинбурге и другое применение, но об этом позже.
В Лабораториях Белла в Мюррей Хилл работать Карделли уговорил МакКвин [Card12]. Там Карделли проработал с апреля 82-го по сентябрь 85-го. Научился использовать C, но язык ему совсем не понравился. Карделли не заинтересовался C++, хотя и слышал что Страуструп делает Симулу из C. Не заинтересовался потому, что из C. Хотя кабинет Страуструпа был дальше по коридору [Card07].
В Bell Labs Карделли сам занялся портированием своего компилятора на Unix. Он использовал не только Berkeley Pascal, далеко не самую быструю имплементацию Паскаля, которая заметно хуже Паскаля для VMS [Nebe83], с которого он начинал. Если для имплементации компилятора Berkeley Pascal еще более-менее подходил, то рантайм Карделли переписал на C. Первый релиз был готов уже 13 августа 1982.
В этой версии в ML вернулись циклы. А именно конструкция if then loop из LCF/ML. Имплементированы массивы.
Как мы помним, в Эдинбурге Карделли не поддавался влиянию языков бывшей Группы Экспериментального Программирования. Но в Мюррей Хилл МакКвин на него, видимо, повлиял и VAX ML сделал первый небольшой шаг к "хопизации": cons оператор называемый в первых версиях _ получил современное имя :: как в HOPE и до того в POP-2. Этот шаг был не последним, но это уже другая история. В предыдущих версиях VAX ML :: называлась конкатенация списков, которая теперь получила современное имя @. И, соответственно, операцию над ссылками Дамаша @ переименовали в современную !.
Вскоре, 24 августа 1982 была готова следующая версия. В ней Карделли добавил функции для ввода-вывода и VAX ML, видимо, впервые стал языком общего назначения. Немного поздно для того, чтоб писать программы для форматирования диссертации Карделли.
В том же году была сделана еще одна версия, от 5 ноября 1982 с новым алгоритмом проверки типов для изменяемых ссылок. Версия вышла незадолго до судьбоносного совещания в лаборатории Резерфорда - Эплтона, которое завершило предысторию ML и начало его историю, но об этом мы расскажем в следующей части.

Новые эксперименты с VAX

Насколько быстрый код генерировал компилятор Карделли? Сам Карделли в своих статьях не приводит таких измерений, только пишет, что производительность можно считать удовлетворительной для типичных применений. Что может означать довольно плохую производительность, когда такое пишет довольный пользователь Симулы. К счастью, компилятор Карделли поучаствовал в том сравнении ранних имплементаций ФЯ [Augu84] [Augu89], которым мы уже пользовались, чтоб продемонстрировать невеселые итоги 70-х:

fib 20 primes 300 7queens insort 100 tak
VAX ML 1.00 1.00 1.00 1.00 1.00
LCF/ML (LISP) 92.0 24.2 18.9 15.0 19.3
SASL (LSECD) 62.0 16.7 18.9 12.0
Miranda (SKI) 144 10.3
LISP int. 42.0 6.50 5.33 6.40 4.75
LISP comp. 2.20 0.92 0.58 0.80 0.19
C 0.92 0.17 0.04 0.11
Pascal 1.84 0.11 0.16

Как видите, компилятор Карделли легко обошел все предыдущие попытки имплементировать ФЯ, предпринятые участниками Эдинбургской исследовательской программы:

VAX ML
▒▓▓
LCF/ML (LISP)
░░░░░░░░░░░░░░░░░░░░░░░░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓
SASL (LSECD)
░░░░░░░░░░░░░░░░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓
Miranda (SKI)
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓
LISP int.
░░░░░░░░░░░▒▒▒▒▒▒▒▒▒▒▓▓▓▓▓▓▓▓▓▓▓
LISP comp.
░▒▓
C
▒

И был примерно равен Franz LISP. Не самой передовой имплементации Лиспа, но достаточно хорошо работающей для компиляции "главной" программы на Лиспе - Macsyma.

VAX ML
░░░░░░░░░░░░░░░░░░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓  
LISP comp.
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓
C
░░░░░░░░░░░░░░░░░▒▒▒▒▒▒▓▓

Итак, компилятор ML Карделли генерировал код, сопоставимый по производительности с кодом, который генерировал компилятор Лиспа. Через несколько лет после того, как соответствующего успеха добился Уоррен при имплементации компилятора Пролога. Соответствие этих успехов, правда, неполное. Компилятор Уоррена хорошо себя показал не только на микробенчмарках, но и при компилировании более серьезной программы - самого себя. Но, в отличие от Уоррена, Карделли не писал свой компилятор на языке, который тот компилировал. Может быть из-за того, что интерактивный компилятор слишком тесно интегрирован с рантаймом, а ML не самый подходящий язык для имплементации рантайма. Может быть потому, что не считал, что системные требования такого компилятора позволили бы им пользоваться. Попытаются использовать компилятор Карделли для имплементации ML не сразу и другие люди. И это уже другая история.

Редукция графов и как её избежать

Если б не Девид Тернер, я не уверен, что занимался бы функциональным программированием.
Л. Августссон [Augu23].

Первая глава истории ФЯ разворачивалась в уже знакомом нам Эдинбурге, в котором прошла значительная часть предыстории. Но в этой главе мы отправляемся в новое для истории ФЯ место, шведский город Гетеборг.

Леннарт Августссон

Леннарт Августссон (Lennart Augustsson) один из первых героев нашей истории, личная история которого уже более-менее нормальна для программиста сегодняшнего дня. Он интересовался [Augu21] компьютерами с детства, написал первую программу в школе в шестнадцать лет, выучился программировать по книге о языке BASIC и даже владел персональным компьютером после окончания университета.
Правда, компьютером, которым он пользовался в школе был PDP-8, а перед успешным изучением BASIC он безуспешно попытался научился программировать по книге с названием вроде "FORTRAN для тех, кто знает ALGOL". Но, понятно, что не все стало обычным сразу.
Августссон закончил Технический университет Чалмерса (Chalmers University of Technology) где учился на инженера-электротехника, специальностей связанных с компьютерными науками там пока-что не было. Пока Августссон был студентом, он больше интересовался системным программированием и параллельными вычислениями и мало что знал о функциональном программировании. Слышал только о языках без операции присваивания и не представлял как такое вообще возможно. Как мы знаем, практически все языки без операции присваивания на тот момент назывались SASL и Августссон слышал именно об одном из них.

SIMPLE Made Easy

Впервые увидел ФЯ Августссон только в 1980 году, начав работу над своей диссертацией. На курсе Сёрена Хольмстрёма (Sören Holmström) по денотационной семантике. Хольмстрём использовал версию SASL из Сент-Эндрюса (и не попал в список известных пользователей SASL Тернера [Turn19]). SASL и чистое, ленивое функциональное программирование вообще произвели на Августссона сильное и положительное впечатление. Он прочел статью [Turn79] Тернера про комбинаторный интерпретатор. SASL из Сент-Эндрюса использовал прокрастинирующую SECD машину. Но её описание было, скорее всего, менее доступно. И Тернер писал о комбинаторном интерпретаторе как о важном шаге вперед. Так что Августссон сделал то, что часто делали имплементаторы ФЯ в 80-е. Имплементировал еще один SASL. Как обычно, SASL не так уж сильно походил на остальные SASLы, но хотя-бы не назывался так же как они. Августссон сначала не называл язык никак, но позднее, по совету своего научрука Бенгта Нордстрёма (Bengt Nordström), решил таки дать ему название и назвал SIMPLE.
От имплементаций SASLов Тернера он отличался тем, что компилятор в комбинаторы был написан на самом SIMPLE. Сам комбинаторный интерпретатор Августссон написал на C и писал в том числе и на своем персональном компьютере. В 1980-ом году C еще не был особенно популярным языком, тем более на персональных микрокомпьютерах. Не понятно, что Августссон использовал. BDS C на CP/M? Small c?
Хотя Тернер в своей статье утверждал, что комбинаторный интерпретатор на порядок улучшает производительность ленивых ФЯ по сравнению с ленивой SECD, из измерений, которые делали как раз Августссон со своим коллегой, мы уже выяснили, что производительность комбинаторного интерпретатора оставляла желать лучшего. Да, для бутстрапа августссоновского транслятора в комбинаторный код в 100-200 строк на SASL-образном языке этого было достаточно, но и только.
Так что Августссон объединил усилия с другим важным героем нашей истории - Томасом Йонссоном (Thomas Johnsson). Они считали, что у чистых, ленивых ФЯ привлекательные свойства, но их применение сдерживает отсутствие эффективных имплементаций.

SKI не едут

Августссон с Йонссоном поставили перед собой цель: сделать ленивые ФЯ практичным инструментом, позволяющим писать "настоящие" программы, такие как компилятор. Которые могут быстро работать на обычных современных компьютерах.
Правда, известные им способы имплементации ленивых ФЯ пока что такого не позволяли. По оценкам Августссона с Йонссоном комбинаторный интерпретатор Тернера работает примерно в сто раз медленнее, чем энергичный код [John95]. Все потому, что шаг интерпретации слишком маленький. Комбинаторы из тернеровского набора делают слишком мало работы по сравнению с прочими издержками интерпретатора, который их применяет.
Не смотря на то, что решение Тернера не выдерживало проверки, Йонссон с Августссоном продолжали считать, что саму проблему он обозначил верно. Проблема с производительностью у функциональных языков из-за имплементации свободных переменных. И если нет свободных переменных - нет проблем. Так что они продолжили работать над комбинаторным подходом, но Йонссон решил, что комбинаторы должны быть существенно больше.
Так что, для начала, Йонссон с Августссоном добавили к набору интерпретируемых комбинаторов большие комбинаторы. Например, функции для работы со списками, такие как map и fold. Поскольку код, пока что, в основном состоит из работы со списками, Августссон с Йонссоном оценили, что это может увеличить скорость на десятичный порядок.
Это не особенно согласуется с их же собственными, сделанными годы спустя, замерами производительности интерпретатора Миранды, в котором Тернер использует ту же идею. Может быть у Тернера имплементировано недостаточно списочных функций, может быть - речь о разных наборах микро-бенчмарков.
Так или иначе, результаты были недостаточно хороши для наших героев-имплементаторов. К счастью, у Йонссона появилась идея получше.
В октябре 1981-го года в городе Портсмут, Нью-Гэмпшир состоялась важная конференция по функциональному программированию (FPCA '81), к которой мы еще вернемся.
В один из дней, после докладов Йонссон с Августссоном обсуждали с Тернером комбинаторные интерпретаторы [Augu23]. Может быть, как раз во время этого разговора (или нет) Йонссон решил, что каждую определяемую пользователем функцию-комбинатор нужно добавлять в интерпретатор как правило перезаписи графов. Пусть компилятор конструирует специализированный комбинаторный интерпретатор для каждой программы [John95].
По воспоминаниям Августссона, Тернеру идея понравилась. Почему же он сам ей не воспользовался? Об этом мы еще расскажем позднее. Августссон с Йонссоном же решили воспользоваться этой идеей.
Что еще лучше, чем комбинаторы, которые проделывают много работы по редукции графа? Вовсе граф не редуцировать. Не нужно для каждого этапа вычисления арифметического или логического выражения создавать и изменять в куче соответствующий граф объектов. Можно использовать для вычисления только стек с машинными числами. И не нужно пытаться снова редуцировать граф, редукция которого уже была закончена.
Эти ранние идеи Йонссон описал в неотсканированном отчете "Генерация кода для ленивых вычислений" 1981-го года. А Августссон на основе этих идей написал на C компилятор функционального языка. В наши дни Августссон рассказывает [Augu21], что язык не имел названия. Но в названии его описания 1982-го года, автором которого является Августссон, язык назван FC. Йонссон в своих статьях [John84] [John95] называет язык fc.
Описание языка не отсканировано и не дошло до нас. Код на нем не сохранился. Известно [John95], что это был небольшой нетипизированный ФЯ без локальных функций и паттерн-матчинга. Никаких свободных переменных. Программист на FC мог определять только комбинаторы. Из которых компилятор этого языка и собирал специализированный комбинаторный интерпретатор на VAX-11 ассемблере. Сначала у имплементации не было даже сборщика мусора. Кто бы мог подумать, насколько это обычная вещь.
Воспоминания Августссона разных лет [Augu21] [Augu23] несколько противоречат друг другу, так что сложно датировать появление этого компилятора точно. Может он был написан в летние каникулы 82-го, а может и 81-го. В этом случае Йонссон придумал, а Августссон имплементировал идеи задолго до конференции и только познакомили Тернера с ними на ней. Ссылки на неотсканированные отчеты не добавляют ясности. Отчет Августссона с описанием FC хоть и датирован 82-м годом, имеет меньший порядковый номер, чем отчет Йонссона 81-го года. Может быть просто более поздней редакцией, как отчеты Тернера с описанием SASL, имевшие много версий и редакций.
Августссон, начавший свою карьеру имплементатора ФЯ с написания очередного SASL на самом себе, не переписал компилятор FC с C на FC. Но, в отличие от тернеровских SASL-ов, на этом нетернеровском "SASL" написали какое-то заметное количество функционального кода. Мы не знаем сколько именно, но предполагаем, что тысячи строк. Августссон с Йонссоном использовали его для написания компилятора другого функционального языка. И, в отличие от Карделли, свои диссертации они напишут про разработку этого компилятора.

Ленивый ML

FC не поддерживал вложенные функции. Но Августссон с Йонссоном не хотели довольствоваться компилируемым KRC. Стремление Тернера к ФП-минимализму они не разделяли и ограниченный набор фич FC был только вынужденным результатом стремления как можно скорее начать писать компилятор ФЯ на ФЯ, а не на C. Они хотели в языке полноценное вложение функций. И не только.
Особенно после того, как Сёрен Хольмстрём и Кент Петерссон (Kent Petersson) привезли из Эдинбурга ленту с ML. По всей видимости, LCF/ML. В Гетеборге LCF портировали на VAX-11 c BSD и разрабатывали на его основе доказатель для интуиционистской теории типов Мартин-Лёфа.
Компилятор Карделли тоже попал в Гетеборг. Карделли записал [Card82c], что первую же его версию получил Лассе Остергаард (Lasse H. Ostergaard). Хольмстрём и Йонссон были подписаны [Card82c] на почтовую (еще не электронно-почтовую) рассылку компилятора Карделли. И Августссон с Йонссоном сравнили с его UNIX-версией свои результаты.
Августссон вспоминает [Augu21] как набирал в терминале map функцию или что-то вроде неё и ML REPL выводил для неё тип. Это "выглядело как магия".
Впечатление "магии" производил вывод типов, а не REPL. Не смотря на знакомство с несколькими интерактивными имплементациями ФЯ (SASL 76, LCF/ML, VAX ML) и не только (Franz LISP), Йонссон с Августссоном, по видимому, не имели особого желания делать свой компилятор интерактивным и совершенно точно не сделали его таким.
Они стали писать компилятор для UNIX так, как было принято в это время писать компилятор на UNIX. И это было куда возможнее, чем утверждал Карделли. Как мы помним, обосновывая странность своего компилятора, Карделли утверждал, что компилятор такого необычного языка как ML, очевидно, должен и сам быть необычным. Но вот Йонссон с Августссоном стали писать компилятор еще более необычного языка как портабельный компилятор C Джонсона с парсером на YACC, использованием системного BSD UNIX-инструментария и без всякой интерактивности. Они считали, что и программы на чистом ФЯ, читающие потенциально бесконечную строку и печатающие свой результат как потенциально бесконечную строку, отлично впишутся в юниксовое рабочее окружение.
Августссона с Йонссоном не привлекли абстрактные типы данных и мутабельные ссылки. Они все равно собирались имплементировать чистый ленивый ФЯ как у Тернера, но синтаксически - подмножество LCF/ML и с выводом типов Хиндли-Милнера. Lazy ML.
В отличие от Карделли, они не внесли множества мелких изменений. map написанный на Lazy ML будет работать и на LCF/ML:

letrec map f l = if null l then [] 
                           else f(hd l).map f (tl l)
in
let y = 2
in map (\x. x + y) [1; 2; 3]

Все выглядит, на первый взгляд, [Augu84] как в LCF/ML. Но язык ленивый, так что можно писать и код, который в LCF/ML не заработает:

letrec from n = n.from(succ n) in from 0

Но самый большой дошедший до нас сниппет кода на первой, минимальной версии ленивого ML работает и в ML-е строгом, хотя и происходит от примера для ленивого SASL:

let mod x y = x - (x/y*y) in
let rec filter p l = if null l then []
                     else 
                        if mod (hd l) p = 0 then
                                filter p (tl l)
                        else    hd l . filter p (tl l)
and     count a b = if a > b then []
                    else a . count (a+1) b
in
letrec sieve l = if null l then nil
                 else hd l . sieve (filter (hd l) (tl l))
in 
let primesto n = sieve (count 2 n)
in
primesto 300

Видимо потому, что это один из бенчмарков, который должен был минимально отличаться от версий на других ФЯ с которыми производилось сравнение.
Со временем Йонссон с Августссоном еще расширят это минимальное подмножество LCF/ML. Но если Карделли расширил его не самыми типичными для современных ФЯ самобытными фичами, то LML расширят как раз теми самыми фичами, которые делают ФЯ типичными языками Эдинбургской программы и мы расскажем об этом в специальной части про то, как это делали (почти) все имплементаторы ФЯ 80-х.

Неполная ленивость

Для компилятора LML написанного на FC, Йонссон доработал свои идеи по имплементации ленивости редукцией графов (и её избеганием) и описал их как G-машину.
В апреле 1983-го Йонссон выступил с докладом о ней на коллоквиуме по декларативному программированию в Университетском колледже Лондона. Из этого доклада о G-машине узнали другие разработчики ФЯ, которые выработали похожие идеи независимо. До наших дней запись доклада не дошла. Первое и последнее сохранившееся описание компилятора Lazy ML на FC - доклад [John84] Йонссона с симпозиума по конструированию компиляторов, состоявшегося в июне 1984-го года.
В G-машине есть окружение, но только глобальное. Как мы помним, сложную манипуляцию окружениями авторы LML, вслед за Тернером считают проблемой для производительности.
У G-машины три стека. На один больше чем в FAM потому, что Йонссон не совмещает, как Карделли, указатели с числами. Для неуказателей вроде чисел и булевых значений предназначен отдельный стек. Но G-машина "виртуальнее", чем FAM, структуры и команды которой были воспроизведены достаточно бесхитростно. И три стека в ней не соответствуют трём стекам в рантайме. Там есть один стек в куче для ссылок, который обходит сборщик мусора. Для сборки мусора используется алгоритм Фенихеля-Иохельсона, модифицированный для поддержки объектов кучи, отличающихся от пар. V-cтек неуказателей и D-стек отображаются на обычный системный. Значения из V-стека могут не попасть и на системный стек, компилятор старается разместить их в регистрах, пока они есть. Да, в отличие от Карделли, Йонссон с Августссоном стараются использовать регистры. Сначала они хотели подставлять для команд виртуальной машины шаблоны кода, как у Карделли и Уоррена. И затем оптимизировать получившийся машинный код, находить в нем не особенно хорошие сочетания команд и исправлять их на те, что получше. Карделли рассматривал такую возможность, но не стал с ней связываться. Не стали связываться с ней и Августссон с Йонссоном, но вместо того, чтоб ничего не делать как Карделли, они решили писать более сложный генератор кода, не просто подставляющий код по шаблонам для команд. Как и компиляторы Карделли и Уоррена компилятор LML находит сочетания команд ВМ и выбрасывает ненужные или заменяет группы на специализированные. Но на этом локальном уровне трансформации не ограничиваются. Не могут ограничиваться.
Как имплементировать ML с помощью G-машины? G-машина исполняет только функции без свободных переменных. Поэтому, в отличие от компиляторов Карделли или Уоррена, компилятор Lazy ML осуществляет нетривиальные трансформации кода. ISWIM-подобная расширенная лямбда ML-кода трансформируется с помощью преобразования под названием лямбда-лифтинг.
Йонссон рассматривает [John85] по крайней мере три способа такого преобразования. Не каждый лямбда-лифтинг подходит для имплементации ФЯ одинаково хорошо. Йонссону нужно, чтоб трансформация отвечала ряду требований. Она не должна производить небольшие комбинаторы - иначе все труды по замене интерпретатора тернера были бы напрасными. Она не должна избавляться от рекурсии, заменяя её Y-комбинатором - G-машина эффективно имплементирует рекурсивные комбинаторы. Она должна производить код в котором больше комбинаторов применены полностью или, хотя-бы, к большому числу аргументов за раз - чтоб вместо развесистых деревьев применений можно было бы использовать "векторные применения", аналог плоских замыканий в FAM.
Что Йонссона не особенно сильно беспокоило - это сохранение такого свойства комбинаторного интерпретатора, которое Тернер называл "самооптимизацией", а сегодня называют "полной ленивостью". И Йонссон его не сохранил. Насколько правильным было это решение? Это мы выясним в главе о тех, кто совершенствовал комбинаторный интерпретатор Тернера независимо от героев этой главы.
Из-за более сложных трансформаций и кодогенератора в компиляторе LML больше стадий и проходов, чем в компиляторе Карделли. И будет еще больше.
Внимание к генерации более эффективного исполняемого кода принесло плоды. Результаты микро-бенчмарков еще лучше, чем у VAX ML Карделли.

fib 20 primes 300 insort 100
LML (FC) 1.00 1.00 1.00
VAX ML 0.54 2.40 2.70
LCF/ML (LISP) 50.0 58.0 40.5
SASL (LSECD) 33.7 40.0 32.4
Miranda (SKI) 78.3 24.6
LISP int. 22.8 15.6 17.3
LISP comp. 1.20 2.20 2.16
C 0.50 0.40
Pascal 1.00

Конечно, к бенчмаркам надо относиться с некоторой осторожностью, ведь их подбирают и делают измерения авторы LML, но у хорошей производительности ленивого ML со временем появились и другие, независимые подтверждения.

LML (FC)
░▓
VAX ML
▒▒▓▓
LCF/ML (LISP)
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓
SASL (LSECD)
░░░░░░░░░░░░░░░░░░░░░░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓
LISP int.
░░░░░░░░░░░░░░░▒▒▒▒▒▒▒▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓
LISP comp.
░▒▓▓

В очередной раз удалось воспроизвести успех Уоррена и Карделли и генерировать код быстрее, чем получается у компилятора Лиспа, не смотря на десятки лет опыта компиляции у лисперов.
И если между имплементацией первого интерпретатора PAL и первого интерпретатора SASL 76 прошло десятилетие, первый компилятор ленивого ФЯ отстал от компилятора строгого только на пару лет. Еще одна демонстрация того, как мало пользы от многолетних "наработок" 60-х и 70-х и как мало было наработано за эти лета.

LML (FC)
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓
VAX ML
░░░░░░░░░░░░░░░░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓
LISP comp.
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓

Компилятор Уоррена, как мы помним, не только показывал приличную производительность на микробенчмарках, но и был достаточно практичным, чтоб компилировать самого себя. Компилятор Карделли сам себя не компилировал, но Йонссон с Августссоном с самого начала собирались повторить и этот успех Уоррена и повторили, переписав компилятор LML на LML. Это, правда, уже другая история.
На этом закончилась история FC, первого ленивого ФЯ, на котором написали компилятор ленивого ФЯ, но история LML только начинается.
Комплекс идей Августссона и Йонссона оказался успешным, но, к сожалению, они не публиковали абляционный анализ, который продемонстрировал бы вклад каждой идеи в отдельности и их сочетаний. Но это сделали другие разработчки компиляторов ленивых ФЯ, в том числе и те, о которых мы расскажем в следующей главе.

Литература

[ALGOL80]: AB45.4.2 ALGOL 68 Implementations - ALGOL 68C - Release 1, Algol Bulletin No. 45, January 1980 http://archive.computerhistory.org/resources/text/algol/algol_bulletin/A45/P42.HTM
[Augu84]: Lennart Augustsson, A compiler for lazy ML. LFP '84: Proceedings of the 1984 ACM Symposium on LISP and functional programming August 1984 Pages 218227 doi:10.1145/800055.802038
[Augu89]: L. Augustsson, T. Johnsson, The Chalmers Lazy-ML Compiler. In The Computer Journal, Volume 32, Issue 2, 1989, Pages 127141 DOI:10.1093/comjnl/32.2.127
[Augu21]: The Haskell Interlude 02: Lennart Augustsson https://www.buzzsprout.com/1817535/9286902
[Augu23]: Lennart Augustsson - MicroHaskell https://www.youtube.com/watch?v=Zk5SJ79nOnA
[Birr77]: Andrew Birrell. System Programming in a High Level Language. Ph.D. Thesis, University of Cambridge. December 1977.
[Card80]: Luca Cardelli, The ML Abstract Machine. https://smlfamily.github.io/history/Cardelli-ML-abstract-machine-1980.pdf
[Card82a]: EDINBURGH ML by Luca Cardelli, March, 1982. A README file accompanying the distribution of Cardelli's ML Compiler for VAX-VMS. https://smlfamily.github.io/history/Cardelli-Edinburgh-ML-README-1982_03.pdf
[Card82b]: Luca Cardelli, Differences between VAX and DEC-10 ML. March, 1982. https://smlfamily.github.io/history/Cardelli-mlchanges_doc-1982_03.pdf
[Card82c]: Luca Cardelli, David MacQueen. Polymorphism: The ML/LCF/Hope Newsletter I, 0. November 1982
[Card82d]: Cardelli, Luca. "Algebraic approach to hardware description and verification." (1982).
[Card83]: Cardelli, Luca. “The Functional Abstract Machine.” (1983).
[Card83b]: Pre-Standard ML under Unix, August 1983. http://lucacardelli.name/Papers/MLUnix.pdf
[Card84]: Luca Cardelli, Compiling a Functional Language in LFP '84. 1984 DOI:10.1145/800055.802037
[Card84b]: Luca Cardelli, ML under Unix Pose 4. 4/5/84 http://lucacardelli.name/Papers/MLUnix%20Pose%204.pdf
[Card07]: Luca Cardelli, An Accidental Simula User, ECOOP 2007, Dahl-Nygaard Senior Prize, August 2, 2007.
[Card12]: Luca Cardelli, Scientific Biography. May 2012 http://lucacardelli.name/indexBioScientific.html
[Card21]: Luca Cardelli, Curriculum Vitae. 2021 http://lucacardelli.name/indexBio.html
[Dama84]: Luís Damas. “Type assignment in programming languages.” (1984).
[Dybv06]: R. Kent Dybvig. 2006. The development of Chez Scheme. In Proceedings of the eleventh ACM SIGPLAN international conference on Functional programming (ICFP '06). Association for Computing Machinery, New York, NY, USA, 112. doi:10.1145/1159803.1159805
[Gord80]: Michael Gordon, Locations as first class objects in ML. https://smlfamily.github.io/history/Gordon-ML-refs-1980.pdf
[Holm98]: Holmevik, Jan Rune. "Compiling Simula: A historical study of technological genesis." (1998) https://staff.um.edu.mt/jskl1/simula.html
[John84]: Efficient Compilation of Lazy Evaluation. Thomas Johnsson SIGPLAN '84: Proceedings of the 1984 SIGPLAN symposium on Compiler construction June 1984 Pages 5869 doi:10.1145/502874.502880
[John85]: Johnsson, T. (1985). Lambda lifting: Transforming programs to recursive equations. In: Jouannaud, JP. (eds) Functional Programming Languages and Computer Architecture. FPCA 1985. Lecture Notes in Computer Science, vol 201. Springer, Berlin, Heidelberg. doi:10.1007/3-540-15975-4_37
[John95]: Thomas Johnsson. Graph reduction, and how to avoid it. In Electronic Notes in Theoretical Computer Science Volume 2, 1995, Pages 139-152 DOI:10.1016/S1571-0661(05)80191-4
[Joy86]: Joy, William N., Susan L. Graham, Charles B. Haley, Marshall Kirk McKusick, and Peter B. Kessler. "Berkeley Pascal Users Manual Version 3.1 April 1986.
[Lieb81]: Lieberman, Henry, and Carl Hewitt. "A Real Time Garbage Collector Based on the Lifetimes of Objects" AI Memo No. 569A October, 1981.
[MacQ14]: Luca Cardelli and the Early Evolution of ML, by David MacQueen. A paper presented at the Luca Cardelli Fest at Microsoft Research Cambridge on Sept. 8, 2014.
[MacQ15]: MacQueen, David B. The History of Standard ML: Ideas, Principles, Culture https://www.youtube.com/watch?v=NVEgyJCTee4
[McKus]: Marshall Kirk McKusick. A BERKELEY ODYSSEY: Ten years of BSD history.
[Miln93]: Frenkel, Karen A. and Robin Milner. “An interview with Robin Milner.” Commun. ACM 36 (1993): 90-97. DOI:10.1145/151233.151241
[Miln2003]: interview with Robin Milner, held in Cambridge on the 3. September 2003 http://users.sussex.ac.uk/~mfb21/interviews/milner/
[Nebe83]: Nebel, B. (1983). Ist Lisp Eine Langsame Sprache?. In: Neumann, B. (eds) GWAI-83. Informatik-Fachberichte, vol 76. Springer, Berlin, Heidelberg. https://doi.org/10.1007/978-3-642-69391-5_3
[Sait82]: N. Saito. ML System on Vax Unix. README for Saitos Unix port of VAX ML, March 1982. [Thom76]: Christopher Mark Thomson. The Run-Time structure of an ALGOL 68 Student Checkout Compiler. Master's thesis, Department of Computing Science, University of Alberta, 1976.
[Thom78]: C. Thomson. ALGOL 68 Compiler for IBM S/370. Announcement. SIGPLAN Notices, Volume 13, Number 11 (November 1978), page 11.
[Turn79]: Turner, D. A. (1979). A new implementation technique for applicative languages. Software: Practice and Experience, 9(1), 3149. doi:10.1002/spe.4380090105