Из-за ограничений gist текст отображается не полностью, и больше не обновляется здесь.
Полный текст и обновления доступны здесь. Продолжить читать начиная с ноябрьского апдейта, который тут отображался не полностью можно здесь.
- История применения и оценки функционального программирования
- Часть 0: что было, когда функционального программирования не было.
- Ветвь обоих Кембриджей
- Эдинбургская исследовательская программа
- От исследовательского программирования к экспериментальному.
- За возвращение функций еще никого не увольняли
- Следующие 700 изобретений продолжений
- Типо-теоретическая альтернатива
- Робин Милнер
- Для вычислимых функций
- Три мили разделения
- Метаязык
- Предыдущие 700 ISWIMов в одном
- Невыносимая тяжесть аннотации
- Смотрите, не перепутайте Моррисов
- Возвращение резолюционизма
- Mutatis mutandis
- Исключения подтверждают правило
- Не могу сконструировать бесконечный тип
- Против методологии
- Когда функционального программирования не было
- Уравнения и неравенства
- Две диссертации
- Надежда умирает последней
- Lambda the Ultimate Misunderstanding
- Литература
Функциональное программирование - это инженерная ветвь конструктивной математики. Олег Нижников.
Мы не ставим перед собой цели дать определение функциональному программированию. Мы, однако же, вынуждены определить какие-то практические рамки для нашего исследования. Выбрать историю чего именно мы пишем, и выбрать что-то обозримое. И многие определения ФП просто непрактичны с этой точки зрения. История языков с первоклассными функциями в наши дни фактически равна истории языков программирования, ведь даже редкие языки в которых их до сих пор нет - вроде C++ или Rust - обычно связаны разными историческими отношениями с языками где первоклассные функции есть. Не годится также и не такая необъятная, но все еще неподъемная группа языков "по каким-то историческим причинам считающиеся функциональными", о которых обычно и пишут те, кто пишут историю ФП. Главная причина того, почему это определения ФЯ для нас не годится - в этой группе языков есть Лисп, и мы не хотим писать историю Лиспа. В основном потому, что история Лиспа слишком громадная тема, чтоб рядом с ней было вообще можно заметить историю прочих языков этой "традиционной" группы. Мало того, сейчас есть проблема, которой во времена выбора "исторически сложившегося перечня ФЯ" просто не было: сегодня Лисп - типичный представитель огромной категории языков. Так же как и в какую-нибудь Java, первоклассные функции были добавлены в Лисп после того, как он уже долгое время существовал и это не такая уж значительная и важная деталь истории языка. И написание истории функционализации Лиспа потребовало бы сравнения с другими языками, прошедшими через тот же процесс. А именно почти со всеми существующими сейчас языками программирования. Мы бы хотели поставить перед собой реальную цель и ограничиться функциональными языками в более узком смысле, историю которых мы рассмотрим немного более глубоко, чем можно позволить себе рассмотреть историю всех языков. Чтоб сузить группу языков мы добавим к первой фиче
- Первоклассные функции
еще какие-нибудь.
- Параметрический полиморфизм и вывод/реконструкция типов
позволяют существенно сократить предмет исследования, но все еще недостаточно. Так что мы добавим
- Алгебраические типы и паттерн-матчинг
Вот эта последняя фича, наконец-то, дает нам семейство языков подходящего размера. К сожалению, объединение фич выглядит довольно произвольным. Кроме того, если когда-нибудь АлгТД и ПМ станут так же распространены как первоклассные функции (мы бы не стали на это особо рассчитывать), проблема истории всех языков вернется, чтоб преследовать наших последователей, если они у нас, конечно, будут.
Языки с этим набором фич, бывает, называются "Эмелеподобными языками". Только вот люди не достаточно часто соглашаются какие языки ML-подобны. Не все ML-и так уж подобны другим ML-ям, а что уж говорить про не ML-и.
Попробуем найти первый язык с такими фичами и определить семейство через него. Хорошие новости: существует один такой язык - Hope, никто, насколько нам известно, не изобретал еще раз такое же сочетание независимо. Он получился из "слияния" языков, в каждом из которых был неполный набор интересных нам фич.
Плохие новости: не все языки, историю которых мы хотели бы писать, происходят от этого языка, так что наши надежды на "Hope-образные языки" не оправдались так же, и по тем же причинам, что и на "ML-образные". "Слияние", вероятно, не самая подходящая перспектива в этом случае. Отношения между языками не хотят принимать вид удобных деревьев или графов.
Правильнее будет говорить про набор языков, каждый из которых, развиваясь, позаимствовал недостающие до нашего определяющего наборы фичи из остальных, а Hope - просто первый результат этого процесса, который мы будем далее называть "хопизацией". ML как единый язык этот процесс не проходил, его прошли по отдельности по крайней мере три языка с "ML" в названии.
В те времена такое взаимодействие языков, проектов и их авторов было бы затруднено, если бы участники находились далеко друг от друга. Они и не находились далеко. Исследовательская программа, историю которой мы будем писать, начиналась в Эдинбурге и соседнем с ним городе Сент-Эндрюсе, так что мы, наконец-то, нашли рамки которые нам подходят, пусть и географические, что, конечно, не лучший вариант, но что есть - то есть.
Почти все языки, происходящие от первоначальной группы языков Эдинбургской программы, сохранили обсуждаемые свойства, а если мы сделаем еще шаг назад и рассмотрим то, от чего они произошли - назовем эту, предыдущую программу "ветвью обоих Кембриджей", то такой однородности всех происходящих от нее языков не будет.
Мы пишем историю "Эдинбургской исследовательской программы". Но это звучит длинновато, так что далее мы обычно будем называть группу языков, которую мы выбрали для написания истории функционального программирования, просто "функциональные языки", как делали и наши великие предшественники. В этом смысле наша работа ничем не отличается от других работ по истории функционального программирования. А в чем отличается?
История развития теории типов, которая в последствии привела к системе типов Standard ML, насчитывает более ста лет. Д. МакКвин, Р. Харпер, Дж. Реппи, История Standard ML [MacQ20]
Не то чтобы литературы по истории ФП было мало. Уже существуют как обзорные материалы по ФП вообще [Huda89] [Turn12], так и истории отдельных языков или их семейств [Hud07] [MacQ20], биографии исследователей [Camp85] [MacQ14]. Зачем нужна еще одна?
Когда, в очередной раз, не хватает десятков гигабайт памяти для компиляции кода на Хаскеле, естественным образом возникает вопрос: в каком же смысле функциональное программирование существовало в каком-нибудь 1973-ем году? К сожалению, материалы по истории ФП обычно не уделяют этому особого внимания. Для историй функционального программирования в них часто слишком мало истории программирования.
Мы не ставим перед собой цели дать определение "программированию", но в этой работе предполагаем, что это процесс написания программ. И наши великие предшественники, в своих работах по истории функционального программирования, не особенно любят писать какого размера программы получались в результате этого процесса.
Более того, часто историк программирования уходит в такие глубины прошлого, про существование программирования в которых можно говорить только с большой натяжкой. Например, предыстория Standard ML начинается аж с 1874 года [MacQ20].
Понятно, что в 1874 функционального программирования не было, но было ли оно, например, в 1974-ом? Какие программы к этому году были написаны на функциональных языках? Какие имплементации были доступны и для кого? До какого года ФЯ могло существовать только так же, как могло и в 1874-ом году: как нотация в книгах, тетрадях, на досках и так далее?
Например, часто утверждается, что ML появился в 1973 году, но что именно произошло в этом году? Упрощенно говоря, в этом году у Робина Милнера появилось желание писать интерактивный решатель теорем не на Лиспе. И в нашей работе мы покажем в каком году Милнер написал часть интерактивного решателя теорем на ML. Какая это была часть, сколько в ней было строк кода. Кто и через сколько лет написал интерактивный решатель теорем полностью на ML. И кто и когда написал интерактивный решатель теорем полностью на ML, имплементация которого, в свою очередь, и сама написана на ML. И сколько строк кода было в этой имплементации.
Между мечтой и возможностью большая разница и существенный временной интервал и мы считаем, что такая перспектива может быть полезной, если кто-то не согласен, что история чего-то начинается с мечты, и на ней же и заканчивается, ведь имплементация идеи тривиальна и не интересна.
Нельзя сказать, что вопрос применимости имплементаций ФЯ для программирования вовсе не поднимается, но он не особенно интересен нашим великим предшественникам. В некоторых обзорах перечисляют имплементации, которые авторы (обзора, но нередко автор обзора является и автором одной из этих имплементаций) считают "неигрушечными", "эффективными" [Huda89], "быстрыми" [SPJ87], но критерии не сформулированы четко, и вполне возможно, что у вас бы сложилось другое впечатление, если б вы узнали об этих имплементациях больше. У нас определенно сложилось другое впечатление. В таких списках через запятую перечислены компиляторы, которые компилировали себя и другие проекты в десятки тысяч строк вместе с компиляторами, которые этого не делали.
Поэтому мы постараемся установить, какого размера программы писали с помощью имплементаций ФЯ, какая была производительность у этих программ.
Если что-то может помешать историку ФП все больше и больше углубляться в прошлое - то это название. Некоторые авторы так любят какое-то название, что продолжают использовать его для все новых и все менее схожих вещей. Синтаксисы многих функциональных языков в 80-х более похожи друг на друга, чем на свои версии с теми же названиями из 70-х. К счастью, первый раз назвали что-то ML очень давно, и нет препятствий для того чтоб начать историю ML с начала, или даже задолго до его начала. К сожалению, многие любят называть одно и то же по-разному. Да, первый компилятор Хаскеля был получен всего за пару месяцев из компилятора Нехаскеля, что явно указывает на то, что уже была проделана большая работа, которую вполне обоснованно можно считать работой по созданию Хаскеля. Три из пяти первых компиляторов Хаскеля разрабатывались долгие годы до того, как появилась сама идея спроектировать этот язык. Увы, Нехаскель не назывался "Хаскель", так что ничего не поделаешь - в истории Хаскеля [Hud07] уделить этому больше нескольких строк нельзя. Да, авторы Хаскеля выбрали другой Нехаскель как основу для первого Хаскель-репорта. И этот Нехаскель похож на Хаскель 1.0 больше чем ML в 82-ом на ML в 84-ом. Извините, название было не "Хаскель" - наши великие предшественники не могут писать историю этого.
Все это, в основном, последствия того, что наши великие предшественники писали в первую очередь истории идей. И они делали это не потому, что это легко. Одна из причин того, что мы не собираемся писать историю идей - проследить историю идей очень сложно. Так что мы пишем историю имплементаций в первую очередь, и только потом историю идей. Для каждой идеи можно найти ту, от которой она произошла, и каждая тащит все дальше в глубины веков, там идеи до самого низа. Так историк функционального программирования и оказывается в 1874-ом году. История имплементаций легко решает эту проблему (решает даже слишком хорошо, но об этом позднее). Идеи оставляют меньше следов, и следов менее удобных для историка. Имплементация оставляет после себя разные версии кода, отметки в нем, репорты, анонсы и описания релизов, из которых понятно что заработало и когда. Идея оставляет после себя статью, которая может быть опубликована через годы и "приведена в порядок" каким-то стирающим историю способом. Что это значит? Обзор родственных работ в статьях документирует отношение идей, но не их историю. Влияние одних идей на другие может декларироваться, но на самом деле "повлиявшая" идея может быть обнаружена только после того, как идея на которую она "повлияла" была переоткрыта самостоятельно. Бывает, что автор прямо говорит об этом. Например Ксавье Леруа пишет что переизобрел некоторые идеи из машины Кривина не зная о ней и добавил ссылки только после того, как другие люди обратили его внимание на сходство. Часть идей по имплементации Chez Scheme ее автор открыл самостоятельно, а потом только обнаружил их в VAX ML, а часть действительно позаимствовал. Но не все считают нужным сообщить об этом. Хуже того, такие свидетельства могут противоречить другим. Например, мнение авторов языка о том, на какие языки они повлияли может не совпадать с мнением авторов этих языков о том, какие языки повлияли на них. Тут нет ничего удивительного, идеи о том, как делать языки программирования, нередко возникают независимо. Даже алгоритм Хиндли-Милнера - нетривиальная и точно специфицируемая идея - был независимо переизобретен, но никто не написал независимо два одинаковых компилятора. Дополнительная сложность тут в том, что добавлять ссылки на статьи сильно легче, чем использовать код компиляторов, так что в графе получается слишком много ребер, если мы пишем историю идей. Для объединения имплементаций в семейства мы будем использовать более редкие и надежно устанавливаемые отношения: общий код, общие авторы, использование одной для бутстрапа другой.
Что представляет из себя наша история имплементаций? В первую очередь - это история компиляторов в код обычных машин, в меньшей степени - история интерпретаторов и специальных аппаратных имплементаций. Мы выбираем компиляторы для обычных машин как наиболее успешное направление имплементации ФЯ. Раз уж они дожили до наших дней (в отличие от специального железа) и больше применялись для написания нетривиальных программ (в отличие от специального железа и интерпретаторов) про них просто больше известно, есть о чем писать. С другой стороны, компилятор достаточно масштабный проект, чтоб их было не слишком много (в отличие от интерпретаторов), так что у нас получается вполне обозримый набор достаточно подробных историй, а не бесконечные списки с немногословными описаниями. Мы, конечно же, опишем и часть истории интерпретаторов и специальных машин в той степени, в какой это необходимо для понимания истории компиляторов, но не ставим перед собой цели сколько-нибудь полно осветить этот вопрос.
Но и у истории имплементаций есть проблемы. История программирования не очень богата событиями, когда функциональные программисты не так уж много пишут нетривиальные программы. Что такое нетривиальные программы? Например, код компилятора этого или другого функционального языка, код интерактивного решателя теорем. Планка не выглядит высокой, но не для ФЯ, конечно. Причем, не то что-бы был какой-то выбор того где должна находится эта планка, потому что кроме того как быстро работают микробенчмарки и как быстро компилирует компилятор написанный на каком-нибудь ФЯ, мало что можно узнать. Можно прочесть про одни имплементации, что числа Фибоначчи вычисляются не сильно медленнее чем на C, и что на языке написан компилятор, компилирующий что-то за приемлемое время, а про другие прочесть то, что компилятор на них не написан, а производительность не так-то и важна. Поскольку история программирования и имплементаций сильно сдвинута к концу истории идей, она плохо ложиться на типичную для истории идей структуру "прогресса" и "формаций", когда все начинается с Лиспа, продолжается строгими ФЯ и завершается ленивыми. Вместо этого у нас после периода, когда ничего не работает, наступает период, когда заработало все и везде. Так что нам нужно как-то дополнительно структурировать "еще не работает" и "уже начинает работать". Нашу историю структурирует жизненный цикл имплементаций: Если имплементация достаточно старая, она могла долгие годы существовать как имплементация языка, который не похож на то, что мы определили как ФЯ в предыдущей главе. Например, Chez Scheme была использована как бэкенд для имплементации языка, который мы считаем ФЯ - Idris 2 - примерно после 35-илетней истории (если не считать один эксперимент). С течением времени, необходимые фичи в этот язык добавлялись, или для имплементации вовсе писался фронтенд для другого языка. Более новые имплементации уже могли с самого начала быть имплементациями ФЯ Эдинбургской программы. Если имплементация существует достаточно долго, то она используется для бутстрапа другой имплементации, написанной с самого начала уже на этом ФЯ. Если имплементация недостаточно успешна, то этого может и не произойти. У этого простого цикла "предыстория-хопизация-бутстрап" могут быть и другие осложнения, разветвления и т.д. но общая структура обычно видна.
И последнее, но не наименее важное, история имплементаций ставит перед нами вопрос, ответ на который требует описывать и историю обычных машин и операционных систем, которую мы, разумеется, тоже будем описывать без каких-то претензий на полноту: Если идея функционального программирования, как это выяснили наши великие предшественники, существовала столько же, сколько и программирование или даже дольше, то почему функционального программирования не было? Когда оно могло бы уже появиться? И что было тогда, когда функционального программирования не было?
И, наконец-то, мы покончили с сильно затянувшимся предисловием к предисловию и у нас впереди сильно затянувшееся предисловие про сильно затянувшуюся эпоху, когда идея функционального программирования была, а функционального программирования не было, ведь первый компилятор функционального языка Эдинбургской исследовательской программы появился только в 1981-ом году.
Было сказано, в частности Морисом Уилксом, что этот проект был полным провалом и его не следовало и начинать. Мартин Ричардс, Кристофер Стрейчи и Кембриджский компилятор CPL [Rich2000]
"Различные формы факториалов", рукопись 6 страниц, без даты Каталог статей и писем Кристофера Стрейчи [Strachey]
История функциональных языков отделилась от истории всех прочих языков, когда в одном Кембридже задумали сделать практичный функциональный язык общего назначения, а затем в другом Кембридже удалось имплементировать два языка: один из них был практичным языком общего назначения, а другой - функциональным.
Нужно заметить, что история языков с первоклассными функциями отделялась от истории всех прочих еще два или три раза, но Эдинбургская Исследовательская Программа произошла именно от этого, двойного кембриджского ответвления, которое называется так потому, что семейство языков разрабатывалось в основном исследователями, которые сначала поработали в одном Кембридже, а потом в другом Кембридже.
До начала истории имплементаций ФЯ была история ошибочных представлений о том, что такая имплементация уже может быть написана. И одним из первых их носителей был Кристофер Стрейчи (Christopher Strachey). В начале 60-х он работал консультантом и в этом качестве участвовал в проектировании компьютеров и разработке ПО. По-видимому, с переменным успехом, но мы не изучали этот вопрос глубоко. Его единственным сотрудником и вторым программистом компании с января 1960 был Питер Ландин (Peter Landin), который часть рабочего времени (согласованно со Стрейчи) тратил на исследования, которые имеют непосредственное отношение к истории ФП [Camp85], а именно: формальную семантику ЯП, трансляцию ЯП в ЛИ и виртуальную машину для интерпретации лямбда-выражений [Land64]. Знакомый Стрейчи Морис Уилкс (Maurice Vincent Wilkes) руководил кембриджской математической (позднее - компьютерной) лабораторией (University Mathematical (Computer) Laboratory, Cambridge). Вместе со Стрейчи Уилкс критиковал ALGOL 60 [Stra61] за отсутствие синтаксических различий между чистыми и прочими, рекурсивными и нерекурсивными функциями. В этой их работе появились первые наброски CPL [Rich13]
Летом 62-го Уилкс пригласил Стрейчи работать в свою лабораторию для участия в разработке языка и компилятора для нового компьютера Titan (позднее Atlas 2 [TITAN]), который лаборатория должна была получить через два года [Camp85]. Переход Стрейчи из консультантов в академики плохо сказывался на его доходах, но это должна была скомпенсировать уникальная возможность поучаствовать в проектировании и имплементации опередившего свое время языка. Именно поучаствовать в проектировании и имплементации, а не спроектировать и имплементировать, потому что, забегая вперед, этого так и не произошло. Язык опередит и существующие в то время возможности его имплементировать.
Первый пропозал CPL (Cambridge Programming Language) был написан Стрейчи, Девидом Барроном (David W. Barron) и Девидом Хартли (David F. Hartley) (которого Стрейчи при первой встрече за несколько лет до того отговорил имплементировать ALGOL 60 [Hart2000]) в августе 62-го. Но уже осенью, по инициативе Баррона и Хартли [Hart2000] [Hart13], начинается сотрудничество с университетом Лондона (London University Computer Unit), который ожидал старшую версию компьютера из той же линейки - Atlas [ATLAS]. К авторам присоединяются Эрик Никсон (Eric Nixon) и Джон Бакстон (John N. Buxton), а CPL становится Combined Programming Language. О Питере Ландине вспоминают как о участнике разработки языка [Rich2000], хотя официально он над языком не работал. В феврале 63 готова статья “The Main Features of CPL” [Barr63]. Не смотря на схожесть компьютеров, каждый университет пишет свою имплементацию CPL [Rich13]. Лондон - более традиционную, а Кембридж - более экспериментальную, основанную на идеях Ландина, видимо о компиляции через промежуточный язык "аппликативных выражений". Но с октября 63-го обе команды имплементаторов совещаются каждый месяц. Встречи комитета частые, долгие и запоминающиеся [Rich2000]. В Кембридже имплементацию планируют закончить в начале 64-го, ко времени получения Titan.
Но Стрейчи все больше переключался на формальное описание семантики, которым заинтересовался под влиянием Ландина. Баррон, один из самых активных участников сначала, тоже переключился на другие дела, хотя какое-то время участвовал в совещаниях, но компилятором уже не занимался. Над компилятором работали аспиранты, года по три, причем последний из трех в основном над своей диссертацией [Rich13] [Hart13].
Летом 64-го Уилкс, недовольный медленной работой над компилятором, назначил руководителем команды имплементаторов Дэвида Парка (David Park), работавшего в Оксфорде и МТИ и имевшего опыт имплементации ЯП. Это не помогло. Вклад Стрейчи все сокращался, ему было не интересно работать над практичной имплементацией, да она и не получалась: предварительная версия компилятора работала медленно, в чем обвинили новаторский подход.
Только вот лондонская имплементация тоже не была завершена, удалось имплементировать только непрактичный компилятор подмножества CPL.
Фактически, над обеими имплементациями более-менее постоянно работало по одному человеку: компилятор в Лондоне писал Джордж Кулурис (George Coulouris) [Coul], а кембриджский компилятор писал Мартин Ричардс (Martin Richards) - важный герой нашей истории, к которому мы еще вернемся.
В 65 Стрейчи уходит из Кембриджа в МТИ на год, а потом в апреле 66-го в Оксфорд. Дэвид Парк тоже уходит из Кембриджа работать со Стрейчи в Оксфорде.
Все это время, впрочем, Стрейчи работает над описанием CPL, что идет не очень хорошо, потому что он годами может взаимодействовать с прочими авторами только по переписке.
В июне 66 комитет назначает Стрейчи редактором мануала, после чего большинство участников уходят из комитета официально потому, что уже и так занимались другими вещами.
Этот мануал не был опубликован, но циркулировал как самиздат [Stra66b]. Проект в Кембридже завершился в декабре 66-го [Rich13]. Тогда же, в декабре 1966, состоялась последняя встреча комитета, на которой договорились прекратить работу. [Hart13]
Джордж Кулурис в Лондоне написал компилятор подмножества CPL под названием CPL1 к осени 67-го [Coul68]. Этот компилятор описывают как более-менее законченный, но компилирующий только небольшие программы и непригодный для практического использования [Hart13]. Кембриджский компилятор описывают как непрактичный в лучшем случае [Rich13] или как вовсе не законченный [Hart13], как полный провал по мнению директора лаборатории [Rich2000]. Те, кому не нравится считать CPL "полным провалом", находили утешение в том, что CPL хоть и был неудачей по обычным стандартам, вроде известности, соблюдения сроков и эффективности имплементаций, но повлиял на другие языки [Camp85] [Rich2000]. Утешение в этом будут находить и следующие поколения дизайнеров и имплементаторов ФЯ.
Участники проекта в своих воспоминаниях объясняют провал проекта потерей интереса: Баррон и Хартли занялись ОС для Titan/Atlas 2, а Стрейчи и Парк - формальной семантикой ЯП [Hart13]. Только вот это была не первая и не последняя неудачная попытка сделать компилятор ФЯ. Что если не прогресс остановился из-за потери интереса, а интерес был потерян из-за остановки прогресса по другим причинам? К выяснению того, что это были за причины мы еще вернемся.
CPL задумывался как единый универсальный язык общего назначения [Hart13], но комитет, не смотря на практически полное согласие со Стрейчи [Hart2000] [Rich2000], мало что функционального специфицировал, а имплементировано было еще меньше. И CPL распался на диалекты: тот, что удалось имплементировать в Кембридже, тот, что удалось имплементировать в Лондоне. Для написания имплементации в Кембридже в 65-66гг. выделили самое простое подмножество из всех - практически виртуальную машину [Rich13]. Наконец, самый известный и влиятельный CPL - тот, что имплементировать было непонятно как, да и не стали пытаться.
Летом 63-го года Стрейчи, Баррон и Филип Вудвард (Philip Woodward) из Королевского института радиолокации прочли несколько лекций по программированию с примерами на "CPL" [Wood66] [Stra66]. Эти лекции были изданы в сборнике в 1966 [Fox66].
Важно уточнить, что код и в этих статьях, и даже в статье, называющейся "Основные фичи CPL" [Barr63] не является кодом на какой-то версии CPL, которая была описана комитетом и тем более имплементирована. Интересно, что и автор лондонской имплементации Кулурис посчитал нужным явно написать, что нет, написать примеры с помощью CPL из мануала [Stra66b] или имплементированного в Лондоне CPL1 нельзя [Coul68].
Это был гораздо более впечатляющий CPL, который был настолько хорошим языком, насколько Стрейчи хотел, чтоб он был, без всяких рамок которые могла накладывать имплементация или даже непротиворечивость самого языка, который мы будем называть псевдоCPL.
Стрейчи просто писал map
и foldr
(который называл Lit
)
let Map[f, L] = Null[L] -> NIL,
Cons[f[Hd[L]], Map[f, Tl[L]]]
Map[F, (1, 2, 3)] where F[x] = x * y where y = 1
let Lit[F, z, L] = Null[L] -> z,
F[Hd[L], Lit[F, z, Tl[L]]]
Lit[F1, 1, (1, 2, 3)] where F1[x,y] = x * y
не зная как его можно имплементировать и можно ли вообще. Как, сделать так, чтоб в коде типизированного языка не было аннотаций типов? Стрейчи даже не написал, что знает как это сделать, но алгоритм не поместился на полях. Что поделать, выхода у будущих имплементаторов ФЯ не было, пришлось придумать способ и не один. И тот, кто хотел аннотировать типы и рекурсию и тот, кто не хотел, могли помнить о псевдоCPL как о языке, который им нравился, и который они хотели бы повторить. Со временем, так научатся делать и с имплементированными языками. Но до этого пока далеко.
Эта идея не была новой. За три года до того уже выходила статья с примерами кода на языке, название которого совпадало с названием языка, который в это время имплементировался. Разумеется, эта имплементация также не поддерживала код из статьи:
maplist[x; f] = [null[x] -> NIL;
T -> cons[f[car[x]]; maplist[cdr[x]; f]]]
maplist[(1,2,3); λ[[x]; x + y]]
(пример в статье отличается отсутствием вызова car)
Мечты о ФП коде в лекциях Стрейчи и д.р. - итерация похожих мечт из этой статьи про Лисп [McCa60], которой оказалось для многих достаточно, чтоб считать Лисп первым ФЯ. Псевдокод не похож на Лисп? Ну да, Лисп-первый-ФЯ не состоялся. Планировалось, что Лисп будет выглядеть так. Это M-выражения, которые не были ни имплементированы, ни даже специфицированы потому, что имплементация фронтенда все затягивалась, и лисперы привыкли писать на промежуточном коде - S-выражениях. В результате, написание транслятора из M в S выражения было заброшено. Но для имплементации ФЯ важнее то, что не заработала как надо передача функций в функции [McCa78]. К этой проблеме мы еще вернемся.
Можно было бы сказать, что основная новация CPL-ной итерации в том, что работать со списками теперь собирались в типизированном языке. Но сложно назвать инновацией одно только намерение это сделать. Ни во время чтения этих лекций, ни во время их издания еще толком не понимали как такой код типизировать. Если посмотреть на расширения ALGOL 60, которые в то время имплементировали в Королевском институте радиолокации [Wood66]
list procedure Append(x, y); list x, y;
Append := if Null(x) then y else
Cons(Hd(x), Append(Tl(x), y))
то видно, что тип списка просто list
без параметра. Но во времена ALGOL 60 не знали и как функции нормально типизировать, там и тип функции параметризован только по возвращаемому типу, например real procedure
. Правда, в 1967-ом кое-какие идеи на этот счет у Стрейчи появились.
К сожалению, не на всех производит впечатление псевдокод в статьях, многие важные имплементаторы ФЯ вспоминают, что на них произвели впечатление работающие вещи. [Turn12][TODO Леннарт Августссон, Марк Джонс, Ксавье Леруа] Но эти работающие вещи для них сделали те, кто просто не мог смириться с тем, что такой фантастический язык остается фантастическим. И позднее они придумали и имплементировали языки, на которых можно писать map
именно так, а можно и лучше.
И писать так map
они хотели достаточно долго, чтоб атавизмы этого стиля вроде head
и tail
попали в ФЯ и дожили до наших дней, хотя сам стиль устарел еще до первой практической имплементации.
<Я> надеялся <...> сдержать создание сомнительных языков программирования, называемых (как группа) OWHY, что означает "Or What Have You". Никто не понял шутки, и попытка была обречена. Дана Скотт, A type-theoretical alternative to ISWIM, CUCH, OWHY, 1993 [Scot93]
CPL разрабатывался в первую очередь как практичный императивный ФЯ с добавленными функциональными фичами, в реальность которого верили и имплементацию ждали [Wood66], но фантазии про который сильно опередили его имплементацию. Параллельно с ним, в обоекембриджской программе развивался второй язык - ISWIM, который двигался в противоположном направлении.
ISWIM (или YSWIM в черновиках [Landin]), что означало "If you See What I Mean", описывал в серии статей Питер Ландин.
Сначала как чисто функциональный псевдокод для описания спецификаций, транслируемый в лямбда-исчисление [Land64]. Затем как императивный псевдокод, транслируемый в лямбда-исчисление, расширенное добавлением мутабельности и средствами захвата продолжения для имплементации произвольного потока исполнения [Land98]. И все еще для описания спецификации, теперь уже ALGOL 60 [Land65a] [Land65b].
Наконец, как "попытку сделать систему общего назначения для описания одних вещей в терминах других вещей, которая может быть проблемно-ориентированной при подходящем выборе примитивов." [Land66]. Неостановимую фабрику ФЯ, следующие 700 языков программирования.
Эти встречные движения привели к тому, что псевдоCPL и ISWIM, а точнее их функциональные подмножества оказались примерно в одном и том же месте и разбор отдельных фич для каждого был бы слишком повторяющимся, так что собран в одной главе.
На ISWIM написаны, по видимому, более крупные фрагменты ФП псевдокода, чем на CPL.
Самый крупный из них - парсер ALGOL 60 в абстрактное синтаксическое дерево, его описание, компиляция этого дерева в императивные аппликативные выражения IAE. Всего ~500LOC функционального псевдокода [Land65b].
Предполагалось, что такое описание семантики можно "исполнять" вручную, с помощью ручки и бумаги, но на практике это, конечно, едва ли осуществимо.
Такой подход к описанию семантики - как наивного интерпретатора написанного на ФЯ - имеет смысл только при механизации этой "семантики", когда интерпретатор действительно работает, как у авторов Clean в восьмидесятые или, например, у Россберга в десятые.
Так что, по крайней мере попытка имплементации была неизбежна. И именно от этой неизбежной механизации "семантики" останется литература по имплементации ФЯ, но о ней позже.
Сложно точно установить что на что повлияло в случае функциональных подмножеств CPL и ISWIM.
Ландин написал статьи в первом приближении еще во время работы у Стрейчи [Land65b], т.е. в 60-62 годах [Camp85], прочитал по ним лекции в 63-ем [Fox66], но опубликовал позднее, когда работал в Univac подразделении Sperry Rand и МТИ.
Функциональное подмножество ISWIM может быть старее CPL, но выглядит современнее и с меньшими странностями. Но современнее выглядит просто то, что больше понравилось авторам современных языков.
Очередная функция map
, на этот раз на ISWIM 65 [Land65b]
rec map f L = null L -> ()
else -> f(h L) : map f (t L)
map f (1, 2, 3) where f x = x * y and y = 1
В Univac к ISWIM проекту присоединился Уильям Бердж (William H. Burge), описывающий типизированные аппликативные выражения TAE [Burg64] [Burg66].
CPL разрабатывался на основе ALGOL 60 [Rich13] [Hart13]. В описании ISWIM ссылаются на псевдоLISP 60-го года и ALGOL 60 [Land66].
"Основа Алгола" в это время - лексическая видимость и поддержка рекурсии. То, что обычно авторы языков и заимствовали из Алгола. Для заимствования этих идей не нужно знать ALGOL 60, если знать про лямбда-исчисление, но многие изобретатели ФП ссылаются на влияние и ЛИ и ALGOL 60. Почему так происходило нам не понятно. Свойства ALGOL 60 по всей видимости не являются изобретенными независимо от ЛИ, а являются следами частично успешной борьбы меньшинства комитета за то, чтоб сделать ALGOL ФЯ [Naur78]. Эта борьба не закончилась на ALGOL 60 и к ней мы еще вернемся. Возможно, ссылающимся на ALGOL была важнее доказанная практикой возможность имплементации.
Синтаксические решения из ALGOL 60 в ISWIM и CPL в основном не попали, о некоторых исключениях - ниже. Не попали и непопулярные, среди заимствующих из Алгола, фичи вроде передачи параметров по имени.
CPL - язык со множеством императивных фич, которые оказали серьезное влияние на мейнстрим. Также, со множеством странных фич. Вроде двух разновидностей имен - однобуквенных из строчной буквы и нуля и более праймов и многобуквенных, начинающихся с заглавной и могущих включать в себя числа. Для чего? Чтоб умножение могло быть не оператором, а отсутствием оператора.
Square[x] = xx
Но не все странные фичи CPL остались в CPL, некоторые присутствовали и в ISWIM и оказали влияние на Эдинбургскую исследовательскую программу.
Этот класс странных фич как раз демонстрирует происхождение Эдинбургских языков от CPL/ISWIM, странными они стали только исчезнув из этих языков в 80-х и 90-х годах. Какой еще язык претендует на то, что от него произошли языки эдинбургской программы, и эта претензия более-менее правдоподобна без учета этих странных фич? Не все сразу!
Отсутствие лямбды довольно неожиданная особенность для ФЯ, и за пределами круга разработчиков CPL/ISWIM идея захватила только еще одного автора ФЯ. И не смотря на то, что это автор нескольких влиятельных языков, и многие имплементаторы ФЯ хотели делать языки как у него, всего этого влияния не хватило для того чтоб популяризировать ФЯ без лямбд. И едва ли для кого-то такой результат покажется неожиданным. Нам не удалось установить почему лямбда не попала в CPL/ISWIM. Возможно, это наследие ALGOL 60. Возможно потому, что авторы увлеклись эквивалентной конструкцией, которую считали одной из основных фич обоекембриджских языков и хотели использовать только ее. Эта фича -
Что делать, если лямбд нет, а хочешь написать
Quad [a, b, \x -> G[x, y]]
? Нужно писать [Barr63]
Quad [a, b, F] where F[x] = G[x, y]
Выражение where
- полный аналог современного let
in
, а не производная и гораздо более новая фича where
-как-часть-других-конструкций, известная по Haskell.
В наши дни выражение where
полностью заменено на let
и where
как часть других конструкций. И хотя уже в 66-ом в описании [Stra66b] обсуждается какая-то смутная неудовлетворенность where
-выражением и неопределенные планы сделать where
-как-часть-декларации, фича существовала десятилетиями. Причем where
-выражение не осталось только в мертвых языках, не попав в современные, а успело побывать в дизайне и имплементациях Haskell, Standard ML и Caml. Редкий пример фичи, которая попала в эти языки и не осталась в них навсегда.
Авторы CPL не претендовали на изобретение where
, утверждая, [Barr63] что позаимствовали конструкцию из калькулятора GENIE [Ilif61], но не в точности. В GENIE вместо ключевого слова where
использовалась запятая.
попал в мейнстрим, но в ФЯ вытеснен конструкциями из ключевых слов и частью других конструкций - гардами, на синтаксис которых он, вероятно повлиял, но не так сильно как синтаксис ветвления в псевдолиспе из [McCa60], возможно через синтаксис ISWIM 64.
let rec fact(n) = n = 0 -> 1, n * fact(n-1)
В первом коде на ML (LCF 77) почти всегда используется тернарный оператор, а не if then else
конструкция.
В CPL let
это часть синтаксиса деклараций, и where
определяется через декларацию в возвращающем ссылку на значение блоке [Stra66b].
E where D === ref of § let D; result is E §
Но в ISWIM let
- выражение [Land65a], вводится как сахар для where
[Land66]
let x = M; L === L where x = M
Одна из первых идей для CPL еще из критики ALGOL 60 [Stra61], которая повлияла на языки Эдинбургской программы - аннотация рекурсии. В своей конечной и наиболее популярной форме - с помощью ключевых слов rec
и and
.
Для ALGOL 60 такие аннотации рассматривали, но отклонили с небольшим перевесом [Naur78]
Не смотря на то, что Стрейчи критиковал ALGOL 60 за отсутствие аннотаций рекурсии, в работах, в которых "CPL" используется как псевдокод, он эти аннотации обычно пропускал [Stra66]. Так что не важно, есть в вашем ФЯ аннотация рекурсии или нет - избежать влияния CPL не удалось. Как и влияния ALGOL 60, разумеется.
Не аннотировать типы в псевдокоде легко, в имплементированном языке уже сложнее. Самое сложное, конечно, когда типы есть и их не надо аннотировать, но можно сделать и проще, если аннотировать нечего или если замести аннотации куда-нибудь не на очень видное место. В каждом из трех первоначальных языков Эдинбургской программы будет один из трех основных способов: хороший, плохой и отвратительный.
В ISWIM [Land64] [Land65a]
let x = M; L
то же, что и
let x = M
L
Зачатки паттерн-матчинга, пока без вложений и ветвлений.
В ISWIM [Land64]
(u,v) = (2*p + q, p - 2*q)
В псевдоCPL [Stra66] и ISWIM 65 [Land65b]
let u, v = 2*p + q, p - 2*q
За считанные месяцы до окончательного прекращения всех работ по CPL Стрейчи делает доклад [Stra67] в котором рассуждает о системе типов, о полиморфизме и различиях между параметрическим и ad-hoc, о том как, возможно, будут сделаны составные типы и что будет, возможно, решено делать ли в CPL параметрические типы и если да, то как. Никто это не решал и не имплементировал, но, благодаря этим анонсам и рассуждениям, Стрейчи стали считать одним из изобретателей полиморфизма.
В докладе Стрейчи наконец-то представляет тип всех тех псевдокодовых map
[Stra67]:
(α ⇒ β, α list) ⇒ β list
Лекции были опубликованы только в 2000 году, так что префиксная форма полиморфных типов может быть более поздней нотацией "как в ML". Но, по видимому, происходит от постфиксных аннотаций вроде string ref
, которые были имплементированы [Coul68] и вероятно происходят от типов из ALGOL 60. Таких как integer array
.
В типизированном ISWIM Берджа [Burg64] эта сигнатура выглядела бы
map ∈ (A -> B) -> (A-list -> B-list)
но именно такой сигнатуры мы в его работах не видели.
Эдинбуржцы ссылаются на работу Стрейчи по полиморфизму [Miln78], но в такой тривиальной форме идея параметрического полиморфизма была, по всей видимости, переоткрыта независимо. Например авторами языка CLU, на который эдинбуржцы также ссылаются. Возможно, переоткрыта даже в рамках Обоекембриджской программы.
Интересно, что автор языка CLU Барбара Лисков (Barbara Liskov) пишет [Lisk93], что параметризованные типы в CLU появились из идеи сделать пользовательские типы вроде упомянутых уже выше массивов или функций ALGOL 60. Т.е. как очевидный шаг, которому не уделяется особого внимания в статьях по CLU и его истории. В отличие от ограниченного "интерфейсами" параметрического полиморфизма, который описывается как интересная и сложная идея и проблема, потребовавшая долгие годы для дизайна и имплементации.
И для интересных проблем, касающихся полиморфизма, от обоекембриджцев решений эдинбуржцам не осталось.
Тернер утверждает, что да [Turn12], и на первый взгляд он прав. Уже в статье [Land64] 64-го года и, вероятно, в лекциях для летней школы 63-го года [Fox66] Ландин использует неформальную нотацию для определения структур данных:
A list is either null
or else has a head (h)
and a tail (t) which is a list.
Описание у Ландина достаточно неформальное, чтоб не связываться с проблемами не изобретенного еще как следует полиморфизма и вообще избегать указывать что представляет из себя поле head
(h
там не параметр, а сокращенное имя поля). Бердж описывает список как параметрический [Burg64], или, может быть, как конвенцию по именованию списков
An A-list is either null and is a nullist
or has a head (or h) which is an A and a tail
(or t) which is an A-list
Если на первый взгляд Тернер прав, то на второй и следующие взгляды сходство уже не так очевидно. Такая декларация объявляет не конструкторы АлгТД, которые могут использоваться и как паттерны. Паттерн-матчинга в ISWIM еще нет. Декларация объявляет предикаты и селекторы, имена которых указывает программист, а также конструкторы, имена которых формируются по правилам из имен предикатов и имени всей структуры:
null(constructnullist()) = true
null(constructlist(x, L)) = false
h(constructlist(x, L)) = x
constructlist(h L, t L) = L
В данном случае имя для cons
вообще не написано (понятно, что такой предикат избыточен, хватит и null
), но в более поздних статьях списки определены с cons
[Land66].
Ладно, паттерн-матчинга нет, но хотя-бы общая идея сумм произведений-то была изобретена? Вроде бы, со вторым, более узким определением Тернер прав. Идея сумм произведений ясна.
Но нам эта идея ясна потому, что мы уже знаем АлгТД как суммы произведений. Посмотрим на более сложную структуру, например описание абстрактного синтаксического дерева ISWIM [Land66]:
an aexpression (aexp) is
either simple, and has
a body which is an identifier
...
or conditional, in which case it is
either two-armed, and has
a condition, which is an aexp,
and a leftarm, which is an aexp,
and a rightarm, which is an aexp,
or one-armed ...
Так, что тут у нас, сумма произведений сумм произведений ...
Нотация для псевдокода, для которого пока особо не придумывали как объявлять типы, а декларации АлгТД должны ведь и типы объявлять. И суммы произведений позволяют это удобно делать. А нотация Ландина - не позволяет, по крайней мере в неизменном виде. В современном ФЯ это выглядело бы как-то так:
data AExp
= Simple { body :: Identifier }
...
-- а как называется тип с конструкторами TwoArmed и OneArmed?
| Conditional ( TwoArmed { condition, leftArm, rightArm :: AExp }
| OneArmed ...)
...
Если посмотреть на то что получилось, когда обоекембриджцы попытались перейти от нестрогого описания к чему-то более точному и типизировать это, то тут-то появляются уже серьезные основания сомневаться в том, что суммы произведений уже были изобретены.
Отступив от бесконечно вложенных сумм и произведений слишком далеко назад они получили отдельные декларации для произведений и для сумм. Или не отступили, а просто позаимствовали эту систему из другого языка, к которому мы еще вернемся.
Ричардс вспоминает, что авторы CPL много обсуждали как сделать композитные типы но так и не остановились ни на чем до того, как он покинул проект [Rich13]. Этот процесс, по всей видимости, не документирован. Известен только конечный результат из того самого доклада Стрейчи [Stra67], в котором он рассказывал про полиморфизм.
node Cons is LispList : Car
with Cons : Cdr
node Atom is string : PrintName
with Cons : PropertyList
element LispList is Atom
or Cons
(да, тип и имя поля на неправильных сторонах :
)
Декларация вводит три новых типа Cons
, Atom
и LispList
и их конструкторы с селекторами.
element
объявляет не тип сумму, а или-тип, конструктор LispList
перегружен, параметр может принимать и объекты типа Atom
и Cons
. Тип, а не имя конструктора определяет что за значение конструируется.
Конструктор для пустого списка не определен потому, что в этом пропозале для CPL есть специальные конструктор NIL
и предикат Null
. Да, для того чтоб делать то, что обожают делать в мейнстримных языках, а вот в ФЯ Эдинбургской программы - не особенно.
Схожая "одноуровневая" система будет и в ISWIM. Далее мы увидим, что один из изобретателей ПМ по АлгТД уже в Эдинбурге понимал типизированную версию Ландинской нотации так же, как и авторы CPL.
Мы здесь рассматриваем возможность, а не практичность. Тем не менее, то, что написано ниже, может быть принято за отправную точку для разработки эффективной имплементации. Питер Ландин, ЛИ подход [Land66b]
Основная литература по имплементации ФЯ от обоекембриджской программы осталась от ISWIM - языка спецификации, который предполагалось "выполнять" с помощью ручки и бумаги. Это ставило реальную цель. Цель была достигнута и первые доклады сделаны Ландином в 63-ем и статья опубликована в 64-ом [Land64].
CPL был амбициозной попыткой имплементировать ФЯ общего назначения, но имплементация функциональной части не особенно продвинулась.
Про имплементацию CPL в Кембридже мало что опубликовано. Раз уж попытка закончилась неудачей, публиковать было нечего.
Мы бы не хотели создать впечатление, что цель Ландина была скромной, или что ее достижение было незначительным результатом. Цель была просто реалистичнее, чем цель CPL-щиков. Которые планировали имплементировать за два года ФЯ не только на самом этом ФЯ, но и как единственный язык для новой машины. Единственный потому, что должен был подходить для любых задач.
Сомнительно, что такую цель кто-то достиг бы и сегодня, при том, что мы знаем как делать ФЯ. А в 62-ом не знал никто. Но, правда, были уже две школы ФЯ-строения, которые либо не знали, что они не знают, либо не очень и хотели делать ФЯ. Причем не знали/не хотели они разные вещи. К этим направлениям недо-ФП-мысли мы вернемся позже.
Ландин всего-навсего был первым, кто придумал работающую идею. Вернее потенциально работающую. В предисловии мы определили практическую имплементацию как компилятор, способный компилировать хотя-бы компилятор ФЯ или что-то схожее по сложности. И наработки Ландина никогда не достигли этого уровня практичности. Но базирующиеся на них - достигли.
До Ландина механизация вычисления лямбда-выражений, которые действительно вычисляются как лямбда-выражения, была только в форме переписывания последовательностей символов или деревьев, как, например, оптимизатор компилятора переписывает код [Gilm63].
Ландин описал стековую машину, что гораздо практичнее, но не без проблем.
Стековая машина - это то, что до Ландина уже позволяло имплементировать язык с рекурсивными функциями [Dijk60] первого порядка, и даже решить половину проблем с функциями высшего порядка, а именно передавать функции в функции [Rand64].
Оставалось решить проблему с возвращением функций из функций.
Для этого Ландин использовал замыкания, сам термин введен им. Он, правда, не изобрел замыкание как структуру данных. Похожие структуры из ссылки на функцию и полей для её свободных переменных использовались, только на стеке, для имплементации передачи аргументов по имени в ALGOL 60 и назывались PARD [Dijk62]. Ландин применил их для возврата функций из функций, аллоцируя в куче со сборщиком мусора, который к тому времени уже применили в Лиспе [McCa60].
У Ландина получилась машина с четырьмя регистрами, названная SECD по именам регистров: Stack, Environment, Control, Dump.
C - программа, список аппликативных выражений, которые машина редуцирует.
S - стек, нужен для имплементации функций.
D - хранит снапшот полного состояния машины (S,E,C,D) и нужен для имплементации лямбд.
E - окружение. Нужно для того, чтоб у лямбд могли быть свободные переменные.
Первая опубликованная версия SECD-машины имплементировала лямбда-исчисление, в которое транслировалась чисто функциональная версия ISWIM [Land64].
В 64-ом Ландин расширил язык, добавив к ЛИ присваивание и операцию захвата продолжения. Это расширенное ЛИ он назвал IAE - императивные аппликативные выражения, а новую версию SECD-машины - sharing machine [Land65a]. Для имплементации захвата продолжения в этой машине добавлена еще одна разновидность замыкания "программа-замыкание", которое добавляет к функции не только окружение E, но все состояние машины D.
Версия статьи про SECD [Land64] 66-го года в сборнике [Fox66] уделяла больше внимания имплементации SECD с помощью компьютера, а не ручки и бумаги [Land66b].
Понятно, что имплементация рекурсии с помощью стека - не самый практичный способ имплементировать ФЯ. Обязательное использование сборщика мусора, практичной имплементации которого еще не существовало, было даже большей проблемой. Но эти проблемы были решаемы, и были решены позже, что мы в дальнейшем рассмотрим подробнее.
Пытались ли имплементаторы CPL использовать идеи Ландина? В воспоминаниях участников упоминается, что для имплементации используются "аппликативные выражения" [Camp85] как у Ландина. Но это скорее всего не означает трансляции в лямбда-исчисление. В описании имплементации нефункционального языка BCPL [Rich69] тоже говорится об аппликативных выражениях, но в данном случае это точно не лямбда - просто промежуточное представление в виде дерева.
Во время работы в Univac Ландин и Уильям Бердж имплементировали прототип SECD [Land66] для Univac 1107 [Burg64]. Эта попытка имплементации по всей видимости не была успешной, потому что от нее осталось только пара упоминаний в статьях и перечне документов из личного архива Ландина [Landin].
И Ландин и Бердж предпримут еще по одной попытке, но уже не в Univac.
Возможно, что вклад CPL в историю языков программирования пока не выглядит выдающимся. Но, как это не удивительно, уже описанного хватило для того, чтоб впечатлить некоторых участников Эдинбургской исследовательской программы. Чтобы впечатлить остальных, а сверх того, еще и сделать CPL непосредственным предком многих популярных современных языков, понадобились еще и труды исследователей и имплементаторов в другом Кембридже. Точнее, в основном, одного имплементатора - Мартина Ричардса.
Мартин Ричардс (Martin Richards) с 1959 изучал математику в том самом Кембридже. В октябре 1963 он приступил к работе над диссертацией, повстречался со Стрейчи, и его затянуло в CPL проект [Comp17]. Стрейчи не мог быть его научруком официально, и им стал Баррон, а после того, как тот потерял интерес к CPL через год - Дэвид Парк [Rich2000], интересы которого тоже со временем поменялись.
Три года, до декабря 1966, Ричардс был практически единственным человеком в Кембридже работавшем только над CPL, занимаясь имплементацией его минимального подмножества Basic CPL, фактически виртуальной машины, которое должно было быть использовано для имплементации полнофункционального компилятора. Который, как мы помним, не имплементировали.
Вырвавшись из трясины обреченного проекта, которым никто кроме него не занимался, Ричардс отправился в МТИ.
Перед этим он заверил Стрейчи, что уж там-то он развернет работу над имплементацией CPL по настоящему.
Или, по крайней мере, Стрейчи заверял что план был именно такой [Strachey]. Еще осенью 67-го он рассказывал в докладе [Stra67], что портабельный компилятор CPL скоро будет готов.
Казалось бы, после того как, брошенный своими научруками, Ричардс провалил написание кембриджского компилятора, можно ли было ждать от него успехов в МТИ? Но успехи не заставили себя ждать.
BCPL - это просто CPL из которого удалены все сложные части. Мартин Ричардс, Как BCPL эволюционировал из CPL [Rich13]
Ряд синтаксических и лексических механизмов BCPL элегантнее и последовательнее, чем в B и C. Деннис Ритчи, позаботившийся об этом [Ritc93]
В МТИ Ричардс работал вместе с Ландиным под началом Джона Возенкрафта (John "Jack" Wozencraft) [Rich2000] с декабря 66-го по октябрь 68-го. [Rich2000] [Comp17]
В МТИ Ричардс написал компилятор Basic CPL на AED-0 [Rich2000], похожем на ALGOL 60 языке, но с указателями и прочими фичами для системного программирования [Ross61]. Затем он написал компилятор BCPL на BCPL. Начальная версия компилятора в 1K строк была готова в начале 67-го года [Rich2000] для CTSS на IBM 7094 [Rich13]. За следующие 10 лет компилятор вырос до 5K строк и еще столько же строк тулинга вроде отладчика и редактора [Atki78].
Получившийся язык не был уже минимальным подмножеством CPL или ВМ для его имплементации. Это был достаточно большой язык со многими фичами позволяющими делать одно и то же разными способами - управляющими структурами и т.д. [Rich74], который сам использовал стековую виртуальную машину Ocode для удобства портирования компилятора [Rich69]. BCPL отличался от CPL тем, что все что в CPL было сложно имплементировать в BCPL не попало.
И все что нам, как историкам ФП, интересно - было сложно имплементировать. Понятно, что в BCPL нельзя было вернуть замыкание, и, соответственно, не требовался сборщик мусора. Но даже ограниченной ФП-функциональности ALGOL с передачей замыкания вниз по стеку не было. Свободные переменные могли быть только статическими [Rich74]:
let a, b = 1, 2
// Так нельзя
let f (x) = a*x + b
// Только так можно
static $( a = 1; b = 2 $)
let f (x) = a*x + b
Ричардс оставил в BCPL один тип - слово. Не стал имплементировать выражение where
, опередив на четверть века моду на выкидывание выражения where
из ФЯ. Убрал аннотации рекурсии.
Не смотря на все эти урезания, BCPL все равно сложный язык, со вложенными функциями, возвращающими значения блоками-выражениями. Компилятор BCPL строил абстрактное синтаксическое дерево [Rich69] и потому имел серьезные для того времени требованиями к памяти. Поэтому Кен Томпсон не мог его использовать на той машине, на которой писал Unix и сделал еще более урезанную версию языка без всех этих вложенностей: B [Ritc93]. Томпсон сделал и некоторые изменения не связанные с экономией памяти. B - один из ранних примеров декембриджизации. Это процесс изменения синтаксиса происходящего от обоекембриджской ветви на любой другой, лишь бы только иначе выглядящий, широко практиковался в наши дни авторами Scala, Rust, Swift и др.
От B произошел C и далее большая часть ЯП-мейнстрима, каким мы его знаем и любим.
Сам BCPL использовался до 80-х годов. В том числе и некоторыми имплементаторами ФЯ.
В апреле 68-го Стрейчи получил письмо от Ричардса о том, что тот передумал имплементировать CPL, а решил вместо этого дальше развивать BCPL [Strachey].
Поскольку никто не пишет реальные программы на PAL, мы можем позволить себе неэффективную имплементацию, которая иначе была бы неприемлемой. Артур Эванс, PAL - язык, спроектированный для преподавания. [Evan68]
Создав первый практический (он же первый нефункциональный) язык обоекембриджской программы, Ричардс не остановился на достигнутом и приступил к имплементации функционального языка с ограниченной практичностью.
Этим языком ограниченной практичности был PAL - педагогический алгоритмический язык [Evan68b], разрабатываемый специально для курса 6.231 МТИ "Programming Linguistics".
Первыми его имплементаторами были теперь работающий в МТИ Ландин и Джеймс Моррис (James H. Morris, Jr.). Они написали на Лиспе то, что конечно же было ISWIM-ом с незначительными изменениями. Но имплементация, видимо, получилась слишком непрактичной даже для языка, непрактичность которого пытались представить как фичу, позволяющую ему имплементировать всякие сумасшедшие вещи вроде лямбда-исчисления. Поэтому вторую версию в 68 году имплементировали Мартин Ричардс и Томас Баркалоу (Thomas J. Barkalow) на BCPL для IBM 7094 под CTSS. Компилятор в байт-код к 70-му году был 1.3KLOC, а интерпретатор 1.5KLOC.
PAL имплементирован с помощью SECD машины, которую авторы PAL почему-то называют CSED-машиной.
Проблема неуказывания типов в псевдокодах решена тем, что язык "динамически" типизирован.
Описывающие PAL говорят, что вторая имплементация отличалась от ISWIM несколько больше [Evan68] [Rich13], но не говорят чем.
Мы рискнем предположить, что это за отличие, тем более, что это отличие практически единственное заметное: в ISWIM добавили лямбду.
def rec map f L = Null L -> nil
! let h, t = L in (f h, map f t)
map (ll x. x + y) (1, (2, (3, nil))) where y = 1
Имплементация поддерживалась до 70-го года и язык несущественно менялся. С 69-го он выглядел так [Woze71]:
def rec map f L = Null L -> nil
| let h, t = L in (f h, map f t)
map (fn x. x + y) (1, (2, (3, nil))) where y = 1
Помимо уже перечисленных имплементаторов, в авторы языка записаны Артур Эванс (Arthur Evans), который написал большую часть статей, репортов и мануалов, Роберт Грэм (Robert M. Graham) и Джон Возенкрафт. Просто поразительно, конечно, сколько авторов может быть у идеи добавить лямбду в функциональный язык.
Следующая по очевидности, после добавления лямбды в ФЯ, идея, которая пришла бы в голову современному человеку, конечно, такая: у нас есть непрактичный, медленный ФЯ PAL и практичный язык на котором он имплементирован BCPL. Что если мы будем "склеивать" в коде PAL-скрипта вызовы быстрых функций, написанных на BCPL? Но нет, никаких следов такого рода идей в 1968 году мы не видели. Такие идеи начнут завоевывать умы только к концу 70-х.
В Австралии 90-х годов именно эта идея будет воплощена в жизнь владельцем софтверной компании Lennox Computer Дугласом Ленноксом. Он использует сочетание придуманных и имплементированных им клонов ISWIM/PAL называемого GTL (General Tuple Language), сильно отставшего от своего времени, и C называемого (не тот)D [Lennox]. Это только первый из описываемых нами здесь, но не последний из ряда случаев, когда какой-то ФЯ или его имплементация находят неожиданное продолжение жизни у антиподов.
В PAL пытались имплементировать Ландинскую аннотацию для описания структур, со всеми особенностями вроде неограниченной степени вложенности
def rec LIST which
is ( HEAD which IsLIST else IsATOM
also TAIL which IsLIST else IsATOM)
else IsNIL
и сложный правил для именования конструкторов и предикатов, которые генерировались по описанию.
def LIST which is
(HEAD also TAIL)
else IsNIL
поскольку тут только один "конструируемый объект" HEAD also TAIL
именем его тега будет имя всей структуры, т.е. LIST
, а именем его конструктора MakeLIST
[Zill70]. Но эта работа не была завершена.
На PAL было написано кода не сильно больше, чем псевдокода на ISWIM. Это были в основном учебные интерпретаторы и другие примеры из курса для которого PAL и создавался [Woze71]. Для каких-то других целей он, судя по отсутствию следов, не использовался ни в МТИ, ни где-то еще. На то, что делали в МТИ язык оказал минимальное влияние. Другой Кембридж не принял ФП в этот раз.
Имплементация PAL самый старый артефакт в этой истории, для которого сейчас доступен исходный код [PAL360]. Исходный код был доступен с самого начала, как и для многого другого разработанного в МТИ на госденьги и являющегося поэтому общественным достоянием. Кроме того, это был код на языке, компилятор которого был переносим на другие машины и был перенесен. Что было редкостью в то время вообще, и особенно в МТИ, где практически все остальные имплементации языков писали на ассемблере, а потом заново, когда меняли машины. Не смотря на все эти качества которые, казалось бы, делали PAL хорошей платформой для разработки ФЯ, он в качестве такой платформы не использовался.
За одним исключением.
Как мы помним, Стрейчи после CPL-проекта стал работать в Оксфорде. И там он был научруком у человека, который попытался имплементировать PAL эффективно. Чтоб PAL можно было использовать как язык общего назначения. Это ему не удалось. Не то чтоб ему с тех пор хоть раз удалось эффективно имплементировать ФЯ. Но ФЯ, которые он разработает будут хотеть эффективно имплементировать другие люди и он будет далее одним из главных героев этой истории функционального программирования.
Уильям Бердж, работавший с Ландином в Univac, после этого работал в IBM Research в Йорктаун Хайтс. Там он вместе с Бартом Левенвортом (Burt Leavenworth) в 1968 году имплементировал на основе SECD-машины еще один язык мало отличимый от ISWIM - McG [Emde06] [Burg71]:
def rec map f x
= null x ? ()
| pfx (f(h x))(map f(t x));
let y = 1; let f x = x + y; map f (1,2,3);
И вот он-то повлияет на отношение к ФП в Йорктаун Хайтс. И Бердж использует его для ФП исследований в 70-е годы.
Пришла пора прощаться с основными действующими лицами обоекембриджской истории. Впереди 70-е годы, такой важной роли в истории ФП они уже играть не будут. Ландин занялся административной работой. Ричардс забросил ФП и сконцентрировался на развитии BCPL. Стрейчи умер. Мы отправляемся на север, в новую основную локацию и знакомимся с новыми главными героями: аспирантом Стрейчи, знакомым Ландина и знакомым знакомого Стрейчи.
Мартин ван Эмден как-то раз вычитал в журнале New Scientist сумасшедшую историю. В Эдинбурге некие Бурсталл и Поплстоун бросили вызов МТИ, написали многопользовательскую ОС на собственном языке для интерактивного программирования! И как раз в этом, 1968-ом году в Эдинбурге проходит конференция IFIP. Конференция не по функциональному программированию и даже не по программированию вообще, а по всему что связано с использованием компьютеров. Связано с этим пока не очень много, так что одной конференции достаточно для всего. Ван Эмден пока еще только сдавал программы на Алголе на бумажной ленте, чтоб их когда-нибудь запустили и очень хотел увидеть интерактивное программирование и более продвинутый чем Алгол язык. И Математический Центр Амстердама отправил ван Эмдена на конференцию.
Конечно же, на конференции было запланировано демо эдинбургской системы. Только кроме ван Эмдена на него никто не пришел. И демо не состоялось из-за проблем с телефонным соединением. Ну что ж. Тем больше времени у разработчиков чтоб рассказать ван Эмдену о проекте. Путь к этому (частичному) успеху, запустившему Эдинбургский университет в TOP4 центров по разработке ИИ, не был прямым [Emde06].
Род Бурсталл (Rodney Martineau Burstall) - физик [Burstall] сначала работавший кем-то вроде тех, кого сейчас называют "квант", а затем программистом [Ryde2002]. Бурсталл как-то раз разговорился с другим покупателем в книжном магазине. Этот другой покупатель - Мервин Прагнелл (Mervyn Pragnell) - организовывал подпольный семинар по логике в Лондоне. Подпольный потому, что что официально Лондонский Университет не имел к нему никакого отношения. На этих семинарах в 61-ом году Бурсталл познакомился с Ландином, а Ландин научил его лямбда-исчислению и функциональному программированию в пабе "Герцог Мальборо".
Связи в функциональном подполье не обошлись без последствий. Несколько лет спустя, в 1964-ом году Бурсталлу позвонил некий Дональд Мики (Donald Michie) и предложил работу в "Группе Экспериментального Программирования", или может быть в экспериментальной группе программирования в Эдинбургском университете. Бурсталл согласился.
Вскоре выяснилось, что кандидатуры рекомендовал Стрейчи, имея в виду, что первые месяцы будущий сотрудник будет работать у него над CPL [Burs2000], тем более, что у экспериментальной группы пока что не было компьютера, так что экспериментальное программирование в ней откладывалось.
Работа Бурсталла над CPL, как это обычно бывало с работой над CPL, не имела особо хороших последствий для CPL. Но прежде чем отправиться работать над CPL в октябре 65-го, Бурсталл познакомился со своим коллегой Поплстоуном.
Робин Поплстоун (Robin John Popplestone) - математик, увлекшийся программированием после того, как впервые попробовал программировать в университете Манчестера на одном из тех самых Атласов. Поплстоун хотел имплементировать логику, но обнаружил, что для удобной работы с деревьями мало аллокаций только на стеке. Чтоб разобраться, как программисты решают такие проблемы, он посетил ту самую летнюю школу по "нечисленным вычислениям", которая популяризировала псевдоCPL [Fox66] (Бурсталл также был знаком с этими лекциями, он писал рецензию на книгу [Burs67]). Наибольшее впечатление на него произвели доклад Стрейчи по CPL, LISP, расширения ALGOL 60 для работы со списками от радиолокационного института, доклад Ландина про то, как использовать стек и сборщик мусора для вычисления выражений. С этого времени Поплстоун стал последовательным пропонентом использования сборщика мусора.
Проблема была в том, что если когда-то у него был доступ к компьютеру, но не было нужных знаний и идей, то сейчас знаний и идей было достаточно, но уже не было доступа к компьютеру, на котором он мог бы их воплощать в жизнь. Теперь он преподавал математику в университете Лидса и программировал на существенно более ограниченной университетской машине. Настолько, что выражения на Лиспе не помещались в "быструю" часть памяти, и Поплстоун придумал и имплементировал стековый язык COWSEL. Разумеется, это будет бесполезно на следующих машинах, но недостатки будут жить и десятилетия спустя.
Кстати, о следующей машине: вот и она. Новая университетская машина, правда, работала только в пакетном режиме, а Поплстоун привык к интерактивной разработке и не обладал важной для программиста того времени способностью писать все правильно с первого раза. Или хотя-бы с сотого. Это означало, что то что Поплстоун делал на предыдущей машине за вечер, теперь могло требовать месяцы. Поплстоун так и не смог освоить старый подход к программированию и перенести на новую машину свой язык и пользоваться улучшенной производительностью.
Ситуация складывалась не радужная. Поплстоун преподавал в Лидсе математику, к которой испытывал все меньше интереса и выпрашивал машинное время на более совместимой с ним машине за пределами университета для своих экспериментов. К счастью, на дворе весна 65-ого года и некий Дональд Мики заглянул в Лидс в поисках каких-нибудь "нечисленных вычислений". Вычисления были обнаружены и Поплстоун приглашен сделать доклад в Эдинбурге. Там он познакомился с Бурсталлом, который уговорил его экспериментально программировать в экспериментальной группе, что удалось очень легко [Popplestone].
Для измученного имплементатора с маленькой целевой машиной мысль о том, что ему придется писать код для поиска свободных переменных в теле процедуры, и организовывать присвоение им значений перед выполнением этого тела, казалась слишком сложной.
Поплстоун, Р. Дж. Ранняя разработка POP [Popplestone].
Экспериментальная группа Дональда Мики была задуман им как воспроизведение другого, существенно более масштабного эксперимента "Project MAC". В разные времена аббревиатура MAC расшифровывалась по разному, а в обсуждаемое время в первую очередь как Multiple Access Computer. Одна из первых (если не первая) система разделения времени была крупным успехом МТИ, привлекшим финансирование, которое было использовано для следующих, не таких успешных проектов. Мики посетил МТИ, и успехи проекта MAC, а также ростки будущих неуспехов проекта MAC произвели на него сильное впечатление. Он решил организовать что-то похожее в Великобритании, но поменьше и подешевле.
Проект мини-МАК - и в узком и в расширенном смысле - должен был не просто воспроизвести большой МАК, а должен был сделать это другим путем. Это было хорошо для всего, что не любили в проекте МАК, например решатели теорем и логическое программирование [Emde06]. Хорошо и для того, к чему там были просто равнодушны, как к функциональному программированию. Проблема была в том, что некоторые вещи необходимые для ФП, такие как сборка мусора, в проекте МАК (пока что) любили. И, разумеется, в экспериментальной группе собирались все писать на ALGOL 60.
Это угрожало полной катастрофой всем замыслам Поплстоуна. Он доказывал, что использование языка без сборщика мусора - ошибка, а добавить сборщик в имплементацию Алгола сложно потому, что не известно где на стеке указатели. Особых результатов это не принесло, но тут - в начале 66-го - экспериментальная группа наконец-то получила компьютер Elliott 4120. Поплстоун сразу же начал тайную имплементацию своего языка на нем, и вскоре имплементировал достаточно, чтоб языком заинтересовался Бурсталл.
Бурсталл организовал для Мики демонстрацию, на которой тот попросил Поплстоуна написать небольшую функцию прямо у него на глазах, что тот легко сделал. Увидев интерактивное программирование, Мики стал энтузиастом языка. Не понравилось Мики только название COWSEL, так что как-то раз Поплстоун вернувшись из отпуска обнаружил, что язык уже называется POP (Package for Online Programming).
Вскоре после того, как первая версия была готова и описана в апреле 66-го, а сборка мусора и интерактивное программирование завоевали себе место в мини-версии проекта MAC, Поплстоун и Бурсталл приступили к созданию POP-2.
Примечательно то, что проработавший три года программистом в индустрии Бурсталл играл роль "теоретика", а программировавший в свободное от преподавания математики время Поплстоун - "практика". Видимо оба очень хотели передохнуть от того, чем занимались на работе.
Разработку сначала планировали начать с нуля, чего не произошло. POP-2 унаследовал, по видимому, самую необычную фичу POP-1 - "открытый" стек. Как авторы POP, так и авторы CPL утверждают, что POP-2 произошел от CPL [Camp85] [Rich2000]. И это утверждение не так легко объяснить, даже с учетом крайне разреженного дизайн-пространства языков в те годы. Даже если выбирать из двух языков со сборкой мусора, одного имплементированного частично и с ошибками и второго, по большому счету, вообще воображаемого, то LISP выглядит как более правдоподобный предок, чем CPL. Изначально, возможно, планировалось сделать язык похожий на CPL, но ряд ключевых решений оттуда были отвергнуты Поплстоуном по "практическим" соображениям и заменены решениями, которые сам же Поплстоун впоследствии называл "ошибками" и "хаками".
В сначала POP-2 планировали лексическую видимость как в ALGOL 60 (и CPL), но решили сделать динамическую. Это решение Поплстоун обосновывал проблемами имплементации: для того чтоб сделать и "открытый" стек и лексическую видимость на целевой машине было слишком мало регистров. Также Поплстоун считал, что язык с динамической видимостью проще отлаживать.
Поплстоун еще и планировал имплементировать функции высшего порядка с помощью метапрограммирования, как в LISP, для чего достаточно дать программисту доступ к компилятору как функции. Что было не как в LISP, так это то, что Бурсталл знал, что этого недостаточно.
function funprod f g;
lambda x; f(g(x)) end
end;
Эта имплементация композиции функций не сработает без лексической видимости: f
и g
свяжутся с функциями видимыми там, куда возвращается анонимная функция из funprod
.
Но аннотацией видимости не обошлось, Поплстоун решил, что поиск свободных переменных в лямбде и конструирование замыкания будет требовать слишком много ресурсов и будет слишком сложно имплементировать. Поэтому программисту нужно делать лямбда-лифтинг и инициализацию замыкания вручную:
function funprod f g;
lambda x f g; f(g(x)) end(%f,g%)
end;
[Burs68] И наш традиционный пример будет выглядеть так:
function map f l;
if null(y) then nil else cons(f(hd(l)), tl(l)) close
end;
vars y; 1 -> y;
map (lambda x y; x + y end(% y %)) [1 2 3]
Поплстоун позднее вспоминал, что математика была их с Бурсталлом общим языком, который позволил им согласовать их цели в разработке POP-2 и достичь того, что язык, при всех его странностях можно было использовать в функциональном стиле. Но их сотрудничество ограничивало то, что Бурсталл интересовался применением математических идей более последовательно, а Поплстоун неохотно поддерживал его формальный подход [Popp2002].
Можно ли его было использовать в функциональном стиле? К этому мы вернемся позже, когда будем обсуждать рождение функционального стиля. Но можно сказать определенно, что языки Эдинбургской программы оказались основаны не на нем, а на тех языках, использовать которые в функциональном стиле было удобнее.
Компромиссные фичи, срезания углов и остатки POP-1 сильно подорвали модернизационный потенциал POP-2. Например, в Эдинбурге добавили статическую типизацию к ISWIM, но добавить ее к POP-2 оказалось слишком сложно: попытавшийся это сделать позднее Поплстоун обнаружил, что из-за открытого стека сигнатуры функций будут тайплевел-парсерами и посчитал, что не стоит связываться с такими сложностями [Popp2002].
Важны ли были эти компромиссы для практичности? Сомнительно. Сейчас нам известно более-менее достоверно, что тот, кто может позволить такие дорогие фичи как сборка мусора или динамическая "типизация" - может позволить и лексическую видимость и компилятор, который сам ищет свободные переменные в лямбдах.
Получился ли язык практичным? Он определенно применялся для написания программ больше, чем PAL и прочие ISWIMы того времени, но это не очень высокая планка. На нем была написана первая версия решателя теорем Бойера-Мура, одной из немногих программ, которая была переписана на LISP, а не с LISPа на какой-то другой язык. Первая имплементация POP-2 и более поздние для PDP-10 использовались для имплементации языков Бурсталла. Имплементация 80-х годов служила бэкендом для компилятора SML. Установленным в первой главе критериям практичности он соответствует. И для этого не понадобилось приносить все функциональные фичи в жертву как в BCPL. Это шаг вперед, по сравнению с обоекембриджской программой. Но не все в POP-2 было шагом назад по сравнению даже с планами для CPL. Но о прогрессивных фичах POP-2 и его влиянии на языки Эдинбургской программы мы подробнее расскажем ниже в соответствующих главах.
Конечно проблемы с практичностью у POP-2 были, сборка мусора мешала программировать роботов, а из-за динамической "типизации" компилятор генерировал медленный код, который проверял теги и вызывал функции для каждой арифметической операции, что мешало писать на нем код для распознавание образов. Но такие проблемы не решены полностью и у современных "динамических" языков со сборкой мусора. [Popp2002]
Почему на POP-2 вообще пытались писать код для управления роботами и распознавания изображений? Потому что он стал тем, чем хотели сделать CPL - единственным языком единственной машины, используемой в экспериментальной группе. И был он единственным не потому, что лучше всего подходил для всего "экспериментального программирования", которым в ней занимались. Операционная система Multipop68, ставшая ответом мини-MACа MACу большому, была системой основанной на языке, безопасность в которой обеспечивается инкапсуляцией и проверками языка. В данном случае проверками тегов в рантайме.
Первоначальный план с аппаратной изоляцией процессов не удалось осуществить потому, что университет не смог приобрести подходящие жесткие диски для свопа. Со временем в Эдинбурге сделали более традиционную систему разделения времени с аппаратной защитой и свопом Multipop 70, но пользователи не любили её за медлительность и предпочитали продолжать пользоваться Multipop68 до получения новой машины PDP-10 в середине 70-х.
Пока POP-2 был обязательным к нему привыкли, он был портирован на другие машины и внедрен в других университетах, стал популярным языком в британском ИИ, как Лисп в американском [Popplestone] [Popp2002]. Мики в 1969-ом даже основал компанию Conversational Software Ltd, которая пыталась POP-2 коммерциализировать [Howe07].
Получив работу в Эдинбургском университете, Робин Поплстоун отправился из Лидса в Эдинбург на катамаране собственной конструкции. В Северном море его судно пошло ко дну. Совершенно точно не вместе с рукописью диссертации Поплстоуна, которого подобрал проходящий траулер [Popplestone]. По какой-то причине и Бурсталл и Лессер считали важным упомянуть, что рукопись диссертации была утрачена каким-то другим (не указанным) способом [Burs04]. Мы же считаем важным упомянуть, что в эти роковые минуты судьба одной из наиболее узнаваемых фич ФЯ висела на волоске.
Три года спустя, в 1968 Род Бурсталл написал статью "Доказательство свойств программ с помощью структурной индукции" [Burs69]. Но прежде чем приступить к доказательствам, он расширил синтаксис используемого им как псевдокод ISWIM для "более удобной манипуляции структурами данных".
Более удобной, чем в Лиспе, подход которого до этого момента воспроизводили в коде на ISWIM/CPL/PAL/McG/POP-2:
let rec map(f, xs) = if null(xs) then nil()
else let x = car(xs) and xs1 = cdr(xs)
cons(f(x), map(f, xs1))
Бурсталл еще не описал свои расширения ISWIM, а он уже выглядит иначе? Ну, ISWIM выглядит иначе в каждой статье, в которой его используют. ISWIM позволяет связывать несколько переменных с элементами туплов
let rec map(f, xs) = if null(xs) then nil()
else let (x, xs1) = splitup(xs)
cons(f(x), map(f, xs1))
и даже вложенных (x,(y, z))
(но имплементированные в то время языки вроде PAL так не могут). Бурсталл решил это обобщить на все структуры, которые объявляются Ландинской нотацией.
Предположим, у нас есть список, в котором α - любой объект
An α-list is either a cons or a nil.
A cons has an α and a list.
A nil has no components.
Эта нотация декларирует множество функций, с генерируемыми по правилам названиями, которые предполагается использовать программисту.
Каждой "конструирующей операции" (construction operation) вроде cons
и nil
соответствуют три функции:
- функция-конструктор, принимает компоненты, возвращает структуру
- функция-деструктор, принимает структуру, возвращает компоненты как упорядоченный тупл
- предикат, проверяет, что объект был сконструирован данной "конструирующей операцией"
Бурсталл предложил использовать один идентификатор - тот, что пишет сам программист, например cons
- для всех, а различать какая именно из трех функций используется по контексту. Возможно, что эта идея происходит от дублетов из POP-2, в котором к функции, работающей как геттер, можно прицепить функцию работающую как сеттер. Обе имеют одно имя, а когда вызывается одна или другая - определяется тем, с какой стороны оператора присваивания находится вызов [Burs68].
Расширенный ISWIM транслируется в обычный ISWIM так:
let x = cons (a, y)
в let x = cons(a, y)
,
let cons (a, y) = x
в let (a, y) = decons(x)
,
x is cons
в compound(x)
и, наконец,
f(cons)
это f(cons, decons, compound)
.
Да, когда конструкторы АлгТД были изобретены, они были первоклассными.
Было бы странно, если бы не были. ISWIM - функциональный язык. Почему же тогда они перестали такими быть? Всему свое время.
Изобретя конструктор АлгТД, Бурсталл пока что не изобрел АлгТД как суммы произведений. Он, как и разработчики типов для CPL годом раньше, считает, что каждая "конструирующая операция" вводит новый тип, их дизъюнкция тоже. Вместо одного типа списка cons
- конструирует значение одного типа, nil
- второго, а list
- третий тип.
Вот что получилось у Бурсталла после всех этих улучшений:
let rec map(f, xs) = if xs is nil then nil()
else let cons(x, xs1) = xs
cons(f(x), map(f,xs1))
О нет! Конструкторы АлгТД используются не так.
И что толку, что разрешено вложение
let cons(x1, cons(x2, xs2)) = xs1
применять такое удобство на практике нельзя, ведь нужно сначала проверить второй cons
, а вложения is
проверок нет.
К счастью, проходящий мимо Робин Поплстоун спас функциональное программирование, как до того проходящее мимо судно спасло Поплстоуна. Избыточность нотации, повторения имен не понравились ему, о чем он честно сообщил Бурсталлу.
if xs is cons then let cons(x, xs1) = xs; ...
Нельзя ли использовать тут cons
и xs
по одному разу?
Бурсталл продолжил поиск, вспомнил предложенный в 67-ом Мартином Ричардсом для CPL case
по типам, и получил современную нотацию:
let rec map(f, xs) = cases xs:
cons(x, xs) : cons(f(x), map(f, xs))
nil() : nil()
Бурсталл также предложил использовать инфиксные "конструирующие операции", например ::
вместо cons
let rec map(f, xs) = cases xs:
x :: xs: x :: map(f, xs)
nil() : nil()
Авторы языков программирования не часто рассказывают чудесные истории о том, как кто-то сказал им однажды: "остановись, подумай, нельзя ли здесь сделать покороче?" и они заметили это и сделали. Судя по тому, как языки программирования выглядят, и происходят такие истории нечасто.
Это тем важнее, что похоже, что такая нотация не была больше переизобретена независимо.
Важно, правда, отметить, что это пока что псевдокод, до первой имплементации еще годы, а до первой статьи о том, как это компилировать еще больше.
Также примечательно, что в статье Бурсталла, как и в этой главе, не упоминаются два английских слова, которыми такую нотацию теперь называют.
После неудачной демонстрации Multipop Ван Эмдену в 68-ом году, тот возвращался в Эдинбург каждое лето. И уже в 69-ом все, что не работало раньше - теперь отлично работало. За этот и следующий визит Ван Эмден освоил POP-2 и интерактивное программирование. Но пришла пора ему связать историю ФП в Эдинбурге с другим ответвлением Обоекембриджской программы.
В конце лета 71-го Ван Эмден отправился в IBM research, Йорктаун Хайтс. Там Ван Эмден познакомился с тем, что он описывает как вторую исследовательскую программу, произошедшую от работ Стрейчи и Ландина. В обсуждаемые времена, скорее даже первую, если судить по более впечатляющим успехам. Вместо слабой четырехпользовательской машины в Эдинбурге - в каждом офисе терминал, подключенный к CP/CMS системе на IBM 360-67 [Emde06]. Вместо изуродованного "практичностью" POP-2 - McG. Настолько полноценный ФЯ, насколько ФЯ вообще мог быть полноценным в 72-ом году. Да, это не очень высокая планка. Пройдет не так много времени, и ситуация изменится на противоположную. В Эдинбурге изобретут первый функциональный язык, как мы их сейчас знаем, а McG так и останется пиком ФП в IBM. По какой-то неизвестной нам причине, начиная со статей 72-го года, код на McG и даже упоминания этого языка исчезают. Возможно произошла какая-то ФП-катастрофа в IBM, и Ван Эмден был свидетелем ФП-утопии в последний год её существования. Но ФП-катастрофа точно не была полной и окончательной. Технически Йорктаунская ветка истории ФП существует и сейчас, но её главный и еще более-менее живой продукт Axiom - не совсем язык программирования и не особенно впечатляющ как ФЯ. Главный итог Йорктаунской ветки не в нем, а во влиянии, которое Йорктаунская программа оказала на Эдинбургскую и некоторые другие. Изобрести функциональный язык, как мы его знаем в Йорктаун Хайтс не смогли. Зато смогли изобрести функциональное программирование, как мы его знаем.
Функциональное программирование занес (в 65-ом) в Йорктаун Хайтс Уильям Бердж, англичанин, который, как мы уже знаем, работал с Питером Ландином в Univac (в 63-ем) и, как вспоминает Ван Эмден, с Родом Бурсталлом [Emde06]. Ван Эмден, впрочем, не вспоминает где именно и в чем заключалась эта работа.
Бердж закончил Кембридж (в 55-ом) и научился программировать в Королевском Радиолокационном Институте (в 54-ом) [Burg75], т.е. скорее всего до того как в этих местах зародился интерес к функциональному программированию.
Бердж, в отличие от Хартли, имплементировал ALGOL 60. Может быть не успел познакомится со Стрейчи. Может быть успел, но Стрейчи не смог его отговорить имплементировать ALGOL 60. Может быть, если бы Стрейчи не отговорил Хартли имплементировать Алгол 60, то Хартли бы потом смог имплементировать ФЯ, как смог Бердж.
Бердж и Барт Левенворт имплементировали McG (называемый также McG360), ISWIM-подобный язык на основе SECD машины в 1968.
Нам мало что известно об этом языке. Это, в основном, полная противоположность другой имплементации ISWIM - PAL. Внутренний продукт IBM, который за пределами лаборатории не мог видеть и исходники которого не были открыты. Описан во внутренних отчетах, которые не были отсканированы и выложены в интернет. Во всех работах его авторов за парой исключений [Burg71] [Leav71] вместо примеров на нем - псевдокод, который отличается в деталях.
По немногочисленным примерам кода и REPL-сессий [Burg71] можно заключить, что он больше походил на ISWIM чем PAL отсутствием лямбд и меньше - отсутствием where
-выражений. Отсутствие и того и другого вполне может быть объяснимо предпочтениями автора кода.
Пользователи McG в IBM research также были противоположностью пользователей PAL в МТИ. McG в отличие от PAL использовали для экспериментов с имплементацией ФЯ. Левенворт дал пользователю языка возможность писать функции, которые могут читать и изменять структуры, описывающие состояние SECD машины. Хотите имплементировать корутины, продолжения? Пожалуйста! [Leav71]. Понятно, что такая идея требовала слишком наивную имплементацию даже для интерпретатора чтобы работать и не имела будущего, но Бердж, видимо, осуществил и уж точно описал эксперимент который будущее имел и к которому мы еще вернемся.
Но самым важным для нашей истории отличием Берджа и прочих пользователей McG от, по большому счету, всех остальных пользователей ФЯ того времени было желание писать код в функциональном стиле.
Тот факт, что большинство функций являются константами, определенными программистом, а не переменными, которые изменяются программой, не является следствием какой-либо слабости системы. Напротив, он указывает на богатство системы, которое мы не умеем хорошо использовать. Дж. МакКарти, Руководство программиста на LISP 1.5
То программирование, про которое писали в 60-х авторы и первые пользователи будущих ФЯ было больше про рекурсивные функции, чем про функции высшего порядка. Что обычно подчеркивается в названии работ вроде "Рекурсивные техники в программировании" [Barr68] или "Рекурсивные функции символьных выражений и их вычисление машиной" [McCa60]. Что странно, по крайней мере, по двум причинам. Во-первых потому, что эффективная имплементация рекурсии еще не используется, хотя изобретена Вейнгаарденом. Но, как и обычно бывает в нашей истории, еще не понята и не принята [Reyn93]. К этому мы еще вернемся. Во-вторых функции высшего порядка появились в упомянутых нами ранее псевдокодовых фантазиях. До того, как были имплементированы языки, на которых их можно писать. Поэтому можно было бы ожидать, что тем, кто получил наконец-то возможность поэкспериментировать с такими функциями, немедленно захочется этим заняться. Но нет. Судя по дошедшим до нас следам, желающих было немного.
ФВП как псевдокод появились в первой же публикации о Лиспе 1960-го года. Строго говоря, ФВП они не были, а были применением метапрограммирования в языке первого порядка. К проблемам такого подхода мы еще вернемся. Если исключить функции анализирующие код, который в них передавали, вроде eval
и apply
оставались еще те, которые можно было бы имплементировать как ФВП. Таких "ФВП" было всего две: maplist
и search
. Влияние этой первой публикации на ФП несомненно, но ни в какой момент эти ФВП не были идентичны тем, что появились в публикациях обоекембриджцев. С самого начала были как менее важные различия вроде порядка аргументов, не имеющего пока что никакого значения из-за некаррированности функций, так и более значительные.
Главная разница была в том, что обходящие список ФВП применяют принимаемую функцию поочередно не к отдельным элементам списка, а к остаткам [McCa60]. Не позднее 66-го года появляются версии этих функций работающие с элементами [Bobr66], но влияние обоекембриджской программы сомнительно. Вероятно, они появились потому что лисперы обнаружили что часто приходится начинать передаваемую функцию с применения head
(в Лисп обычно car
), отсюда именование новых модификаций вроде mapcar
. Но идея о том, что пользователь функции map
может выбирать с какой частью остатка списка работать не умерла и даже получила развитие - появились версии map
принимающие функцию, вызываемую вместо tail
(в Лиспе cdr
) для управления обходом списка [Teit74].
Примечательно, что search
помимо предиката принимала функции, которые вызывались в случае, если подходящий элемент найден и если нет. Один из изобретателей продолжений Локвуд Моррис позднее писал, что главным вдохновением для него был этот паттерн для обработки ошибок. Моррис не припоминает, чтоб он видел как МакКарти или еще кто-то в то время писал нетривиальный код в таком стиле [Reyn93]. И, вероятно, видеть было нечего, потому что паттерн не стал популярным и сменился на возвращение из функции пустого списка.
Пара "ФВП" из первой публикации о Лиспе не была просто примером, выборкой из многих придуманных лисперами функций. За статьей последовали мануалы и там идей не стало больше, из полутора сотен функций стандартной библиотеки LISP 1.5 было только несколько ФВП - вариации maplist
и search
и пара функций применяющих продолжения для обработки ошибок [McCa62].
maplist[x;f] = [null[x] -> NIL;T -> cons[f[x];maplist[cdr[x];f]]]
search[x;p;f;u] = [null[x] -> u[x];p[x] -> f[x];T -> search[cdr[x];p;f;u]]
Разумеется maplist
описывалась функциональным псевдокодом только в мануале, а в библиотеке была реализована как сотня строк ассемблерного кода. За все последующие 60-е годы и начало 70-х лисперы продолжали инкрементальную модификацию maplist
, самая интересная из которых была аналогом concatMap
, и добавили пару функций, принимающих предикат вроде сортировки.
Никаких следов влияния всей этой работы на Эдинбургскую программу мы не видели. Многие наработки и не могли быть перенесены в будущие ФЯ из-за типизации. Например, в конце 60-х - начале 70-х в MacLISP семейство функций map
сделали принимающими произвольное количество списков и работающими как обобщенные zipWithN
. Примерно в то же время лисперы перестали документировать свои функции тем псевдокодом из первой статьи [Whit70] [Moon74].
POP-2 похож на Лисп не только некоторыми языковыми решениями, но и библиотекой ФВП [Dunn70], правда, помимо maplist
там были кое-какие интересные идеи о которых ниже.
Обоекембриджская программа подарила нам привычные map
и filter
(тогда назывался select
) в работах Ландина по описанию семантики ALGOL 60, которые писались в начале 60-х и были опубликованы в 65-ом [Land65b]
rec map f L = null L -> ()
else -> f(h L) : map f (t L)
rec select(p)(L) = null L -> ()
p(h L) -> h L:select(p)(t L)
else -> select(p) (t L)
Скобки вокруг аргументов в коде Ландина появляются или нет бессистемно.
Также, в одной из этих работ [Land65b] можно увидеть пару странных ФВП для моделирования циклов, которые неработоспособны в последующих ФЯ и вводятся только как неправильное решение проблемы, к правильному решению которой мы еще вернемся.
Стрейчи по всей видимости представил map
на летней школе в 63-ем и опубликовал в 66-ом [Stra66]. Аргументы у map в современном порядке, в отличие от лиспового, но это скорее всего просто случайность потому что функции некаррированы.
Стрейчи делает шаг вперед по сравнению с лисперами: пытается найти более фундаментальное представление для обхода списка и изобретает foldr
(у Стрейчи называется lit
- List ITeration), приводит ряд примеров выражения функций через Lit
(пока что) полным применением [Stra66].
let Lit[F, z, L] = Null[L] -> z,
F[Hd[L], Lit[F, z, Tl[L]]]
let Map[g, L] = Lit[F, NIL, L] where F[x, y] = Cons[g[x], y]
let Rev[L] = Lit[F, NIL, L] where F[x,y] = Append[y, x]
let Product[L] = Lit[f, List1[NIL], L]
where f[k, z] = Lit[g, NIL, k]
where g[x, y] = Lit[h, y, z]
where h[p, q] = Cons[Cons[x,p], q]
Что отличается от псевдокода лисперов, которые заново описывают раз за разом рекурсивные обходы списков.
Выражение функций через Lit
произвело позднее сильное впечатление на деятелей Эдинбургской программы. Аспиранты Бурсталла Майкл Гордон (Michael Gordon) и Гордон Плоткин (Gordon David Plotkin), более известный другими своими работами, в 72 задались вопросом о том, что можно, а что нельзя выразить через Lit
. Майкл Гордон даже определил язык, трансформирующийся в комбинацию Lit
-функций (май 73) [Gord2000b].
Но все эти наработки в Обоекембриджской программе так и остались псевдокодом. В библиотеке PAL функций высшего порядка не было вообще [Evan68b], так что не было и никакой итеративной работы по совершенствованию описанных Стрейчи и Ландином наборов функций, вроде той что была у лисперов. Показательно, что представленные в этой истории функции map
на псевдоCPL и ISWIM написаны их авторами, а map
на PAL написана нами, не смотря на то, что это реально существовавший язык для которого доступны исходные коды, мануал, учебный курс и исходники примеров для курса. Почему пользователи PAL не хотели писать код в функциональном стиле? Или, если писали, то почему не оставили никаких следов этой деятельности? Нам не удалось разгадать эту загадку. Можно понять нежелание писать ФП код у пользователей лиспов (из-за имплементации "ФВП" с помощью метапрограммирования) и пользователей POP-2 (из-за неудобства лямбд), но у PAL таких проблем нет, и для обсуждаемых экспериментов даже несерьезной имплементации достаточно. Да, в МТИ тогда не было особого энтузиазма по отношению к функциональному программированию, а энтузиазма по отношению к PAL не было никогда, но к исходникам PAL получали доступ, например, аспиранты Стрейчи, будущие активные участники Эдинбургской программы, для которых ФП должно было бы быть гораздо интереснее.
Но если с функциями, принимающими другие функции, дела обстояли не особенно весело, то с еще одной разновидностью ФВП все было еще печальнее. В мануале LISP 1.5 функции вроде maplist
называются "функционалами". Есть там и определение функционала - это "функция, принимающая другие функции" [McCa62]. Как же они называли функции, которые возвращают другие функции? - спросите вы. Что-что делают? - спросят в ответ лисперы.
Возможные варианты использования для практических целей функций, производящих функции, на сегодняшний день в значительной степени не изучены. У. Бердж, Техники рекурсивного программирования [Burg75]
Что если вернуть функцию из функции? В 60-е годы так мало задавались этим вопросом, что бывало замечали только через годы обсуждения, что язык, в котором есть "лямбды", не позволяет их использовать как лямбды. Рассказ об этом языке еще впереди. Справедливости ради, сейчас не 60-е годы и для широких направлений программистской мысли это все еще не самый очевидный вопрос и многие популярные языки вроде C++ или Rust не очень хорошо приспособлены к таким потребностям программиста. Передача функций в функции прочно закрепилась в мейнстриме, но возвращение функций из функций и сегодня экзотическая техника, популярная только в рамках Эдинбургской программы. Не смотря на распространенность ФЯ сегодня, большинству интереснее передавать функции в функции, а не возвращать их.
На заре Эдинбургской программы, эта техника в основном ограничивалась даже не одной исследовательской программой, а работами с участием одного человека: главного героя этой части нашей истории Уильяма Берджа.
Заметно, что придумать простую, но более-менее общеполезную функцию, возвращающую другую функцию авторам ФП-учебников того времени дается не легко. И в учебном курсе на PAL [Woze71] и в мануале POP-2 [Burs68] для демонстрации ФВП используется функция
let Twice f x = f(f x)
а не, например, реалистичная функция (.)
, которую гораздо позднее использовал Поплстоун для иллюстрации проблем POP-2 [Popplestone], как и мы в этой работе.
Бердж объявлял и использовал композицию функций не позднее статьи 64-го года [Burg64] в псевдокоде вполне современного вида:
(f . g) = f (g x)
И тогда же ссылался на "Комбинаторную Логику" Карри [Curr58].
Статья содержит, видимо, первый нетривиальный ФП-код (ISWIM-псевдокод) в котором функции возвращают функции - комбинаторный парсер. Точнее матчер, разбирающие строку функции не возвращали значения, только успешность разбора и остаток.
nullist x = (true, x)
<I> x = null x -> (false, x)
else -> eq (h x) 'I' -> (true, t x)
else -> (false, x)
V f g x = 1st(f x) -> f x
else -> 1st(g x)-> g x
else -> (false, x)
C f g x = 1st(f x) -> 1st(g(2nd(f x))) -> g(2nd(f x))
else -> (false, x)
else -> (false, x)
* <x> = V (C <x> (* <x>)) nullist
Единственный нетривиальный ФП-код с возвращением функций функциями, произведенный обоекембриджской программой был опубликован Ландином, когда он работал в Univac, где Бердж работал начальником небольшой исследовательской группы, которая занималась семантикой ЯП [Burg75]. Ландин столкнулся с проблемой моделирования Алгольных циклов с помощью функций строгих списков и применил возвращение функций из функций для описания "стримов" [Land65a].
nullist* = λ().()
L :* M = λ().(L,M)
null*(S) = null(S())
h*(S) = 1st(S())
t*(S) = 2st(S())
rec while*(e, p) = λ().p' -> [e', while*(e, p)]
else -> ()
where e', p' = e(), p()
Эта работа из двух частей также одна из немногих работ обоекембриджской программы, в которых используется композиция функций и частичное применение. Правда, так мало, что можно пропустить если моргнуть не вовремя.
Нужно заметить, что статья не посвящена стримам, они просто один из инструментов, которые используются в статье об описании семантики Алгола. Бердж вспоминает, что Ландин рассказал ему о стримах в 62-ом году [Burg75b], но статьи о них Ландин не писал. Позднее Бердж пишет про стримы больше и подробнее [Burg71] [Burg75] [Burg75b].
Бердж мог изобрести комбинаторные парсеры под влиянием Ландиновских стримов, но и наоборот, на изобретение стримов Ландином могли повлиять комбинаторные парсеры Берджа. Ландин, описывая [Land98] технику индикации успешной/неудачной работы через возвращаемый результат, ссылается на статью о комбинаторных парсерах [Burg64]. Эта техника выглядит как что-то, что изобреталось независимо бессчетное количество раз, но в ретроспективе многие изобретения кажутся очевидными.
Статьи про семантику Алгола и про комбинаторные парсеры к тому же производят впечатление использования одного "словаря" функций. Например, zip
в статье Берджа и unzip
в статье Ландина.
Стримы, конечно, не являются решением проблемы контрол-структуры, ведь результаты стрима не сохраняются и их может быть нужно перевычислять. Так что через несколько лет изобретение получило развитие в POP-2.
Авторы POP-2 изобрели "динамические списки". Динамический список производился ФВП fntolist
из функции, возвращающей элементы последовательности. Получалась пара из ссылок на булево значение и функцию. Функции получения головы и хвоста списка проверяли cons-ячейку на то что она имеет такую специальную форму и вызывали функцию вычисления следующего элемента. Если функция возвращала нулевое значение - cons-ячейка переписывалась в кодирующую пустой список. Если возвращался результат - в cons-ячейке переписывали на месте ссылку на булево значение на результат вызова функции, а ссылку на функцию - ссылкой на новую специальную cons-ячейку.
Вся эта машинерия была имплементирована в библиотеке [Burs68].
function fntolist f; cons(false, f) end
function solidified l; vars f x;
if isfunc(back(l))
then back(l) -> f; f() -> x;
if x = termin then true -> front(l)
else x -> front(l); conspair(false, f) -> back(l)
close; l
else l
close
end;
function hd l; front(solidified(l)) end;
lambda i l; i -> front(solidified(l)) end -> updater(hd);
В 70-е Бердж ссылается на это изобретение, но сам его применяет существенно позже [Burg89].
Помимо ISWIM, на котором Бердж писал комбинаторные парсеры, он знает [Burg66] еще один неимплементированный еще язык с возвращающими функции функциями - CUCH. CUCH (CUrry CHurch) - это эзотерический язык Коррадо Бёма (Corrado Böhm), более известного другими своими работами и другим эзотерическим языком [Böhm72]. На нем Бердж комбинаторные парсеры не писал, этот язык хуже подходил для написания более-менее практичного кода из-за однобуквенных констант и параметров и прочих ограничений. Язык разработан в 64-ом году [Card2006], но Бердж ссылается на него впервые в работе 66-го года. Как выглядел CUCH:
I = (λxx)
K = (λx(λyx))
Другой язык, который мог бы повлиять на комбинирование комбинаторов Берджем - APL. Про который тот, конечно, знал, работая в IBM и на который ссылался в 72-ом году [Burg72]. Мы, правда, не видим особого сходства и Бердж не уделил особого внимания сравнению. CUCH-программа, правда, некоторое внимание уделила. В 1962-64 Мариса Вентурини Дзилли (Marisa Venturini Zilli) занималась энкодингом операций APL с помощью комбинаторов, а в 82-ом Бём делал это для родственного языка FP [Card2006]. Еще одна задача для будущих историков идей.
С 65-ого года Бердж работает в IBM Research, где изучает "методы упрощения программирования использованием высокоуровневых языков" [Burg75]. Высокоуровневый язык, правда, готов только в 68-ом году. В отличие от всех тех, кто с того же самого года имел доступ к имплементации PAL, Бердж решил опубликовать свои идеи о том, как ФП код может и должен выглядеть. В частности, что можно сделать, если язык позволяет возвращать функции.
В статье 71-го года [Burg71] Бердж приводит примеры REPL-сеансов и кода на языке McG. Бердж выделил повторяющийся код в функциях работы со списками:
def rec list1 a g f x =
null x ? a
| g (f(h x))(list1 a g f (t x));
Бердж не ссылается на статью Стрейчи в которой тот описывает Lit
. Кроме того, его функция отличается названием, порядком и числом параметров, так что вполне можно допустить независимое изобретение. Бердж работал вместе с Ландином, который скорее всего был знаком с этой функцией, но возможно просто не придавал изобретению такого значения, которое придавали ему в Эдинбурге.
Обратите внимание на один ненужный параметр, вместо которого можно композицию функций использовать. Что довольно странно, если учесть что Бердж если не первый, то один из первых, кто начал использовать композицию.
Бердж объявил комбинаторы из книги Карри:
def i x = x;
def k x y = x;
И использовал их далее для определения функций частичным применением ФВП:
def plus x y = x + y;
def sum = list1 0 plus i;
def length = list1 0 plus (k 1);
def map f = list1 () pfx f;
Бердж называет три языка, которые поддерживают программирование в таком стиле: McG, PAL и POP-2. Но примеры на PAL и POP-2 не приводит. И POP-2 не тот язык на котором такой стиль будет хорошо выглядеть. [TODO]
И если foldr
называется list1
, то вы наверное уже догадалась какая функция называется list2
. В статье 71-го года для foldl
, видимо, не нашлось места, и её код появляется в статье 72-го года [Burg72].
def list2 a g f x =
if null x
then a
else list2 (g(f(h x))a) g f (t x)
def reverse = list2 () prefix I
Бердж отмечает, что list2
может быть имплементирована как цикл, т.е. эффективнее, чем в рекурсивной форме. list1
может быть выражена как двойной проход
def list1 a g f x = list2 a g f (list2 () prefix I x)
что все еще может быть эффективнее, чем рекурсивная имплементация. Это изобретение будет использоваться в библиотеках начиная с первых же имплементаций ФЯ до относительно современных вроде mlton.
Это не код на McG, а ISWIM-псевдокод - со статьи 72-го года начинается упомянутое выше McG-стирание.
В статье 71-го года Бердж описал еще несколько функциональных EDSL. Комбинаторы парсеров теперь именно парсеры, а не матчеры, возвращают значение. Стримы называются "последовательностями", но все те же, что и в статье Ландина [Land65a]. Бердж, по всей видимости, положил начало популярной у функциональщиков демонстрации стримов с помощью нерешета Неэратосфена, но использовал не ту версию, которая используется в наши дни.
Бердж предложил параметризовать алгоритм функцией обхода структуры
def summing f = f 0 plus i;
def sum = summing list1;
Но не только потому, чтоб выбирать суммировать список обходя его list1
или list2
, а потому, что Бердж использует несколько структур, как современные функциональные программисты, не только одни односвязные списки, как было популярно у "рекурсивных программистов" того времени.
Но как описывать структуры в ФЯ того времени? Имплементировали ли в McG Ландинскую нотацию для сумм и произведений? Нет, но Бердж придумал ФП-EDSL для их описания. Описания структур собираются функциями du
(direct union) и cp
(cross product).
def rec tree = du(int,cp(tree,tree));
def atom, nonatom = predicates tree;
def atomclass, compoundclass = partition tree;
def left, right = selectors compoundclass;
def ctree x y = make compoundclass (x,y);
автоматизация генерации конструкторов, предикатов и геттеров, конечно, оставляет желать лучшего. Чтоб получить их для произведения приходится разбирать сумму функцией partition
.
Статья 72-го года [Burg72] уделяет описанию структур больше внимания, к тому же у них появляются параметры.
def rec list A = du(cp(), cp(A,list A));
def null, nonnull = predicates (list A);
def nullc, nonnullc = parts (list A);
def h, t = selectors nonnullc;
def prefix x y = construct nonnullc (x, y);
def nullist = construct nullc ();
Да, в описаниях геттеров и предикатов A не определено. Это, правда, ISWIM-псевдокод, в статье с примерами на McG таких параметризованных определений не было.
Добавив к этим материалам подробностей, теории по ЛИ и комбинаторам и сведений об имплементации ФЯ Бердж написал первую современную книгу о функциональном программировании [Burg75], хотя по традиционному названию про рекурсию этого не скажешь. Книга оказала важное влияние на Эдинбургскую программу, к которому мы еще не раз вернемся. Переведена на русский язык [Берд83].
В книге Берджа, по всей видимости, впервые опубликована жемчужина функционального программирования, с которой с тех пор неразрывно связана наша история. Но еще до издания книги, Ван Эмден привез эту жемчужину в Эдинбург. Ван Эмден был одним из многих героев нашей истории, которые приехали работать в Эдинбург (и его окрестности) в это время. Но в отличие от большинства из них, Ван Эмден вскоре стал героем не нашей истории. В одно темное утро в октябре 72-го года в Эдинбурге Боб Ковальски предложил Ван Эмдену показать программирование на логике.
Тот самый "квиксорт", который мы, функциональные программисты, так любим ненавидеть и ненавидим любить, показал Ван Эмдену имплементатор McG Левенворт. В 1972-ом году, в Йорктаун Хайтс [Emde06]. Самая ранняя версия, которая была опубликована, датируется 1975-ым годом [Burg75].
def rec qs x =
if null x
then ()
else let d = h x
let y, z = partition d (t x)
concat (qs y, u d, qs z)
where rec
partition d x =
if null x
then (), ()
else let y, z = partition d (t x)
if h x < d
then h x:y, z
else j, h x:z
В книге Берджа он вводится как трисорт, в котором процесс построения (несбалансированного) дерева и его сплющивание в список объединены. Сортировка, в которой эти процессы происходят последовательно, описывалась и раньше [Barr68]. Почему же этот фьюзед-трисорт называют квиксортом в этой книге и позднее? Это все еще остается загадкой.
Трудно поверить, но существует разновидность программистов, на которых этот "квиксорт" произвел еще большее впечатление, чем на функциональных программистов.
Летом 70-го, во время очередного посещения, Ван Эмден стал свидетелем того, как в Эдинбурге завоевала для себя плацдарм еще одна исследовательская программа. Единственная разновидность программирования, к которому программисты вообще и в проекте МАК в частности относятся еще хуже, чем к функциональному: логическое программирование.
Неприязнь к зарождающемуся логическому программированию была в МТИ такой жгучей, что они не могли держать ее в себе и решили сделать о ней доклад в Эдинбурге. Доклад "Нерелевантность Резолюции" (The Irrelevance of Resolution) должен был прочесть Сеймур Пейперт (Seymour Papert), но он опоздал, и доклад сделал один из наших будущих героев, студент Джеральд Сассман (Gerald Jay Sussman). К концу доклада Сассмана приехал Пейперт и сделал доклад снова, еще раз. Двойное послание не могло быть более ясным: это не то. Сюда лучше не лезть. Серьезно, любой из вас будет жалеть. Лучше закройте тему и забудьте что писалось у Алана Робинсона.
После того, как позиция большого МАКа была выражена так недвусмысленно, у малого МАКа просто не осталось выбора. Чтоб разобраться, что же именно по мнению из МТИ лучше не делать, исследователи в Эдинбурге попытались изучить их собственные наработки в этой области. Знакомый Ван Эмдена Роберт Ковальски (Robert Kowalski), пострадал больше многих, ему пришлось разбираться с написанными на Лиспе PLANNER и CONNIVER, а он не знал и ненавидел Лисп.
К счастью, всего через несколько лет пришла помощь из Франции. Сначала ее анонс. В 1973 Ален Колмероэ (Alain Colmerauer) и Филип Руссель (Philippe Roussel) посещают Эдинбург и рассказывают о своей имплементации Пролога. Посетители из Марселя показали как писать конкатенацию списков, но, в общем-то и все. Как программировать на логике пока еще не знали. Но Ван Эмден придумал как написать на логике "квиксорт". И это была настоящая революция. Первая нетривиальная программа, настоящий алгоритм. Теперь антирезолюционщики из МТИ увидят как они ошибались!
Имплементации логического программирования в Эдинбурге не было, но Мики решил попробовать запустить "квиксорт" на доказателе Бойера-Мура, и был доволен и горд, что решатель теорем смог сортировать список.
Только в феврале 74-го, когда в Эдинбурге уже появилась новая машина, Дэвид Уоррен привез из Марселя коробку с картами - имплементацию Пролога на Фортране. Возможность запускать программы имела большое значение и Ван Эмден написал и испытал много маленьких программ [Emde06], которые стали примерами в книге Элдера Коэлью (Helder Coelho) "Как решить это на Прологе", напечатанной намного позже, но до этого циркулировавшей как самиздат [Coel82]. Логический "квиксорт" ван Эмдена 73-го года в версии 75-го [Warr75] выглядел так:
+LET(
QSORT(*L.*R,*Y,*Z):_
SPLIT(*R,*L,*R1,*R2)&
QSORT(*R1,*Y,*L.*W)&
QSORT(*R2,*W,*Z);
QSORT(NIL,*Z,*Z)
).+LET(
SPLIT(*L.*R,*X,*L.*R1,*R2):_
*L LE *X & SPLIT(*R,*X,*R1,*R2);
SPLIT(*L.*R,*X,*R1,*L.*R2):_
*L > *X & SPLIT(*R,*X,*R1,*R2);
SPLIT(NIL,*X,NIL,NIL)
).
Мы не ставим перед собой цели написать историю логического программирования, и временно прощаемся с ним, но раскол протоэдинбургской программы - не последнее влияние которое оно окажет на функциональное программирование.
В ходе ранней истории продолжений основные понятия открывались независимо друг от друга выдающееся число раз. Дж. Рейнольдс, Открытия продолжений [Reyn93].
Лучше бы я, конечно, выбрал название покороче. К. Вадсворт, Continuations Revisited [Wads2000]
Связи между большинством остальных будущих участников Эдинбургской программы нам поможет установить Джон Рейнольдс (John C. Reynolds), написавший историю изобретения продолжений.
Физик, увлекшийся компьютерными науками [Broo14], Рейнольдс более известен другими своими работами, но в конце 60-х он был одной из упоминавшихся выше необоекембриджских исследовательских программ, независимо получивших ФП из Алгола и ЛИ, но состоящей из одного человека. Программа породила один ФЯ - GEDANKEN. Встречается мнение, что язык относится к ветви обоих Кембриджей и происходит от PAL, но Рейнольдс утверждает, что язык разработан большей частью до того, как его автор узнал о PAL [Reyn2012] [Reyn69]. Лучшим подтверждением этого является несомненная самобытность GEDANKEN [GEDANKb]:
MAP ISR #(F, L) IF L = NIL THEN NIL
ELSE CONS(F(L 1),MAP(F, L 2));
Y IS 2;
MAP(#X ADD(X,Y), (1, (2, (3, NIL))));
Символ для лямбды #
в статьях обычно заменен на λ
.
Но только большей частью, Рейнольдс узнал о PAL еще до того как описал GEDANKEN в отчете [Reyn69] и позаимствовал из PAL энкодинг функций многих переменных с помощью функций одной переменной и туплов.
Все началось с того, что Рейнольдс придумал как описать структуры данных с помощью функций, но не так как мы (и, возможно, вы) подумали, а так:
CONS IS #(X, Y) #Z IF Z = 1 THEN X ELSE Y;
CAR IS #X X 1;
CDR IS #X X 2;
такая техника не работала в ALGOL и скомпилированном LISP 1.5 [Reyn2012] [Reyn69] и Рейнольдсу пришлось изобретать язык с лексической видимостью в котором можно возвращать функции.
Рейнольдс имплементировал GEDANKEN в апреле 69-го для того, чтоб проверить не допустил ли он ошибок в операционной семантике языка для статьи [Reyn70], просто переписав семантику максимально близко к её тексту на стенфордском LISP360, уложившись в 500LOC [GEDANK]. Описание семантики, впрочем, так в статью и не попало. Имплементация требовала заметно больше памяти, чем нужно и была "медленной как ледник".
В начале 70-х Рейнольдс предпринял было попытку более серьезной имплементации, но был "деморализован" тем, что не смог придумать как решить проблемы производительности. После появления Scheme и ML, Рейнольдс решил, что страдать с GEDANKEN больше нет смысла. Так закончилась его история, и история всей этой исследовательской программы.
Более полезным для ФП были уже те работы, которыми Рейнольдс как раз известен, о параметрическом полиморфизме [Reyn74]. Подход Рейнольдса был более формальным, чем у Стрейчи, он не ограничился парой примеров на псевдокоде, а смог описать что параметрический полиморфизм означает и как работает. Работает он как лямбда-исчисление:
(Λt. λf ε t -> t. λx ε t. f(f(x)))[integer]
λf ε integer -> integer. λx ε integer. f(f(x))
Работа Рейнольдса была проделана независимо от работы Жана-Ива Жерара (Jean-Yves Girard) о том же [Gira72]. Похоже, что работать над чем-то независимо от вас намного легче, если вы публикуетесь на французском языке. Хенк Барендрегт (Hendrik Pieter Barendregt) называет и другую причину: Жерар работал над параметрическим полиморфизмом для теории доказательств, а Рейнольдс для описания семантики языков программирования [Bare92].
Эта же причина, уже по мнению Рейнольдса, была у множественного независимого переизобретения продолжений. Продолжения изобрели имплементаторы ЯП, изобрели занимающиеся операционной семантикой ЯП, изобрели занимающиеся денотационной семантикой ЯП.
Но что означают эти переизобретения? Что именно они переизобретали? Все эти люди читали и ссылаются на работы Ландина про J-оператор. Проблема в том, что Ландин изобрел продолжения как фичу интерпретатора, определенную через изменения состояния интерпретатора. И те, кто определяли семантику языков через интерпретатор вслед за Ландином, хотели интерпретатор попроще. Для занимающихся денотационной семантикой интерпретатор был проблемой в принципе. То что все они хотели изобрести - это энкодинг goto
и вызова по значению в ЛИ, т.е. CPS. Имплементация языков программирования с помощью CPS в Эдинбургской программе не будет применяться еще долго, так что мы вернемся к ней в рассказе о других попытках сделать ФЯ, результаты которых Эдинбургская программа в конце концов все же позаимствовала.
Итак, Рейнольдс делает вывод о том, что продолжения переизобретали в меньшей степени из-за плохой коммуникации между исследователями, а больше из-за разнообразия областей в которых продолжения посчитали полезными [Reyn93]. Но из описываемых им самим событий напрашивается противоположный вывод.
CPS-преобразование изобрел научрук Дейкстры и Ван Эмдена и автор алголов Адриан Ван Вейнгаарден (Adriaan van Wijngaarden) и сделал о нем доклад в 1964. И на этом докладе присутствовали как операционщики, например Ландин, так и денотационщики, например Стрейчи. Но благополучно забыли и/или не поняли о чем речь. Реакция имплементаторов была еще хуже, так что к этому эпохальному моменту мы еще вернемся.
В ноябре 70-го Локвуд Моррис (Lockwood Morris), защитивший диссертацию в Стенфорде и преподающий в Эссекском университете, выступил с докладом "Следующие 700 формальных описаний языков" [Morr93] по приглашению Ландина в Лондонском колледже (сейчас - университете) королевы Марии (Queen Mary College (University of) London). Моррис работал в Стэнфорде с МакКарти, который слушал доклад Вейнгаардена, но, видимо, забыл про него. Так что Моррису пришлось переизобрести продолжения самостоятельно. Нельзя сказать, что МакКарти вовсе не помог ему. Как мы уже упоминали выше, одной из идей вдохновивших Морриса была техника обработки ошибок из LISP 1.5 [McCa62] и статьи МакКарти [McCa60]. Другое влияние - механизм обработки ошибок в Snobol.
Моррис называл продолжения "дампы" (по названию регистра SECD-машины) и "значения-метки".
Рейнольдс слушал доклад, первый раз увидел использование продолжений. Из этого произошла его собственная работа над определяющими интерпретаторами, которая популяризировала продолжения [Reyn72]. Еще до издания статьи Рейнольдс популяризировал продолжения докладами. В 71-ом Левенворт (McG) приглашает Рейнольдса и Артура Эванса (PAL) провести семинар по применению лямбда-исчисления в программировании [Reyn98].
Рейнольдс отмечает, что во время обсуждения доклада Морриса Ландин ничего не говорит о работе Вейнгаардена, хотя еще в середине шестидесятых помнил о ней и ссылался на неё.
В декабре 70-го Рейнольдс посещает университет Эдинбурга. Там он рассказывает про доклад Морриса Бурсталлу и Крису Вадсворту (Chris Wadsworth), пишущему в это время диссертацию под руководством Стрейчи в Оксфорде. Вадсворт в свою очередь рассказал, как переизобрел продолжения он сам. Буквально вот только что.
В октябре 1970 Стрейчи показал Вадсворту препринт “Proving algorithms by tail functions” Мазуркевича (Mazurkiewicz), которую он получил на встрече IFIP WG2.2. Напечатали статью только в 71-ом. Рейнольдс затрудняется назвать её переизобретением продолжений, но считает её как минимум шагом к такому переизобретению [Reyn93].
Но Вадсворту одной фразы "tail functions" в заголовке оказалось достаточно - все встало на свои места [Wads2000]. До этого они со Стрейчи два года безуспешно пытались изобрести энкодинг goto
и меток с помощью лямбд. Разумеется, Стрейчи тоже слушал Ван Вейнгаардена, но это не помогло. Вадсворт называл продолжения продолжениями - именно его термин стал ходовым. С ноября они распространяли рукопись, но Стрейчи считал что не стоит торопиться публиковать статью, она должна настояться. Да, именно поэтому некоторые из статей Стрейчи, на которые мы тут ссылались, изданы через десятилетия после его смерти или не изданы вовсе. Эта статья, правда, была напечатана еще при жизни Стрейчи в 74-ом, после того как Вадсворт защитил свою диссертацию на более важную для Эдинбургской программы тему, чем продолжения.
Хотя в Эдинбургской программе соберутся использовать CPS-преобразование для имплементации ФЯ еще не скоро, там собрались имплементировать продолжения как фичу языка. И без первого результаты второго им не особенно понравились.
Бурсталл хотел имплементировать J оператор Ландина и урезанная версия продолжений, соответствующая по мощности исключениям была в POP-2 с самого начала. Полноценные продолжения имплементировали в 1970, но сохранение стека копированием в хип было дорогим [Popp2002]. Не могло быть и речи об использовании этого как основной управляющей конструкции.
В Оксфорде Стрейчи - научрук Дэвида Тернера (David Turner), посоветовал ему имплементировать PAL эффективно, что Тернер безуспешно пытался сделать три года с 69-го по 72-й. И главной проблемой с производительностью Тернер посчитал первоклассные метки в PAL, т.е. продолжения [Turn19]. Позднее Тернер найдет, что использовать вместо продолжений: наработки из диссертации Вадсворта и книги Берджа.
Для участников Эдинбургской программы продолжения - сложная в реализации и медленная фича, а использование CPS-преобразования для имплементации ФЯ - экзотическая и непопулярная техника. В результате, не смотря на количество переоткрывателей продолжений в Эдинбургской программе, продолжения не найдут себе в ней важного места.
Из воспоминаний Ван Эмдена и Рейнольдса мы знаем, что какая-то связь между будущими компонентами эдинбургской программы поддерживалась. Достаточная для того, чтоб узнавать о чем-то до публикации, но недостаточная для того чтоб сразу обнаруживать что работа ведется над одним и тем же. Да, в отличие от обоекембриджцев они хотя-бы слушали, что говорят другие, находящиеся в той же комнате. Но они не так и часто бывали в одной комнате, так что коммуникация все ещё оставляла желать лучшего.
К счастью, приближается момент, когда это взаимодействие улучшится настолько, что можно будет уже говорить о единой Эдинбургской исследовательской программе, внутри которой переизобретение велосипедов минимально. И в эту доинтернетную эпоху такой результат мог быть достигнут только одним способом - перемещением основных участников в одну местность. Что они, по счастливому совпадению, и сделали.
Читая все эти воспоминания о ранних годах Эдинбургской программы, мы никак не могли избавиться от странного ощущения. Чего-то в них не хватает такого, что там точно должно быть. Но нет.
Что странно во всех этих воспоминаниях - это отсутствие упоминаний Робина Милнера.
Я посчитал программирование довольно неэлегантным. <..> Мне показалось, что программирование - это не очень красивая вещь. Поэтому я решил, что больше никогда в жизни не подойду к компьютеру! <..> И устроился в Ferranti, где я работал программистом три года. <..> Да, я взялся за это без особого энтузиазма, но я решил, что надо же найти какую-то работу.
Робин Милнер, интервью 2003-го года. [Miln2003]
На протяжении всей этой истории Робин Милнер был с нами: как многие наши герои, он учился в Кембридже [Miln2003]. Как многие наши герои работал программистом в Ferranti [Miln93] [Plot2000], как многие наши герои напрограммировался и ушел в академию. Милнер пару раз посещал лондонский подпольный семинар [MacQ15], когда преподавал в Лондонском городском университете (City, University of London) в 63-ем. Познакомился с Ландином, Стрейчи и Бурсталлом [Plot2000] [Plot10]. Интересовался CPL, дружил с Дэвидом Парком [Miln2003]. Но герои и наблюдатели за героями тех времен не рассказывают как кто-то научил его лямбда-исчислению в баре, или как он научил кого-нибудь функциональному программированию в баре. Никто не придумывает историю о том, как собака съела его диссертацию, и поэтому он ее не защитил (как многие наши герои). Обоекембриджцы и ранние Эдинбуржцы не заметили самого важного, по мнению многих, отца функционального программирования.
Которым он мог бы и не стать. Во время работы в Университетском колледже Суонси (University College of Swansea, сейчас Университет Суонси) в 68-70гг. Милнер увлекся резолюционизмом, восхищался результатами Робинсона, и мы могли бы потерять его как Ван Эмдена. К счастью, Милнер написал автоматический доказатель теорем и тяжелый опыт безуспешных попыток делать с его помощью что-то интересное убил в Милнере резолюциониста. Или, по крайней мере, на какое-то время обезвредил [Miln93] [Miln2003].
После Суонси, в 71-ом году Милнер отправился работать в страну, из которой в Великобританию приезжали все те выдающиеся мыслители чтоб отговорить от занятий всеми этими робинсонизмами - США. Отправился в Стенфорд, к МакКарти, который перешел туда из МТИ.
В Стенфорде Уитфилд Диффи (Whitfield Diffie), более известный другими своими работами, научил Милнера писать на Лиспе [Gord2000], но не научил любить Лисп.
По собственному заявлению Милнера, Лисп повлиял на дизайн его будущего ФЯ [Miln93], но в основном не примером того, как надо делать, а примером того, что надо делать по другому. Что, как мы увидим дальше, будет довольно обычным подходом в Эдинбургской программе.
В Стенфорде хотели сделать что-то практическое и работающее, хотя бы в прямом смысле - выполняющееся на компьютере.
И, по мнению Милнера в это время, практическое и работающее - это инструмент проверки доказательств для логики Скотта [Miln93] [Miln2003].
"Инструмент проверки" означает, что над доказательством работает в основном человек, причем от него требуются не только интересные идеи, но и много, много рутинного труда [Gord79].
Логика Скотта, которую Милнер назвал Logic for Computable Functions или LCF - это неопубликованная в свое время работа Скотта "Типо-теоретическая альтернатива для ЕВПОЧЯ, КАЧЕ, ИВТП" [Scot93]. Основную часть статьи Дана Скотт (Dana Stewart Scott) написал в октябре 1966 во время посещения исследовательской группы по программированию (Programming Research Group) Стрейчи в Оксфорде. В ней Скотт посмеивается над нетипизированным подходом, который используют Бём, Стрейчи и Ландин. Как мы помним, Стрейчи советовал побольше тянуть с публикацией на тот случай, если со временем выяснится, что статью лучше не публиковать [Wads2000]. Скотт не вспоминает, советовал ли ему так делать Стрейчи, но все сработало как было задумано.
Долго настаивать статью не понадобилось. Уже через месяц у самого Скотта появились кое-какие нетипизированные идеи. Перечитывать собственные шуточки над нетипизированной логикой Скотту стало стыдно и он решил статью и не публиковать [Scot93].
Но у читавших препринт Милнера и Плоткина отношение к статье не изменилось и они продолжали распространять копии. В результате в 93-ем её называют самой известной неопубликованной рукописью в PLT [Gunt93], и в том же году её конечно же опубликовали.
Скотт переписывался с Милнером и рекомендовал ему отказаться от типизированной логики, которая больше не нужна. Это не помогло. Милнер ответил, что имплементация уже почти написана, не переписывать же ему все заново [Gord10].
В 71-72гг Милнер и присоединившиеся к нему Ричард Вейраух (Richard Weyhrauch) и, позднее, Малкольм Ньюи (Malcolm Newey) написали LCF [Miln82] [Gord2000] на MLISP2 на PDP-10 [Miln72]. MLISP 2 - это "алголообразный" синтаксис для Лиспа, для тех, кто не любит скобки [Smit73]. Что-то вроде POP-2.
EXPR MAP(F,L);
IF NULL(L) THEN L
ELSE F(CAR L) CONS MAP(F,CDR TERM);
В Суонси Милнер хотел попробовать верифицировать какую-нибудь "реальную" программу, и программа которую он нашел была написана химиком на Фортране, с матрицами, хранящимися в одном массиве [Miln2003] и другими характерными Фортран-вещами, верификация которых не задалась. Так что на этот раз он верифицировал очень простой компилятор очень простого языка в код очень простой машины, что получилось гораздо лучше [Miln93] [Miln2003].
В своей диссертации (выпущенной как отчет в январе 75-го) Ньюи описывает планы на будущий LCF2 [Newe75].
LCF был по большей части системой проверки доказательств с не особенно развитой автоматизацией в виде нескольких предопределенных простых команд. Такой инструментарий использовать утомительно, так что в LCF2 нужно двигаться в сторону генерации доказательств, для этого нужен высокоуровневый командный язык с фичами высокоуровневых ЯП. Насколько высокоуровневых? Первоначальные планы не выглядят амбициозными. Идея о том, что доказатель должен управляться императивным кодом не нова. Это, по большому счету то, что МИТ продвигал как альтернативу резолюционизму, т.е. (Micro)PLANNER [Hewi09]. Но авторы LCF не планируют, что этот императивный язык будет Лиспом. Они хотят типизированный язык. Ньюи предлагает по крайней мере четыре типа для объектов доменной области, процедуры, функции, операторы ветвления и циклов, блоки кода. Может быть даже функциональные параметры у процедур.
Поскольку после типизированной логики Скотт стал продвигать нетипизированную, для Ньюи это еще вопрос, будет ли язык в следующем LCF типизированным или нет. Но скорее всего будет. Просто потому, что первый LCF типизирован. Нужно заметить, что ранее, в январе 72-го года такой вопрос не стоял. Милнер писал [Miln72], что само собой разумеется, что в следующей версии логика будет нетипизирована, ведь Скотт перешел на нетипизированную. Это, впрочем, не должно было оказать прямого влияния на то, будет ли типизирован новый командный язык.
Милнер не хотел больше жить в США, хотел жить и работать в Великобритании, желательно в Оксфорде, где он бывал и где познакомился с работами Стрейчи и Скотта. Так что окончательная консолидация Эдинбургской программы могла бы и не состояться. Но еще больше Милнер хотел жить на одном месте, а не менять университет каждые пару-тройку лет. И постоянную позицию давал Эдинбургский университет, а не Оксфорд [Gord10].
Робин Милнер начал работать в Эдинбургском университете в 73-ем [Gord2000] [Plot2000] [Miln2003]. Но не с нашими старыми знакомыми Бурсталлом и Поплстоуном в Отделе экспериментального программирования, который в это время назывался Департамент машинного интеллекта (Department of Machine Intelligence) [Howe07]. И хорошо, что не там, но об этом позже.
Милнер работал в Департаменте компьютерных наук в Кингс Билдингс, а бывший Экспериментальный отдел программирования располагался на Хоуп Парк Сквер [Ryde2002] в трех милях от него.
Нельзя просто так взять и пройти/проехать три мили. МакКвин называет это расстояние серьезным барьером между двумя функциональными сообществами. Сообщества поддерживали связь с помощью того, что МакКвин называет "обществом памяти Боба Бойера". Бойер (Robert Stephen Boyer) не умер, а ушел из университета работать в SRI. Общество памяти собиралось раз в две недели дома у Бурсталла или у Милнера. Потому, что все остальные "ютились в лачугах", вспоминает МакКвин [MacQ15].
Итак, Милнеру не понравился ни доказатель теорем, который все делает сам, но ничего интересного не может доказать, ни доказатель, который что-то может проверить, но ничего не интересного не делает сам. Милнер с Ньюи еще в Стенфорде решили, что пользователь должен иметь возможность автоматизировать рутинные действия программами на высокоуровневом языке.
И в Эдинбурге проект этого языка стал существенно амбициознее. По всей видимости потому, что изменилось отношение Милнера к автоматизации. Гордон считает, что Милнер поверил в более амбициозную автоматизацию, ознакомившись в Эдинбурге с работами Бойера и Мура (J Strother Moore) [Gord10]. Мы не видели прямого утверждения Милнера об этом, но предположение косвенно подтверждается ссылками Милнера на их доказатель теорем как пример того, что можно было бы имплементировать на скриптовом языке для нового LCF [Miln76] [MilnBird84].
Бойер и Мур тоже занимались резолюционизмом, как и полагалось в Эдинбурге. С помощью своей резолюционной системы Baroque они могли доказать существование списка из трех элементов и прочие не особо интересные вещи. Так что летом 72-го года они решили автоматизировать индукцию и в 73-ем году получили не только автоматический, но и быстрый доказатель, который за несколько секунд доказывал что-нибудь вроде ассоциативности конкатенации списков [Boye75] [Moor13]. Проблема в резолюционизме, а не в автоматизации.
Раз уж автоматизация это хорошо и быстро, то нужен скриптовый язык подходящий для написания более-менее серьезных объемов более-менее сложного кода, вроде POP-2 на котором писали Бойер и Мур (вынужденно, больше не на чем было, как мы писали выше).
Сыграла роль и ФП культура в Эдинбурге и бэкграунд участников проекта. Проект по созданию следующей версии LCF - Edinburgh LCF был начат в 1974-ом году [Miln82] [Miln93]. Первыми ассистентами Милнера были работавший с ним в Стенфорде Малкольм Ньюи и, упоминаемый нами раньше, один из изобретателей продолжений Локвуд Моррис [Miln82]. Эти трое и являются основными авторами первой версии языка [Gord2000].
В 82-ом году Милнер описал радикальный взгляд на MetaLanguage (ML). На самом-то деле это DSL: язык, спроектированный для более-менее одной задачи, в отличие от других ФЯ. Но вышло так, что ML оказался хорошим языком общего назначения. Этот факт открыли не его авторы, а посмотревший на ML свежим взглядом будущий герой нашей истории Карделли.
Милнер заявляет, что дизайн языка определен его целью в такой степени, что едва ли получится сделать хороший язык для скриптования системы доказывания теорем, который бы существенно отличался от ML [Miln82]. Это утверждение выглядит странным хотя-бы потому, что в той же статье рассматриваются дизайн-решения, которые сам же Милнер считает сомнительными, ну или по крайней мере не несомненными. И эти решения из тех, что существенно влияют на дизайн языка. Сам Милнер со временем изменит свое мнение об одном из этих решений и будущий ML станет таки заметно отличаться от Edinburgh LCF версии ML-я.
В материалах 78-79гг о ML фрейминг противоположный. ML - это язык общего назначения, уверяют авторы. Ладно, это не совсем так, но специализирован он не потому что в нем чего-то нет, а потому что в нем есть цитаты и антицитаты для конкретного синтаксиса логики нового LCF - PPλ (Polymorphic Predicate λ-calculus):
"F X == Y & ^(mkinequiv("Z:^t ", x))"
where t = ":tr" and x = "X"
отчего ML и называется "метаязык". Также, в нем есть встроенные структуры для абстрактного синтаксиса PPλ. Да, встроенные объекты предметной области. Но это уже исправлено добавлением возможности описывать такие структуры программисту, так что можно считать, что язык общего назначения [Gord78] [Gord79].
Хотя исследования в области дизайн языков программирования не были главной целью создания языка, авторы считают, что новые фичи в языке подходят для использования. Авторы отмечают, что когда говорят про "эксперименты" и "исследования" фич языка, это значит, что не то чтоб они проводили какие-то исследования, хотя бы даже только и сравнительные с другими языками. Они просто имплементировали новые фичи, и пользователи языка вроде как быстро осваивают их и довольны [Gord79]. Спасибо за честность, но авторы языков обычно об этом и говорят, когда говорят про "эксперименты".
И раз уж исследования в области дизайн языков программирования не были главной целью создания языка, авторы решили не экспериментировать со многими другими фичами.
Другими словами, ML - это ISWIM с некоторыми важными инновациями и инструментарием для манипуляции PPλ-кодом. Но, как мы помним, ISWIM в каждой статье и в каждой имплементации разный. Это ISWIM с лямбдами как PAL или с каррингом как McG? ISWIM с if then else
как в статьях Бурсталла, или с CPL-ным тернарным оператором как в статьях Ландина? ISWIM с туплами или со списками?
Да.
Авторы заявляют, что ML это ФЯ "в традиции" ISWIM [Gord78] [Gord79] и PAL [Gord78], что хорошо видно:
letrec map f l = null l => nil
| f(hd l) . map f (tl l);;
map (\x. x + y) [1; 2; 3] where y = 2;;
Также, "в традиции" GEDANKEN и POP2 [Gord78] [Gord79], чего не видно вовсе.
И если язык несомненно "в традиции" PAL, то стандартная библиотека LCF/ML вовсе не в традиции PAL и содержит богатый набор ФВП.
Милнер разработал полностью функциональный дизайн в стиле Берджа (на книгу которого Милнер и др. ссылаются [Miln78] [Gord78] [Gord79]) со всеми соответствующими требованиями к языку.
Пользователь системы применяет к целям тактики. Тактика возвращает набор подцелей и валидацию: функцию, для того чтоб из достигнутых подцелей получить цель. Валидации - функции из теорем в теоремы - должны быть объектами как и цели. Так что тактика - функция, возвращающая функцию. Валидация в общем случае создает замыкание, поэтому нужна лексическая видимость.
Пользователь строит тактики из других тактик с помощью комбинаторов тактик. Часто не только тактик, а просто функций. Функций принимающих и возвращающих функции [Miln82].
Эти стандартные комбинаторы помимо обычных (.)
(в LCF/ML называется o
), id
(I
) и const
(K
) включают гораздо более экзотические, соответствующие функциональным специализациям функций стрелок и аппликативов вроде (&&&)
(commaf
), (***)
(#
) и liftA2
(oo
). Есть даже отсутствующий в хаскельной стандартной библиотеке комбинатор для композиции функций вроде concat
и map
:
let $o2 (f,g) x y = f(g x y);;
[LCF77] $
тут означает, что объявлен инфиксный оператор.
Другими словами, с точки зрения доктрины Милнера 82-го года, функциональность ML была предопределена тем, что у Edinburgh LCF функциональный дизайн. Это логично, но рассуждение тут циркулярное. Сам функциональный дизайн для системы доказания теорем вовсе не неизбежен. Так, в первой версии его в основном удалось избежать. Мы не утверждаем, что нефункциональный дизайн был бы хорошей идеей, но идеи не обязаны быть хорошими.
Милнер в 60-е увлекался "симуляцией", что в основном было тем, что сейчас называют ООП [Miln2003]. Так что мы вполне могли бы потерять Милнера, как мы потеряем одного из будущих главных героев нашей истории. Угроза ООП вполне реальна.
До того, как Милнер решил, что все было предопределено самой целью сделать скрипт для LCF, ML был ФЯ просто потому, что это то, что делают авторы языков с таким бэкграундом в такие времена и в таком месте.
Что до предопределенности, то решение сделать скрипт не то что функциональным языком, а еще и ISWIMом, на самом деле, серьезно мешает имплементировать более важное свойство скрипта, с которого его изобретение и начиналось.
Ньюи пишет о типизированном скрипте в первую очередь, а о функциональных параметрах только по возможности [Newe75]. И эта возможность в то время была совсем не очевидна. Типизированные ФЯ в то время не то чтобы далеко продвинувшееся направление, но типизированный ISWIM это еще более сложный вызов. Как воплотить весь этот псевдокод без аннотаций типов в реальность?
Почему скрипт для LCF должен был быть типизированным?
Важно, чтоб пользователь мог применять валидации только к теоремам, а не любым объектам, которых получилось много видов. Нетипизированный язык обходится дорого, утверждает Милнер, нужно тратить время на поиск ошибок. Так что нужны типы [Miln82].
Гордон обычно акцентирует внимание не на том, что нужно тратить время, а на том что надо тратить память.
Только процедура доказательства может производить значения типа thm
так что, пока система типов обеспечивает безопасность, нельзя получить, например, утверждение True == False
типа thm
. Это освобождает от хранения в памяти деревьев для доказанных утверждений. Что было проблемой для Stanford LCF [Gord79] [Gord10].
Функциональная архитектура Милнера не требует какой-то продвинутой типизации:
deftype goal = form # (simpset # form list)
and proof = thm list -> thm;;
deftype tactic = goal -> goal list # proof;;
[LCF77]
Композить эти функции можно также специализированными комбинаторами с простыми типами. Очередной случай, когда неизбежность ML сильно преувеличена. То, чего хватило бы LCF-скрипту, Милнеру не достаточно.
Решено делать функциональный язык. И, отмечает Милнер, это предполагает определение функций, которые хорошо работают с широким разнообразием объектов. Эта гибкость практически необходима для такого стиля программирования. Типизация как, например, в ALGOL 68 запрещает эту гибкость и следовательно запрещает весь такой подход к программированию [Miln78]. Декларировать новую функцию map
для каждого нового типа "невыносимо" [Miln82]. Милнер ссылается на доклад Стрейчи про параметрический полиморфизм [Stra67]. Это как раз то, что нужно! Но Милнеру нужно больше. Для него "невыносимо" даже явно указывать тип параметра для параметрически полиморфной функции map
[Miln82]. Все типы должны выводиться.
Это очень высоко установленная планка, практически все авторы других языков с параметрическим полиморфизмом, которые появлялись в то время вполне могли это вынести. И уже второй язык в котором применили наработки Милнера требовал указывать типы функций. Справедливости ради, это использовалось для имплементации фичи, которой не было в ML, и это не был командный язык в REPL доказателя теорем, для которого не указывать типы важнее. Тем не менее, вывода типов в ФП без этих аномально высоких требований могло бы и не быть, не смотря на то, что алгоритм вывода типов был изобретен независимо несколько раз. Дело в том, что он обычно изобретался не программистами, и никакой программист кроме Милнера не смог довести изобретение до конца.
Первым изобрел алгоритм, частично предвосхитивший остальные обсуждаемые тут, Максвелл Ньюман (Maxwell Herman Alexander Newman) в 42-ом году. Скорее всего, ни один из прочих изобретателей алгоритма не видел этой работы на момент своего изобретения той или иной степени независимости. Хиндли узнал об этой работе Ньюмана только в 2005-ом году [Hind07].
Ключевую часть алгоритма разработал не позднее 54-го года Кэрью Мередит (Carew Meredith), он был использован в статье вышедшей в 57-ом, но не определен в ней формально. Двоюродный брат Кэрью Дэвид Мередит запрограммировал алгоритм на UNIVAC1 в том же году.
Карри продемонстрировал алгоритм вывода типов на примерах и описал неформально [Curr58] в 58-ом году. С этой работой знакомы, наоборот, все последующие изобретатели. Карри описал алгоритм формально и доказал его корректность в 66-ом, но опубликовал только в 69-ом. Другой алгоритм, использующий алгоритм унификации был изобретен Хиндли (J. Roger Hindley) в 67-ом и опубликован в 69-ом [Hind07]. Карри и Хиндли знали о работах друг друга и общались во время их написания. Различные, но эквивалентные алгоритмы в этом случае осознанный выбор, а не результат независимого открытия [Card2006].
Об этом изобретении Милнер узнает уже после собственного изобретения [Miln78] [Miln82]. Гордон считает, что Милнер и Хиндли практически наверняка знали друг друга, когда работали в Суонси, но, видимо, не говорили про типы [Gord10]. Это, наверное, довольно нормально. Например, Рейнольдс в апреле 74-го выступал с докладом о своем типизированном ЛИ в Париж VII где преподавал Жерар, но никто там не сказал ему, что вот у нас тут Жерар тем же самым занимается [Card2006].
Работой, о которой Милнер знал, была диссертация Джеймса Морриса.
Джеймс Моррис (James Hiram Morris, Jr.), дальний родственник Локвуда Морриса [Reyn93], уже появлялся в нашей истории. Он писал вместе с Ландином первую имплементацию PAL. Моррис один из тех загадочных авторов PAL, которые проводили много исследований в области ЯП, но не использовали для этого PAL. Впрочем, обсуждаемая работа Морриса частично решает эту загадку: ее результаты не могут быть использованы в таком языке как PAL.
В своей диссертации 68-го года [Morr68] в МТИ Джеймс Моррис описал алгоритм вывода типов для типизированного ЛИ на основе решения уравнений в стиле Карри. Моррис знал о работе Карри 58-го года [Curr58] но не о работах Карри и Хиндли 66-69гг [Card2006] [Hind07].
Независимое изобретение вывода типов только одно из аналогичных независимых изобретений Морриса. Он также изобрел равенство по Лейбницу независимо от Лейбница и алгоритм Кнута-Морриса-Пратта независимо от Кнута. Как и значительная часть героев этой истории, Моррис независимо изобрел продолжения, к чему мы еще вернемся в главе про изобретения продолжений имплементаторами. Почему же мы читаем о Моррисе в главе про Милнера, а не о Милнере в главе про Морриса? Моррис, как обычно бывает у обоекембриджцев, не был особенно успешен в практическом плане.
Алгоритм Морриса может выводить типы для типизированного ЛИ, расширенного операторами над типами их конструкторами для построения композитных структур (к которым мы еще вернемся). На первый взгляд это то, что нам нужно как основа для ISWIM с типами, но Моррис обнаружил ряд проблем, которые делали его расширенную лямбду слишком скучной для языка программирования.
Начнем с того, что Y-комбинатор на этом языке не проходит проверку типов.
> y = \f -> (\x -> f(x x))(\x -> f (x x))
cannot construct the infinite type: t0 ~ t0 -> t
Хуже того, если мы попробуем дать имя какому-нибудь подвыражению, то выражение может перестать проходить проверку типов. Например, когда функция, которой мы дали имя, применяется к значениям разных типов.
> (\twice -> (twice tail "FOO", twice not True))(\f x -> f(f x))
Couldn't match type `Bool' with `[Char]'
Но что это за функциональное программирование, если мы не можем объявить полиморфную функцию?
Погодите-ка, эти примеры не работают и в современных ФЯ, в которых можно объявлять рекурсивные и полиморфные функции. Может быть система Морриса не так и сильно от них отличается, и он в паре-тройке шагов от успеха? Так и есть, и он даже сделал один из этих шагов.
Моррис расширил свой язык операцией rec
, объявлять Y-комбинатор больше не нужно. Решение, правда, только частичное. Бесконечные типы можно получить и другими способами, в коде для стримов, например. Любые рекурсивные типы в языке Морриса запрещены, S-выражениями с типом
S = A + S x S
пользоваться нельзя, хотя Моррис считал, что ограничение можно ослабить.
Проблема полиморфизма (Моррис, конечно, ссылается на Стрейчи [Stra67]) так просто ему не далась. Точнее, проблема полиморфных функций, определяемых программистом, как сам Моррис её сформулировал. Встроенных полиморфных функций он добавил целую кучу, все эти конструкторы и селекторы для композитных типов, а потом еще и rec
. Все что придумал для решения этой проблемы Моррис - это поредуцировать немного выражение прежде чем типизировать его, изобретя таким образом шаблоны C++ независимо от Страуструпа. Не видели, чтоб кто-то приписывал ему это изобретение. Но может это и хорошо, когда вам не приписывают изобретение шаблонов.
> ((\f x -> f(f x)) tail "FOO", (\f x -> f(f x)) not True)
("O",True)
Сработало! Но какой ценой? Моррису и самому такое решение не особенно понравилось.
К концу диссертации Моррис совсем пал духом и пишет, что может типизация это не для ФЯ? Да, в каком-нибудь BCPL любое применение функции не к тому значению - неопределенное поведение. В LISP не всегда, там как повезет. Но если не повезет, то только держись. Если применить списочные селекторы к атому, можно получить список - таблицу его свойств и продолжить весело обходить её, например. Надо просто проверять все в рантайме и поскорее падать как в PAL, если что не так. Может быть этого будет достаточно?
Такое пораженчество Милнера, конечно, не устроило.
Проблему определяемых программистом полиморфных функций решил Рейнольдс:
> (\(twice :: forall a. (a -> a) -> a -> a) -> (twice tail "FOO", twice not True))(\f x -> f(f x))
("O",True)
Но, как мы помним, аннотировать типы "невыносимо".
Вы, вероятно, недоумеваете, к чему все эти сложности. Функциональный программист объявляет функции не так, а вот так:
> let twice f x = f (f x) in (twice tail "FOO", twice not True)
("O",True)
Но для обоекембриджца let f arg = body in ... f ...
это то же самое, что (\f. ... f ...)(\arg. body)
, как завещал Ландин. В языке Эдинбургской программы это не одно и то же. Что помешало Моррису использовать то же самое решение, которое он уже применил к проблеме Y-комбинатора? Сделать специальный let
со своим правилом типизации, как он сделал специальный rec
? Мы не знаем. Но потому, что он этого не сделал, а Милнер - сделал, вы сейчас читаете параграф о Моррисе в главе про Милнера, а не параграф о Милнере в главе про Морриса.
Милнеровский опыт резолюционизма в очередной раз повлиял на историю ФЯ, когда Милнер не стал использовать подход Морриса с решением уравнений. Милнеровский алгоритм W основан на алгоритме унификации Робинсона. Проблема в том, что алгоритм W медленный. Дорогая операция подстановки применяются слишком часто. Медленные алгоритмы - это нормально для резолюционистов, но потому они и наработали массу способов делать медленные вещи быстро. Милнер использует идею, знакомую ему из литературы по доказателям теорем, основанных на методе резолюций. Подстановки композируются, но применяются только тогда, когда это необходимо. Получается алгоритм J, который и использован для вывода типов в LCF/ML [Miln78].
Хиндли придумал использовать алгоритм унификации раньше, о чем Милнер узнал не позднее 77-го года [Miln78]. А мог бы на десятилетие раньше, если бы интересовался тем, над чем работают его коллеги по университету [Gord10]. Хиндли, правда, не изобретал полиморфный let
, изобретя который Милнер решил главную проблему системы Морриса [Miln78].
Милнер знает, что достигнута не вся "гибкость", которая возможна в нетипизированном языке и даже в языке, где надо аннотировать полиморфные типы. Он знает о работах Рейнольдса [Reyn74], языке EL1, работе Лисков и др. над будущим CLU, к которой мы еще вернемся. Также Милнер ссылается на некоторые языки, публикации о которых появились позже, чем его система типов была имплементирована, так что едва ли эти работы уже могли повлиять на его решение.
Милнер заявляет, что предпочитает так много "гибкости", сколько возможно без явного указания параметров типов и не больше. Заявления заявлениями, но на практике последующие авторы ФЯ будут хотеть немного побольше. И даже сам Милнер в той же статье, в которой он делает это заявление [Miln78].
Милнер чувствует, что не все разделяют его мнение по "невыносимости" (обязательной) аннотации. "Можно спорить," - допускает он - "что отсутствие аннотаций типов затрудняет понимание". Свой подход он защищает так:
- Не аннотировать типы удобно, особенно в однострочниках.
- Тайпчекер настолько простой, что обязательное указание типов не сделает его заметно проще.
- Аннотируйте если хотите, возможность аннотировать есть.
И хотя о необходимости вывода типов "можно спорить", он определенно сыграл важную роль в развитии Эдинбургской программы, произведя впечатление на людей, сыгравших важную роль. Многие наши будущие герои описывают первый опыт с ML REPL как чудо, волшебство [Augu21] [Plot10]. И если уточняют, что было таким волшебным, то говорят про вывод типов, о том как REPL выдает тип функции которую они только что набрали. Чудо выглядело так:
#letrec map f l = if null l then []
# else f(hd l).map f (tl l);;
map = - : ((* -> **) --> ((* list) -> (** list)))
#map (\x.x*x) [1;2;3;4];;
[1; 4; 9; 16] : (int list)
[Gord79]
Итак, Милнер потребовал от нового языка нечто невиданное, о чем Обоекембриджцы могли только мечтать, и все у Милнера получилось. И Милнер утверждает, что повезло, что система типов, которая понадобилась для этого такая простая, а её имплементация такая элегантная [Miln82].
Милнер отмечает, что он не работал над преобразованиями типов и перегрузкой, но думает, что это все может быть интегрировано [Miln78]. И, походя отмахнувшись от одной из главных проблем, с которой будут десятилетиями страдать его последователи, переходит к тому, что пошло не так с самого начала. На самом деле не все так просто и элегантно.
Все (прото)функциональные языки, о которых мы писали до сих пор имели операторы присваивания. История CPL началась с изменяемых ссылок еще до того, как он стал назваться CPL и ISWIM получил оператор присваивания еще до того, как он стал называться ISWIM.
В обоекембриджской программе если и существует дискуссия о том, должна ли в языке быть мутабельность - она не очень развита. Да, у нас есть затруднения с описанием семантики мутабельности и доказательствами корректности кода на языке с мутабельностью, но мы не умеем делать все что нам нужно без мутабельности, так что мутабельность в языке будет [Land66].
Ко времени разработки ML тут мало что поменялось, так что если он создавался как язык общего назначения, то едва ли есть смысл говорить о каком-то "решении". Справедливости ради, примерно в то же время обсуждалась иммутабельность в CLU, но только потому, что первоначально это должен был быть dataflow-язык [Lisk93].
С точки зрения доктрины-82 ML - DSL, и уже есть какой-то смысл говорить об обосновании для мутабельности как фичи. И обоснование такое: первый LCF работал с мутабельными деревьями, так что решили, что и в ML мутабельность будет [Miln82]. Это обоснование критикует Гордон. После добавления абстрактных типов большие деревья не нужны [Gord10].
Даже если мутабельность подается как расширение чисто-функционального подмножества, как в ISWIM и PAL, расширение всегда ограничивается только добавлением присваивания, а не отдельного способа декларации мутабельных объектов. Все является мутабельным, даже туплы:
let t = 1,2,3 in
t 1 := 4;
Print t -- напечатает (4,2,3)
код, который в PAL-мануале [Evan68b] иллюстрирует "наиболее распространенную ошибку в PAL":
let i = 1 in
let t = 1, 2, i, 4 in
Print t; -- напечатает (1, 2, 1, 4)
i := 4;
Print t -- напечатает (1, 2, 4, 4)
ну, ничего не поделаешь.
В отличие от Обоекембриджских языков и POP-2, в ML есть разделение на декларации констант и мутабельных ссылок.
#let x = 1;;
x = 1 : int
#x := 2;;
UNBOUND OR NON-ASSIGNABLE VARIABLE x
TYPECHECK FAILED
Не для того, чтоб как-то бороться с главной ошибкой PAL, а из-за типизации. По этой же причине Милнер относит решение о добавлении мутабельности в ML к проблемным [Miln82].
В чем же проблема? После добавления присваивания можно получать значения любых типов из значений любых типов, а значит и типа thm
- теоремы - из любой формулы form
. Вот Локвуд Моррис доказывает, что True == False
[Gord79]
let store, fetch =
letref x = [] in
(\y. x := [y]), (\(). hd x);;
store "TT == FF";;
let eureka :thm = fetch();;
Готово! Да, все труды с добавлением и выведением типов были напрасны.
Добавления специально типизированного let
было мало, нужен еще один специальный оператор для декларации - letref
. Но что должно быть специальным в его типизации?
Можно было бы запретить полиморфные мутабельные ссылки вообще, но они нужны. Не то чтобы эффективная имплементация рекурсии в это время была вовсе неизвестна, но уж точно не принята. Поэтому, если писать функции аналогичные левой свертке, то лучше использовать цикл [Gord79], как завещал Бердж:
let rev l =
letref l,l' = l,[] in
if null l then l' loop (i,i' := tl l, hd l.l');;
Цикл здесь это if then loop
. Невиданная ни до, ни, вероятно, после конструкция структурного программирования, объединяющая (многоветочный) условный оператор и пару разновидностей операторов циклов [Gord79]:
{if e1 {then|loop} e1'
if e2 {then|loop} e2'
.
.
.
if en {then|loop} en'}
{{else|loop} en''}
Это основное отличие ML от ISWIM после типизации. Эдинбургская программа отвергла продолжения как основную управляющую структуру из-за медлительности. И большинство дизайнеров ML отвергли goto
как в Лиспе, за которое выступал Ньюи [Gord10].
В это время неумение имплементировать рекурсию, отказ от продолжений и структурное программирование (отказ от goto
) идут в одном пакете. И если вы затрудняетесь увидеть связь, то это нормально, недопонимание их и связывает. К этой истории мы еще вернемся.
Итак, для имплементации полиморфных функций с помощью циклов нужны полиморфные мутабельные ссылки. Или какие-то еще более невиданные циклы, с изобретением которых эмелисты пока решили не связываться. Они решили связываться с изобретением хаков для типизации мутабельных ссылок, чем они и занимались в последующие десятилетия.
И, в случае LCF/ML, они пока остановились на запрете присваивания полиморфным нелокальным ссылкам в функциях, что заодно потребовало аннотации типа в некоторых случаях. Милнер доказал надежность системы типов только для подмножества без мутабельных ссылок. Милнер и Рейнольдс не позднее 77-го решили, что нужно разработать язык с контролем эффектов [Miln78], но Милнер не стал заниматься этим, а Рейнольдс занимался без особого успеха.
В 79-ом Гордон и другие пишут, что мутабельные ссылки особо не пригодились, редко используются и не ясно, стоило ли их добавлять вообще [Gord79]. В коде LCF 77 один letref
на ~50 прочих объявлений или один на сто строк, что может быть и можно назвать редким. Но не потому, что мутабельные деревья не нужны или чего-то вроде того. Причина гораздо хуже.
Мутабельность и циклы нужны для имплементации функций вроде foldl
(в LCF/ML revitlist
) и reverse
(rev
), но в LCF 77 они написаны на Лиспе. Код на ML используется только для каррированной обертки:
let revitlist f l x = revitlist(f,l,x);;
и foldr
(itlist
), двупроходная как у Берджа, имплементирована с использованием этих двух лисповых функций:
let itlist f l x = revitlist(f, rev l, x);;
Так имплементированы большинство функций списков. Вот так выглядит библиотечный map
:
let map f l = map(f,l);;
в другом файле заголовок на лиспе с типом и арностью:
(PUTPROP (QUOTE map) 2 (QUOTE NUMARGS))
(PUTPROP (QUOTE map) (MKTIDY (QUOTE (((%a /-> %b) # (%a list)) /-> (%b list)))) (QUOTE MLTYPE))
в третьем файле обертка для библиотечной лисповой функции MAPCAR
(DEFPROP map
(LAMBDA (%%F L) (MAPCAR (FUNCTION (LAMBDA (X) (AP %%F X))) L))
EXPR)
которая имплементирована как сотня строк ассемблера.
Имплементация большинства этих функций на ML написана, но только как документация в мануале. Как Лисперы в 60-е использовали свой M-псевдокод.
Диалекты LCF/ML без мутабельных ссылок еще появится, самый первый - в 80-е годы, а самый новый в 2011г. (последний релиз - в 2012)[EventML].
Авторы LCF/ML считают исключения одной из трех важнейших фич ML (вместе с выводом типов и поддержкой ФП)[Gord79] и одной из двух основных инноваций (вместе с выводом типов) [Miln82].
С точки зрения Милнера 82-го года, это одна из неизбежных фич ML потому, что тактика сама определяет свою успешность, и должна иметь средство сигнализировать о неуспешности. Да, стримы, комбинаторные парсеры и некоторые стандартные лисповые функции, которые в Эдинбургской программе уже известны, сигнализируют о своем (не)успехе возвращаемым значением.
Милнер комментирует, вероятно, именно их, когда заявляет, что не хочет, чтоб тип населяли какие-то еще посторонние значения. И справедливости ради, некоторые из обсуждаемых решений сообщают о результате таким способом, который работал бы только в нетипизированном языке или языке с null
. Понятно, что Милнер не хочет ничего такого. Но! Во-первых, не все решения такие. И во-вторых, наличие плохих значений типа не вполне решается непроверяемыми исключениями.
Ну, с неизбежностью все понятно, а что насчет инновационности? Отвергая продолжения как неимплементируемую эффективно фичу, Эдинбургская программа хорошо относится к их ограниченной форме - исключениям. Мы уже упоминали об исключениях в POP-2, и авторы ML даже упоминают о влиянии POP-2, но исключения из этого языка если и повлияли на ML, то разве что как пример того, как делать не надо.
В стандартной библиотеке LCF/ML есть функция tryfind
, которая применяет функцию к списку и возвращает первый результат после успешного завершения. Если функция бросает исключение, она применяется к следующему значению из списка. Попробуем имплементировать её с исключениями из POP-2, если б они были в ML (их не было).
Начнем с того, что и tryfind
и та, которую tryfind
применяет к списку должны принимать функцию, бросающую исключение. Почему? Исключения в POP-2 это урезанный J-оператор Ландина, такая вот ФВП:
jumpout(функция_обработчик, сколько_кладет_на_стек) -> функция_бросающая_исключение
Забудем про число результатов на стеке, в ML нам это не пригодится. функция_бросающая_исключение
принимает исключение, завершает функцию, в которой вызвана jumpout
, пропускает исключение через функция_обработчик
и возвращает из функции, в которой вызывали jumpout
результат функция_обработчик
. Хорошо, если обработчик может вернуть какое-то дефолтное значение, а если нет? Ну, придется использовать ту самую сумму, которую не хотел использовать Милнер.
Получаем что-то такое:
letrec tryfind f fail l =
null l => fail []
| let g x = let fail = jumpout(K(tryfind f (tl l)))
in f fail (hd x)
in g l;;
что используется так:
let fail = jumpout(I) in tryfind (\fail x. [... fail[] ...]) ...
В POP-2 передавать функцию, бросающую исключения было не нужно, там jumpout
использовался бы примерно так:
let fail = jumpout(I);;
letref fail2 = I;
letrec tryfind f l =
null l => fail []
| let g x = fail2 := jumpout(K(tryfind f (tl l)));
f (hd x)
in g l;;
tryfind (\x. [... fail2[] ...]) ...
Но в LCF/ML, как мы помним, присваивать нелокальной полиморфной ссылке из функции нельзя. Не то чтобы использование как в POP-2 выглядит лучше, впрочем.
Возможно, мы просто не в состоянии правильно использовать этот инструмент. Но выглядит так, что он не очень подходит для использования. По крайней мере для такого использования, которое задумано в LCF.
Исключения, которые придумал Милнер, позволяют имплементировать и применять функцию так:
letrec tryfind f l = null l => fail
|(f(hd l) ? tryfind f (tl l));;
tryfind (\x. ... fail ...) ...
(Позволяют. Но она, разумеется, имплементирована на Лиспе. Такой код только в документации).
Исключения - единственная фича LCF/ML, которую Милнер записывает и в список интересных инноваций, и в список основных фич и в список фич проблемных и недоработанных [Miln82]. В чем же недоработка? Как бы плохо не подходила функция jumpout
для обработки ошибок, она позволяет делать раннее завершение и возвращать результат, которым может быть произвольное значение. Милнеровские исключения такого не позволяют, исключением может быть только токен, т.е. массив символов. Как бросать другие значения и как это типизировать авторы LCF/ML не придумали.
Такое ограничение может показаться не очень страшным, если использовать исключения для обработки ошибок, а не как управляющую структуру для обычного кода, но эмелисты определенно собираются использовать исключения именно так. Вот реальный пример из мануала [Gord79]: switch
в языке не нужен, просто кидайте строку и обрабатывайте это "исключение" вот так
let termvars t =
failwith phylumofterm t
?? ``const`` nil
?? ``var`` [t]
?? ``abs`` (let x,u = destabs t in ...
?? ``comb`` (let u,v = destcomb t in ...
Отказ от сигналов через результат в LCF/ML последователен. Функции, которые в современных ФЯ возвращают Maybe
, в LCF/ML бросают исключения, что не особенно хорошо сказывается на информативности их типов. Аналог функции mapMaybe
имеет такой тип [HOL88]:
mapfilter : (* -> **) -> * list -> ** list
у этого есть и положительная сторона, частичные функции вроде head
и tail
нормально комбинируются с другими, а не являются странными реликтами прошедших эпох, как в современных языках:
#mapfilter hd [[1;2;3];[4;5];[];[6;7;8];[]];;
[1; 4; 6] : int list
Вот только нормально интегрируются не все частичные функции. Если ошибка возникает в лисповом коде, который получен трансляцией из ML, то происходит выпадение из LCF-REPL в REPL лисповый. Например, в случае переполнения целых чисел.
Нежелание или неспособность изобрести Maybe
интересно еще и тем, что это практически единственный полезный параметризованный тип, который можно было использовать в, по крайней мере ранней версии, системы типов ML.
Стараясь сделать из типизированной лямбды более практически интересный язык, Джеймс Моррис расширил ее примитивами для создания, использования и типизации композитных типов, которые он, по большей части, позаимствовал из работы МакКарти [McCa61]. МакКарти ввел декартово произведение (двухместный кортеж) и прямое объединение (Either
) как способы композиции элементарных типов и соответствующие конструкторы, предикаты и селекторы (для объединений - частичные функции). Никакие параметризованные сложные типы с их помощью он не конструировал, но приводил пример рекурсивного типа для для S-выражения: S = A(+)SxS
где A
- атом. Мы уже встречались с производной от этой системой - функциональный EDSL Берджа для конструирования и разбора структур данных.
Моррис [Morr68], позаимствовав общую идею, дал конструкторам и селекторам произведений более запоминающиеся названия (у МакКарти они имели названия вроде i
и j
, p
и q
, r
и s
. Попробуйте угадать что из них что), а от предикатов и частичных селекторов отказался вовсе, введя конструкцию switch
, аналогичную хаскельному оператору (|||)
switch x into (\y.y+1) or length
Эту конструкцию Милнер заимствовать не стал. Зачем, когда можно обрабатывать исключения, бросаемые частичными селекторами? Пришлось изменить и названия операций. Моррис называл головой и хвостом первый и последний элементы двухместного кортежа, а Милнеру они были нужны для еще одного встроенного типа - list
.
В ISWIMах обычно были или только туплы, как в PAL, и списки нужно было собирать из них. Или только списки как в McG, и нужно было использовать их вместо туплов. Соответственно, только для какой-то одной конструкции был синтаксис, использующий запятые и скобки. Скобки в ISWIMах часто можно было использовать и круглые и квадратные, в зависимости только от того, что программисту кажется более читаемым в данном случае. Конечно, пары скобок должны быть одного вида.
Наличие типов в LCF/ML сделало использование туплов вместо списков и наоборот затруднительным, так что потребовалось изобрести разные синтаксисы для них. Возможно, что впервые потребовалось. В LCF/ML синтаксис для туплов это запятые с опциональными круглыми скобками, а синтаксис для списков - ;
с обязательными квадратными скобками. Почему бы не использовать запятые в обоих случаях? Первопроходцы разделения синтаксисов для туплов и списков могли просто не задать этот вопрос. Также, вероятно существует причина связанная с имплементацией, к которой мы еще вернемся. Многие ФЯ, в том числе и некоторые ML-и, пересмотрят это, любимое многими программистами на Ocaml и F#, решение. Авторы LCF/ML также выбрали .
как инфиксный cons
. Как в Лиспе [McCa62] и Прологе того времени [Colm96] [Warr77]. Это решение не стало популярным в Эдинбургской программе. Даже в её расширенном толковании: в Прологе (H.T)
позднее заменят на [H|T]
.
В LCF/ML также есть и синтаксис для разбора и туплов v1,v2
и списков:
v1.v2
[]
[v1;v2 ... ;vn]
Но "паттернов" для объединений нет.
Это один из первых имплементированных ISWIMов, в которых такие конструкции могут быть вложенными. Но нет никакой switch
/case
образной структуры как у Бурсталла [Burs69]. Эта конструкция, возможно, пала жертвой исключений так же как конструкция Морриса для элиминации объединений или любой другой из известных к тому времени свитчей. Неудачное сопоставление с "образцом", которые эмелисты называют "varstruct" выбросит исключение, которое надо обработать. Техника реально использовалась в коде LCF [LCF77] (да, не все функции списков написаны на Лиспе):
letrec split l = (let (x1,x2).l' = l in
(x1.l1',x2.l2') where l1',l2' = split l'
) ? (nil,nil);;
что примерно соответствует такому коду:
split ((x1,x2):l') = (x1:l1',x2:l2') where (l1',l2') = split l'
split _ = ([],[])
Наш обязательный map
в таком стиле:
letrec map f l = (let x.xs = l in f x . map f xs) ? []
Это современный код, ни в коде LCF, ни в мануале [Gord79] такого map
нет.
Как вы, наверное, догадываетесь, такой подход не стал популярным в ФЯ. Но он мог повлиять на работы по компиляции паттерн-матчинга или быть переизобретен заново. Например, при компиляции ПМ методом Вадлера [SPJ87] на определенном этапе в промежуточном представлении ветви ПМ могут содержать операцию аналогичную fail
и скомбинированы оператором аналогичным ?
, но это промежуточное представление не должно компилироваться в выбрасывание и обработку исключений. Еще одна недоработка по сравнению с ISWIM-псевдокодом Бурсталла [Burs69] - невозможность декларировать конструкторы вроде ,
и .
, разбираемые таким "матчингом".
В 82-ом году Милнер назовет неиспользование наработок Бурсталла проблемой, еще одним сомнительным решением. Но, как и в случае с мутабельностью, есть основания сомневаться в историчности такого "решения". На тот момент, когда "решение" должно было быть принято, едва ли можно говорить о том, что ПМ готов для использования. Так что и решать нечего.
Сложнее понять почему нет менее амбициозных свитч-конструкций. LCF/ML не старались делать минимальным языком и обсуждаемый ранее if-then-loop не заменен циклом, управляемым исключениями, который в LCF/ML, разумеется, тоже есть. Практически все в LCF/ML можно делать многими способами. Это, видимо, самый большой ФЯ на момент своего появления. Определенно самый большой из имплементированных. Но вполне сравним даже с самым воображаемым из воображаемых CPL-ей, хотя воображаемость мешает установить это точно. Только ключевых слов для деклараций в LCF/ML почти два десятка, хотя в основном потому, что комбинации ключевых слов вроде let rec
заменены их конкатенациями.
Но является ли list
встроенным типом только из-за специального синтаксиса? Специальный синтаксис может поддерживать совсем не специальные типы объявленные в библиотеке.
И в LCF/ML есть система МакКарти для композитных типов. Которая, на первый взгляд, производит впечатление мощной и удобной. Первоклассные комбинаторы типов!
МакКарти предполагал, что ей можно типизировать S-выражения, а мы попробуем сделать списки:
> nil = Left ()
> isNil = isLeft
> cons h t = Right(h,t)
> hd l = fst(fromRight (error "hd") l)
> tl l = snd(fromRight (error "tl") l)
пока все хорошо...
> map f l = if isNil l then nil else cons (f (hd l)) (map f (tl l))
error: cannot construct the infinite type ...
Ох. Система МакКарти полностью бесполезна для языка с Милнеровским выводом типов. С любым рекурсивным типом проблема та же, что с написанием Y-комбинатора. Решение Морриса с rec
не решило проблему полностью. Что же делать?
Типы - это множества значений. <...> Постулат ни оригинальный, ни спорный.
Джеймс Моррис, ЛИ модели языков программирования [Morr68].
Типы - это не множества.
Джеймс Моррис, Типы - это не множества [Morr73a].
Барбара Лисков (Barbara Liskov) разочаровалась в методологии программирования к осени 1972-го года. Работы по методологии оперировали туманными определениями сущностей, которые программист должен был находить, но не объясняли как. Давали рекомендации, которым трудно было следовать на практике. Например, методология рекомендует запрещать программистам читать неинтерфейсную часть кода, который они используют (не шутка).
Какой должна быть методология? Лисков считает, что лучший способ разработать методологию - это разработать язык. Такой, что решения, использующие методологию - это программы на этом языке. А значит нет проблем с отображением дизайна на программы. Язык точно определен, а значит и методология определена точно. Язык - инструмент для объяснения и понимания методологии, можно продемонстрировать что и как (не)работает [Lisk74] [Lisk93].
Нужно обсуждать не невнятные рекомендации, а конструкции языков, которые способствуют написанию программ с хорошим дизайном. Если связать невнятные "единицы инкапсуляции" с типами данных, то не будет проблем с их идентификацией и использованием, ведь абстрактные типы данных имеют четкое определение и компилятор может проверить правильность их использования [Lisk93].
Лисков выступила с докладом об этих идеях в апреле 73-го и нашла единомышленника - Стефана Циллеса (Stephen N. Zilles). Мы уже знакомы с Циллесом, он работал над попыткой адаптации нотации Ландина для PAL [Zill70]. К осени 73-го они разработали абстрактный тип как языковую конструкцию алголоподобного языка и опубликовали свои наработки в сентябре 73-го. Более известна версия этой статьи, опубликованная в апреле 74-го [Lisk74]. Между этими публикациями в октябре 73-го года состоялась встреча исследователей, занимающихся такими проблемами в Гарварде. Из тех, кто позднее обсуждал дизайн ML с его авторами там были Рейнольдс и более известный другими своими работами Хоар (Charles Antony Richard Hoare) [Gord79]. И авторам ML должно было быть интересно то, что они могли рассказать. "Непрозрачность" типов не только позволяет определять самому структуры данных вроде встроенных в ML "теорем", но и решить проблему с рекурсивными типами.
Лисков и Циллес не единственные, кто начал работать над абстрактными типами данных, и даже не единственные из тех, о ком знали в Эдинбурге [Miln78]. Но работа Лисков отличается от большинства работ того времени практическим подходом. Лисков хотела имплементировать языковую конструкцию, получить работающий язык с ней как можно скорее. Поэтому первым делом было выброшено то, над чем работающие над АТД в основном и работали - описание спецификации АТД. Лисков решила, что спецификацию точно не удастся проверить компилятором в ближайшей перспективе. Циллес, как и большинство исследователей, как раз интересовался спецификацией и потому не принимал в дальнейшей разработке языка особого участия.
Разработка нового языка не лучший способ получить что-то работающее, так что для начала Лисков хотела модифицировать существующий язык. Студент Лисков написал обзор языков, в которых есть похожие на АТД конструкции [Aiel74], и это скорее обзор языков, в которых нет таких конструкций. Наибольшее сходство было найдено с классами Simula 67. В Simula 67 не было инкапсуляции, что означает не очень сильное сходство классов со средством для инкапсуляции, разработкой которого занималась Лисков. Хуже того, добавить инкапсуляцию в Симулу было недостаточно. Лисков хотела, чтоб АТД были средством добавления в язык типов аналогичных встроенным, таких как массивы. И массивы параметризованы, а классы Симулы - нет. С существующими языками с параметрическим полиморфизмом дела обстояли неважно. Так что было решено разрабатывать новый язык - CLU.
Нет, Лисков не нашла никого работающего над языком с параметрическим полиморфизмом. Про ML она узнала только в конце 70-х годов.
Единственной более-менее (и скорее менее) практической работе по сокрытию, известной Циллесу и Лисков в 73-ем году, была работа Джеймса Морриса. Да, снова Моррис, может быть это все-таки должна была быть глава про него?
Моррис один из тех загадочных авторов PAL, которые проводили много исследований в области ЯП, но не использовали для этого PAL. Эта его работа 1971-го года (но не опубликованная до 73-го) могла бы использовать PAL. Но использует GEDANKEN [Morr73b]. Не то чтобы Моррис забыл про PAL, эта работа упоминает PAL, как и его работа о выводе типов.
Для начала, Моррис описывает интервал на GEDANKEN "неправильно":
[Createint IS #(X, Y) IF X <= Y THEN X, Y ELSE Y, X;
Min IS #Z Z(1);
Max IS #Z Z(2);
Sum IS #(X,Y) (X(1) + Y(1)), (X(2) - Y(2));
Createint, Min, Max, Sum]
Разумеется, программист может сконструировать некорректный интервал без помощи Createint
и писать код, который зависит от внутреннего устройства интервала.
Моррис отмечает, что программист, пишущий функцию, принимающую такое значение, может оборонительно проверять его целостность, но это непрактично для нетривиальных структур. Отсортированных массивов, например. Проблема та же, что решали авторы LCF, только они делали акцент на память, а Моррис - на время. Вопрос нужно ставить не "что представляет из себя значение?", а "кем сконструировано значение?" или "откуда значение пришло?", настаивает Моррис [Morr73a].
Чтобы воспрепятствовать неправильному использованию данных, предлагает Моррис, нужно конвертировать между структурой для которой определены селекторы и конструкторы вроде тупла в обсуждаемом случае и объекта с другим тегом, который эти селекторы не распознают и вызовут ошибку.
[Seal, Unseal IS Createseal();
Createint IS #(X, Y) Seal (IF X <= Y THEN X, Y ELSE Y, X);
Min IS #P(Unseal(P))(1);
Max IS #P(Unseal(P))(2);
Sum IS #(P,Q) [P' IS Unseal(P);
Q' IS Unseal(Q);
Seal((P'(1) + Q'(1)),(P'(2) + Q'(2)))];
Createint, Min, Max, Sum]
Моррис использует замыкания для ограничения доступа к этим функциям запечатывания и распечатывания данных. Только нужные функции захватят ссылки на правильные экземпляры Seal
и Unseal
.
Это не новая идея. Замыкания первоклассных функций могут ограничивать доступ к своей внутренней структуре. Или не ограничивать. В POP-2 были обе разновидности замыканий. Ограничивающие доступ замыкания использовались для обеспечения безопасности в Multipop68. Но Джеймс Моррис не ссылается на эти Эдинбургские наработки.
Замыкания также используются Моррисом для защиты генератора новых тегов, используемого функцией Createseal
, создающей пары из запечатывателя и распечатывателя. Моррис также разрабатывает более сложную машинерию со списками доступа для более интересных разновидностей доступа к внутренней структуре объектов.
После чего критикует свою систему как непрактичную из-за медлительности проверок и удержания ссылок на объекты кучи, не способствующих нормальной работе сборщика мусора [Morr73b].
Поэтому в следующей работе [Morr73a] Моррис описывает расширения для мейнстримного типизированного языка. В мейнстримном языке нет замыканий, так что как основу для системы сокрытия он изобретает "модуль". Это не модуль в привычном нам смысле, он не образует пространство имен и сам не имеет имени, это скорее аналог конструкции local in end
в SML.
Для запечатывания вводятся обертки, проверяемые компилятором и не имеющие представления в рантайме, сходные с newtype
или запечатыванием в параметризованных модулях:
type +complex > real array [1:2];
Но если динамическая система из первой статьи [Morr73b] описывает имплементацию полностью, то вторая статья - просто набор пожеланий Морриса, как все это имплементировать - не понятно.
По этой причине Лисков и Циллес не были уверены, что инкапсуляция может быть имплементирована полностью статически и были готовы начать с динамической системы Морриса и сделать статически так много проверок как смогут. И оказалось, что смогут все. Идеи Морриса в конце концов не пригодились [Lisk93], так что глава не будет про него.
Не смотря на желание имплементировать АТД как языковую конструкцию, так называемые "кластеры", Лисков и студенты даже не начали имплементацию до того как вышла статья [Lisk74], на основе которой авторы ML разработали АТД для ML [Miln78] [MacQ15].
Авторы ML ссылаются на другие работы по АТД, но время публикации этих работ, отсутствие описания спецификации и параметризованность не оставляют особых сомнений.
Язык Лисков как и ML - язык с универсальным представлением и сборкой мусора.
Так выглядела бы имплементация списка с помощью "кластера" Лисков:
list: cluster(element_type: type)
is cons, null, hd, tl;
rep(type_param: type) = oneof(nil: null,
cons: (h: type_param;
t: list(type_param);
e_type: type))
create ... end
cons: operation(s: rep, v: s.e_type) returns rep; ... end
hd: operation(s: rep) returns s.e_type; ... end
tl: operation(s: rep) returns rep; ... end
null: operation(s: rep) returns boolean; ... end
end list
А так на ML [Gord78] [Gord79]:
absrectype * list = . + * # * list
with nil = abslist(inl())
and $.(x,l) = abslist(inr(x,l))
and null l = isl(replist l)
and hd l = fst(outr(replist l))
and tl l = snd(outr(replist l))
Если не понятно, что означает . + * # * list
, то это unit + 'a * 'a list
и () + (a, List a)
. Нотация со звездами обычна для LCF/ML. Выводимые в REPL типы и код LCF использует её. Но не единственная. Синтаксис LCF/ML позволяет записывать параметры типов и похожим на современные ML-и образом (но с *
вместо '
), и еще одним непохожим:
* # ** -> *
*a # *b -> *a
*1 # *2 -> *1
Как мы уже писали, LCF/ML - большой язык. Будущие ФЯ позаимствуют каждый и трех вариантов, но не все варианты одновременно.
Конечно, помимо сходства, между "кластерами" и abstype
есть и очевидные различия. Лисков пишет, что не хотела неявных преобразований типов в языке, но все преобразования между абстрактным типом и его представлением в "кластере" неявные. Милнер, как мы помним, решил не думать как неявные преобразования будут работать с его системой типов, так что в LCF/ML эти преобразования между АТД и представлением явные, как у Морриса.
"Кластер" образует пространство имен и CLU требует использовать его функции с полной квалификацией и с указанием типа параметра:
list(int)$cons(1,l)
list(boolean)$hd(l)
abstype
пространства имен не вводит, как и изобретения Морриса.
Функции АТД должны иметь как минимум один параметр этого типа, поэтому конструктор - это специальная языковая конструкция. Это странное требование происходит, в основном от страха и непонимания ALGOL 68 [Lisk93]. Милнера это все не волнует, таких требований к функциям, объявленным в abstype
, нет. И специальных конструкторов нет тоже.
Основная многословность определения "кластера" получается из-за разделения определения типа и его представления списком функций. Лисков считала, что идейно важно, что в декларации АТД is
список функций над ним, а не is
внутреннее представление. АТД - это не его представление, а его интерфейс. Но это не важно для Милнера.
Из разговоров с Рейнольдсом, который в это время как раз занимался параметрическим полиморфизмом, Лисков сделала вывод, что статическая проверка возможна в ближайшей перспективе. Но в статье пока что параметру типа соответствует значение в рантайме и поле для него в представлении типа [Lisk74]. Справедливости ради, сложная проблема которую решали авторы CLU - ограниченный полиморфизм. Значение в рантайме позволяет проверить, что к значению типа-параметра можно применить сравнение, например, чтоб имплементировать множество [Lisk93]. Авторы LCF/ML над этой проблемой пока что не думают и множества не имплементируют.
Авторы LCF/ML отмечают, что абстрактные типы в нем и в CLU не "по-настоящему абстрактные" [Gord78], как в работах Циллеса и Гуттага. Милнер позднее делает [Miln82] загадочный комментарий о том, почему наработки Бурсталла не были использованы. Он связывает его работу с работами Гуттага над этими "настоящими" абстрактными типами данных. Что все это значит? Разберемся в следующей главе!
Прототип имплементации CLU был готов раньше, чем имплементация ML. Но готовая для использования имплементация CLU появилась позже. Так что, можно считать, что LCF/ML стал первой законченной имплементацией этой идеи (его авторы не претендуют на это). Понятно, что требования к "законченности" скрипта для доказателя теорем и языка общего назначения существенно отличаются. Компилятор CLU, например, был переписан на CLU за годы до того, как компилятор CLU был "закончен". Имплементацию LCF/ML посчитали законченной до того, как начали писать его компилятор хоть на чем-то, не то что на ML.
Абстрактные типы позволяют объявить список с помощью сумм и произведений, "непрозрачность" решает проблему с бесконечными типами. Теперь на ML можно написать и типизировать практически весь код из третьей главы книги Берджа [Burg75], что посоветовали Милнеру рецензенты его статьи [Miln78]. Наконец-то системы МакКарти и Милнера заработали вместе. Все рекурсивные типы вроде стримов и деревьев больше не нужно встраивать в язык. Нужно отметить, правда, что в библиотеке списки так не объявлены. Заявленная причина: это ухудшило бы производительность [Gord79].
Списки, сконструированные с помощью сумм и произведений, имеют неэффективное представление в памяти [LCF77]:
┌───┐ ┌───┬───┐ ┌───┬───┐ ┌───┬───┐ ┌───┬───┐ ┌───┬───┐
│ ├──►│*T*│ ├──►│ │ ├──►│*T*│ ├──►│ │ ├──►│NIL│NIL│
└───┘ └───┴───┘ └─┬─┴───┘ └───┴───┘ └─┬─┴───┘ └───┴───┘
│ │
▼ ▼
┌───┐ ┌───┐
│ 1 │ │ 2 │
└───┘ └───┘
по сравнению со списками как в Лиспе:
┌───┐ ┌───┬───┐ ┌───┬───┐
│ ├──►│ │ ├──►│ │NIL│
└───┘ └─┬─┴───┘ └─┬─┴───┘
│ │
▼ ▼
┌───┐ ┌───┐
│ 1 │ │ 2 │
└───┘ └───┘
Встроенные списки в LCF/ML именно такие как в Лиспе.
Абстрактные типы как в CLU - это самая новая фича ML из всех, что не были разработаны специально для ML. Можно предположить, что декларации абстрактных типов в ML появились не сразу, потому что все "стандартные" абстрактные типы встроены, кроме одного - структуры данных simpset
, используемой одной из более-менее сложных тактик. Вполне возможно, что некоторые типы встроены по той же причине, что и списки. МакКартиевская система создает действительно плохое представление в памяти почти для всего и simpset
это не касается потому, что это обертка над списком. Но thm
тоже только обертка для другого встроенного типа. Нет причин не имплементировать "теоремы" используя abstype
, если только thm
не появился в языке раньше, чем abstype
.
По той причине или по иной, но в коде LCF 77 abstype
используется только один раз. Ни параметризованные, ни рекурсивные (absrectype
) абстрактные типы не используются вовсе[LCF77].
И особенно богатой истории использования у них и не будет. Их время, как и выражения-where
, пройдет в 80-е, в новых ФЯ они будут или неиспользуемым реликтом, или вовсе в них не попадут.
Если алгебраические типы данных происходят от нотации Ландина, то путь от неё до АлгТД совсем не выглядит прямым, нотация CPL уже свернула куда-то не туда с разделением сумм и произведений. Моррис считал, что МакКартиевская "сумма" (которую Моррис называет "неоднозначный" тип) - это способ вернуть "свободу" нетипизированных языков в типизированный, и планировал развивать систему в соответствующем направлении, с объединениями типов без "конструкторов" и пересечениями типов [Morr68]. Даже развитие системы МакКарти в ML и обход её проблем с помощью абстрактных типов данных выглядит как продолжение движения не туда. Когда же мы придем к современным АлгТД по этому пути?
Никогда. Современные АлгТД появились иначе. Но это уже другая история.
Милнер, Ньюи и Локвуд Моррис имплементировали LCF на новой (для Эдинбурга) машине DECsystem-10 [Gord79]. Известной также как PDP-10. Той самой, появление которой положило конец монополии POP-2 в группе экспериментального программирования, позволило Ван Эмдену писать программы на Прологе. Да, первый LCF Милнер с Ньюи тоже писали на PDP-10, что многое говорит о том, насколько система была новой (не для Эдинбурга).
Локвуд Моррис дописал транслятор из ML в Лисп за шесть недель до того, как закончил работать в Эдинбурге. Милнер утверждает, что никто не нашел в нем ошибок с тех пор как Моррис закончил работу над транслятором и до того, как он был переписан уже в 80-е [Miln93]. Если эта история про отсутствие ошибок кажется вам неправдоподобной, то подождите, скоро мы раскроем обстоятельства, которые делают эту историю намного правдоподобнее.
Малкольм Ньюи написал парсер ML с помощью техники Вона Пратта (Vaughan Pratt). Пратт провел лето 75-го года в Эдинбурге и научил этой технике местных имплементаторов ФЯ, например МакКвина [MacQ14], нашего будущего героя. Это не обязательно указывает на время начала работ над парсером LCF/ML, потому что работа Пратта была опубликована раньше. Ранее метод захватил связанные с Эдинбургской программой центры, такие как Другой Кембридж и лабораторию IBM в Йорктаун Хайтс [Prat73]. К тому же, Пратт и Ньюи были знакомы еще с Австралии [MacQ15].
МакКвин считает, что разные разделители в списках и кортежах, а так же ;;
появились в LCF/ML потому, что писать парсер по Пратту проще, когда один и тот же оператор не используется для разных целей [MacQ14]. Вот только другие имплементаторы, включая и самого МакКвина с этим как-то справились.
Имплементация LCF/ML была в первом приближении закончена в 75-ом году [Miln78], и сам LCF использовался с того же или следующего года [Gord78]. Первое описание LCF/ML было опубликовано в сборнике IRIA (да, это будущая INRIA) 75-го года [Miln75], но не отсканировано, так что трудно сказать точно, насколько оно отличается от основных публикаций о LCF 78-79гг. Но судя по обсуждениям работы в [Tenn77], в ML уже есть вывод типов. И Локвуд Моррис поучаствовал в дизайне АТД в ML [Miln78]. А Моррис с Ньюи закончили участие в проекте в 75-ом году.
Локвуд Моррис отправился в Сиракузский университет (Syracuse University) в штате Нью-Йорк [Gord2000] работать с самим Робинсоном и другими резолюционистами над LOGLISP. А Ньюи - в Австралийский национальный университет (Australian National University) и вернется в нашу историю только через десяток лет.
Вместо них ассистентами Милнера стали уже знакомые нам Майкл Гордон и Крис Вадсворт [Miln82], которые занимались в основном LCF, и только какими-то деталями в ML [Miln93] [Miln90] и закончили имплементацию к 1978 году.
LCF/ML - это первая имплементация ФЯ для которой есть не просто исходный код, но история исходного кода. К сожалению, на 70-е годы приходится только одна отметка этой истории: код LCF в октябре 77-го года [LCF77].
С начала проекта и до конца 77-го года Милнер, Ньюи, Моррис, Гордон и Вадсворт написали 10KLOC на Stanford LISP 1.6 [Gord79], из них 2KLOC на MLISP2 - фронтенде с более мейнстримным синтаксисом для этого Лиспа. И аж 1162 строки на ML. Да, тысяча строк. Тут нет опечатки, никакие цифры не пропущены. Это мы и имели в виду, когда писали, что функционального программирования не было.
И, кстати, мы думаем, что написать тысячу строк на языке, не используя даже все его фичи, вроде параметризованных и рекурсивных АТД - это не самый верный способ обнаружить все ошибки бэкенда компилятора этого языка.
В 1978-ом году проект Edinburgh LCF завершен. Милнер занялся другими проектами [Plot2000]. Вадсворт отправился работать в Лабораторию Резерфорда - Эплтона (Rutherford Appleton Laboratory). Гордон до конца 70-х в Эдинбурге и использует LCF для верификации железа, а в 81-ом году отправляется туда, где наша история началась [Gord2000] - в Кембридж.
История имплементации на этом не заканчивается. У неё впереди десятилетия истории. Самый поздний её релиз - это NuPRL 5 2002-го года [NUPRL2002]. У имплементации будет несколько форков. Пока что это не компилятор, но будет компилятором. Пока что имплементирует не то, что мы определили как ФЯ Эдинбургской программы, но будет имплементировать.
Язык LCF/ML даже в минимально измененном виде доживет до 2012-го года [EventML], но помимо этих около-NuPRL реликтов, будут ML-и, произошедшие от LCF/ML более-менее напрямую, в результате инкрементальных изменений. Некоторые из которых активно развиваются и популярны (по меркам ФЯ) и сегодня. Но, надо заметить, что не все языки с ML в названии произошли в результате инкрементальных изменений.
Как видно, LCF/ML достаточно близко подошел к тому, чтоб стать первым ФЯ в том смысле, какой мы определили в предисловии. Общий предок всех ФЯ, историю которых мы пишем, сильно упростил бы ее и наименование для этих ФЯ. Но ML не стал таким языком. Понадобилось еще два языка Эдинбургской программы, чтоб разобраться с недочетами и спорными вопросами, которые Милнер обсуждает [Miln82] в 82-году.
Так что пришло время вернуться к экспериментальному программированию нашего старого знакомого Бурсталла.
Было решено, что <...> новый язык должен быть как можно ближе к стандартной математической нотации и быть читаемым без особых дополнительных объяснений.
Хайнц Рутисхаузер, Описание ALGOL 60, том 1 (1967) [Ruti67]
Я считаю, что было бы невозможно утверждать, что ALGOL 60 удовлетворяет <этому требованию>.
Петер Наур, Европейская сторона последней фазы разработки ALGOL 60 (1978) [Naur78]
ALGOL 60 не выглядит как язык, приближенный к математической нотации, но именно таким его планировали сделать по крайней мере некоторые из его авторов [Ruti67]. Уже в январе 59-го года Вуджер утверждал, что это не может быть сделано. Некоторые алголисты еще в 1978 году придерживались мнения, что математической нотации для порядка выполнения не существует вовсе [Naur78]. Разумеется, такая нотация или, точнее, нотация от которой произойдет такая нотация в ЯП, существует. Это форма записи множества и нотация для определения кусочно-заданных функций. Еще в 20-е годы в статьях [Acke28] [Neum23] можно было увидеть примеры нотации,
φ(a, 0) = 0,
φ(a, n + 1) = φ(a, n) + a
и
M(f(y); y ∈ Ξ, y < x)
напоминающей код на языках, уже имплементированных к тому 78-ому году, в котором алголисты писали, насколько это неосуществимо.
Происхождение от существующей математической нотации делает попытки проследить историю идей еще сложнее. Идея использовать нотацию Цермело-Френкеля (которую, похоже, изобрели не они) в ЯП гораздо более воспроизводима независимо, чем идея такой нотации.
Первоначально мы предполагали, что наличие каких-то сходных деталей синтаксиса может более-менее точно указать заимствование из одного языка в другой. Но форма записи множества стабилизировалась только в 60-е годы, незадолго до первого появления в ЯП, а до того существовала в виде бесчисленных вариаций, так что даже такой узнаваемый синтаксис как
f(x) for x in xs
мог бы появиться в двух языках независимо на основе нотации из книги Тарского 40-х годов или чего-то другого похожего.
Ожидаемо, что если не первым, то одним из первых языков, имеющих приближенный к математической нотации синтаксис стала система компьютерной алгебры SCRATCHPAD. Менее ожидаемо то, что среди систем компьютерной алгебры она была исключением. Правилом был синтаксис, приближенный к Алголу [Hulz83].
И даже это редкое исключение было мало кому доступно, ведь SCRATCHPAD разрабатывался в уже знакомом нам с вами скрытом царстве функционального программирования: лаборатории IBM в Йорктаун Хайтс. SCRATCHPAD, в отличие от McG, не исчезнет. Вторая его версия станет сначала не особо успешным продуктом, а потом опенсорсом под названием Axiom, который существует и сегодня. Все это начнет происходить только в 80-е, а в 70-е SCRATCHPAD не использовался за пределами IBM [Hulz83]. Но мы уже рассказывали про некоторое движение идей между лабораторией IBM и Эдинбургом, а авторы и имплементаторы опубликовали несколько статей, так что система оказала какое-то влияние на первые ФЯ.
SCRATCHPAD позволял писать код вроде такого [Jenk71]
(p<n> | n in (1,...,5))
((x:f) | x in float(rp), x > 0)
а позднее такого [Jenk74] [Jenk75] [Jenk79]
(p<n> for n in (1,...,5))
((x:f) for x in float(rp) | x > 0)
Откуда взялся этот for
? Авторы SCRATCHPAD пишут [Jenk74], что на дизайн этой нотации повлияли идеи [Earl73] более известного другой своей работой Джея Эрли (Jay Earley).
{F(A)} (FOR A ∈ S | P(A))
Конструкция не совсем такая, как в SCRATCHPAD. Там for
является разделителем, а у Эрли между конструкцией с FOR
, определяющей A
и операцией над этой переменной требуется другой разделитель. Но влияние узнаваемо.
Идею этой конструкции Эрли позаимствовал из раннего описания [Schw71] SETL, где это просто псевдокод, мало отличающийся от обычной математической нотации:
{f(x),x∈e|C(x)}
Эрли в своей статье не только ссылается на ранние описания SETL, но и утверждает, что SETL особенно сильно повлиял на его идеи. В отличии, видимо, от идей прочих авторов, кого он цитировал в этой статье. Например, Эдгара Кодда.
Часто можно увидеть, как SETL называют первым языком с такой нотацией. И авторы SCRATCHPAD даже ссылаются на этот язык как на один из оказавших влияние. Но ссылаются позже, в восьмидесятые [Jenk84] на описание 79-го года [Dewa79] в котором есть выражения такого вида:
{a : a in y | a>5}
{[x**2,x] : x in {1..5}}
и конструкции для описания циклов вот такого:
(for i in {1,2..10} | even i) ... f(i) ... end;
Операция не с той стороны от for
, с какой у Эрли, в SCRATCHPAD или в том языке, где такую конструкцию большинство впервые видит сегодня. Т.е. какие-то существенно отличающиеся от математической нотации детали не совпадают. Также в 70-е годы авторы SCRATCHPAD не используют название для этой конструкции из SETL - "set former". А использование в ЯП конструкции, похожей на форму записи множества - не похоже на идею, которая не может прийти независимо во множество голов.
Давайте посмотрим на то, как такие конструкции выглядели в SETL до того, как были имплементированы в SCRATCHPAD в начале 71-го [Jenk74]. В ноябре 70-го [Harr70] вот так:
SETC(MAPX(TL A,
PROC(X),
MAPX(TL B, PROC(Y), IF G(X,Y) THEN F(X,Y) ELSE UNDEF
END)
END))
и планировалось, что будет выглядеть так:
CONSET(IF G(X,Y) THEN F(X,Y), X IN A, Y IN B)
А как выглядели ближе к появлению более нового вида с for
?
Вот [Mull73] так:
≤X → A ↑ X GT. 2≥;
Ох. Да, это означает [x | x <- a, x > 2]
, направление стрелки просто необъяснимо.
Думаем, что есть серьезные основания подозревать, что упоминание SETL - это просто обзор всей проделанной работы, а не обзор реально повлиявших на дизайн языков. Первое выдается/принимается за второе довольно часто.
В SCRATCHPAD, у Эрли и в SETL есть много идей о том, как должны выглядеть символы и ключевые слова для скобок, принадлежности к множеству и разных разделителей, но этим идеи, похоже и ограничиваются. Идей о деталях и удобствах которые являются само собой разумеющимися в такой нотации сегодня еще нет. Например, паттерн-матчинг слева от in
не используется для отбора элементов множества или последовательности. Даже в SCRATCHPAD, в котором паттерн-матчинг есть и широко используется.
SCRATCHPAD позволял писать код вроде такого [Jenk71]
p<1> = 1
p<2> = 1 + x
p<i> = x*p<i-1> - d*p<i-2>, i in (3,4,...)
Тут <
>
у параметров - подсказка для рендерера двухмерного вывода, отображается как нижний индекс.
SCRATCHPAD позволял и намного больше - паттерном слева от =
может быть выражение языка, записанное в конкретном, не абстрактном синтаксисе [Jenk79].
df<t>(u+v) = df<t>u + df<t>v
df<t>log(t) = 1/t
Вычислитель находил подходящее такому паттерну выражение языка и заменял его на выражение справа от =
. Что могло бы быть отличной фичей для метаязыка вроде ML и существенно улучшить его качество как специализированного языка. Не смотря на обсуждаемые ранее заверения Милнера о том, что улучшить его уже невозможно. Для языка общего назначения - уже не такой хорошей фичей, с учетом её цены. Такие уравнения добавлялись в набор правил переписывателя по алгоритму Маркова. И, хотя имплементацию можно (но не легко) сделать довольно эффективной [Jenk76], языки общего назначения появляются обычно уже после того как придумают более эффективный способ имплементации чем такое переписывание. Как уже произошло с энергичной лямбдой после изобретения SECD и еще пары случаев, описание которых впереди.
Дело не только в проблеме для имплементатора, но и в удобстве. Обратите внимание на гард после третьего уравнение в определении p
- он необходим потому, что каждого уравнения должно быть достаточно для матчинга. Да, со временем правила перестали применять по отдельности и стали строить из них всех единое дерево решений, но это только оптимизация. Пользователь системы должен определять их как самодостаточные.
Правила перезаписи для упрощения алгебраических выражений в других системах компьютерной алгебры обычно не походили на определения в SCRATCHPAD и, соответственно, не походили на определения функций в ФЯ:
MPRED(X):=IF (SIGNUM(X)=-1)THEN TRUE ELSE FALSE
DECLARE(M,MPRED)
TELLSIMP(COS(M),COS(-M))
DECLARE(N,INTEGER)
TELLSIMP(COS(N*PI), (-1)**N)
Эти команды добавляют в упроститель системы компьютерной алгебры MACSYMA два правила. Гарды привязываются с помощью "динамических типов". И да, if ... then true else false
. Реальный код из статьи [Fate71].
И это еще с паттернами, которые похожи на деревья, которые они матчат. Более типичное описание правил было бы с условиями, предикатами и геттерами для внутреннего описания дерева на языке, на котором написана переписывающая по этим правилам система.
Поэтому трудно говорить о происхождении нотации объявления функций в ФЯ от правил перезаписи. Правила перезаписи бывают очень разного вида. И обычно серьезно отличающегося от ФЯ вида. Исключений не так и много, еще одну нотацию для описания правил перезаписи от которой синтаксис ФЯ мог бы произойти, как и от SCRATCHPAD, мы еще рассмотрим позднее.
В конце семидесятых началась разработка SCRATCHPAD 2, который должен был получить типы, модули, более традиционные (и эффективно имплементируемые) языковые конструкции и стать в достаточной степени языком общего назначения, чтоб имплементировать на нем его собственный компилятор [Jenk77]. Но в 70-е, даже для автора ФЯ, с соответствующими не самыми высокими требованиями к производительности и практичности решений, естественно заключить, что для языка общего назначения нужен хоть и не такой амбициозный, но зато более эффективно имплементируемый паттерн-матчинг. Например, как в статье Бурсталла [Burs69], синтаксис из которой - cases:
также можно было использовать в SCRATCHPAD [Jenk74].
d u = cases: u
x+y: d x + d y
Эта статья Бурсталла написана на основе доклада в Йорктаун Хайтс. Так что идеи двигались не только из IBM Research в Эдинбург, но и в обратном направлении, из Группы Экспериментального программирования в Йорктаун Хайтс. И, следовательно, в SCRATCHPAD. Если не непосредственно, то через работы Клиффорда Джонса [Jone78] [Jone78b], в неисполняемом языке спецификации которого такая конструкция тоже была.
Но в ФЯ Эдинбургской программы эта конструкция для паттерн-матчинга найдет применение только после другой, из языков гораздо менее требовательных к производительности, чем системы компьютерной алгебры и даже вовсе не предназначенных для выполнения.
Итак, ФЯ не выглядят в точности как математическая нотация и как системы компьютерной алгебры. Они выглядят более или менее так:
df t (Plus u v) = df t u + df t v
df t (Log u) | t == u = 1/t
Так как что они выглядят?
Эпоха не предназначенных для исполнения языков началась в 70-е. В 60-х даже язык спецификации вроде ISWIM должен был исполняться. Потому, что а что еще с ним делать? В 70-е же, наконец, нашли что. Доказывать теоремы.
Почему про функциональные языки часто уверенно говорят, что свойства кода на них легко доказывать? Ведь свойства кода на этих (как и прочих) языках не особенно часто доказывают, не говоря уже о том, чтоб легко. Дело в том, что они происходят и от языков, которые буквально только для этого и разрабатывали.
И если язык не должен выполняться вовсе, то просто нет никаких пределов для тех степеней в которых он может быть неэффективным и высокоуровневым. Даже языки "исполняемой спецификации" вроде ISWIM или "педагогические языки" вроде PAL не могли себе позволить быть настолько неэффективными, как как языки, для которых эффективность исполнения вовсе не имеет смысла. Это сковывает воображение их авторов.
Все на что оказалась способна Обоекембриджская программа - это изобрести существенно урезанную Java 8, что может и впечатляюще для 60-х, но не поражает воображение уже в 70-е. Недостаточно амбициозно для следующих 700 непопулярных языков. Для того, чтоб изобрести то, что не попадет в мейнстрим так просто как обоекембриджские идеи эдинбуржцам нужно было, хотя бы на время, сбросить оковы исполняемости.
Разумеется, со временем находятся причины запускать код даже на совсем не предназначенном для исполнения языке. И впоследствии оказалось, что нужно не так уж много отступить назад от такого высокоуровневого языка, чтоб получить язык вполне исполняемый и даже быстрый. Но попасть в эту точку минуя область полной неисполняемости и неэффективности видимо было психологически сложно.
Одним из первых таких языков был Pure Lisp Бойера-Мура. Так называемый "pure lisp" уже был описан в мануале LISP 1.5 [McCa62], но это не один и тот же язык. И дело не в том, что Pure Lisp Бойера и Мура - это EDSL для POP-2 и имеет синтаксис его подмножества для определения списков. Это новый язык и даже новая разновидность языков. Pure Lisp МакКарти - довольно типичное явление 60-х, как чистое подмножество ISWIM или PAL. Как и чистое подмножество PAL оно полностью эфемерно не смотря на то, что LISP 1.5 и PAL имплементированы. Эти части языков имеют названия только для того, чтоб использовать их для построения фраз, вроде фразы Тернера "Pure Lisp никогда не существовал". (Не)соответствие подмножеству не может быть механически проверено даже в ограниченном смысле LCF/ML, в котором можно защитить от изменения ссылки, не объявив их как мутабельные. Pure Lisp Бойера и Мура - совсем другое дело. Чистота защищена надежно: никаких нарушающих её фич в языке и нет.
Даже Чистый Лисп Бойера-Мура и другой Чистый Лисп Бойера-Мура - это не один и тот же язык. Изобретение этого Лиспа происходило в два этапа. На первом, резолюционном, Бойер и Мур начали с доказателя-логического языка BAROQUE, который назван в честь разновидности шахмат Эббота "Барокко". Также они изобрели практичный способ имплементации логических языков. Правда, практичный в основном только на компьютере, который в Эдинбурге еще даже и не появился, и ни на каком другом, но это уже другая история.
Авторы языков того времени и Бойер с Муром в особенности не любят показывать в статьях, диссертациях и книгах как код выглядит на самом деле. Видимо потому, что считают его слишком страшным для печати. Может бумага все стерпит, но читатель стерпит не все. Когда увидит реальный код - будет сюрприз! Нам известно, как выглядел язык Бойера-Мура во второй фазе, так что можем предположить, что код на BAROQUE выглядел как-то так:
КАКАЯТОФУНКЦИЯPOP2("LEN1", [[+ [V [LENGTH [NIL]] 0]]]);
КАКАЯТОФУНКЦИЯPOP2("LEN2", [[+ [V [LENGTH [CONS X Y]] Z]]
[- [V [LENGTH Y] U]]
[- [V [ADD U 1] Z]]]);
но в диссертации Мура [Moor73] это выглядело так:
LEN1: ((+ (V (LENGTH (NIL)) 0)))
LEN2: ((+ (V (LENGTH (CONS X Y)) Z))
(- (V (LENGTH Y) U))
(- (V (ADD U 1 ) Z))).
и даже так:
LEN1: (LENGTH NIL) -> 0;
LEN2: (LENGTH (CONS X Y)) -> Z
WHERE
(LENGTH Y) -> U;
(ADD U 1) -> Z;
END;
Ага, вот они уравнения, вот откуда все пошло! Нет. Это нотация для правил перезаписи. Один из тех псевдокодов, который лисперы использовали в статьях для того, чтоб не отпугнуть читателя Лиспом. Использовалась в известной в Эдинбурге статье [McBr69] о расширении Лиспа.
RULE D
D1: (N X)->0 when (NUMBERP N)
D2: (X X)->1
D3: ((+ U V)X)->(+(D U X)(D V X))
D4: ((* U V)X)->(+(* U(D V X))(* V(D U X)))
D5: ((— U)X)->(-(D U X))
Реальный код на Лиспе, разумеется, выглядел иначе:
DEFRULES((
(+(DARG(A B)
(AP1((A B)(LIST("+)A B))
(TP1((+ A B)(("+)B A)))))
(*(DARG(A B)
(AS1((A B)(LIST("*)A B))
(TS1((* A B)(("*)B A))
TS2(A(("*)1 A))
TS3(A(("*)A 1))))) ...
Наиболее очевидное отличие этой нотации от уравнений с паттерн-матчингом в функциональных языках - каждое правило перезаписи имеет собственное имя. Не только вся группа таких правил - функция. Эти правила с индивидуальными именами можно увидеть и в ФЯ, но не как основной способ описания функций [GHC23]:
{-# RULES
"map/map" forall f g xs. map f (map g xs) = map (f . g) xs
"map/append" forall f xs ys. map f (xs ++ ys) = map f xs ++ map f ys
#-}
Бойер и Мур посчитали уравнения BAROQUE низкоуровневыми и "ассемблероподобными". Это только фундамент для построения по-настоящему высокоуровневого языка - Лиспа.
LENGTH: (LENGTH X) -> U
WHERE
(COND X
(ADD1 (LENGTH (CDR X)))
0) -> U;
END;
Не беспокойтесь, у последователей Бойера и Мура в Эдинбурге конечно же будет противоположное представление о том, что более высокоуровнево.
Наконец то, ради чего все и затевалось. Поскольку это логический язык, можно вызвать функцию LENGTH "наоборот"
(LENGTH X) -> 2;
и доказать, что список из двух элементов действительно существует. Но это по большому счету и все, что удалось доказать.
Бойеру и Муру этого было мало, они хотели доказывать более интересные утверждения. Например, что длина конкатенации двух списков равна сумме длин этих списков.
Чтоб доказывать более интересные теоремы Бойер и Мур написали следующий доказатель, поддерживающий структурную индукцию. В этот раз Чистый Лисп был встроен сразу в POP-2, а не сначала в логически язык. Пользователи доказателя, они же его авторы, могли определять рекурсивные функции такого вот вида [Moor18b]:
DEFINE
([MAPLIST
[LAMBDA [X Y] [COND X [CONS [APPLY Y [CAR X]] [MAPLIST [CDR X] Y]] NIL]]]);
и к осени 73-го написали пару сотен строк такого кода. Также написали сотню теорем такого вот вида:
COMMENT 'THEOREMS INVOLVING MAPLIST';
[T 3 1]::
[EQUAL [MAPLIST [APPEND A B] C] [APPEND [MAPLIST A C] [MAPLIST B C]]];
[T 3 2]::
[EQUAL [LENGTH [MAPLIST A B]] [LENGTH A]];
[T 3 3]::
[EQUAL [REVERSE [MAPLIST A B]] [MAPLIST [REVERSE A] B]];
которые доказатель доказывал полностью автоматически (но не мог проверить завершимость, это должен был обеспечить пользователь) [Boye75]. Серьезный шаг вперед, по сравнению с доказательством существования списка из двух элементов!
Время доказательства средней такой теоремы было 8-10 секунд. Самые сложные, включающие SORT
доказывались 40 - 150 сек. [Moor73] на ICL 4130 машине.
Третий язык в доказателе Бойера и Мура был пока что только псевдокодом в комментариях. Правила переписывания для упростителя были обычным POP-2-кодом. Такого EDSL, как для двух других языков, для правил перезаписи не было [Moor18]:
FUNCTION REWRITE TERM;
VARS TERM1 TERM2 TERM3;
COMMENT 'IF TERM IS AN EQUALITY`;
IF HD(TERM)="EQUAL" THEN
HD(TL(TERM))->TERM1;
HD(TL(TL(TERM)))->TERM2;
COMMENT '(EQUAL KNOWN1 KNOWN2) => T OR NIL`;
IDENT(TERM1,TERM2) -> TERM3;
IF TERM3 = NIL THEN NIL; EXIT;
IF TERM3 THEN "T";EXIT;
COMMENT '(EQUAL BOOL T) => BOOL`;
IF TERM1==1 AND BOOLEAN(TERM2)THEN TERM2 EXIT;
IF TERM2==1 AND BOOLEAN(TERM1) THEN TERM1 EXIT;
COMMENT '(EQUAL (EQUAL A B) C) =>
(COND (EQUAL A B) (EQUAL C T) (COND C NIL T))`;
IF SHD(TERM1) = "EQUAL" OR SHD(TERM2) = "EQUAL" AND (SWAP;1)
THEN
[% "COND", TERM1,
REWRITE([% "EQUAL", TERM2, "T" %]),
REWRITE([% "COND", TERM2, NIL, "T" %]) %] -> TERM;
GOTO COND;
CLOSE;
Понятно, что код, свойства которого проверял доказатель - это не тот код, который в то время писали даже и на первых ФЯ. Доказатель не мог доказывать даже свойства функции с хвостовой рекурсией вроде такой:
(REVERSE1 (LAMBDA (X Y)(COND X
(REVERSE1 (CDR X)
(CONS (CAR X) Y))
Y)))
Раз уж свойства такой функции не доказывались, то и реального кода нет, так что мы воспользуемся случаем и сделаем её примером того, как Бойер и Мур оформляли код на своем Чистом Лиспе в своих статьях.
Да, свойства реального кода не проверить, но не лучше ли программисту и писать такой высокоуровневый код, предлагает Мур [Moor73].
Что же делать потом с этим высокоуровневым кодом? К счастью, в Эдинбурге как раз существует еще один проект. Разрабатывается система для трансформации такого наивно-рекурсивного кода в циклы. Системы дополняют друг друга. Трансформации делают Чистый Лисп имплементируемым, а доказатель может доказывать равенства нужные для трансформаций.
Важно отметить, что про эту синергию пишут [Moor73] [Boye75] авторы доказателя Бойера-Мура, успешного проекта с большим будущим. А не только авторы проекта по трансформации кода, о котором никто ничего сейчас не знает, пытающиеся уцепиться за успешную вещь, набирающую ход. Правда, вероятно, что доказатель оказался успешным как раз потому, что такая сцепка вскоре оказалась ненужной.
Не смотря на все заявления о том, как трансформационный проект и доказатель дополняют друг друга, не смотря на общего научного руководителя - Бурсталла, система для трансформации кода работала не с Чистым Лиспом Бойера-Мура, а другим языком, который появился раньше, чем второй Лисп Бойера-Мура, но не факт, что раньше, чем первый. Оба языка небольшие и транслировать один в другой было бы не особенно сложно даже в обсуждаемые времена, но это не было сделано и совместное использование - не более чем возможность, которая не была реализована.
У Милнера тоже был ЯП этой новой разновидности, не предназначенный для исполнения. Язык назывался L, корректность компилятора которого Милнер доказывал [Miln76]. Но, не смотря на расстояние 1 между их названиями, ML не произошел от L. По крайней мере переходные звенья между ними не сохранились. ML сформировался в относительно современном виде за пару лет.
Другое дело - S-0. Второй основной протоязык Эдинбургской программы имеет более долгую, инкрементальную и богатую названиями и описаниями разных фаз историю. И на протяжении большей части истории, которая разворачивалась в 70-е, язык не был даже функциональным. Поскольку нефункциональность может означать много чего, скажем точнее: был языком первого порядка. Язык в 70-е годы сменил три названия, но даже трех названий мало, для того чтоб назвать все существенно отличающиеся версии, так что мы будем по необходимости приписывать год к названию, хотя такая система именований его авторами не использовалась.
Как и у Милнера, у Бурсталла несколько соавторов, и первый из них - Джон Дарлингтон (John Darlington). Дарлингтон написал первую версию трансформационной программы работая над своей диссертацией.
В момент начала работы Дарлингтона Бойер и Мур еще ничего интересного не доказывали, и не известно было будут ли, идея программы появилась иначе.
Все началось с того, что Пэт Эмблер (A.P.Ambler) [Darl76], Поплстоун [Burs71] и Бурсталл [Burs71] [Darl76] написали набор процедур на POP-2 для манипуляции множествами. Библиотека из 43 (сорока трех) строк кода (в 1968) [Burs71] является редким примером функционального программирования тех лет. Одна из функций в библиотеке даже определена частичным применением функции к частично примененной функции:
VARS SUMSET;
LIT(% NIL,UNION(%NONOP=%) %)->SUMSET;
что примерно соответствует такому коду:
sumset = foldr (unionBy (==)) []
Пользователи библиотеки столкнулись с проблемой, которую авторы функциональных библиотек такого типа пытаются решить и сегодня. Хотя эти функции можно использовать для написания других функций, сразу бросается в глаза, что можно было бы написать гораздо более эффективную программу, если манипулировать массивами или списками непосредственно.
Ну что же, значит вместо библиотеки нужен язык с абстрактными множествами и операциями над ними. И трансформационная система должна инлайнить тела этих операций и осуществлять слияние циклов и прочие необходимые оптимизации.
Манипуляции с абстрактными множествами важны для того, в каком направлении стал развиваться второй Эдинбургский язык, но не для Бойера с Муром. Более интересным для них было преобразование рекурсии в итерацию, что система также должна была делать.
Преобразованием рекурсии в циклы уже занимались в лаборатории IBM в Йорктаун Хайтс [Stro70], но без особых практических последствий. Дарлингтону с Бурсталлом не было известно ни о каких примерах использования таких преобразований в компиляторах, за исключением преобразования самых простых случаев рекурсии в BBN LISP.
Авторам идеи не хватило смелости делать трансформации автоматическими. Система трансформации программ Дарлингтона - это не оптимизирующий компилятор, а скорее что-то похожее на систему компьютерной алгебры: пользователь работает в REPL и переписывает программу в полуавтоматическом режиме, решая какие преобразования и где применить.
S-0 - это язык на котором пользователь пишет первоначальную наивную рекурсивную программу, работающую с абстрактными множествами [Darl72]. Этот код в полуавтоматическом режиме транслировался через ряд промежуточных языков. Сначала S-0 транслировался в S-1 - язык с циклами и переменными. Множества пока что оставались абстрактными, но на следующем этапе пользователь должен был выбрать конкретное представление, произведя трансформацию в B-0 или L-0.
В L-0 операции над множествами имплементированы как операции над иммутабельными списками. В B-0 - как операции над массивами. Код на B-0 - конечный результат, а код на L-0 можно было преобразовать еще раз в код на L-1, в котором операции над списками производятся деструктивно, на месте. Это переиспользование cons-ячеек Бурсталл и Дарлингтон называют "сборкой мусора времени компиляции" [Darl76].
Система трансформирует в эффективный код важнейшие ФП функции fact
и fib
, с чем современные компиляторы ФЯ не справляются. Почему так?
Для трансформации в эффективный код системе нужно использовать свойства операций, такие как коммутативность и ассоциативность. Трансформации, которые преобразуют код в эквивалентный даже без учета таких свойств Дарлингтон и Бурсталл посчитали слишком слабыми для использования на практике.
И это, обычно, такие свойства, которые может доказать доказатель Бойера-Мура [Boye75] [Moor18b] :
DEFINE
([APPEND [LAMBDA [X Y] [COND X [CONS [CAR X] [APPEND [CDR X] Y]] Y]]]);
[T 1 1]::
[EQUAL [APPEND A [APPEND B C]] [APPEND [APPEND A B] C]];
Именно это имеет в виду Мур [Moor73] когда пишет что не только трансформатор дополняет доказатель, но и доказатель дополняет трансформатор.
Вооруженный знанием о ассоциативности сложения целых чисел, трансформатор конвертирует функцию вычисления факториала в цикл. Наивный вариант ускоряется в 10 раз.
Наивная функция разворачивания списка при трансформации становится функцией, которая конкатенирует списки из одного элемента с результирующим, а не наоборот, так что ускорение еще больше, чем просто от преобразования в цикл - 30 раз.
Наконец наивная функция вычисления чисел Фибоначчи ускоряется в 100 раз на примере из статьи. В двух последних случаях улучшается асимптотика алгоритма, так что результаты могли бы быть лучше на машине, память которой вмещает более впечатляющие списки. Но к моменту появления таких машин оптимизировать эти функции перестали.
В современных компиляторах ФЯ преобразования, которым нужны для корректности свойства вроде ассоциативности сложения не делаются, но они делаются компиляторами C++.
В переписывателе Дарлингтона и Бурсталла трансляции этих однострочников производятся по командам пользователя системы, которые выполняются за десятки (в редких случаях - единицы) секунд. На той же машине, на которой разрабатывался доказатель Бойера и Мура - ICL 4130.
Дарлингтон защитил диссертацию [Darl72] в 72-ом, но первая версия системы готова только в январе 73-го [Darl76]. С 73-го года ведется разработка второй версии системы.
Если ML начали имплементировать уже на новой машине, то наработки Бурсталла и Дарлингтона, возможно, нужно было переносить на новую машину. Но нужно ли - зависит от того, что означало то, что "разработка ведется". Но если какой-то код уже писали с 73-го, а не только собирались писать, переезд на новую машину прошел довольно безболезненно.
Это вполне возможный вариант развития событий потому, что POP-10 для PDP-10 (удачного различения в дальнейшем тексте D
и O
) был написан еще в 1969 году Малькольмом Аткинсоном (Malcolm Atkinson) и Реем Данном (Ray Dunn) в Университете Ланкастера [Popp2002]. Это воспоминание Поплстоуна, правда не находит подтверждения в воспоминаниях Сломана [Slom89], который не упоминает Университет Ланкастера и утверждает, что разработал POP-10 Джулиан Дэвис (Julian Davies). POP-10 оставил после себя слишком мало следов, чтоб мы могли найти третий источник, который подтвердил бы правоту того или другого. Но мы, конечно, поверим нашему старому знакомому Поплстоуну (который скорее всего больше не появится в нашей истории), а не новому знакомому Сломану, появления которого еще впереди.
Заблуждение Сломана объясняется тем, Девис из Университета Западного Онтарио, Канада, по всей видимости занимался поддержкой POP-10 в это время [Davi76]. Эта имплементация POP-2 (с расширениями) разделяла код с коммерциализированной имплементацией, которой владела компания Мики Conversational Software Ltd., которая ничем не поможет пользователю POP-10 в случае чего, но может помешать.
Имплементация становилась все востребованнее потому, что PDP-10 получили и другие университеты, и теперь пытались использовать POP-2, ставший более-менее стандартным языком в Великобритании. Среди прочих, важный для нашей истории Имперский колледж Лондона (Imperial College London). Видимо, такой сомнительный статус имплементации, привел к тому что правительственная структура SRC выделила деньги на создание новой имплементации. В 76-м в Эдинбурге Роберт Рэй (Robert Rae) [Popp2002] [Slom89] и Аллан Рэмси (Allan Ramsay) [Slom89] написали еще одну имплементацию POP-2 для PDP-10 - WonderPOP (обычно WPOP), на которую перешел и Бурсталл.
Первая версия системы начинала с преобразования рекурсии в циклы [Darl76]. Бурсталл и Дарлингтон решили, что это было ошибкой. Нужно делать как можно больше преобразований с рекурсивной формой и только заканчивать трансформацией в циклы. Вторая версия [Darl75], работа над которой велась с 73-го года, использует этот подход. Работа с рекурсивным кодом проще и использует наработки из доказателя Бойера-Мура.
К июлю 1975 основа новой системы имплементирована [Darl76]. Новая система воспроизвела почти все преобразования из первой за исключением переписывания cons-ячеек. Это направление было заброшено и, по большому счету остается заброшенным и в наши времена.
Но вторая система [Darl75], в отличие от первой, не представляет собой некую полуавтоматическую имплементацию, преобразующую код на чистом рекурсивном языке в императивный код. Который можно запускать с помощью какой-нибудь имплементации POP-2 или похожего языка. В этот раз трансформируется только чистый рекурсивный код в чистый рекурсивный. Имплементировано только то, что авторам в тот момент интереснее всего. Авторы утверждают, что могут использовать свои наработки из первой системы для трансформации рекурсивного кода в циклы, но не используют.
Авторы используют наработки Мура [Moor75], который решал обратную задачу перехода от итеративного кода к рекурсивному, для работы с хвостовой рекурсией и аккумуляторами. Это сделало более интересную работу с рекурсивным кодом возможной. Это также сделало поддержку итерации в доказателе Бойера-Мура возможной и, соответственно, всю эту синергию первого трансформатора и их доказателя ненужной Бойеру с Муром.
Что еще нового из трансформаций? Как мы помним, Эдинбургская программа оказалась не особенно дружественна к продолжениям и выработала ряд ответов на вопрос "как же быть без продолжений?". Один из классических примеров, демонстрирующих полезность продолжений - это пример Хьюита [Hewi74]. Задача: проверить, что два бинарных дерева имеют одинаковую последовательность листьев. Естественное решение: одна функция сплющивает дерево в список, другая сравнивает два списка. Понятно, что в этом случае делается лишняя работа: даже если первые же элементы не совпали, оба дерева должны быть сплющены полностью. Продолжения позволяют сохранить модульность, обеспечив раннее завершение. Но это позволяет и вторая версия трансформатора программ [Darl75]. Трансформация соединяет эти две функции в одну, которая лишней работы не делает.
Как мы знаем, трансформационная система не имеет будущего. Пользователи последующих имплементаций ФЯ не будут выбирать где и как производить оптимизации вручную в интерактивном режиме. Что имеет будущее, так это язык. Как он выглядит? Сначала как ISWIM, т.е. по разному в каждой статье о нем. И даже по разному в одной и той же статье.
В диссертации Дарлингтона [Darl72] в псевдокоде слева от ветвей выбора везде подрисованы от руки "акколады", фигурные непарные скобки как в одном из вариантов нотации для задания кусочных функций. В более приближенном к реальному коде из примера REPL сессий в статье [Darl73] этим скобкам ничего не соответствует. Язык выглядит как ISWIM в статьях Ландина:
union(x,y)=
nullset(x)->y,
not nullset(x)->cosset(choose(x),
union(minus(choose(x),x),y))
В самой поздней публикации [Darl76] о первой версии системы выглядит как ISWIM в статьях Бурсталла:
reverse(x) = if null(x) then nil
else concat(reverse(tl(x)),
cons(hd(x),nil))
В статье 73-го года [Darl73] в некоторых примерах кода вместо =
между именем функции и телом появляется обратная стрелка <=
. Эта стрелка - одна из самых узнаваемых деталей языка. В следующие десять лет в статьях про вторую трансформационную систему [Darl75] и сам язык стрелка используется последовательно во всех примерах:
concat(x,y) <= if x=nil then y else
cons(car(x),concat(cdr(x),y)) fi
Но что такого особенного в этих языках? S-0 и Pure Lisp Бойера и Мура - просто урезанный ISWIM. Да, но на вид будущих ФЯ Эдинбургской программы повлияет не ISWIM-подобная часть ввода доказателя Бойера-Мура.
В статье [Darl75] представленной на конференции в апреле 75-го года Дарлингтон и Бурсталл пишут, что система работает пока что с традиционным ISWIM-подобным синтаксисом, но у них уже запланировано расширение, которое в основном и используется в статье. Другими словами, примеры в статье - это в основном псевдокод, который только станет реальным. И в этом псевдокоде есть ошибки. Но заявлено, что система работает, и все примеры трансформаций кроме одного имплементированы, просто для языка со старым синтаксисом.
Поскольку трансформатор программ требует спецификации операций и работает с абстрактными данными, только вопрос времени, когда эта исследовательская программа соединится с другой, занимающаяся спецификацией операций над абстрактными данными. И мы будем считать, что это произошло в 74-ом году, когда Бурсталл познакомился с вторым основным своим соавтором 70-х - Джозефом Гогеном (Joseph Goguen) [Burs2006].
И структуры данных, над которыми не производятся операции и программы, которые не производят операции над структурами данных не интересны.
Джозеф Гоген, Некоторые принципы дизайна и теория OBJ-0, языка для выражения и исполнения алгебраических спецификаций программ.
Через год после написания статьи о доказательстве свойств программ [Burs69], в которой вводится нотация для паттерн-матчинга в ФЯ, Бурсталл написал новую статью [Burs70] о доказательстве свойств программ, в которой вводит другую нотацию:
Sorts: values, states, expressions.
...
numeral ⊆ expressions
plus, minus, times, equal: expressions x expressions -> expressions
val: expressions × states -> values
...
Axioms:
...
numeral(m) => val(m,s) = numeralval(m)
val(plus(e,e'),s) = val(e,s) + val(e',s)
val(minus(e,e'),s) = val(e,s) - val(e',s)
val(times(e,e'),s) = val(e,s) × val(e',s)
val(e,s) = val(e',s) => val(equal(e,e'),s) = true
val(e,s) ≠ val(e',s) => val(equal(e,e'),s) = false
То есть, изобретает вторую из двух нотаций для паттерн-матчинга в ФЯ? Это уже похоже на объявления функций с помощью уравнений с паттерн-матчингом, если не считать необычный вид "гард". Да, это они слева перед =>
. Но не торопитесь. Пока что никто не собирается ничего матчить и применять какие-то функции. Это описание спецификации, похожие декларации свойств были и в предыдущей статье [Burs69], но не такие сложные. Так что никаких интересных деталей вроде гард и групп уравнений, определяющих одну функцию там не найти:
(i) cons(car(x), cdr(x)) = x
(ii) car(cons(x, y)) = x
(iii) cdr(cons(x, y)) = y
Код, свойства которого эти уравнения описывают, там на псевдоисполняемом псевдокоде ISWIM. Из определения cons(car(x), cdr(x)) = x
функцию так просто не сделать.
Бурсталл описывает более развитую нотацию из следующей статьи [Burs70] как "обычную", с некоторыми расширениями для краткости. Но если посмотреть на нотацию в работах, на которые ссылается Бурсталл, то видно, что обычной её можно назвать только при очень широком толковании слова "обычный". Бурсталл пишет, что в наибольшей степени эта его работа основана на работе Кордела Грина (Cordell Green) [Gree69]. Давайте посмотрим, как нотация выглядела там:
M5. (∀i,j,s,p,b)[test(p,s) = b ⊃
f(select(p,b,i,j),s) = f(i,s)]
M6. (∀i,j,s,p,b)[test(p,s) ≠ b ⊃
f(select(p,b,i,j),s) = f(j,s)]
Но это псевдокод для того, чтоб не пугать читателя Лиспом, реально язык описания свойств выглядит так:
MB5 (FA(I J S P B) (IF(EQ(TEST P S) B)
(EQ(F(SELECT P B I J) S) (F J S))))
MB6 (FA(I J S P B) (IF(NEQ(TEST P S) B)
(EQ(F(SELECT P B I J) S) (F J S))))
Эта нотация выглядит как обычная нотация для правил перезаписи, так что мы, видимо, нашли точку отделения предка синтаксиса для объявления функций в ФЯ от правил перезаписи.
Этот язык уравнений Бурсталла - язык первого порядка, чтоб доказательства было легче механизировать. И Бурсталл попробовал его механизировать с помощью доказателя теорем, основанного на методе резолюций, написанного Изобел Смит (Isobel Smith) и с помощью инструмента проверки доказательств Андерсона (D. B. Anderson). Опыт у него был тот же, что и у Милнера. Доказатель нельзя было применить ни к чему больше пары присваиваний, а инструмент проверки позволял проверить программу из пары десятков операторов, но ценой большого объема однообразного ручного труда. Как выглядел при этом код, точно соответствующий псевдокоду из статьи, мы не знаем, но судя по всему был "квадратным" Лиспом из списков POP-2, как позднее у Бойера с Муром. Язык описания спецификации Бурсталла опередил свое время, но через несколько лет ситуация изменилась. Бойер с Муром написали доказатель который работает быстро, а ручного труда не требует и с середины 70-х языки описания спецификаций вообще и абстрактных типов данных в частности стали серьезной исследовательской программой.
Разработка абстрактных типов данных, которой занимались наш знакомый по разработке композитных типов для PAL и CLU Циллес и важные герои этой главы Гоген и Гуттаг, продолжалась уже пару-тройку лет с начала 70-х.
Самые ранние описания алгебраических спецификаций появляются в 74-ом году у Циллеса [Zill74]
CREATE: -> set
INSERT: set x integer -> set
REMOVE: set x integer -> set
HAS: set x integer -> boolean
1. INSERT(INSERT(s, i), j) ≡ if i = j then INSERT(s, i)
else INSERT(INSERT(s, j), i)
2. REMOVE(INSERT(s, i), j) ≡ if i = j then REMOVE(s, j)
else INSERT(REMOVE(s, j), i)
3. REMOVE(CREATE, j) ≡ CREATE
4. HAS(INSERT(s, i), j) ≡ if i = j then true
else HAS(s, j)
5. HAS(CREATE, j) ≡ false
Это "схемы" свойств. Для получения теоремы, выдвигается какая-то гипотеза о переменных в схеме:
(∀s, i, j)[i ≠ j => HAS(INSERT(s, i), j) = HAS(s, j)]
Что практически в точности соответствует нотации из [Gree69]. Нотация описывает свойства CLU-кластера, скрывающего работу с изменяемым массивом
intset = cluster is create, insert, remove, has, ...;
rep = array of int;
create = oper() returns cvt;
...
Это та половина работ Лисков и Циллеса, которую LCF/ML не получил (как и CLU).
Влияния Бурсталла пока не видно, но влияние или, может быть, переизобретение того же самого будет видно у других авторов этого направления. Во второй половине 70-х описатели свойств абстрактных типов данных алгебраического направления придут к общей структуре описания, отличающейся по большему счету деталями синтаксиса [Gutt78]:
type Stack[elementtype:Type]
syntax
NEWSTACK -> Stack,
PUSH(Stack, elementtype) -> Stack,
POP(Stack) -> Stack U {UNDEFINED},
TOP(Stack) -> elementtype U {UNDEFINED},
ISNEW(Stack) -> Boolean.
semantics
declare stk: Stack, elm: elementtype;
POP(NEWSTACK) = UNDEFINED,
POP(PUSH(stk, elm)) = stk,
TOP(NEWSTACK) = UNDEFINED,
TOP(PUSH(stk, elm)) = elm,
ISNEW(NEWSTACK) = TRUE,
ISNEW(PUSH(stk, elm)) = FALSE.
Описание типа, следующее за ним описание сигнатур функций этого типа, затем спецификация - набор алгебраических аксиом в виде уравнений.
Эта интерфейсная часть предполагала возможность разработки кода без наличия или знания имплементации, которая состояла из скрытого представления типа данных в памяти и имплементаций функций типа.
representation STAK(Array[Integer, elementtype], Integer)
-> Stack[elementtype],
programs
declare arr: Array, t: Integer, elm: elementtype;
NEWSTACK = STAK(NEWARRAY, 0),
PUSH(STAK(arr, t), elm)
= STAK(ASSIGN(arr, t + 1, elm), t + 1),
POP(STAK(arr, t)) = IF t = 0 THEN STAK(arr, 0)
ELSE STAK(arr, t - 1),
TOP(STAK(arr, t)) = ACCESS(arr, t),
ISNEW(STAK(arr, t)) = (t = 0),
REPLACE(STAK(arr, t), elm)
= IF t = 0 THEN STAK(ASSIGN(arr, 1, elm), 1)
ELSE STAK(ASSIGN(arr, t, elm), t).
Описатели АТД часто представляют сигнатуру как (абстрактный) синтаксис встроенного языка [Gogu79], спецификацию - как денотационную семантику этого языка, а имплементацию - как его операционную семантику [Gogu79].
Параметризация типов и в псевдокоде появилась не сразу, а c имплементацией дела обстояли еще хуже. Описатели АТД хотели накладывать ограничения на параметры для того, чтоб статически проверять какие операции над ними доступны. И просто не могли придумать как это сделать. В CLU, для которого АТД хотели имплементировать как можно скорее, просто временно перенесли эти проверки во время выполнения, в ML не сделали пока что ограниченный полиморфизм ни в каком виде, а многие имплементаторы языков для описания АТД не сделали параметризацию вообще.
Как мы помним, Лисков выкинула часть с аксиомами примерно в то время, когда Бойер и Мур научились их использовать для проверки имплементаций. Но, справедливости ради нужно заметить, что и десятилетия спустя такая проверка так и не стала мейнстримом.
В отличие от Лисков, большинство работающих над АТД больше интересовались интерфейсной частью, которую они считали в основном языконезависимой. Эти исследователи имплементировали ряд систем, состоящий из доказателей и языков исполняемой спецификации в сочетании с доказателями и без них. В качестве языков имплементации АТД, если до языков имплементации АТД вообще доходило дело, они выбирали какой-нибудь существующий язык не особенно похожий на то что описывало спецификацию. Например, PASCAL [Muss80a].
Так что, по мере развития темы, представления АТД у разных авторов стали меняться чтоб лучше отражать их интересы и цели.
Что получилось с АТД после выкидывания аксиом мы уже выяснили в главе про ML, а теперь посмотрим что получилось у тех, кого больше интересовала спецификация, а не имплементация. Но что интересного это направление может дать для нас, интересующихся имплементацией функциональных языков?
Одну из важнейших систем описания абстрактных типов данных разрабатывали Гуттаг (John V. Guttag), Мюссер (David R. Musser) и другие в Институте Информационных наук Университета Южной Калифорнии в Лос-Анджелесе (USC Information Sciences Institute).
Описания свойств функций над АТД должны были проверяться доказателем, но для того, чтобы писать код независимо от имплементации одних статических проверок мало, утверждает Гуттаг [Gutt78]. Программист очевидно захочет исполнять и тестировать код. Для этого понадобится написать хотя-бы наивную имплементацию абстрактного типа данных. Но понадобится ли?
Представьте себе, такая имплементация - Гуттаг и др. называют её непосредственной - у нас уже есть. Это секция с уравнениями, которая описывает семантику.
Посмотрим на определение АТД для стека на языке Гуттага и Мюссера [Muss80b]:
type Element;
interface errElement : Element;
end {Element};
type Stack;
declare s : Stack;
declare e : Element;
interface
newstack,
e push s,
pop(s),
errStack
: Stack;
interface
top(s)
: Element;
interface
isnew(s)
: Boolean;
axiom
pop(newstack) = errStack,
pop(e push s) = s,
top(newstack) = errElement,
top(e push s) = e,
isnew(newstack) = true,
isnew(e push s) = false;
end {Stack};
Некоторые функции, такие как newstack
и push
не имеют соответствующих уравнений в секции аксиом. Результаты их применения только используются в уравнениях для таких функций как pop
, top
и isnew
.
Первая категория функций - это конструкторы абстрактного синтаксического дерева языка, а вторая - программа, интерпретатор этого языка, работающий с AST. И в данном случае соответственно []
, :
, tail
, head
и null
.
Непосредственные имплементации могут быть полезны не только для тестирования. В некоторых случаях - рискует предположить Гуттаг - они могут служить даже в качестве конечной имплементации. Сумасшедшая идея! Не факт, что эта идея впервые пришла в голову именно Гуттагу, но у него явно было больше желания её объяснять.
Понятно, что не всякую спецификацию, записанную как уравнения, можно вот так просто брать и выполнять. Интересно, что среди спецификаций, которые проверял доказатель Бойера-Мура [Boye75] практически не было исполняющихся. И исключения довольно скучные, вроде:
(EQUAL (DOUBLE A) (MULT 2 A))
Единственный нетривиальный пример который в одном шаге от исполняемости это:
(GT (LENGTH (CONS A B)) (LENGTH B))
Если б только Бойер с Муром были вынуждены описывать спецификацию как уравнения, а не неравенства, могло бы получится что-то исполняемое вроде:
(EQUAL (LENGTH (CONS A B)) (ADD 1 (LENGTH B)))
(EQUAL (LENGTH NIL) 0)
Но пример со стеком не был подобран Гуттагом и Мюссером специально из-за исполняемости спецификации, этот пример типичен для литературы об АТД еще с тех времен, когда спецификации не собирались исполнять [Zill75]. Но если не говорят, что собираются - не обязательно означает, что не думают. Так Циллес переходит от не настолько очевидно исполняемого примера в статье 74-го года [Zill74] к легко исполняемому примеру в статье 75-го [Zill75]:
Functionality:
CREATE: -> STACK
PUSH : STACK X INTEGER -> STACK
TOP : STACK -> INTEGER U INTEGERERROR
POP : STACK -> STACK U STACKERROR
Axioms:
1' TOP(PUSH(S,I)) = I
2' TOP(CREATE) = INTEGERERROR
3' POP(PUSH(S,I)) = S
4' POP(CREATE) = STACKERROR
Может быть просто совпадением.
Итак, придется подбирать выполняющиеся уравнения. Но и тут Гуттаг видит плюсы. Программист, по его мнению, часто не готов писать спецификации. Так пусть он пишет наивные имплементации на высокоуровневом языке. С этим он скорее справится.
Какие уравнения выполняющиеся - зависит от имплементации их исполнителя. Существует широкий спектр возможностей: от сложных систем имплементирующих правила переписывания до различных имплементаций паттерн-матчинга, как в Прологе или как в современных ФЯ.
Гуттаг и др. начали с планов о более амбициозном исполнителе. В ноябре 76-го они уже обсуждают идею о том, что некоторые спецификации можно исполнять символической интерпретацией [Gutt76]. Но многих человеколет для амбициозной имплементации не понадобится! Как они считают. Основные усилия нужные для этого уже сделаны и существуют в системах компьютерной алгебры, например в SCRATCHPAD. Да, SCRATCHPAD нельзя использовать из-за его закрытости, но такая машинерия для переписывания выражений существует во всех системах компьютерной алгебры: в MACSYMA, в REDUCE, не только в тех, где языки с уравнениями, похожими на языки спецификации АТД. Один из соавторов Гуттага - Дэвид Мюссер уже работал над использованием системы компьютерной алгебры REDUCE для доказательства свойств программ [Muss74]. Так что Гуттаг с Мюссером планировали использовать REDUCE для имплементации исполнителя аксиом.
Но в мае 78-го, когда система под названием DTVS наконец имплементирована, никаких признаков использования REDUCE для исполнения спецификаций не видно. Остается только некое влияние идей из SCRATCHPAD. Со временем, языки для описания АТД сами повлияют на вторую версию SCRATCHPAD может быть даже и больше, чем SCRATCHPAD повлиял на них.
Имплементация исполнителя спецификаций достаточно простая, это даже не компилятор паттерн-матчинга в современном смысле, который пытается построить более-менее оптимальное дерево условий. Подсистема тем не менее называется "компилятор паттерн-матчинга" - PMC. Аксиомы транслируются в функции-конструкторы на Interlisp, конструирующие значения с еще более неэффективным представлением в памяти, чем у композитных типов МакКарти. Это лисповые списки в которых первый элемент - тег конструктора, а остальные элементы - его параметры. Также генерируются селекторы, обходящие эти списки и извлекающие из них тэги и прочие элементы. Теги проверяются гораздо чаще, чем нужно.
Решением проблемы этих лишних проверок может быть другая часть системы CEVAL (Conditional EVALuator). Это доказатель для проверки свойств, родственный доказателю Бойера-Мура. В доказателе есть переписыватель, который знает свойства условных выражений, а значит может использоваться как оптимизатор получившегося промежуточного кода. Переписыватель может использовать для оптимизаций и аксиомы из спецификации. В последующих ЯП, вроде GHC Haskell, конечно, исполняются одни уравнения, а используются оптимизатором - другие.
Гуттаг отмечает, что неэффективные представление и диспетчеризация - не проблема непосредственной имплементации вообще, а только конкретной имплементации. Гуттаг и др. думают о компиляции паттерн-матчинга в свитчи. А непосредственные имплементации могут иметь более эффективное представление в памяти, чем у композитных типов, планировавшихся для CPL и тем более чем в МакКартиевской системе с двухместными произведениями и суммами.
Давайте сравним эти представления для вот такого вот дерева:
node(leaf(1),2,leaf(3))
значения, которые не конструируются этими двумя конструкторами мы покажем подписями, а не блоками на диаграмме чтоб убрать лишние детали, скрывающие представление конструкторов дерева. Но на практике это чаще всего тоже указатели на объекты в памяти. В первой системе МакКарти, как в LCF/ML, где только пары и атомы, представление будет таким:
┌───┐ ┌───┬───┐ ┌───┬───┐ ┌───┬───┐ ┌───┬───┐
│ ├──►│*T*│ ├──►│ │ ├──►│ 2 │ ├──►│NIL│ 3 │
└───┘ └───┴───┘ └─┬─┴───┘ └───┴───┘ └───┴───┘
│
▼
┌───┬───┐
│NIL│ 1 │
└───┴───┘
В другой системе МакКарти, придуманной им позже и для другого языка, рассказ о котором впереди, а также в пропозале композитных типов для CPL [Stra67] с отдельными типами-суммами и типами-произведениями представление такое:
┌───┐ ┌───┬───┐ ┌───┬───┬───┬───┐ ┌───┬───┐
│ ├──►│001│ ├──►│003│ │ 2 │ ├──►│002│ 3 │
└───┘ └───┴───┘ └───┴─┬─┴───┴───┘ └───┴───┘
│
│ ┌───┬───┐
└►│002│ 1 │
└───┴───┘
Непосредственная имплементация может иметь такое представление:
┌───┐ ┌───┬───┬───┬───┐ ┌───┬───┐
│ ├──►│001│ │ 2 │ ├──►│002│ 3 │
└───┘ └───┴─┬─┴───┴───┘ └───┴───┘
│
│ ┌───┬───┐
└►│002│ 1 │
└───┴───┘
Упоминая его, Гуттаг ссылается на Хоара, который продвигал такое представление в памяти, к этому мы еще вернемся.
Но в реальности система Гуттага использовала представление в памяти, которое хуже, чем все эти варианты:
┌───┐ ┌───┬───┐ ┌───┬───┐ ┌───┬───┐ ┌───┬───┐
│ ├──►│001│ ├──►│ │ ├──►│ 2 │ ├──►│ │NIL│
└───┘ └───┴───┘ └─┬─┴───┘ └───┴───┘ └─┬─┴───┘
┌───────────┘ ┌───────────┘
▼ ▼
┌───┬───┐ ┌───┬───┐ ┌───┬───┐ ┌───┬───┐
│002│ ├──►│ 1 │NIL│ │002│ ├──►│ 3 │NIL│
└───┴───┘ └───┴───┘ └───┴───┘ └───┴───┘
Если используется какой-то уже готовый бэкенд, особенно лисповый, то система управления памятью может просто не поддерживать конструирование каких-то объектов кучи больше пар. К этому мы тоже еще вернемся.
В псевдокоде в статьях Гуттага использованы "схемы типов" т.е. параметризованные типы, но это не было имплементировано в 70-х и отсутствует в реальном коде из статьи Мюссера [Muss80b].
В 1979-ом Мюссер работает над системой, которая теперь называется AFFIRM [Muss80a], уже без Гуттага. Использует систему для написания спецификации небольшой программы в 1KLOC. Осенью 79-го Мюссер ушел из института. В восьмидесятых Мюссер будет соавтором Степанова и будет работать над продвижением обобщенного программирования в мейнстрим.
Авторы AFFIRM рассматривали систему исполнения спецификаций как вспомогательную по отношению к доказателю и не особенно старались имплементировать исполнитель эффективно и исполнять как можно больше возможных спецификаций. Но были и исследователи, для которых исполнение спецификации стало главной целью. Язык исполняемых спецификаций должен исполнять больше спецификаций и исполнять их быстрее. Они хотели сделать исполнение настолько эффективным, насколько это возможно, при условии, что не нужно будет слишком жертвовать способностью исполнять спецификации.
По сложному пути пошел Гоген - автор языка OBJ и соавтор Бурсталла. Гоген работал над алгебраическим описанием семантики с 1972-го года [Gogu85], но дизайн языка, который предполагалось имплементировать начался только в 76-ом году [Gogu2000].
Гоген работал в том же городе, что и Гуттаг с Мюссером - Лос-Анджелесе, но в Калифорнийском университете (University of California, Los Angeles).
OBJ - язык для написания и исполнения "абстрактных формальных спецификаций программ". Гоген отмечает, что его также можно рассматривать как довольно неэффективный, но весьма высокоуровневый язык программирования. Да, для программирования, а не для временного тестирования, пока не написана имплементация на Паскале. Гоген и Мюссер ссылаются на работы друг друга. Мюссер критикует OBJ за отсутствие доказателя [Muss80a], который не то чтобы обычная часть имплементации языка программирования, если не считать таковым любой тайпчекер, который в OBJ есть. Гоген критикует AFFIRM за то, что это неудобный язык программирования, которым AFFIRM и не должен был быть [Gogu82]. Справедливости ради, первая версия OBJ как язык и имплементация ЯП, на наш поверхностный взгляд, не выглядит лучше AFFIRM.
В OBJ АТД называется объектом и для его декларации используется ключевые слова OBJECT
или OBJ
, отсюда и название языка [Gogu79].
OBJECT STACK-OF-INT
SORTS STACK / INT BOOL
OK-OPS
PUSH : INT STACK -> STACK
POP_ : STACK -> STACK
TOP_ : STACK -> INT
BOTTOM : -> STACK
EMPTY? : STACK -> BOOL
ERROR-OPS
UNDERFLOW : -> STACK
TOPL : -> INT
VARS
I : INT;
S : STACK
OK-SPECS
(POP PUSH(I,S) = S)
(TOP PUSH(I,S) = I)
(EMPTY? BOTTOM = T)
(EMPTY? PUSH(I,S) = F)
ERROR-SPECS
(TOP BOTTOM = TOPL)
(TOP BOTTOM = UNDERFLOW)
TCEJBO
RUN PUSH(TOP POP PUSH(2,PUSH(1,BOTTOM)),POP POP PUSH(3,BOTTOM)) NUR
AS STACK: >>ERROR>> PUSH(1,UNDERFLOW)
Обратите внимание на объявления ошибок для каждого типа и разделение функций на работающие с ошибками и прочими - специфика языков спецификаций. Это идея Гогена о том, что исключения не должны просто населять любой тип как у Милнера в ML, например.
То, что "скобки" закрываются ключевым словом, написанным наоборот может показаться несуразным и ненормальным, но это довольно распространенная синтаксическая идея в 70-е, придуманная не Гогеном.
У уравнений бывают гарды ( = IF ) для имплементации которых в языке есть специальный хак. В OBJ-0 нет полиморфизма но для каждого "сорта" (типы в OBJ называются сортами) S
автоматически определяется IF_THEN_ELSE_ : BOOL,S,S -> S
и _==_ : S,S -> BOOL
. В синтаксисе объявления условного оператора ничего специального нет, это доступная пользователю фича называемая "distributed fix".
Экспериментальная первая версия OBJ с зачаточными возможностями для объявления и исполнения спецификаций под названием OBJ-0 была имплементирована Джозефом Тардо (Joseph Tardo) и Гогеном в 1977-79 годах [Gogu2000] для IBM 360/91 на LISP 1.5. Также они начали имплементировать OBJ для DEC-10 на Rutgers/UCL LISP [Gogu79].
OBJ должен исполнять больше уравнений, чем AFFIRM. В отличии от языка Гуттага и Мюссера OBJ-0 может исполнять программы, которые выглядят зацикливающимися. Например
A * B = B * A
Эвалуатор может запоминать состояния в которых он был и останавливаться чтоб не перейти в состояние в котором уже был. Но, конечно, и в нем есть ограничения по сравнению с чистой спецификацией из-за исполняемости: например, нельзя использовать справа от =
переменные, которых нет слева от =
.
Гоген более амбициозен чем Гуттаг и др. Для него производительность OBJ важна, нужно чтоб работало так быстро как только возможно в рамках желаемой Гогеном семантики, не слишком хорошо совмещающейся с быстротой. Но в 70-е эти амбиции не были реализованы.
Гоген ссылается на еще два языка, хоть и не для описания АТД, но для исполнения уравнений, и авторы которых старались добиться того, чтоб исполнять больше уравнений. Первый из них и, возможно, даже первый исполнитель групп уравнений с паттерн матчингом вообще назывался
Доказатель Бойера-Мура работал с тремя языками: Лисп-ISWIM-образными выражениями для описания кода, уравнениями для описания спецификаций и недоступными в первой версии для добавления пользователем правилами перезаписи. Но можно обойтись для всего этого только одним языком с уравнениями и паттерн-матчингом. Как в доказателе Леви (Giorgio Levi) и Сировича (Franco Sirovich) TEL [Levi75].
Примерно в то же время эта идея о том, что все эти обоекембриджские конструкции и не нужны, если есть уравнения с ПМ - самая радикальная декембриджизация из всех - появится и у автора ФЯ, но это другая история.
TEL - это не язык описания спецификаций АТД. Так что то, что у Гуттага и др. называется непосредственной имплементацией в TEL, как и у Бойера с Муром называется символьной интерпретацией. В TEL 'функции', которые интерпретируются символьно, обозначены кавычками:
plus('zero',y)=y
plus('s'(x),y)='s'(plus(x,y))
Это больше похоже на более современный подход, когда конструкторы отличаются от имен функций синтаксически, но отличается от подхода в языках для описания АТД и языке Бурсталла и Дарлингтона. Они в это время хотели, чтобы разницы было как можно меньше. Эта синтаксическая особенность сохранилась и в современном ФЯ с, пожалуй, наиболее прямым происхождением от языка Бурсталла и Дарлингтона, но не в большинстве других современных ФЯ.
Леви и Сирович работали в Пизе (Istituto di Elaborazione dell’Informazione), но эта их работа - часть расширенной Эдинбургской программы. Все их ссылки в статье на Бойера, Мура, Бурсталла и прочих эдинбуржцев. Из тех, кто во второй половине 70-х занимались символьным интерпретатором [Mart2008] TEL, наиболее известный исследователь, вероятно, Уго Монтанари (Ugo Montanari), разработавший вместе с Альберто Мартелли (Alberto Martelli) быстрый алгоритм унификации [Mart82] (издательство получило статью еще в сентябре 79-го). В своей статье Мартелли и Монтанари отмечают, что алгоритм унификации полезен не только для имплементации резолюционистских доказателей, но и для имплементации языков программирования. С уравнениями и паттерн-матчингом (ссылка на TEL, который действительно использует алгоритм унификации для паттерн-матчинга и язык Бурсталла и Дарлингтона [Darl77], который не использует) и без (ссылка на тайпчекер LCF/ML). Также они ссылаются на работы Уоррена, рассказ о которых впереди. Но такого разнообразия связей с Эдинбургом и ссылок на TEL нет в их более ранней работе по унификации [Mart76]. Такой связи с какими-то работами кроме работ над резолюционистскими доказателями нет и в другой работе того времени над эффективной унификацией [Pate76] не смотря даже на то, что один из соавторов работал в Йорктаун Хайтс.
Особого влияния работа этой итальянской четверки на Эдинбург не оказала, но вскоре из Пизы в Эдинбург приехал один из будущих главных героев нашей истории.
Еще одной работой над "непосредственными" имплементациями АТД, на которую ссылается Гоген, была работа Митчелла Уанда (Mitchell Wand), Индианский университет в Блумингтоне (Indiana University Bloomington) [Wand77] [Wand80] и ссылающегося на его работы Майкла О'Доннела (Michael J. O'Donnell), сначала Университет Пердью (Purdue University) в соседнем городке, но работа продолжалась и в других университетах.
Митчелл Уанд (ссылавшийся, кстати, на ту самую статью Бурсталла с уравнениями [Burs70]) вскоре переключится на работу над гораздо более известным языком программирования, рассказ о котором впереди. О'Доннел же упорно продолжал работать над языком с достаточно амбициозной системой переписывания по правилам.
Бойер и Мур не давали названий своим доказателям между Baroque и ACL2, называя их просто "доказатель" в разговорах друг с другом. В результате нам постоянно приходится упоминать их фамилии чтоб было понятно о каком же доказателе идет речь. Та же история и с языком О'Доннела, который нам придется называть языком О'Доннела. Сам О'Доннел называет его "языком программирования уравнений" (equational programming language) и так можно называть все языки, о которых рассказывает эта глава.
О'Доннел начал проект имплементации языка уравнений "без семантических компромиссов" в 1975 [O'Do87]. С самого начала О'Доннел столкнулся со скепсисом со стороны коллег. Они сомневались в возможной эффективности имплементации такого языка. Так что О'Доннел уделял эффективности особое внимание. Но только в той степени, какую позволял "бескомпромиссный" подход.
О'Доннел настаивает, что разделение на функции и конструкторы и следующее из этого упрощение паттерн-матчинга не приведет к существенному улучшению производительности [O'Do84]. Выдержала ли эта теория проверку?
В 1978-81 годах О'Доннел, Кристоф Хоффман (Christoph M. Hoffmann) и два студента (Giovanni Sacco, Paul Golick) работали над прототипами имплементации [O'Do84]. В 1979-80 написанный Джованни Сакко на Паскале интерпретатор (2700 строк транслятора в то, что будет интерпретироваться и 1200 строк рантайма) работает у О'Доннела на CDC 6600 и в университете Киля у Хоффмана. Исполняет он описания чистого Лиспа (не Бойера-Мура) и языка Lucid [O'Do82].
Разворот списка этот интерпретатор выполняет в шесть раз медленнее компилятора Лиспа для CDC 6600. И это компилятор Лиспа не из лучших. Из его руководства пользователя [Gree75] можно узнать, что он производит код, работающий в 2-7 раз быстрее интерпретатора, да и используется не часто.
На этом этапе О'Доннел не знает как компилировать язык в принципе, хотя недавний опыт с одним Эдинбургским компилятором (рассказ о котором впереди) и выглядит заманчиво. И это итоги 70-х для данной исследовательской программы. Но, справедливости ради, и прочие имплементации языков с уравнениями и паттерн-матчингом обычно не отличаются хорошей производительностью в эти годы.
Со временем, в 80-е, О'Доннел и др. смогли компилировать язык более-менее эффективно, компилируя паттерны в автоматы и используя для редукции стек, с чего, как мы помним, и начинается практическая имплементация.
Насколько эффективно? Сравнимо с Franz Lisp на некоторых примерах, но и сам О'Доннел признает, что это имплементация не особенно эффективная. Но О'Доннел считает, что скорость определенно можно считать приемлемой, и он таки утер нос скептикам! Разница производительности с имплементациями того же времени с упрощенным паттерн-матчингом, правда, все-таки была значительной.
Исполнитель уравнений О'Доннела мог иметь и имел несколько фронтендов с разными синтаксисами. Один - похожий на математическую нотацию:
add(0,x) = x
add(s(y),x) = s(add(y,x))
Почти как TEL, но никаких кавычек.
Другой вариант синтаксиса "лиспоподобный", но Лиспом О'Доннел называет M-LISP, псевдокод из 60-х, т.е. foo[x;y]
вместо foo(x,y)
Symbols
cons: 2;
nil: 0;
add: 2;
length: 1;
include integer_numerals, truth_values.
For all a, x:
length[()] = 0;
length[(x.a)] = add[length[a]; 1];
include addint.
Что именно означает "бескомпромиссность" семантики, которую продвигает О'Доннел? Её можно разделить на две части. Первая часть касается в основном использования функций слева от =
.
О'Доннел противопоставляет свой подход имплементаторам, которые позволяют использовать слева от =
только конструкторы [O'Do84], но позднее признает [O'Do87], что это ограничение позволяет писать практически все программы, которые программисты на практике хотят писать и писать такие программы легче. С другой стороны, "бескомпромиссная" семантика вводит ограничения, которые неудобны для программистов. Например, на практике перекрывающиеся паттерны удобно использовать, а писать код который решает проблему иначе - сложно. Также, многие считают удобным нарушение того, что О'Доннел называет левой линейностью. Левая линейность нарушена, когда в паттерне один байндинг встречается больше одного раза:
equal(x,x) = true
но это более редкое пожелание и не такое важное, как перекрывающиеся паттерны, справедливо считает О'Доннел.
О'Доннел приходит к таким неутешительным выводам, конечно, не первый, а один из последних. В конце 80-х. Но его рассуждения позволяют представить как рассуждало большинство имплементаторов языков с уравнениями в конце 70-х и начале 80-х. Зачем страдать со сложным матчингом, когда простой позволяет получить более удобный для использования язык?
Но если отказ разделять функции и конструкторы - тупиковая идея, не имеющая будущего в ФП, другой аспект бескомпромиссности О'Доннела разделяло больше имплементаторов ФЯ, хотя и меньшинство. Влияние на сложность имплементации этого второго набора идей может еще и большее, но и плюсы считались более важными. Но про это мы расскажем в следующей главе про третий Эдинбургский протоязык.
Переход от использования нескольких промежуточных и целевых языков к трансформации кода из одного языка в код на этом же языке сделал название ненужным и года до 77 второй Эдинбургский язык как правило не называется никак.
Не позднее 77-го года язык Бурсталла и Дарлингтона начинают часто называть NPL, что означает "новый язык программирования". Да, название не очень удачное. Только вопрос времени, когда язык перестанет быть новым. Но не беспокойтесь, через пару-тройку лет он получил другое имя.
Как мы помним, в статье [Darl75] вводится новый синтаксис для языка, который еще не называется NPL в этой статье, но который мы будем называть NPL 75. Но есть оговорка - новый синтаксис пока не имплементирован. В более поздней версии статьи про вторую систему трансформации [Darl77] уже нет оговорок о том, что синтаксис только запланирована. Так что можно предположить, что к январю 1976 новый синтаксис был имплементирован.
И вы, конечно же, уже догадались, что этот новый синтаксис - уравнения с паттерн-матчингом. В 1975-ом году вербозный вариант ISWIM без первоклассных функций
f(x) <= if x=O or x=1 then 1 else
f(x-1)+f(x-2) fi
concat(x,y) <= if x=nil then y else
cons(car(x),concat(cdr(x),y)) fi
превратился в язык, выглядящий как и прочие языки этой главы. Или же прочие языки этой главы выглядят как NPL [Darl75]:
f(O) <= 1
f(1) <= 1
f(x+2) <= f(x+1)+f(x)
concat(nil,z) <= z
concat(cons(x,y),z) <= cons(x,concat(y,z))
Можно использовать x::X
вместо cons(x,X)
и []
вместо nil
и объявлять операторы:
nil.Y <= Y
(x::X).Y <= x::(x.Y)
у уравнений могут быть гарды:
member(x1, x::X) <= true if x1 = x
member(x1, X) otherwise
member(x1, []) <= false
c if
как в OBJ, но не как в языке О'Доннела. В AFFIRM гарды вовсе отсутствуют.
NPL - язык первого порядка, что обычно и для языков описания спецификаций, так что когда конструктор АлгТД был переизобретен, он не стал первоклассным, как в тот раз, когда Бурсталл изобрел конструктор АлгТД впервые [Burs69].
Зачем такой синтаксис в NPL? Авторы программы-трансформатора кода утверждают, что новый синтаксис удобнее для трансформации [Darl75]. Что Дарлингтон и Бурсталл внезапно открыли проработав над трансформацией ISWIM-подобного языка годы, но сразу после того, как Эдинбургская программа соприкоснулась с программой описателей АТД.
Да, еще в 69-ом году Бурсталл называет [Burs70] свою спецификацию языка интерпретатором, "написанным на исчислении предикатов как языке программирования". И в 72-ом году использовал для описания вычисляемых функций вместо обычного ISWIM-псевдокода псевдокод с уравнениями и ПМ:
rev: X* -> X*
rev(1)=1
rev(xa)=rev(a)x for x ∈ X, a ∈ X*
subst(s,a,a)=a
subst(s,a,b)=b if b ≠ a and b is an atom
subst(s,a,cons(t1,t2))=cons(subst(s,a,t1),subst(s,a,t2)).
А Рейнольдс в том же году писал [Reyn72], что алгебраические спецификации Бурсталла принципиально не отличаются от его подхода с определяющими интерпретаторами. Просто у Бурсталла интерпретатор не на ISWIM написан.
Но до того, как Бурсталл познакомился с Гогеном в 74-ом году никаких признаков работы над имплементацией этого неISWIMа нет.
Последняя статья про будущий NPL без упоминания уравнений с ПМ [Darl76] получена издательством осенью 74. Тогда же, осенью 74 Гоген делал доклад о своих идеях, которые привели к разработке OBJ [Gogu79], но о каких конкретно идеях мы не знаем. Сам OBJ Гоген разработал только в 76-ом [Gogu88]. Но это год, когда Гоген придумал первый способ работы с ошибками для OBJ, а не год, когда Гоген решил имплементировать язык уравнений.
Все герои этой главы постоянно ссылаются друг на друга. Помимо упомянутых уже ссылок, Гуттаг упоминает NPL как наиболее похожий на OBJ язык и критикует язык Гуттага как менее удобный по сравнению с NPL [Gogu79]. Для Гогена NPL вместе с OBJ и AFFIRM сначала один из трех важнейших языков описания спецификаций [Gogu81]. Но и позднее NPL под названием NPL и под следующим своим названием всегда упоминается как родственный OBJ исследовательский проект вместе с языком О'Доннела [Gogu85]. NPL под разными названиями также может упоминаться в обзорах языков описания спецификаций [Berz83] в 80-е, но не более поздних [Wirs95].
Интересно, что упоминается героями этой главы только NPL-линейка, но не другие ФЯ с уравнениями и ПМ, которые вскоре становятся довольно нормальным явлением. О'Доннел в 84 [O'Do84] может ссылаться на работу Бурсталла об очередной версии NPL, которую он критикует за недостаточно выразительный паттерн-матчинг, но не на прочие языки с уравнениями, которые Бурсталл упоминает в этой статье. Еще позже, в 90-е О'Доннел откроет для себя эти языки и будет описывать как "языки с уравнениями" все ФЯ вообще. Но в конце 70-х и начале 80-х NPL-линейка явно особенная для описателей АТД и переписывателей по правилам, по сравнению с ФЯ, которые позаимствовали из NPL-линейки уравнения. Может быть так же, как в начале истории ФЯ, в ФЯ записывали все что только можно, так и в начале истории языков исполняемой спецификации записывали все, что имеет хоть какое-то отношение?
Никто из героев этой главы не пишет кто и у кого что позаимствовал, но и не пишет что сам изобрел. Самое близкое к этому - утверждение Дарлингтона о том, что уравнения с ПМ - это инновация NPL [Darl81].
Мы не можем установить насколько независимо друг от друга герои этой главы изобрели исполнение уравнений с ПМ и как эта идея распространялась по их социальной сети. Можем только в очередной раз порадоваться, что не пишем историю идей и констатировать, что году в 75-ом появилось "сообщество" в котором принято делать языки с уравнениями и паттерн-матчингом, как до того в другом, но частично пересекающемся "сообществе" было принято делать ISWIM.
По меркам языков описания спецификаций, паттерн-матчинг в NPL не позволяет очень уж сложных вещей. Слева от <=
только переменные и конструктор-функции [Darl75]. Но он пока что сложнее такого в современных ФЯ. Последовательность уравнений не имеет значения для ПМ.
Уравнения - не единственное, что связывает NPL с описателями АТД. Как мы помним, первый переписыватель программ Дарлингтона переписывал код, работающий с встроенными абстрактными множествами в код, оперирующий встроенными конкретными имплементациями. Для второго переписывателя была поставлена более амбициозная цель: обобщенный механизм переписывания кода, работающего с абстрактными типами в код, работающий с конкретными.
Для этого они использовали идею Хоара - функцию представления. Технически, для трансформации нужно две функции из абстрактных конструкторов в конкретные и обратно, но первоначально Дарлингтон и Бурсталл надеялись на то, что одна из этих функций будет автоматически генерироваться из другой.
Так что трансформация абстрактного типа
niltree ∈ labelled-trees
ltree: atoms × labelled-trees × labelled-trees
-> labelled-trees
в его непосредственную имплементацию для гипотетического Лисп-бэкенда
nil ∈ binary-trees
atoms ∈ binary-trees
pair: binary-trees × binary-trees
-> binary-trees
поддерживается стандартными средствами переписывателя. Что не поддерживается переписывателем, так это такие декларации функций-конструкторов, также напоминающие о языках спецификации. В NPL 75 это псевдокод.
Определяем функцию представления
R: binary-trees -> labelled-trees
R(nil) <= niltree
R(pair(a,pair(p1,p2))) <= ltree(a,R(p1),R(p2))
и система трансформирует такой вот (псевдо)код
twist: labelled-trees -> labelled-trees
twist(niltree) <= niltree
twist(ltree(a,t1,t2)) <= ltree(a,twist(t2),twist(t1))
в такой
twistp: binary-trees -> binary-trees
twistp(nil) <= nil
twistp(pair(a,pair(p1,p2))) <= pair(a,pair(twistp(p2),
twistp(p1)))
более-менее автоматически. И для полуавтоматической трансформации доступны более впечатляющие конкретные имплементации, например неявное дерево в изменяемом массиве.
Сигнатуры вида f: S -> T
также напоминающие сигнатуры в языках описания АТД - это тоже псевдокод, как в статьях Бурсталла и Берджа 60-х годов об ISWIM.
Но, как и бывшие сначала псевдокодом уравнения с МП, эти декларации и аннотации являются псевдокодом временно.
Определение того, что в NPL поддерживается, а что нет осложняется тем, что описывается обычно инструмент для этого языка - полуавтоматический переписыватель Дарлингтона, а не его имплементация - интерпретатор Бурсталла, написанный им, скорее всего, где-то между 75 и летом 77 [Darl81]. Что означает, что он имплементирован раньше, чем OBJ-0, AFFIRM и язык О'Доннела, но примерно одновременно или позже, чем TEL. Язык, поддерживаемый переписывателем и интерпретатором также, технически, не один и тот же язык. Бурсталл написал тайпчекер для интерпретатора раньше, чем синтаксис для аннотаций и объявлений типов стал поддерживаться трансформатором. И раньше, чем он стал использовать информацию о типах для переписывания кода. Также переписыватель может работать с неисполняемым кодом на NPL [Darl81]. Например таким, в котором слева от <=
есть применения функций. Также, не то чтобы абстрактность конструкторов имела какой-то смысл без переписывателя, для интерпретатора есть только конструкторы с конкретной, непосредственной имплементацией.
NPL продолжает иметь более одного синтаксиса в одной статье и не всегда понятно, что из этого псевдокод специально для того, чтоб примеры выглядели красиво, а что разница в поддержке языка. Так ISWIM-образный синтаксис для конструкции и деконструкции туплов или является псевдокодом или не поддерживается трансформатором, потому что в примерах REPL-сессий вот такой код
exp where <a, b> = <i, j>
выглядит вот так
exp wherep maketupl([a, b]) == maketupl([i, j])
а вместо условного оператора в стиле Бурсталл-ISWIM
if p then a else b
может быть "функция"
cond(p,a,b)
Еще одна разница между интерпретатором и трансформатором: правила перезаписи для трансформатора записываются в NPL синтаксисе уравнениями с ПМ, но им соответствуют вычисления, которые происходят не так, как вычисляется NPL-код интерпретатором.
В библиотеке для работы с множествами, с которой начался NPL были ФВП, которые в NPL объявить было нельзя (пока он еще назывался NPL), так что для работы с множествами в NPL есть специальный синтаксис
<: a + b : a in A & p(a), b in B :>
exist x in X & p(x) : f(x)
forall x in X & p(x) : g(x)
который не имеет каких-то синтаксических деталей, характерных для SCRATCHPAD или SETL, так что вполне может быть происходящим от математической нотации непосредственно.
Интерпретатор может вычислять такие выражения, но Дарлингтон отмечает, что они часто вычисляются неэффективно или вовсе не вычисляются и предназначены только для переписывателя.
Несмотря на наличие в NPL паттерн-матчинга, использовать его слева от in
нельзя [Feat79], как и в SCRATCHPAD.
В 77-ом году Дарлингтон отправился работать в Имперский колледж Лондона (Imperial College London), но это не было концом переписывателя кода в Эдинбурге. Развитием переписывателя занялся другой человек.
Тем временем Бурсталл работал над тем, чтоб сделать из NPL что-то большее, чем язык для экспериментальной системы переписывания кода. И один из вариантов этого большего объясняет особое отношение описателей АТД к NPL.
Гоген был соавтором Бурсталла и их совместным творением стал язык описания спецификаций или, как его называют Бурсталл и Гоген, язык для структурного описания теорий Clear [Burs77].
Бурсталл и Гоген считают, что антирезолюционисты продвигают процедурное представления знания вместо логического в числе прочего и потому, что наработан опыт структурирования программ, но нет такого опыта для структурирования теорий. И Бурсталл с Гогеном исправляют эту недоработку. Да, они ассоциируют языки с уравнениями с логическим программированием. И некоторые герои этой главы будут последовательны в этом. Гоген еще будет продвигать OBJ как более логическое программирование, чем Пролог, а О'Доннел напишет в 90-е обзор [Gabb98] логических языков, в котором функциональные языки будут разновидностью логических.
Clear - язык описания спецификаций, но не такой, как OBJ или AFFIRM, а такой, какими эти языки хотели сделать их авторы [Muss80b], но не смогли. По крайней мере в 70-е. Спецификации, которые описывает этот язык (Бурсталл и Гоген называют их теориями) могут быть параметризованы. И параметризованы более интересным образом чем Лисковские АТД в CLU и LCF/ML.
В Clear два языка. Один, в котором аналоги объектов OBJ и типов AFFIRM - это значения, теории-константы, которые могут быть объявлены локально let T = ... in ...
, переданы в теории-процедуры, возвращены из них. Параметризованный стек будет теорией-процедурой:
p͟r͟o͟c͟ Stack (Value: Triv) =
i͟n͟d͟u͟c͟e͟ e͟n͟r͟i͟c͟h͟ Value + Bool b͟y͟
s͟o͟r͟t͟s͟ stack
o͟p͟n͟s͟ nilstack: -> stack
push : value,stack -> stack
empty : stack -> bool
pop : stack -> stack
top : stack -> value
e͟r͟r͟o͟r͟o͟p͟n͟s͟ underflow: -> stack
undef : -> value
e͟q͟n͟s͟ empty(nilstack) = true
empty(push(v,s)) = false
pop(push(v,s)) = s
top(push(v,s)) = v
e͟r͟r͟o͟r͟e͟q͟n͟s͟ pop(empty) = underflow
top(empty) = undef
pop(underflow) = underflow
e͟n͟d͟e͟n͟
И второй язык, на котором описаны уравнения - это в основном NPL в который добавлены декларации функций и конструктор-функций. По крайней мере NPL в его более приятном виде для публикаций с where
и <a,b>
туплами, if
гардами. Но с =
вместо <=
.
Эти языки не похожи друг на друга. Например функции в языке уравнений могут быть рекурсивными, а в языке теорий - нет.
Бурсталл и Гоген планировали начать со спецификации программ. Затем, естественно, перейти к исполняемой спецификации, что по их мнению не должно было стать проблемой, с учетом опыта имплементации NPL и OBJ. Так что те псевдокодовые декларации из статей про трансформатор кода должны были стать реальным кодом. Когда-нибудь. Но не в 70-е. Пока что это все еще псевдокод. В 1977 году Clear и не пытались еще имплементировать.
Псевдокодовость Clear 77 проявляется особенно очевидно в том, что в нем отсутствует конструкция, представляющая собой третий способ имплементации свободного от аннотаций типов кода. Мы уже сталкивались с двумя способами имплементировать псевдокодовые фантазии 60-х: отсутствие типов и вывод типов. Третье решение - внешние аннотации. Внешние аннотации - это не только сигнатура с типом функций отдельно от уравнения вроде
top : stack -> value
push : value,stack -> stack
отдельно от
top(push(v,s)) = v
Такие аннотации были и в псевдокоде 60-х, у Берджа и у Бурсталла. Такие аннотации имплементируются в сочетании с выводом типов и существуют в ФЯ и сейчас, считаются полезными для читаемости. Речь об аннотациях, которые нужно делать не для читаемости, а потому, что вывода типов нет. В этом случае нужны и внешние сигнатуры для v
и s
. И эти сигнатуры отсутствуют в Clear 77, но есть в тех языках, которые реально были имплементированы, а именно в OBJ-0 [Gogu79]
VARS V : VALUE;
S : STACK
в AFFIRM
declare v : Value;
declare s : Stack;
и наконец, но не в последнюю очередь в NPL.
var v : value
var s : stack
не в NPL 75 или 77, а в том, речь о котором еще впереди. Такой синтаксис появился даже в Clear, который был имплементирован, даже не смотря на то, что к тому времени вывод типов сделал его ненужным в современной ему версии NPL.
Clear был ближе к тому, что описатели АТД хотели получить, так что Clear позднее описывают [Sann94] [Wirs95] как первый такой язык, существенно повлиявший на остальные не смотря на то, что некоторые языки описания спецификаций были имплементированы раньше. Это, правда, может объясняться и тем, что наш будущий герой Саннелла имеет отношение к имплементации Clear.
Итак, теперь мы знаем все, что нужно чтобы понять загадочные комментарии Милнера [Miln82] о состоянии работ Бурсталла над паттерн-матчингом. Авторы ФЯ и наши великие предшественники писавшие историю ФП не особенно интересовались всем этим взаимодействием Бурсталла с языками описания спецификации. Для них уравнения с паттерн-матчингом просто появились из ниоткуда, и даже не один раз [SPJ87], а алгебраические типы данных каким-то чудом родились из нотации Ландина. Поэтому даже Гордон бессилен объяснить [Gord10] комментарий Милнера, смысл которого в свете наших открытий теперь очевиден. Уравнения с паттерн-матчингом и Clear связаны для Милнера напрямую и если Clear еще не готов, то и Милнеру использовать уравнения с паттерн-матчингом в LCF/ML рано. Сегодня это звучит так, что Милнер путает АТД и АлгТД, но в то время это звучало как то, что Милнер путает АТД и АТД.
Но, как и прочие такие заявления Милнера 82-го года, оно не совсем исторично. Когда уравнения только появились - в 75-ом году - основная работа над дизайном LCF/ML уже закончилась, и даже если бы Бурсталл и Гоген решили все Clear-вопросы в том же году когда они возникли, они бы опоздали повлиять на Милнера, к этому времени уже забросившего работу над LCF/ML.
Тем временем, пока Clear все продолжал оставаться неимплементированным, развитие NPL повернуло в другую сторону и он обзавелся другими конструкциями для декларации типов данных и функций, а затем и конструкциями для их группировки.
Но это разделение языка на два из Clear, которое многие читатели должны узнать, все эти сигнатуры и функции из одних теорий в другие в NPL еще будут, но только когда он будет называться ML, да еще и стандартным ML. Будучи одним из самых нестандартных ML из всех.
До этого еще далеко, а пока что, в 75-77 годах NPL сильнее всего сблизился с другим языком, который разрабатывался в то время в Эдинбурге. И этот проект и его основной участник представляют собой существенный сдвиг парадигмы в Эдинбурге. Если авторы языков с уравнениями соревнуются кто придумает больше требований к тому, что можно считать спецификацией и кто больше усложнит жизнь для имплементатора, то авторы этого языка на многое готовы закрыть глаза. Сойдет за спецификацию и это! И если авторы языков с уравнениями несмело говорят о применении своих языков для тестирования, прототипирования или как отправной точки для полуавтоматического преобразования в некий серьезный код, то сошедший за спецификацию код на этом новом языке - код реальных программ. Имплементатор этого языка заявляет открыто и смело:
Только в 1976 году, когда я работал в Имперском колледже в Лондоне, я наконец оценил гениальный, тонкий баланс, достигнутый в Prolog между довольно примитивным, но полезным средством проверки теорем и очень высокоуровневым языком программирования. Роберт Ковальски, Ранние годы логического программирования [Kowa88]
Основная идея называется "логическое программирование" и имеет интересную историю. Более 2300 лет назад Аристотель и его последователи...
Дэвид Уоррен, Прикладная логика: её использование и имплементация как инструмента для программирования. [Warr78]
Дэвид Уоррен (David H. D. Warren) уже поучаствовал в нашей истории, и мы расстались с ним в феврале 74-го, когда он привез из Марселя коробку с картами [Emde06] - второй интерпретатор Пролога, написанный Баттани (Battani), Мелони (Meloni) и Баццоли (Bazzoli) [Korn22] на Фортране в конце 73-го года. Интерпретатор был имплементирован вторым, но был первым, который использовал первый способ практической имплементации логических языков, придуманный в 1971 году Бойером и Муром [Moor18c]. Это на десятилетие позже, чем изобретенный Ландином первый способ практической имплементации ФЯ - SECD-машина. Если учесть это отставание и неудачный опыт Бойера и Мура - есть все основания усомниться, что из доказателя, который удалось применить только для доказательства существования списка из двух элементов, можно сделать рекордно производительную имплементацию декларативного ЯП, написанную на нем самом. Причем не только раньше, чем на не слишком-то предназначенных для исполнения языках, о которых мы рассказывали в этой главе, но и определенно предназначенных для исполнения ISWIMах, о которых шла речь до того. С таким-то настроем – конечно. У Уоррена был другой настрой.
Программирование и компьютеры отвратительны для Ковальского, он явно говорит об этом [Kowa88], как и Милнер [Miln2003]. Судя по поздним высказываниям Бурсталла - его отношение к программированию тоже плохое, хотя и сложнее [Burs92]. Важно, что среди авторов языков есть люди которые не любят программировать, ведь если б они любили - зачем бы им было придумывать что-то не похожее на то, что они в программировании не любят? Но важно, чтоб среди имплементаторов иногда попадались люди, которые не просто не хотят делать как раньше, а хотят делать что-то практичное и видят новую практику в новой теории. Насколько обоснованно - это уже другой вопрос.
Для истории ФП важно, что в конце 70-х появился человек, который стал для Милнера тем, кем Поплстоун был для Бурсталла, и Уоррен был для Ковальского. Но если Поплстоун и Бурсталл плодотворно сотрудничали какое-то время, а Ковальски, по крайней мере, не помешал Уоррену, то этому человеку с Милнером так не повезло.
Один из будущих героев нашей истории Сломан в своих воспоминаниях смеется над тем, что любимый язык Уоррена - ALGOL 68 [Slom89]. Ну, не всем же любить только языки для доказательства существования списков. Человек, который позднее написал первый компилятор ФЯ Эдинбургской программы тоже начал с хорошего отношения к ALGOL 68.
Какими же были последствия у такого настроя и бэкграунда Уоррена и его коллег? Да, уже Ван-Эмден использовал логический язык не для доказательства существования списков, а для их "быстрой" сортировки. Но от такого рода программ до компилятора, компилирующего самого себя большой путь. Который был пройден очень быстро. Осенью 74-го года Ван-Эмден был в Марселе и Колмероэ показал ему компилятор простого алголоподобного языка на Прологе [Emde06]. Это не то, что Колмероэ хотел писать на Прологе. Он занимался ИИ, пониманием естественного языка. Но как раз то, что было интересно Уоррену. Почти еще один год был потерян потому, что Ван-Эмден забыл об игрушечном компиляторе и показал его Уоррену только летом 75-го года. Перед тем, как уйти из Эдинбургского Университета.
Уоррен с энтузиазмом набросился на пример компилятора и вскоре стал присылать Ван-Эмдену все более сложные компиляторы Пролога на Прологе [Emde06]. Код первой версии, которая дошла до нас датирован 13 сентября 1975 [Prolog75]. Эта первая версия была размером всего в 902LOC и еще 80 строк комментариев, что сопоставимо с тем кодом, который был написан на ML пару лет спустя. Код на ML, правда, не был кодом компилятора. Но то, что компилятор ФЯ на ФЯ не помещается в 1KLOC - это проблема ФЯ. К 81-му году компилятор Уоррена вырастет до 6KLOC. Получается, что по нашему не очень требовательному критерию логическое программирование уже было, когда функционального программирования еще не было.
Компилятор Пролога на Прологе появился через 4 года после того, как была изобретена его практическая (что как обычно означает "с помощью стека") имплементация. Сравним это с успехами обоекембриджцев и ФП-эдинбуржцев. После изобретения SECD прошли 60-е, прошли 70-е, а компилятора ФЯ на ФЯ так и не появилось. Поразительно, что даже такой головокружительной скоростью прогресса логические программисты недовольны и рассуждают [Cohe88] о том, что же помешало все это сделать быстрее. Ну, наверное это логично: ничего не сделаешь быстро, если не будешь недоволен тем, как все медленно делается. Для нас недовольство медленным развитием Пролога не выглядит обоснованным. Ну, разве что, если считать его историю с Аристотеля, как в исторической справке в диссертации Уоррена, легко обошедшей историю SML начинающуюся всего-то с 1874. Коэн преувеличивает скорость развития Лиспа, когда сравнивает его с Прологом [Cohe88]. Например, компилировать лямбда-исчисление лисперы начали уже после того, как Уоррен написал компилятор Пролога. Но это уже другая история.
Недостаточная скорость и скромные успехи Обоекембриджской и Эдинбургской программ по имплементации ФЯ, по нашему мнению, наоборот, требуют объяснения.
Имплементаторы ФЯ с таким же настроем, как у Уоррена вскоре появятся, но одного настроя недостаточно. Недостаточно и для Пролога тоже, ведь эта имплементация Уоррена - тупиковая ветвь прологовских имплементаций, не дожила до наших дней в отличие от LCF/ML. Те свойства, которые сделали её успешной предопределили и её конец. Но рассказ о гибели целых программ и культур имплементации языков еще впереди. Сначала разберемся с тем, как они вообще появились.
Почему Уоррен хотел и мог имплементировать компилятор Пролога на Прологе? Уоррену нравилось многое из того, что прочим героям этой главы не нравилось совершенно. Ковальски, например, не любил Пролог. Не смотря даже на то, что написал статью на основе которой Пролог был создан [Kowa88]. Пролог - плохой доказатель теорем даже с точки зрения резолюциониста, не только по той причине по какой такие доказатели были заброшены Бойером и Муром. Пролог также плохой язык описания спецификации, за что его критиковали авторы языков спецификации вроде О'Доннела [O'Do84]. И Уоррен негодует, что Ковальскому не нравится Пролог [Kowa88]. Потому, что то, что не нравится Ковальскому и прочим - это то, что делает Пролог практичным ЯП [Warr77]. Возможно, "практичный" это не то, что обычно ассоциируется с Прологом, так что на этом мы остановимся подробнее.
Начнем с того, что делает Пролог практичным по сравнению с языками описания спецификации. Как мы помним, те авторы языков описания спецификаций, которые делали языки для исполнения в первую очередь, хотели исполнять как можно больше спецификаций, даже наивно написанных. И авторы Пролога с самого начала отказались от этого, не смотря на то, что Пролог предназначен в первую очередь именно для исполнения (как OBJ), а не для статического анализа (как AFFIRM). Программисту не следует писать наивные спецификации, он должен иметь в виду процедурную семантику Пролога и писать код, который будет эффективно работать.
Еще Марсельская группа отказалась от проверки зацикливания во время выполнения [Cohe88], в отличие от Гогена, который хотел исполнять некоторый зацикливающийся код корректно [Gogu79].
С точки зрения декларативной семантики не должно быть важно, в какой последовательности написаны утверждения и цели, но это важно с точки зрения семантики процедурной. Эта очередность обхода исполнителем утверждений и целей, а также управление обходом с помощью cut
- способ управления потоком контроля для программиста. Это, понятное дело, существенный недостаток для Ковальского [Kowa88], О'Доннела [O'Do84] и даже Бурсталла, судя по тому, какой паттерн-матчинг он изобрел.
Имея в виду процедурную семантику, программист может написать вычислительно эффективный код, который можно легко и быстро читать благодаря декларативной семантике. Это станет обычным компромиссным подходом для последующих практичных декларативных языков.
Пролог практичнее и "чистого Лиспа": логические переменные, которые могут быть недоопределены, позволяют делать то, что в Лиспе можно сделать только с помощью мутабельности.
Вся эта практичность не особенно помогла Марсельской имплементации быть производительной. Начнем с самой очевидной её проблемы - это интерпретатор.
Заслуга Дэвида Уоррена и примкнувшего к нему Луиса Перейры (Luis Moniz Pereira) в том, что они написали первый компилятор Пролога в машкод [Warr77]. Он же первый компилятор декларативного языка. Луис Перейра работал над компилятором в Эдинбурге в 75-ом году, после чего вернулся в Лиссабон и там до 78-го года продолжал разработку компилятора вместе с Фернандо Перейрой (Fernando Pereira) [Kowa88].
Сначала они планировали компилировать в знакомый нам BCPL, но быстро поняли, что такой подход не позволит использовать особенности PDP-10, которые позволяют генерировать эффективный код [Warr78]. BCPL, не смотря на свою кажущуюся низкоуровневость, не подходит для компиляции через него языков, на BCPL не похожих. Эта проблема с компиляцией через какой-то язык будет серьезной проблемой для имплементаторов необычных языков программирования на протяжении всей последующей их истории.
Менее очевидная, но может быть даже более важная проблема Марсельской имплементации была в управлении памятью. Техника Бойера и Мура позволяет управлять всеми аллокациями при помощи стека, сборщик мусора не требуется. Это же хорошо, да? Нет, как и в ряде более поздних попыток сделать высокоуровневый язык без сборщика мусора, это приводит только к тому, что значительная часть памяти освобождается позже, чем нужно.
ИИ-применения Пролога, где на нем описывались "знания", которые извлекались из получившейся базы с помощью запросов, предполагали, что прологовские процедуры обычно возвращают ряд альтернативных результатов. Но если писать на Прологе более традиционные программы, которые сортируют списки или компилируют Пролог, то это совсем не так обычно. Значит, решил Уоррен, можно разделить стек на два: локальный и глобальный. Наиболее распространенная "традиционная" процедура, которая быстро завершается, может быстро освободить свои данные (каких большинство), аллоцированные на локальном стеке. А более редкая процедура, которая все не возвращает и не возвращает результат, вместо этого только вызывая продолжения, аллоцирует на стеке глобальном. Который обходит компактифицирующий сборщик мусора, обеспечивающий, что продолжает жить и жить только то, что нужно.
Но этого еще недостаточно. "Многоцелевые" процедуры, которые работают в обе стороны и с помощью которых Бойер с Муром доказали существование списка из двух элементов существенно затрудняют разделение данных на те, что идут в локальный стек и те, что идут в глобальный. Так что Уоррен добавил в язык аннотации, с помощью которых программист мог указать какие параметры процедур только принимают, какие только возвращают, а какие, как по умолчанию и полагается в Прологе делают и то и другое в зависимости от обстоятельств.
:-mode concat(+,+,-).
Итак, придумав, как получать выгоду от не использования основных фич Пролога, Уоррен радикально сократил потребление памяти по сравнению с марсельским интерпретатором. Компилятор Уоррена, скомпилированный самим собой требовал в десять раз меньше памяти, чем исполняемый марсельским интерпретатором, с помощью которого Уоррен осуществил бутстрап.
Пока что все это выглядит как изобретательное использование языка не для того и не так, как задумывали его авторы. Или, точнее, изобретательное неиспользование его основных фич. Но осталась одна главная фича, которой Уоррен не предлагает не пользоваться. Наоборот, предлагает пользоваться как можно больше: паттерн-матчинг.
Быть практичнее языков описания спецификаций и практичнее "чистого" Лиспа - не очень высокая планка. Но Пролог, по смелому утверждению Уоррена, практичнее и более важного языка. Пролог практичнее Лиспа. Не какого-то там "чистого", просто Лиспа. И причина, по которой он практичнее - паттерн-матчинг.
Если для авторов языков описания спецификаций паттерн-матчинг - это проблема, наличие которой надо обосновывать какими-то семантическими сложностями от которых авторы Пролога просто отмахнулись, то для Уоррена паттерн-матчинг - это решение. О'Доннел утверждает [O'Do87], что паттерн-матчинг не настолько сложный, как в его языке с уравнениями не оправдывает труда, потраченного на его имплементацию. Не стоит замедления исполнения по сравнению с кодом с селекторами. Разница с лисповым подходом с селекторами для О'Доннела не достаточна. Для Уоррена же разница принципиальна. По мнению Уоррена ПМ не создает проблемы для имплементатора, а решает их. Уоррен компилирует ПМ в такой эффективный код как свитчи и получает производительность лучше, чем у кода с селекторами.
Уоррен утверждает, что Пролог лучше Лиспа подходит для описания операций над структурированными данными. В Прологе есть не только cons
, можно использовать конструкторы именованных структур с безымянными полями. То же самое, что конструкторы Бурсталла и непосредственные имплементации операций в языках описания спецификаций. Представление в памяти лучше составления всего из пар как в обычном Лиспе. И использование паттерн-матчинга для работы с этими конструкторами лучше Лиспового подхода с селекторами. Лучше и для программиста - код с паттерн-матчингом безопаснее постоянного использования head
и tail
. Лучше и для имплементатора - код с паттерн-матчингом позволяет на практике компилировать в машкод с меньшим числом проверок, чем код с селекторами в лисповом стиле. Может быть оптимизирующий компилятор, который трансформирует код непрактичного для того времени размера за непрактичное для тех лет время и сможет получить тот же результат. Но на практике этого не произойдет. К тому же, использование cons
в Лиспе обязательно приводит к аллокации в куче и создании работы для сборщика мусора, а использование техники Бойера-Мура позволяет размещать большую часть Прологовых структур на стеке. Ценой дополнительной косвенности, но пока сборщики мусора делать как следует не умеют (об этом мы еще расскажем подробнее) и учитывая сильные стороны PDP-10 - это того стоит.
Уоррен осуществил на практике то, о чем авторы языков с уравнениями и ПМ даже не мечтали. Реальное приложение, компилятор на языке вроде тех, которые они посчитали недостаточно пригодными для своих существенно менее амбициозных целей. Вроде написания первоначальной наивной имплементации, которую затем поэтапно переписывается со значительным использованием ручного труда в реальный код. Или вовсе только для тестирования описаний спецификаций для реального кода. Компилятор написан на декларативном языке и код довольно простой и, можно сказать, наивный. Конечно, код компилятора выглядящий в 75-ом году так:
+LENGTH(*T.*A,*N1)
-LENGTH(*A,*N)
-PLUS(*N,1,*N1).
+LENGTH(NIL,0).
в 81-ом году, когда практичность Пролога достигла новых, невиданных до того уровней, выглядел уже так
length(L,_):- wd(count):=0,nolength(L),!,error(length(L)),fail.
length(_,N):- N is wd(count).
nolength(X):- var(X),!.
nolength([X,..L]):-!,wd(count):=wd(count)+1,nolength(L).
nolength([]):-!,fail.
nolength(_).
Но и первая версия была достаточно практичной, чтоб компилятор компилировал компилятор. Без всяких сложных трансформаций.
Как эти неортодоксальные идеи и выдающиеся достижения Уоррена могли повлиять на функциональную половину протоэдинбургской программы и наоборот, насколько ФП могло повлиять на такой новый взгляд на Пролог? Мы уже выяснили, что один из первопроходцев такого стиля - Ван-Эмден вдохновлялся идеями Берджа и Левенворта о функциональном программировании.
Насколько вообще Ковальски, Ван-Эмден и Уоррен были связаны с бывшей группой экспериментального программирования? Организационно они были в другом департаменте - Вычислительной логики, бывшая Метаматематическая группа [Howe07] [Bund21]. МакКвин, прибывший в Эдинбург летом 75-го, не упоминает их, хотя упоминает, например, сотрудничество с Гогеном [MacQ15]. И Ковальски вспоминает, что они были довольно сильно изолированы от тех, кто уже не занимался резолюционизмом [Kowa88]. И Уоррен не цитирует работы группы экспериментального программирования новее чем POP-2 и резолюционные эксперименты Бойера и Мура. Но Ван Эмден и Ковальски пишут, что не позднее 74-го года обсуждали статью [Emde76] с Майклом Гордоном. Эта статья цитирует ту статью Бурсталла [Burs70], в которой используется ранняя версия нотации, похожей на уравнения с ПМ.
Мог бы Уоррен с такими-то взглядами написать не компилятор Пролога, а компилятор NPL75? Уравнения с паттерн-матчингом вполне похожи на то, что он хотел писать.
Такие конструкции, по всей видимости, просто опоздали произвести впечатление на Уоррена, как опоздали произвести впечатление на дизайнеров LCF/ML. Уоррен уже мог запускать код на Прологе в Эдинбурге начиная с весны 74-го, и маловероятно, что уравнения с ПМ имплементированы в NPL раньше лета 75-го.
Может быть это Пролог повлиял на NPL? Авторы NPL не пишут об этом, так что остается только смотреть на какие-то детали, которые скорее всего не будут придуманы независимо. И таких деталей нет. Сам Пролог в 74-75гг. выглядел не особенно похоже на языки с уравнениями [Colm96]:
+APPEND(NIL,*Y,*Y).
+APPEND(*A.*X,*Y,*A.*Z)
-APPEND(*X,*Y,*Z).
Обратите внимание на префиксы предикатов +APPEND
-APPEND
и специальный синтаксис для связанных переменных (*A.*X,*Y,*A.*Z)
. Авторы языков с уравнениями аннотировали их с помощью отдельных конструкций, даже там где не было типов, как в языке О'Доннела. Может быть они повлияли на синтаксис параметров типов в LCF/ML?
Но Ковальски не позже 74-го года пользовался для объяснения логического программирования псевдокодом [Kowa74], несколько более похожем на языки с уравнениями. И существенно более похожим на будущий синтаксис Пролога, который назовут "Эдинбургским" [Korn22]
Append(nil,x,x) <-
Append(cons(x,y),z,cons(x,u)) <- Append(y,z,u)
Бросается в глаза отличие в регистре имен предикатов (вроде Append
) и "функторов" (это имена конструкторов роде cons
). Языки с уравнениями с ПМ обычно избегали такого различия в это время. Направление стрелки вполне логично в случае логических языков, но не так понятно в случае NPL. Могли ли стрелки <-
вдохновить NPL-ные <=
? Это влияние если и было, то не через Пролог, где позднее выбрали для представления этих стрелок совсем другие символы :-
. Мы считаем, что такое влияние крайне маловероятно потому, что <=
довольно обычная деталь псевдокода, который использовался для описания частичных функций в известных Бурсталлу статьях [Mann70] [Mann71] [Cadi72] [Vuil73] [Vuil74], лучше подходящих по времени к появлению такой стрелки в протоNPL в 73-ем году. Скорее всего такая стрелка происходит от ≃
в книге Клини [Klee52]. Функции-то частичные, нельзя просто так взять и написать =
!
Вдохновил ли паттерн-матчинг Пролога паттерн-матчинг в NPL? Это тоже сомнительно, ПМ в Прологе отличается в деталях, а общие идеи вроде конструкторов в паттернах [Burs69], уравнений с ПМ [Burs70] [Burs72] и сама возможность имплементировать ПМ [McBr69] были известны Бурсталлу до появления Пролога.
Влияние Пролога на более поздние ФЯ с ПМ гораздо более вероятно. После появления диссертации Уоррена, его статей и доклада о ненужности Лиспа (слайды которой сохранились [Warr77b]), его труды заметили его эдинбургские коллеги, работающие над переписывателем.
Подход Уоррена, в котором программа состоит из спецификации и аннотаций, которые помогают имплементации эффективно его исполнять они называют интересным [Feat79] [Schw82], заслуживающим большего внимания. Работавшие над переписывателем Дарлингтона Альберто Петторосси (Alberto Pettorossi) и Джеральд Шварц (Jerald Schwarz) стали с энтузиазмом придумывать, какие аннотации для компилятора могли бы быть полезны в NPL.
Петторосси предложил [Pett78] аннотации для деструктивного изменения объектов. Например, succ<1>(y)
означает, что результат succ
т.е. n+1
может быть записан в переменную y
вместо n
. Соответственно 0
в minus<10>(x,y)
означает, что переписывать y
не надо.
Шварц разработал более сложную и многоцелевую систему аннотаций [Schw82] (издательство получило статью еще осенью 77-го). Так он предложил описывать concat
записывающий результат конкатенации поверх первого передаваемого списка:
concat-destructive(cons/label(nd)(x, list), m) <=
cons/use(nd)(x, concat-destructive(list, m))
Даем имя nd
конструктору с помощью аннотации /label(l)
и указываем его как место аллокации для нового конструктора аннотацией /use(l)
. Аннотации можно выносить из описания функций
declare concat-destructive(/destroy,)
Помимо новых конструкций для аннотаций Петторосси Шварц описывает и другие аннотации. Такие как /copy
для устранения ненужного разделения и /memo
для мемоизации параметров функций. Но рассказ об остальных придуманных Шварцем аннотациях лучше подойдет к теме следующей главы.
Все эти идеи не были имплементированы. Никто не написал для NPL такой компилятор, который Уоррен написал для Пролога. Работавшие над полуавтоматическим трансформатором кода продолжили и дальше работать над ним. Успехи Уоррена вскоре затмит другой компилятор.
Если ПМ в будущих ФЯ станет несколько больше похож на ПМ в Прологе, то про все остальное такого не скажешь. В 75-77 годах NPL и Пролог были так близки как они уже никогда не были после того: два нетипизированных языка первого порядка с паттерн-матчингом, работающим с не требующими объявления конструкторами.
concat(nil,L) <= L
concat(cons(X,L1),L2) <= cons(X,concat(L1,L2))
concat(nil,L,L).
concat(cons(X,L1),L2,cons(X,L3)) :- concat(L1,L2,L3).
В 77-ом году развитие NPL совершило серьезный поворот, который сделал его гораздо более сложным и большим языком, чем Пролог. И дальше он и происходящие от него языки становились только больше и сложнее. И это должно было быть проблемой для того, кто захотел бы написать компилятор NPL.
Но по крайней мере на один из языков уравнений с ПМ Пролог оказал более прямое влияние. Итальянцы, работавшие над TEL [Levi75], расширили его Прологовыми фичами и получили FPL (Functional plus Predicate Logic) [Levi82]
ndiv: NAT x NAT --> NAT x NAT
ndiv(in:x,y;out:0,x),lt(x,y)=true <--
ndiv(in:x,y;out:s(q),r),lt(x,y)=false <--
ndiv(in:z,y;out:q,r),minus(x,y)=z
isfact: NAT x NAT --> BOOL
isfact(x,y) = false,ndiv(in:y,x;out:z,s(r)) <--
isfact(x,y) = true, ndiv(in:y,x;out:z,0) <--
И "почти доделали" имплементацию на Лиспе с использованием той же техники Бойера-Мура что и в марсельском интерпретаторе и имплементациях Уоррена. Но создание таких языков - не те выводы которые нужно делать из результатов Уоррена. И какие были сделаны. Можно написать компилятор, который из спецификации и кое-каких аннотаций может производить достаточно эффективный код. Чтоб программист, вооружённый таким компилятором и пониманием того, как спецификация исполняется, мог писать реальные приложения. Например, такой компилятор. Более конкретное послание Уоррена к авторам и имплементаторам последующих языков программирования [Warr77]: паттерн-матчинг - это не какая-то экзотическая дополнительная фича - это предпочтительный способ работы с данными и для пользователя языка и для имплементатора. И даже если делаете не такой высокоуровневый язык как Пролог - сделайте хотя-бы паттерн-матчинг как в статье Хоара, который добавил паттерн-матчинг в алголоподобный язык.
Тип данных - это просто тупой язык программирования.
Приписывается Биллу Госперу [Wand80]
Энтони Хоар вместе с Эдсгером Дейкстрой и Оле-Йоханом Далем написал программную книгу "Структурное программирование". Хоар написал ту часть, что о типах данных [Hoar72]. Точнее скомпилировал. Книга вышла в 1972, но состояла в основном из написанных незадолго до того работ. Часть Хоара про типы данных основана на серии лекций, которые он читал в 1970. И "незадолго" тут может быть проблемой, потому что это похоже на первый случай в нашей истории, когда от совета Стрейчи сидеть на статье пока не передумаешь её публиковать могло бы быть больше пользы, чем вреда. Вскоре после выхода этой книги Хоар существенно изменил свои представления о том, как нужно делать типы данных. В результате его первоначальные недоработанные наработки были опубликованы в книге, существенно повлиявшей тех, кто сделали мейнстримное программирование таким, каким мы его сейчас знаем. Когда же Хоар все-таки доработал эти наработки - они были опубликованы как статья, которая влиятельной уже не была. С другой стороны, насколько была бы влиятельна книга, идеи из которой не будут выглядеть практичными следующие пару десятилетий? Дело в том, что доработанные наработки Хоара опередили свое время и, как мы уже убедились на примере CPL, ничего хорошего для наработок это не предвещает.
Как же работала первая версия типов данных Хоара и что с ней было не так?
В первой версии типы имеют "плоское" представление, по крайней мере пока могут. Пары из имени поля и его типа, когда соединяются ;
просто занимают место нужное для типов полей друг за другом. В этом случае имя не имеет представления во время исполнения и позиция поля определяется во время компиляции. Если же пары соединяются ,
, то имя становится тегом, который занимает место и проверяется во время выполнения и занимают место нужное для наибольшего из перечисляемых через эту запятую. Т.е. значение такого вот типа
type demo = (foo:int;
(optional:(bar:int;
baz:int),
nothing);
quux:int
).
сконструированное так demo(1,optional(2,3),4)
имеет такое вот представление:
┌───┬───┬───┬───┬───┐
│ 1 │001│ 2 │ 3 │ 4 │
└───┴───┴───┴───┴───┘
а сконструированное так foo(1,nothing,4)
такое:
┌───┬───┬───┬───┬───┐
│ 1 │002│ │ │ 4 │
└───┴───┴───┴───┴───┘
Размер сохранился, два слова просто не используются. Определение типов похоже на систему Ландина с её бесконечным вложением. Но Хоар не сослался на Ландина, а сослался на систему МакКарти. О системе Ландина Хоар наверняка знал, потому, что они оба участвовали в создании важного для нашей истории языка, рассказ о котором еще впереди. Это ожидаемо практичный подход, вполне адекватный и сегодня, тем более в то суровое время. Но что же происходит, когда размер неизвестен? Первая система Хоара позволяет определить такой вот рекурсивный тип:
type list = (null, cons:(head:int;tail:list)).
но представление значения такого типа больше не "плоское", поле tail
содержит указатель. Что никак не обозначено синтаксисом или типами, и вот это уже неожиданно. Языки с таким "плоским" представлением типов обычно обозначают ссылку тем или иным способом. Почему же Хоар так не делает? Он не любит ссылки. Да, критика ссылок Хоаром во многом справедлива, но понятно как и почему большинство разработчиков ЯП исправят этот дизайн. Не так, как это сделал Хоар.
Следующая проблема в том, что Хоар не придумал хорошей конструкции для проверки создаваемых ,
-конструкцией тегов, хотя попытался это сделать, считая, что конструкция, позволяющая безопасно работать с типами данных важна и нужна. Получилось у него вот что:
function reverse (l: list): list;
with l do
{null: reverse := l,
cons: reverse := cons(head, reverse(tail))}
И, наконец, еще одной проблемой было неудачное использование запятых. Тип данных(bar:int;baz:int)
можно записать как (bar,baz:int)
и тут запятая не означает то же, что тут (bar:int,baz:int)
. Это кажется незначительной деталью, но позднее позволило сформулировать более важную идею.
Уже в октябре 73-го [Hoar89] Хоар написал отчет о новой системе типов данных, в которой все эти проблемы исправлены. Версия отчета 74-го года опубликована как статья [Hoar75] только в 75-ом году, но отчет имел хождение и до того. По крайней мере Уоррен на него ссылается [Warr77]. Для решения проблем Хоар использовал идеи Бурсталла [Burs69] и Кнута.
В январе 73-го более известный другими своими работами Дональд Кнут написал открытое письмо Хоару [Knut73] (и по одному письму остальным двум авторам книги "Структурное программирование", но эти письма не важны для нашей истории). В письме Кнут, помимо прочего, раскритиковал синтаксис для типов и предложил сделать его более похожим на BNF. На BNF как в описании ALGOL 60 [Back63], т.е. c |
, а не как в статье Бэкуса [Back59] с ключевым словом or
. Эта нотация имела разные варианты, как и произошедшая от нее нотация для описания типов. И если бы Хоар не посчитал нужным отметить, что эту идею подал ему Кнут, мы бы сочли, что это его собственная идея. Хоар по всей видимости был в шаге от нее потому, что у Кнута эта идея появилась, когда он смотрел на страницу в книге Хоара [Hoar72], на которой описывающий AST тип данных непосредственно соседствует с соответствующим описанием конкретного синтаксиса с помощью BNF. Среди описателей АТД были распространены идеи о том, что тип данных - это язык программирования [Wand80], что конструкторы типа данных - это абстрактный синтаксис [Gutt78]. Так что можно уверенно предположить что идея о том, что имеет смысл сделать нотацию для описания абстрактного синтаксиса похожей на нотацию для описания синтаксиса конкретного, носилась в воздухе. И могла быть переоткрыта независимо множество раз, но не проговаривалась настолько явно.
Более важным изменением было новое представление типов данных в памяти, которое и определило судьбу этого проекта Хоара. Почему Хоар не любил явные ссылки? Потому, что считал, что ссылка для типов данных - это то же, что goto
для потока контроля. Низкоуровневая фича, которую программист должен использовать для воспроизведения более высокоуровневых фич, которые как раз ему и нужны для написания программ. И будет использовать плохо и с ошибками. Вместо goto
в языке должен быть набор удобных конструкций для управления, например рекурсивные функции. Так что Хоар решил сделать такие типы данных, которые для явных ссылок будут тем же, чем рекурсивные функции для goto
.
И если первая версия системы Хоара избегала ссылок до последнего, чтоб внезапно их добавить, то вторая система - это средство их массового создания по умолчанию. Хоар рассудил, что ссылки в основном нужны для создания деревьев в куче. Ну так вот вам конструкция для создания деревьев в куче. Причем иммутабельных деревьев, потому что циклы в таких структурах Хоар посчитал нежелательными.
Суммируем: вскоре после вполне практичного дизайна для низкоуровневых задач, который был бы похож на типы данных в языке Rust или анбокснутые суммы из Хаскеля, если бы не странное отношение к явным ссылкам, Хоар представил систему ссылочных иммутабельных типов данных со структурной проверкой равенства значений, требующих имплементацию со сборщиком мусора и много памяти.
Иммутабельность не такая и проблема, утверждает Хоар, который как раз ознакомился с работами Бурсталла. Т.е. в числе прочих и с системой трансформации [Darl73], которая может прозрачно для программиста переписывать значения на месте там, где это возможно. Хоар почему-то называет эту систему автоматической, хотя она в лучшем случае только полуавтоматическая. И говорит о возможностях системы как о возможностях оптимизатора компилятора, которым она не является.
Не смотря на этот оптимизм, Хоар сохраняет некоторые опасения того, что с оптимизациями все может получиться и не совсем хорошо, и что такие типы данных будет трудновато внедрить в мейнстриме 70-х годов, так что предусмотрел и мутабельную версию с ключевым словом class
вместо type
.
Хоару не нужно было убеждать авторов ФЯ принять самые тяжелые для "практиков" дизайн-решения, к которым авторы ФЯ и так уже пришли, хотя и по совершенно другим причинам. В ФЯ в это время все значения - неявные ссылки на объекты в куче из-за "динамической" типизации или параметрического полиморфизма. Эти неявные ссылки как раз в это время становятся иммутабельными из-за того, что непонятно как сочетать мутабельные с полиморфизмом.
Но эта статья нацелена не на авторов ФЯ, а на авторов языков вроде Паскаля. По отсутствию ПМ в которых понятно, чем все закончилось. Но не смотря на понятный конец этой истории, мы все равно к ней еще вернемся.
Что нужно было принять в этих типах данных авторам ФЯ эдинбургской программы, так это их вид, способ группировки конструкторов. Типы данных Хоара теперь выглядят как "традиционный" синтаксис для определения АлгТД, а точнее "традиционный" синтаксис выглядит как они:
type list = (null | cons(int, list))
Хотя и не без синтаксических странностей, которых нет в более современных версиях. Так data Or = Left Foo | Right Foo
в системе Хоара можно записать type or = (left,right(foo))
.
"Традиционный" там в кавычках потому, что в свете наших изысканий традиционность такого синтаксиса, по сравнению с перечислением сигнатур конструкторов, довольно сомнительна. Но это несомненно самый популярный синтаксис, который господствует в языках Эдинбургской программы и в наши дни, хотя и немного потеснен перечислением сигнатур конструкторов, возвращающихся начиная с 90-х.
Трудно сказать наверняка, что это было первое и последнее изобретение АлгТД в таком виде, но оно определенно довольно раннее. Пока остается загадкой, почему с 73-го года и до года 78-го или 79-го авторы ФЯ продолжали использовать для декларации типов перечисление сигнатур конструкторов или, как в LCF/ML вовсе требовать имплементировать конструктор вручную с помощью конструкторов элементарных типов и их комбинаторов, пока вдруг первые из них не решили, что BNF-образный вариант это то, что нужно.
Наконец, для обхода деревьев определенных таким образом Хоар теперь предлагает использовать конструкцию, позаимствованную у Бурсталла, которую авторы языков Эдинбургской программы не хотели имплементировать более десятилетия:
function reverse (l: list): list;
reverse := cases l of
(null -> l |
cons(h,t) -> cons(h, reverse(t)));
Если точнее, ухудшенную конструкцию Бурсталла, но это только наиболее вероятная из возможных интерпретаций.
Дело в том, что в статье [Burs69], описывающей первые конструкции Бурсталла для паттерн-матчинга, этим конструкциям уделяется не так много внимания. Ведь статья не про них, а про доказательства свойств программ. cases
-выражение вводится только как нотация, которая делает эти доказательства читаемее. Более того, значительная часть места, которое осталось паттерн-матчингу съедено описанием отвергнутых неудачных вариантов. И хотя Бурсталл обсуждает вложение паттернов, описывая первые неудачные варианты, когда доходит до примеров с cases
-конструкциями, вложение уже не использует. Посчитал ли он, что в cases
вложение не нужно или оно просто не понадобилось? Примеров, в которых вложение не используется, хотя было бы полезно он не приводит.
Дарлингтон использует cases
-конструкцию в псевдокоде, которым описывает трансформации в своей диссертации [Darl72]. И в его псевдокоде вложенные паттерны тоже не встречаются. Но вполне возможно, что тоже по причине ненужности для конкретных примеров.
С другой стороны, в разработанном в IBM неисполняемом языке спецификации META-IV [Jone78] (да, это каламбур, меты с другими номерами не было), который по всей видимости также позаимствовал конструкцию cases
у Бурсталла, вложенные паттерны использовались [Jone78b].
И если в случае псевдокода Бурсталла могут быть какие-то сомнения о том, запрещает ли он вложение паттернов, то в случае псевдокода Хоара все ясно: он приводит BNF для своих конструкций. Вложение паттернов невозможно. С учетом того, что у Хоара (как и у Бурсталла) последовательность ветвей значения не имеет, его cases
ближе к case
в Core, чем в SML и Haskell. Но вне зависимости от того, как Хоар трактовал неопределенность в описаниях Бурсталла, идея о вложенности паттернов не только в let
должна была быть ему знакома. По крайней мере по статье [McBr69] МакБрайда (не того) и др. о матчере, на который Хоар ссылается как на имплементацию cases
Бурсталла. Нужно отметить, что это не имплементация cases
Бурсталла потому, что матчит не конструкторы АлгТД, а списки списков и атомов. Т.е. это как если бы представление, в которое компилировались паттерны в системе Гуттага [Gutt78] было на пользовательском уровне.
Итак, Хоар должен был уже быть знаком с идеей вложенных паттернов, так что он сознательно выбрал, что их не должно быть. Можно предположить, что это из-за его требований к работе cases
-конструкции. Хоар хотел, чтоб можно было объявлять разные типы с одинаковыми именами конструкторов. Также хотел проверять полноту матчинга. Ну и конечно хотел, чтоб cases
можно было скомпилировать в эффективный код. Возможно, он считал что все это будет сложно имплементировать в случае разрешения вложенных паттернов.
Но, как мы уже отмечали выше, принять эту конструкцию в каком бы то ни было виде авторы языков Эдинбургской программы пока не готовы. Но Бурсталл готов принять нотацию для объявления типов данных.
В 1977 NPL получил тайпчекер, имя и программный доклад на конференции [Burs77b]. Статью, которая не отсканирована. Все, что нам остается это судить о ней по тем материалам, которые её цитируют. Действительно ли типизированный NPL в какой-то момент выглядел как пример
FUNCTION sum : NATLIST |-> NATURAL
sum(nil) = 0
sum(u::l) = u + sum(l)
из диссертации [Vase85] или просто переделан до неузнаваемости автором, чтоб выглядеть лучше/хуже/иначе? (Что совершенно реальная вещь которую в то время делали). Действительно ли конструкторы одного типа и уравнения одной функции в NPL77 не составляли единую конструкцию и могли добавляться инкрементально, как пишут в статье [Dugg96]? Неизвестно. МакКвин, спустя десятилетия вспоминает [MacQ15] добавляемые конструкторы, но нам трудно доверять этим утверждениям. У нас есть основания сомневаться, что он хорошо помнит эту часть истории NPL.
Эта версия NPL, от которой осталось меньше следов, чем от всех прочих версий NPL, быстро сменилась следующей версией. Как это обычно и бывало с версиями NPL. Последняя версия NPL с названием "NPL" (назовем её NPL79) была первой на которой написали какой-то код кроме однострочников. Этот код писался для того, чтоб испытать переписыватель программ на чем-то кроме однострочников. Чем занимался в 77-79 годах Мартин Фезер, основной разработчик переписывателя в Эдинбурге после ухода Дарлингтона. Мартин Фезер поместил в приложении к своей диссертации [Feat79] описание NPL79 и это первое сохранившееся описание языка NPL-линейки. Интересно, что эта версия NPL, которая документирована лучше, чем любая из предыдущих версий и на которой написано больше кода, чем на всех предыдущих вместе взятых не оставила никаких следов в памяти МакКвина.
Мы мало что знаем про указание и объявление типов в NPL77 потому, что в NPL79 они по какой-то неизвестной причине были полностью переделаны. CLEAR все ещё не готов, так что они снова выглядят не так как в CLEAR. Переписыватель, судя по всему, никогда не поддерживал синтаксис типов как в NPL77, что заставляет задуматься, были ли они вообще имплементированы, или были только псевдокодом в той неотсканированной статье. И был ли их синтаксис в NPL79 придуман именно таким из-за вечного отставания поддержки языка в переписывателе по сравнению с интерпретатором Бурсталла. Иначе зачем коду выглядеть вот так?
+++ length(list alfa) <= num
VAR H : alfa VAR T : list alfa
--- length(nil) <= 0
--- length(H::T) <= succ length(T)
Может это для того, чтоб легко вырезать только то, что поддерживает переписыватель простым препроцессором? Обратите внимание на соглашения о регистре имен переменных как в Прологе и также на то, что каждое имя теперь нужно объявлять с указанием типа, как полагается в большинстве языков описания спецификаций из этой главы. Вывода типов-то нет. Но такие объявления - общие для всех функций, они не составляют одну конструкцию с уравнениями. Как и сигнатуры функций, которые могут быть сгруппированы отдельно, как в Хаскеле.
То, что типизированный NPL оброс ключевыми словами и закорючками - более-менее ожидаемо. ISWIM в стиле Бурсталла имел синтаксис потяжелее Ландинского уже в 60-е, и с годами ситуация становилась только хуже. Возможно из-за влияния на Бурсталла языков спецификации вообще или Гогена в частности. Поплстоун уже работал над ЯП вместе с Бурсталлом и не мог остановить его, как в той истории с синтаксисом паттерн-матчинга. Да и большая часть синтаксиса, придуманного Поплстоуном, особой легкостью не отличалась.
Все происходящие от NPL79 языки уже никогда не смогли оправиться от этой чудовищной метаморфозы. Во всех есть какие-то ключевые слова перед декларацией функции и какие-то символы между уравнениями. К счастью, существуют языки, произошедшие от NPL75 и сохранившие легковесный синтаксис уравнений из него и языков спецификации.
NPL79 - первый NPL с привычными объявлениями алгебраических типов данных.
INF 4 ::
DATA list(alfa) <= nil ++ alfa::list(alfa)
Они уже не перечисления сигнатур конструкторов как в языках описания спецификаций. Имена конструкторов все еще могут выглядеть как имена функций. Все как предлагал Хоар. Если не считать |
символа. И, что более важно, параметризованности. Как в таком случае отличать имена конструкторов типов от имен параметров типов? Имена параметров типов надо объявлять как и имена всех прочих параметров VAR omega : type
. alfa
, beta
, gamma
, delta
и epsilon
уже объявлены.
Ключевое слово data
работает не так как в Хаскеле, взаимно рекурсивные типы данных объявляются так:
DATA option1 <= none1 ++ some1(option2);
option2 <= none2 ++ some2(option1)
Такое потрясающее средство для объявления типов используется без особой фантазии. Стандартные типы это булевы значения, списки, множества, которые на самом деле списки:
DATA set(alfa) <= nilset ++ consset(alfa, set alfa)
и натуральные числа, которые, совершенно верно, списки:
DEF
/// define numbers, addition, multiplication and factorial
PRE 20 succ
DATA num <= 0 ++ succ num
VAR N,M : num
+++ num + num <= num
--- 0 + N <= N
--- succ M + N <= succ(M + N)
INF 6 *
+++ num * num <= num
--- 0 * N <= 0
--- succ N * N <= N + M*N
+++ factorial(num) <= num
--- factorial(O) <= 1
--- factorial(succ N) <= succ N * factorial(N)
END
VAL factorial(3) END /// evaluates to 6
На этом языке было написано несколько программ. Не особенно больших, но и не однострочников. И, разумеется, работающий над трансформатором Мартин Фезер обнаружил, что трансформатор Дарлингтона требует слишком много ручного труда и слишком много памяти для работы с чем-то, что больше однострочников. Фезер добавил больше автоматизации и сделал возможным загрузку только части кода трансформируемой программы в оперативную память. В результате почти весь код Дарлингтона был переписан и новому трансформатору дали имя ZAP. Пока трансформатор был один - он, как обычно, был безымянным.
Настало время проверить, что практичнее: подход Уоррена или полуавтоматический трансформационный подход, над которым Бурсталл с Дарлингтоном проработали все 70-е. И проверка показала, что полуавтоматический подход не очень-то практичен. Даже после всех улучшений в системе Фезера, он обнаружил, что с самой большой из тех программ, которые он пробовал трансформировать, работать очень тяжело. Все еще слишком много ручного труда. Размер этой самой большой программы? 600LOC (шесть сотен) и 152 строк комментариев. Значит подход Уоррена победил? Не в битве за сердца Фезера с Дарлингтоном, которые пока что не оставили надежду.
Пока Фезер испытывал свой с трудом трансформирующий трансформатор на последнем NPL, называвшемся NPL, Бурсталл и МакКвин готовили новую версию, с новым названием. Фезер упоминает эти работы и утверждает, что в новой версии будут исправлены два основных недостатка NPL. Первый недостаток - отсутствие абстракции данных. CLEAR не успел стать такой системой для NPL, успеют ли его доделать для того, чтоб он стал такой системой для нового языка? Второй недостаток - отсутствие функций высшего порядка [Feat79]. Да, новый NPL должен наконец-то стать функциональным языком. Весь этот затянувшийся рассказ про языки первого порядка в истории ФЯ был не зря.
В конце 1979-го года работающие над этим новым языком члены бывшей Группы Экспериментального Программирования перешли в Департамент Компьютерных Наук в Кингс Билдингс. Туда, где работали Милнер с Гордоном. На этом история Группы Экспериментального Программирования и закончилась.
С ней закончилась и история NPL как более-менее обособленного проекта. И конец этой истории стал началом истории NPL как компонента, который авторы языков Эдинбургской программы смешивают с ISWIM в разных пропорциях, получая узнаваемый стиль, присущий только продуктам Эдинбургской программы. Позаимствованный у Обоекембриджской ветви CPL/ISWIM стал прародителем многих, в том числе и мейнстримных языков и не смотря на неустанную декембриджизацию все еще узнаваем в них. Другое дело - NPL. Это компонент, определяющий всю самобытность языка Эдинбургской программы. За пределами которой все эти уравнения с паттерн-матчингом можно найти только в том, что и языком программирования обычно не считается. Но могли не считаться такими и языки Эдинбургской программы. Пока что их имплементаторы не предприняли особых усилий, чтоб на этих химерах, объединяющих в себе два языка исполняемой спецификации, можно было писать какие-то программы. Если авторы и имплементаторы Пролога практичнее вас, то довольно безопасно предположить, что вам следует уделить практичности побольше внимания.
Закончилась и предыстория алгебраических типов данных. Наши великие предшественники обычно перескакивают от нотации Ландина сразу к современным АлгТД. И не без причины. Этот совсем нетривиальный переход оставил не так уж много следов, которые не так-то легко датировать и у которых не так просто определить авторство. Мы полагаем, что между этими событиями были:
- Изобретение конструкторов, которые можно разбирать в
case
-выражениях. Но конструкторы сначала первоклассные потому, что изобретены для функционального языка. Также, каждый конструктор конструирует значение отдельного типа и в один тип их нужно объединять специальным комбинатором типов как у МакКарти. - Изобретение исполняемых уравнений с паттерн-матчингом, которые изображают запись свойств в алгебраических спецификациях. Конструкторы теперь абстрактные функции в АТД и больше не являются первоклассными потому, что и все прочие функции в языках алгебраической спецификации первоклассными не являются.
- Идея о том, что АТД должны вводить пространство имен для своих членов как,например, у Лисков. Но многие не принимают эту идею.
- Идея о том, что у Абстрактных Типов Данных может быть сколько угодно конструкторов. Логично, ведь у АТД может быть сколько угодно функций. Специальные комбинаторы типов больше не нужны.
- Не особенно успешная борьба за то, чтоб между конструкторами и прочими функциями АТД было как можно меньше разницы, следы которой до сих пор остаются.
- Идея о том, что АТД можно считать языком, а его конструкторы считать его абстрактным синтаксисом. Прочие функции - это его семантика, разница между конструкторами и прочими функциями вполне естественна.
- Идея о том, что удобно придать нотации для определения абстрактного синтаксиса вид как у нотации для конкретного синтаксиса.
Готово! Первая версия современных АлгТД, которые далее будут эволюционировать в основном в сторону все большего стирания следов борьбы за сходство конструкторов и прочих функций.
NPL начинает очередную трансформацию, которая сделает его не просто ФЯ, а первым сформировавшимся языком Эдинбургской программы. Но перед тем, как рассказать об этом, нам нужно отправиться в соседний с Эдинбургом город, в котором появился третий прародитель функциональных языков.
Вернемся в конец 60-х годов к трудам Кристофера Стрейчи, который в Оксфорде возглавляет экспериментальную группу по программированию и является научным руководителем двух важных героев нашей истории.
Один из них нам уже знаком - это Кристофер Вадсворт, один из изобретателей продолжений и авторов LCF/ML. Как мы помним, изобретение продолжений для Вадсворта было только отступлением [Reyn93] от работы над его диссертацией. Так что же, была основная работа более важной, чем отступление? Вероятно.
Нам не удалось с ней ознакомиться. Дело в том, что диссертация Вадсворта [Wads71] - один из тех документов, которые не были отсканированы. Но это один из самых цитируемых источников, который не был отсканирован.
Потому, что Вадсворт в том же ряду, что Ландин и Бойер с Муром. Один из тех, кто придумали практические способы имплементировать целые разновидности ЯП. Или, с другой точки зрения, придумал как имплементировать корректно [Vuil73] то, что до него придумывали как имплементировать не совсем корректно.
Когда мы писали о том, как Ландин придумал способ имплементировать лямбда-исчисление мы не сделали важной оговорки о том, что SECD не может вычислить целые классы вполне корректных выражений. Причина - аппликативный порядок редукции.
Но это не очень волновало многих имплементаторов ФЯ. Они справедливо посчитали, что ЛИ-выражений, которые вычисляются, в сочетании с некоторыми расширениями будет вполне достаточно для того, чтоб писать программы. Тем более, что практической альтернативы все равно еще не было. Был только вызов по имени, постоянно перевычисляющий одно и то же. Но такое решение приняли не все. Так, непрактичность вызова по имени не помешала авторам ALGOL 60 добавить его в язык и даже как способ передачи параметров по-умолчанию. Конечно, нужно учитывать, что в значительной степени это - результат борьбы за и против того, чтоб сделать из Алгола ФЯ (к которой мы еще вернемся). ALGOL 60 - это линия фронта между фракциями, которые и десятилетия спустя обожают рассказывать как же были неправы их оппоненты. Так что никого не должно удивлять то, что такое решение определенно не стало популярным. В языках, происходящих от ALGOL 60 от него отказывались.
Вадсворт придумал [Turn19] как решить, по крайней мере в принципе, проблему перевычисления одного и того же. Нужно работать не с деревьями, как SECD, а редуцировать графы. Значения с которыми работает переписыватель представляются как ссылки, которые указывают на один и тот же объект в куче. А значит ссылаются и на результат, получаемый после вычисления этого объекта, когда этот результат впервые понадобится. Вадсворт также ввел одно из менее популярных названий для этой идеи - вызов по необходимости (call-by-need).
Фактически происходящие от Алгола языки тоже заменили передачу по имени на ссылки и мутабельность но не таким скрытым от программиста способом.
Разумеется, для замены вычисления на его результат нужно чтоб результат вычисления всякий раз был одним и тем же. Так что явная мутабельность долго будет в таких языках нерешенной проблемой. В гораздо большей степени проблемой и в гораздо меньшей степени решенной, чем нерешенные проблемы сочетания мутабельности с параметрическим полиморфизмом из позапрошлой главы. Зато решение первой проблемы в ленивых ФЯ решит в них и вторую.
Временная необходимость отсутствия императивных фич в языке для нормальной работы вызова по требованию была еще одним препятствием. Но, как мы выяснили в прошлой главе, языки без императивных фич как раз стали появляться вскоре после защиты диссертации Вадсворта, хотя и не в следствии её. И авторы этих языков, не очень-то предназначенных для исполнения спецификаций, оценят принципиальность нормальной редукции. Так О'Доннел считает её важным требованием для принципиальной имплементации языка с уравнениями [O'Do87].
Ландин и Бойер с Муром не ограничились только изобретением практического способа имплементации и попытались, в основном безуспешно, воплотить свои идеи в жизнь и написать практические имплементации. В отличие от них Вадсворт не стал пытаться и, защитив диссертацию, оставил это направление навсегда.
Другой герой нашей истории, наоборот, не защитил диссертацию и писал и писал код таких имплементаций c 70-х годов и до настоящего времени. По крайней мере писал еще в 2020-ом году.
Дэвид Тёрнер (David Turner) работал в Оксфорде над диссертацией под руководством Стрейчи с 69-го года [Turn19], но был в другой "партии" соискателей и не был знаком с Вадсвортом. В отличие от работы Вадсворта, результатом работы Тернера должна была стать практическая имплементация, но не идей Вадсворта, а идей Ландина.
Джо Стой (Joseph E. Stoy) привез [Turn19] из МТИ ленты [Turn12] с интерпретатором PAL и Стрейчи посоветовал Тернеру написать эффективную имплементацию PAL. Что Тернер три года безуспешно пытался сделать. Тернер уверенно называет продолжения основной причиной своей неудачи. О какой эффективной имплементации языка может идти речь, если в нем эквивалентная продолжениям фича? Тернер упоминает [Turn19] об одной из конкретных проблем продолжений: слишком много объектов задерживается в куче. Да, это та проблема, которую Уоррен решил пять лет спустя в своем компиляторе Пролога.
Так что взгляд Тернера на продолжения, который по всей видимости был достаточно типичный для протоэдинбургской программы, не следует воспринимать некритично. К счастью, это один из тех вопросов, ответ на который вполне возможно найти. Выяснить когда эффективная имплементация ФЯ вообще стала возможной и почему - одна из основных целей нашей работы. Тернер - автор многих важных имплементаций ФЯ и осталось достаточно свидетельств об их эффективности чтоб установить его личный вклад в эту неудачу. И, конечно, были и другие попытки имплементировать язык с продолжениями и многие их результаты тоже известны. Все это будет рассмотрено более подробно в соответствующих главах.
В октябре 72-го Тернер покинул Оксфорд без степени и отправился читать лекции в Сент-Эндрюс [Turn12], город в 80км от Эдинбурга.
На особую оригинальность SASL не претендует - более того, целью проекта было сделать нотацию как можно более "стандартной".
Дэвид Тёрнер, Руководство по SASL
В Сент-Эндрюсе Тернер использовал для преподавания псевдокод, который был примерным чисто-функциональным подмножеством ISWIM. К удивлению Тернера, его коллега Тони Дейви (Tony Davie) за выходные имплементировал этот псевдокод на Лиспе. Раз уж язык был имплементирован, вспоминает Тернер, ему пришлось дать имя: SASL (St Andrews Static Language) [Turn12]. "Статический" тут означает "иммутабельный".
В это время Тернер не был знаком с LISP 1.5 [Turn19], но вскоре вынужден был познакомится. Потому, что курс ФП до него использовал Лисп. И когда Тернер познакомился с Лиспом, он обнаружил, что лямбды работают не так как надо [Turn12]. К истории о неправильных лямбдах в Лиспе мы еще вернемся.
Разумеется, Тернер решил использовать для преподавания SASL. Но, как и первая имплементация PAL на Лиспе, первая имплементация SASL на Лиспе работала недостаточно хорошо. Поэтому во время пасхальных каникул 1973 Тернер имплементировал SASL заново, на том же языке, что имплементировали PAL - BCPL, для младшей модели той же линейки машин, на которой имплементировали McG - IBM 360/44. В первой версии этой имплементации, представляющей собой компилятор в SECD байт-код и SECD интерпретатор, было только немного больше чем 300 строк кода [Turn12]. Конечно, это воспоминание Тернера, озвученное сорок лет спустя. Существует более близкое к описываемому времени свидетельство о том, что только в интерпретаторе SECD-байт-кода на BCPL было 350 стейтментов [Somm77]. Стейтменты, конечно, не строки, и это по-видимому больше, чем 300 строк на все, не в одном только интерпретаторе. Но все еще очень мало.
"SASL был не очень большим языком" - говорит Тернер. И Тернер предпринимал усилия для того, чтоб сделать язык еще меньше. Поэкспериментировав с некоторыми деталями синтаксиса let
, он отказался от аннотаций рекурсии. Это вероятнее всего произошло в 75-ом году.
Хотя Тернер рассказывает, что начал работу над SASL в 72-ом году по крайней мере с 82-го года [Turn82], самые старые ссылки на неотсканированные репорты с описанием SASL и его имплементации датируются 75-ым годом. Составить представление об их содержимом можно по отсканированным и выложенным в интернет диссертациям, которые писали об имплементации SASL [Somm77] или о программах на SASL [Well76]. В этих диссертациях есть не только ссылки на неотсканированные документы, но и код на SASL, его стандартная прелюдия (да, называется как в Хаскеле) и описания SASL. Включая и BNF.
Первый мануал по SASL вышел в отчете в январе 75-го. SASL был функциональным языком, так что мы можем, наконец-то, после долгого перерыва на нефункциональные языки с уравнениями, вернуться к нашему традиционному примеру:
_REC MAP F X
_BE X=() -> (); F(_HD X),MAP F(_TL X)
_IN _LET Y = 1
_IN MAP (_LAMBDA X. X + Y) (1,2,3,)
SASL - название для многих языков. Бывает, что имеющих мало общего между собой, так что мы будем называть разные версии обычным способом, добавлением года. Сам Тернер такие наименования не использует. Этот SASL из январского отчета мы назовем SASL 74. Почему 74?
Уже 16 сентября 75-го вышла ревизия мануала. Из SASL убрали rec
и получили SASL 75. Код map
из стандартной библиотеки этой версии дошел до нас [Well76]:
_LET MAP F X || 'MAPS' F ALONG THE LIST X
_BE X=() -> (); F(_HD X),MAP F(_TL X)
Эти be
и ||
комментарии - из синтаксиса BCPL.
Итак, в отличие от библиотеки PAL, в библиотеке SASL были ФВП. И некоторые из них даже написаны с применением ФВП и частичного применения функций! Но не c применением foldr
(Lit
) Стрейчи, не смотря на то, что он был научруком Тернера. Возможно, что в 70-е Lit
был интересен Бурсталлу с Гордоном больше, чем самому Стрейчи. Набор ФВП для работы со списками не очень богатый и повлияли на него, по всей видимости, только статьи Ландина о SECD и ISWIM, но не Стрейчи и Берджа. Но в библиотеке есть набор комбинаторов из книги Карри [Curr58]. Тернер узнал о комбинаторах из лекций Даны Скотта в Оксфорде в 1969 [Turn19].
Если говорить о подмножествах реально имплементированных языков, то SASL 75 скорее подмножество не PAL, с имплементацией которого Тернер столько страдал, а McG, который Тернер, правда, не упоминает. Есть let
но нет where
, функции объявляются каррированными, синтаксис для списков вместо синтаксиса для туплов, строки как списки символов. Последнее Тернер считает [Turn19] собственным изобретением и одной из важных инноваций SASL, но нет, уже было [Burg71] у Берджа и Левенворта в McG. В SASL 75 была лямбда, как в PAL, но с другим ключевым словом. Правда, в отсутствии лямбды в McG мы не полностью уверены.
На первый взгляд, SASL 74/75 выделяется из обсуждаемых нами раньше ISWIM-ов только урезанием всех императивных фич. И такое урезание тоже не является чем-то необычным в это время. Это скорее правило для нового поколения не очень-то предназначенных для исполнения исполняемых языков спецификации из прошлой главы. Следующие версии SASL интереснее, но и SASL 75 выделяется из ряда ФЯ, которые мы обсуждали до сих пор. На SASL 75 была написана программа под названием IDEA, которая на момент написания была больше чем любая другая сохранившаяся программа на протоФЯ. Но не беспокойтесь, функционального программирования все еще нет, размер этой рекордной программы только 1565LOC. Да, это почти как самая крупная программа на LCF/ML и самая крупная программа на NPL вместе взятые. Но размер кода не особенно впечатляет. И это даже не компилятор, как в случае Пролога. Но эта программа, как и компилятор, работает с деревьями. Небольшими деревьями. Программа дифференцирует и интегрирует [Well76].
Автор-первопроходец делится с нами опытом разработки таких гигантских программ на ФЯ. Опыт не особенно приятный. Да, есть и плюсы. Автор считает, что код на SASL лучше читается, чем код на Лиспе и меньше чем у аналогичной программы на Лиспе. Функциональность программы на Лиспе больше, так что сравнение не совсем честное. Также, автор сравнивает размеры этих программ в разных единицах. 1600 карт против 70 страниц. Ну, карты это просто строки. В распечатке дошедшей до нас программы на одной странице 44 строки. Получается, что размер программы на Лиспе в два раза больше. Неплохо, но зависит от того какая разница в функциональности.
Автор считает, что скорость работы программы удовлетворительная. У пользователя может сложиться другое мнение потому, что интеграция "в большинстве случаев требует менее пяти минут". Скоростью работы компилятора в байт-код автор программы уже не так доволен. Имплементация Тернера явно не предназначена для того, чтоб писать программы в 1KLOC. Раздельной компиляции нет. Нужна ли она в для программ в 1KLOC? В 76-ом году еще как. Интерпретатор Тернера компилирует в байт-код при каждой загрузке и весь этот процесс занимает три минуты. Программирование получается не очень интерактивным. Также, программа не умещается в обычный пользовательский лимит в системе разделения времени. То есть разрабатывать и даже использовать программу обычный пользователь не может, ему нужно получать разрешение на увеличение лимитов. По этой причине даже сделана специальная версия, которая только дифференцирует и может быть использована обычным пользователем.
Автор IDEA также недоволен отсутствием в SASL императивных фич, а именно процедуры для вывода. Интерпретатор выводит только результаты функций.
Нужно отметить, что небольшой размер SASL может быть обманчив. Да, в нем нет двух десятков разновидностей let
как в LCF/ML. Но было несколько инноваций, которые делали имплементацию не такой уж простой. Репорт об имплементации SASL 74/75 вышел в марте 75-го и тоже не отсканирован. Так что об этих инновациях остается судить только по существенно более поздним воспоминаниям Тернера [Turn19] и некоторым следам в коде на SASL. Тернер вспоминает, что в SASL была впервые имплементирована фича ISWIM-псевдокода, о которой мы уже рассказывали в главе про ML - вложенный паттерн-матчинг в let
. Вложенный паттерн-матчинг в SASL еще бесполезнее, чем в LCF/ML (в котором он появился не намного позднее) из-за отсутствия средств для обработки его неуспешности. Но для этого Тернер вскоре найдет более современное решение.
В примерах SASL 74 [Somm77] и BNF, описывающем SASL 75 [Well76] вложенные паттерны есть. Но есть несколько поводов заподозрить, что фича была в языке не с самого начала. А может быть была в языке но не в имплементации. Помимо заявляемого Тернером малого размера имплементации, в которой не так много строк даже для недо-компилятора такого недо-ПМ, вызывает вопросы наличие ключевых слов для для head
(_HD
) и tail
(_TL
). Можно бы было ожидать, что автор, который старается сделать язык поменьше, воспользовался бы случаем и не делал бы явно дублирующие ПМ конструкции. Еще один довод в пользу того, что вложение паттернов появилось не сразу - оно практически не используется в дошедшем до нас коде [Well76]. Но автор этого кода пишет, что имеет небольшой опыт программирования.
Еще одна возможная недоделка или баг. В реальном коде на SASL лямбду записывали _LAMBDA (X,). X + Y
хотя BNF-описание позволяет _LAMBDA X. X + Y
.
Но не стоит уделять этим вопросам много внимания. Такие фичи SASL как let
с паттерн-матчингом и лямбды в этой истории с нами не надолго.
Тернер утверждает [Turn12], что эта имплементация SASL осуществляла оптимизацию хвостового вызова. Что подтверждается хвосторекурсивной имплементацией конкатенации списков (но не map
) в стандартной библиотеке [Well76].
_LET SHUNT X Y
_BE X=() -> Y; SHUNT(_TL X) (_HD X,Y)
_NEW REVERSE X _BE SHUNT X()
_LET APPEND X Y || CONCATENATE THE LISTS X AND Y
_BE SHUNT(REVERSE X) Y
Едва ли вы захотите так имплементировать конкатенацию, если нет оптимизации хвостового самовызова.
К обсуждаемому времени помимо имплементаций на LISP 1.5 и BCPL в Сент-Эндрюсе написали интерпретаторы SECD-кода на микрокоде компьютера, который университет собирался, но так и не смог купить и на низкоуровневом языке, который должен был компилироваться в разные микрокоды [Somm77]. Это не последние имплементации, написанные в этом университете, но еще больше имплементаций появится после того, как о SASL узнают за его пределами.
Летом 1975-го Тернер выступил с докладом о SASL в Суонси на межуниверситетском коллоквиуме. С этого времени другие университеты стали интересоваться SASL. Распространение SASL познакомит с ФП будущих важных героев нашей истории, например Леннарта Августссона. Но распространением интерпретаторов, написанных в Сент-Эндрюсе все не ограничилось. Удачные идеи Тернера о языковом дизайне, не такие удачные работы Тернера как имплементатора и не такой уж большой размер языка привели к тому, что SASL стал в конце 70-х и 80-х ФЯ с самым большим числом имплементаций. Не стоит, конечно, ожидать, что все эти имплементации "SASL" могли исполнять один и то же код. Как не исполняют один и тот же код все имплементации языков, называющихся "ML", например. Также, не все эти диалекты "SASL"-ов будут называться SASL. Имплементаторы ФЯ так привыкнут имплементировать языки Тернера, что возникнут некоторые проблемы, когда окажется, что Тернер не очень-то этим доволен.
Нужно только отметить, что такое распространение получил не SASL, о котором мы только что рассказали, а SASL, о котором нам только предстоит рассказать. Уже в следующем, 1976-ом году все эти наработки были забыты, смыты бурным потоком идей. "SASL" стали называть совсем другой язык.
Мы уже вскользь упоминали эту проблему в предыдущей главе. Нужно проверить, что два дерева имеют одинаковую последовательность листьев. Сделать это нужно с помощью двух "функций". Одна получает "последовательность" листьев. Другая сравнивает две "последовательности". Требуется написать эти функции так, чтоб сравнение могло завершаться неуспешно без материализации всех последовательностей листьев и без полного обхода деревьев.
Так уж вышло, что эту проблему использовали для обоснования нужности ленивости. Разумеется, для решения этой проблемы не нужна ленивость. С ней справится простой итератор, никакого ненужного перевычисления не будет. Но эта проблема традиционно использовалась для обоснования нужности существенно более мощных конструкций, чем нужно для её решения. Мы не видим ничего плохого в простых примерах для демонстрации нужности чего-то. Но если это и в самом деле нужно.
С другой стороны, до этого самым близким к обоснованию нужности ленивости производительностью было упоминание в статье Жана Вюийемена (Jean Vuillemin) [Vuil73] о том что вот такая функция
Ble(X,Y) <= IF X = 0 THEN 1 ELSE Ble(X-1,Ble(X-Y,Y))
работает быстрее при нормальном порядке редукции, чем при аппликативном. Можно порадоваться, что речь хотя-бы зашла о сколько-нибудь реальной задаче.
Хьюит [Hewi74] использовал её для того чтоб обосновать нужность продолжений, а точнее построенных на них абстракций: обменивающихся сообщениями акторов и обобщения стримов Ландина. Все просто, пишете акторы, принимаете и шлете сообщения:
[same-fringe ? <=
(=>
[=tree1 =tree2]
(start loop
[(streamer tree1)(streamer tree2)]
(=>> [=s1 =s2]
(next s1
(then-to :
(=>>
(#stream (first := x1)(rest := c1))
(next s2
(then-to :
(=>>
(#stream
(first := x2)
(rest := c2))
(rules x1
(=> x2
(restart loop
[c1 c2]))
(else
(#no)))))
(else-to :
(=>> (#exhausted)
(#no)))))
(=>> (#exhausted)
(next s2
(then-to :
(=>> (#stream)
(#no)))
(else-to :
(=>> (#exhausted)
(next s2
(then-to :
(=>> (#stream)
(#no)))
(else-to :
(=>> (#exhausted)
(#yes)))))))))))]
[streamer <=
(=>[=the-tree]
(=>
(#next(else-to := the-complaint-dept))
(internal-streamer
the-tree
the-tree
(=>
(#next)
(% the-complaint-dept (#exhausted)%)))))]
[internal-streamer <=
(=>
[
=the-node
=the-customer
= the-alternate-supplier]
(rules the-node
(=> (terminal)
(%the-customer
(#stream
(first : the-node)
(rest : the-alternate-supplier))%))
(else
(internal-streamer
(left the-node)
the-customer
(=>
(#next (else-to := the-complaint-dept))
(internal-streamer
(right the-node)
the-alternate-supplier
the-complaint-dept))))))]
Что, не очень просто? Это похоже на то, что используют серьезные программисты в мейнстриме и сегодня. Разумеется, не с таким синтаксисом. Но наша история про будущих функциональных программистов, для которых, как ни странно, это выглядит не особенно хорошо. Конечно, все это может выглядеть лучше, как выглядят стримы о которых писали Ландин и Бердж. Но все это не совсем то, что нужно. Будущие функциональные программисты, например, хотели работать с рекурсивными структурами с помощью рекурсии. Почему? Во-первых потому, что они обычно этого хотели, но во-вторых потому, что Хьюит заявил, что все эти рекурсивные подходы несовместимы с модульностью и эффективностью. Списки антимодульны. Что толку от комбинирования комбинаторов, если их комбинация строит все эти ненужные списки и обходит деревья, которые не нужно обходить, хотя уже давно понятно, что последовательности листьев деревьев не равны?
На следующий, 1975-й год Бурсталл с Дарлингтоном "решили" проблему полуавтоматической трансформацией наивных рекурсивных функций в одну. Но, как мы знаем, такие трансформации хорошо не заработали. Что же заработало?
Не прошло еще и года после этого и на радикальное заявление Хьюита нашелся ответ у нашего старого знакомого Джеймса Морриса. Разумеется, для иллюстрации своего очередного независимого изобретения он использовал
а не PAL, как и полагается одному из авторов и имплементаторов PAL. Работа Питера Хендерсона (Peter Henderson) и Джеймса Морриса [Hend76] была представлена на конференции в январе 76-го года. Отличие "гипер-чистого" Лиспа от просто "чистого" Лиспа МакКарти в том, что в "гипер-чистом" есть работающие лямбды. Авторы, по всей видимости, придумали наиболее распространенное название для фичи - "ленивость".
Авторы ссылаются на диссертацию Вадсворта и статью Вюийемена [Vuil73].
Первый пример использования ленивого языка - взятие головы от хвоста бесконечного списка.
integers[i] = cons[i;integers[i+1]]
car[cdr[integers[0]]]
Что должно работать, а не бесконечно вычислять бесконечный список. Невероятно!
Второй пример интереснее, это решение проблемы кромок [Hewi74] без продолжений.
EqLeaves[x;y] = EqList[Flatten[x];Flatten[y]]
Flatten[x] = if atom[x] then cons[x;NIL]
else Append[Flatten[car[x]];
Flatten[cdr[x]]]
Append[x;y] = if null[x] then y
else cons[car[x];Append[cdr[x];y]]
EqList[x;y] = if null[x] then null[y] else
if null[y] then false else
if eq[car[x];car[y]]
then EqList[cdr[x];cdr[y]]
else false
Да, код выглядит в точности, до последней детали как "немодульный" рекурсивный код на строгом языке, но работает как надо. Шах и мат, Хьюит!
Ну, вернее должен работать, если его имплементировать. Это псевдокод. Хендерсон и Моррис, в отличие от Хьюита, пока еще из тех лисперов, которые считают, что вместо того, чтоб показывать как выглядит Лисп на самом деле нужно приводить примеры на M-LISP псевдокоде. Но со временем таких будет все меньше и меньше.
Что-то похожее в конце концов имплементировал О'Доннел, так что может быть нам следовало называть его язык Гипер-Чистый Лисп?
Наконец, есть еще и третий пример.
primeswrt[x;l] = if car[l] mod x=O then primeswrt[x;cdr[l]]
else cons[car[l];primeswrt[x;cdr[l]]]
primes[l] = cons[car[l];primes[primeswrt[car[l];cdr[l]]]]
primes[integers[2]]
То самое квадратичное нерешето неэратосфена, которое демонстрируется в момент написания этого текста на сайте https://www.haskell.org/ как пример кода на хаскеле. Конечно у Хендерсона и Морриса он не в такой краткой форме. Это ставший классическим пример с вычислением простых чисел. Разумеется, существует более современный его вариант, который демонстрирует ленивость лучше. Хорошая демонстрация нужности ленивости была так возможна, но возможность упущена. Как мы видим здесь и видели в истории с ФВП, придумывание небольших понятных примеров, иллюстрирующих использование новых языковых фич, дается не легко!
Независимо от кого Моррис изобретал на этот раз? В том же январе 76-го, когда был сделан доклад про Гипер-Чистый Лисп и Ленивый Вычислитель, Дэниел Фридман (Daniel P. Friedman) и Дэвид Уайз (David S. Wise) выпустили отчет о том, что cons
не следует вычислять свои аргументы. Под названием "CONS не следует вычислять свои аргументы" [Frie76]. В минимальном ленивом Лиспе Фридмана и Уайза больше ничего менять для поддержки ленивости не требуется, потому что функция применяется к списку параметров. И список ленивый потому, что cons
не вычисляет свои аргументы. Разумеется, если писать реалистичную имплементацию, то одним cons
все не ограничится. Фридман и Уайз написали даже прототип имплементации на полторы страницы.
На основе этого отчета был сделан доклад [Frie76b] на международном коллоквиуме по автоматам, языкам и программированию в Эдинбургском Университете в июле 76-го. Эта версия содержит уже ссылку на Хендерсона и Морриса, а так же на книгу, изданную в 1975, в которой способ имплементации ленивого ФЯ описан раньше, чем это сделали Хендерсон, Моррис, Фридман и Уайз. Но ссылка сделана из-за описания стримов. Возможно, что описания имплементации ленивого ФЯ в ней ссылающиеся вовсе не заметили. Ну или мы не заметили какую-то их реакцию на это.
Эта книга, которую читали практически все авторы протоэдинбургских языков, вдохновила Тернера на то, на что не вдохновляла никого из остальных.
появляется в уже известной нам книге Берджа [Burg75] словно из ниоткуда. Ближе к концу главы о SECD Бердж вдруг пишет о том, что вот, кстати, можно и так вычислять выражения. Книга Берджа обычно рассказывает о том, о чем у Берджа уже были статьи. Но не в этом случае. Бердж много писал про стримы [Burg75b], генераторы в SCRATCHPAD сделаны на основе стримов Берджа [Jenk74], описанных в неотсканированном отчете Берджа 73-го года под названием "Еще более структурное программирование". Позднее Бердж пишет про материализующиеся в список стримы для Scratchpad II [Burg89] наподобие динамических списков POP-2. Все это можно использовать для решения проблемы кромок, но не для опровержения заявлений об антимодульности списков. Но как-то так вышло, что в свободное от всех этих занятий время Бердж решил и проблему антимодульности списков и описал в книге. Раньше чем вышли статьи про ленивый вычислитель и откладывающий вычисления cons
. Ну, Берджу не впервой опережать свое время.
Бердж обычно испытывал свои идеи с помощью McG. Имплементировал ли Бердж прокрастинирующую машину? Существовал ли уже ленивый ISWIM до SASL? Неизвестно.
Статьи Хендерсона, Морриса, Фридмана и Уайза привлекли больше внимания, но часть книги Берджа про прокрастинирующую машину привлекла внимание наиболее важное для нашей истории. Внимание Тернера.
Моррис, Фридман и другие не имели ничего против продолжений. Моррис - один из их изобретателей и оба они с Фридманом из тех, кто работал с продолжениями как средством имплементации ФЯ. Но, как мы помним, у функциональной части Эдинбургской программы отношение к ним неважное, а хуже всего - у Тернера, имевшего с ними негативный опыт. Ничего удивительного, что Тернер ухватился за их замену. Да, он смотрит на ленивость именно как на замену продолжений. Тернер утверждает, что ленивые списки заменяют корутины (спасибо за отличный пример, который так хорошо помогает понять для чего нужны корутины, Хьюит), а метод, который через десяток лет Вадлер назовет "список успехов", заменяет бэктрекинг [Turn19]. Про ленивость Тернер мог бы узнать на годы раньше, если бы больше интересовался тем, над чем работают его коллеги. Но похоже, что отсутствие такого интереса довольно типично для героев нашей истории.
Хотя книга Берджа, вдохновившая Тернера, вышла в 75-ом году, Тернер написал первую имплементацию ленивого по-умолчанию языка в 76-ом году, в котором уже были сделаны все прочие основные доклады о ленивости, когда идея уже широко обсуждается. Сложно назвать имплементацию серьезной, но серьезнее, чем прототип Фридмана и Уайза на Лиспе. Как имплементации PAL и SASL на BCPL были серьезнее соответствующих прототипов на Лиспе. Такого уровня серьёзности никто кроме Тернера не достигал еще долгое время.
Итак, первая более-менее работающая имплементация ленивого ФЯ появилась в конце 76-го. Так что ленивость опоздала ко времени дизайна LCF/ML еще больше паттерн-матчинга и да, она тоже перечислена [Miln82] Милнером в его антиисторичном списке нерешенных вопросов ML дизайна, как будто было что решать.
Отчет с руководством по SASL 76, как мы будем его называть, вышел в декабре 76-го. У этого руководства три редакции авторства Тернера, которые мы будем называть SASL 76, SASL 79 и SASL 83. Но были отсканированы и сейчас доступны только руководство по SASL 83 [Turn83] и версия руководства по SASL 76 - руководство [Abra81] по SASL, имплементированному не Тернером. В руководстве по SASL 83 не очень подробно, но задокументирована история изменений SASL между версиями 76 и 79 и между 79 и 83. Но не между 75 и 76 и более ранними. SASL, о котором мы рассказывали до сих пор, там вовсе не упоминается.
Тернер пишет [Abra81] (в более поздних версиях руководства перестает), что SASL не претендует на новизну. Эпиграф к главе про строгий SASL взят из руководства по SASL 76. И то, что в нем говорится справедливо для SASL 75, но не для SASL 76.
SASL 76 называется в комментариях [Coro83] имплементации "Новый SASL (нестрогий, с группами уравнений и функциональной композицией)". "С функциональной композицией" тут означает, что этот оператор является встроенным, а не библиотечной функцией. Как и все прочие, например ++
для конкатенации списков и :
для их конструирования. Конкатенация списков и в прошлом SASL была единственной оптимизированной библиотечной функцией, а тут стала языковой фичей.
Да, с группами уравнений. Будто бы одной смены порядка вычисления было мало, Тернер решил еще и позаимствовать некоторые фичи NPL 75. Первым начав таким образом процесс объединения наработок Эдинбургской программы в одном языке. И этот процесс назывался бы не так, как мы его назвали в предисловии, если б Тернер не завершил его одним из последних.
Тернер был знаком с Дарлингтоном и ездил в Эдинбург пообщаться с ним несколько раз [Turn19]. Как мы помним, Сент-Эндрюс не так и далеко. Раз уж Тернер был знаком и общался с Дарлингтоном, то в своих воспоминаниях он присваивает авторство большинства идей из NPL Дарлингтону. Например уравнения с ПМ, с заимствования которых Тернер и начал. И, как мы выяснили в прошлой главе, Тернер скорее всего заблуждается, автор уравнений с ПМ не Дарлингтон. Происхождение синтаксиса SASL 75 от NPL времен работы над ним Дарлингтона сохранило для Эдинбургской программы легковесный синтаксис уравнений, в самом NPL и в произошедших позднее от него языках синтаксис уравнений перестал быть легковесным.
Если бы Тернер не заявлял прямо, что позаимствовал уравнения с паттерн-матчингом в NPL об этом было бы нелегко догадаться. Уравнения - возможно, но паттерн-матчинг? Паттерн-матчинг в SASL 76 не такой как в NPL и не такой, какой делали в языках с уравнениями. SASL ленивый, что соответствует одному из требований О'Доннела к "принципиальному подходу". С остальными требованиями О'Доннела все хуже. Функции не только нельзя применять слева от =
, к этому мы уже привыкли. Но нет и объявляемой пользователем специальной их разновидности на этот случай - конструкторов. Мало того, паттерны перекрываются, их порядок имеет значение и они еще и нелинейные:
member () a = false
member (a : x) a = true || обратите внимание на повторяющееся `a`
member (a : x) b = member x b
Другими словами, паттерн-матчинг в SASL 76 больше напоминает ПМ в Прологе, чем ПМ в NPL 75 и прочих языках с уравнениями. Тернер не рассказывает про влияние Пролога, но если перекрытие паттернов - это то, к чему вполне можно просто прийти независимо, совмещая удобство с простотой имплементации, то воспроизведение нелинейности уже сомнительно. Потому, что не делает имплементацию проще.
Тернер, как и Уоррен считает, что ПМ принципиально лучше подхода с селекторами как в Лиспе.
Но из-за отсутствия пользовательских конструкторов ПМ получился и не как в Прологе. На матчинг не конструкторов а только списков, как в языках вроде PLANNER [Hewi09] и в матчере МакБрайда [McBr69], он похож еще меньше.
Нельзя также сказать, что матчинг в SASL 76 это шаг в сторону такого ПМ, каким он обычно бывает в современных ФЯ. Перекрытие паттернов больше похоже на обычный ПМ в ФЯ сегодня, но отсутствие пользовательских конструкторов и гардов - шаг назад по сравнению с NPL.
Не хватает не только фич NPL связанных с матчингом, нет и пользовательских операторов. SASL 76 меньше NPL, даже NPL того же года.
Но для нашего традиционного примера хватит и встроенных конструкторов списка :
и ()
:
def
map f () = ()
map f (a : x) = f a : map f x
map f (1,2,3) where
f x = x + y
y = 1
В стандартной библиотеке есть map
, но все еще нет никаких разновидностей fold
и filter
.
Одна за другой определены как уравнения почти одинаковые функции вроде length
, sum
, product
, or
. Почти нет переиспользования кода с помощью комбинирования комбинаторов. Это и общая бедность библиотеки ФВП (их стало только меньше потому, что из неё исчезли комбинаторы вроде I
и K
) плохо совмещается с чтением книги Берджа. Но может это и понятно, когда столько нового сразу. Тернеру просто хотелось писать уравнения с паттерн-матчингом.
Но следы влияния книги Берджа заметны в другом. Например, тот самый "квиксорт":
sort () = ()
sort (a : x) = sort m ++ a : sort n
where m, n = split a x () ()
split a () m n = m, n
split a (b : x) m n = b < a -> split a x (b : m) n
split a x m (b : n)
видимо, старейший дошедший до нас вариант на функциональном языке (не на псевдокоде Берджа и не на Прологе).
Тернер не приводит решение все равно не требующей ленивости проблемы кромок. Вычислитель простых чисел в руководстве Тернера покороче, но не лучше по сути:
primes = sieve (from 2)
sieve (p:x) = p:sieve (filter p x)
filter p (a:x) = a rem p = 0 -> filter p x
a : filter p x
Зато есть вполне приличный минимальный демонстратор ленивости, который применяется и сегодня:
fib = 1 : 1 : map sum (zip (fib, tl fib))
Если NPL, получив уравнения с паттерн-матчингом, в основном сохранил свое старое ISWIM-образное подмножество, то SASL был существенно переработан. Поскольку Тернер хотел сохранить язык небольшим, он не только добавил новые фичи, но и убрал из языка их старые аналоги.
Помните, как обоекембриджцы писали, что если есть where
- то лямбда не нужна? Похоже, что нашелся первый автор языка, которого они убедили. Лямбд в SASL больше нет. Но нет и обычного для ISWIM let
/where
дуализма. Добавив where
, Тернер убрал let
. Странно, что остался тернарный оператор, хотя Тернер должен был видеть замену для него в NPL - гарды. Но не будем забегать вперед.
Как видите, новый язык имеет с SASL 75 не так много общего. Но Тернер снова назвал язык "SASL". Может быть для того, чтоб университеты, которые, по его словам, заинтересовались SASL в 75-ом году нашли не старый заброшенный язык, а новый и живой. Тернер больше не называл два существенно отличающихся языка одним и тем же именем. Следующие его новые языки не будут называться SASL, хотя отличаются от SASL 76 меньше, чем он отличается от SASL 75.
Новая имплементация SASL с помощью "прокрастинирующей" SECD написана на C для UNIX [Abra81], после Тернера над ней работал Уильям Кэмпбелл (William Campbell) [Abra81] [Coro83]. Описание вышло как неотсканированный отчет в июле 79-го. Значительная часть этой работы была проделана в Университете Сент-Эндрюса уже без участия Тернера, который перешел работать в Университет Кента (University of Kent) в январе 77-го [Turn12].
Интерпретатор переписывали на BCPL для MTS в Университете Британской Колумбии [Abra81] и в Сент-Эндрюсе на разработанный там язык S-algol для экспериментов с эмулятором параллельной машины. Переписан он был из-за того что с кодом на C было тяжело экспериментировать [Coro83]. Код на S-algol дошел до нас и в нем меньше 3K строк. Размер кода на C был скорее всего примерно таким же, языки отличаются непринципиально.
Эта реконструкция событий не очень надежна. Почему она вообще важна? Она может определить наследие Тернера как имплементатора. Насколько ему удалось сделать ленивые ФЯ практичнее, а не только навести на мысль о том, как это сделать. К этой проблеме мы вернемся позднее.
В коде на ленивых языках из приводимых нами до сих пор примеров не хватает чего-то важного. Чего же не хватает этим рекурсивным уравнениям с ПМ? Конечно же аннотаций.
Мы уже упоминали написанную в 77-ом но изданную намного позднее работу Джеральда Шварца [Schw82] об аннотациях для NPL, которые предоставляют компилятору информацию, нужную для того, чтоб компилировать уравнения с ПМ в эффективный код. Но основная часть статьи лучше подходит к этой главе, чем к предыдущей. Потому что NPL, который Шварц расширяет аннотациями - ленивый по-умолчанию NPL.
В своей трансформационной системе для NPL Фезер рассматривает вместе с прочими "проблему телеграммы". "Проблема телеграммы" была сформулирована Питером Хендерсоном (да, одним из авторов статьи про ленивый вычислитель) и Сноудоном как упражнение по структурному программированию. В упражнении нужно разбирать поток символов неизвестной длины в поток телеграмм, для каждой из которых определяется число подлежащих оплате слов и наличие слов длиннее заданного лимита. Фезер решал эту проблему слиянием функций, как Бурсталл с Дарлингтоном решали проблему кромок. Шварц решает проблему так же, как решали проблему кромок Хендерсон с Моррисом - с помощью ленивости. Но не только.
Внимание на проблему телеграммы обратил Рейнольдс, который посчитал, что она идеально подходит для демонстрации ленивости. Он, по словам Шварца, был удивлен, что одной ленивости по-умолчанию для наилучшего решения таких задач недостаточно. "Может показаться, что вызов по необходимости всегда лучше, чем вызов по значению" - пишет Шварц. Но использование стека для уменьшения аллокаций в куче и избегание других оверхедов ленивости делают вызов по значению предпочтительнее в некоторых случаях.
Так что Шварц предлагает аннотировать способ передачи параметров. Передача параметра по значению аннотируется f(t/value)
. Шварц рекомендует аннотировать так аккумуляторы.
Паттерн-матчинг также может быть разной степени ленивости. Если у Тернера все паттерны "неопровержимые" по-умолчанию, то в ленивом NPL - нет. Там как в Хаскеле требуется аннотация. Аналог хаскельного where (~a,~b) =
выглядит у Шварца так: where/lazy <a,b> =
.
Шварц рассматривает проблему аналогичную известной хаскельной проблеме вычисления среднего значения списка чисел, которое материализует весь список. И для решения придумал аннотацию /opportunity
, а также еще несколько идей, которым мало что соответствует сегодня. Вроде /almost-tail-recursive
и /constructor
. Шварц отмечает, что считает свой синтаксис аннотаций неудачным и одной из основных проблем своей работы.
Все это Шварц изобрел рисуя диаграммы. Потому, что ленивый NPL, судя по всему, не был имплементирован. Ленивость появилась в NPL совсем в другом виде.
С января 1977 Тернер работает в Университете Кента. В том же году и Дарлингтон уехал из Эдинбурга, так что контакт Тернера с Эдинбургской программой временно оборвался. Тернер еще не закончил адаптировать идеи, которые узнал от Дарлингтона, но на время отвлекся от этого занятия.
В первые два семестра у Тернера было не очень много преподавательской работы, так что он решил воплотить в жизнь идею, над которой думал уже не один год [Turn12].
В это время для имплементации передачи функций в функции, хотя-бы и только вниз по стеку, используются структуры в памяти - окружения. И уже существует широкий спектр подходов для имплементации окружений.
От наивного подхода первых интерпретаторов Лиспа со списками пар. Функция pairlis
создает окружение, параллельно соединяя два списка: список имен аргументов и список значений, к которым применена функция, и присоединяя получившийся список пар к начальному. Функция assoc
- предок современных функций вроде Data.List.lookup
- работает с такими окружениями. Обходит в куче такой односвязный список пар в поисках нужного имени и возвращает первый найденный результат, который таким образом затеняет все добавленные до него [McCa62].
И до подхода из имплементаций ALGOL 60 со статической цепью и дисплеем. Статическая цепь - это список участков стека в порядке лексического вложения, а дисплей - это массив ссылок на эти участки [Rand64].
Имплементации SECD пока что ближе к первому представлению и улучшить их вполне можно.
Но все это давалось имплементаторам тяжело, с большим числом ошибок и небольших и концептуальных. Из-за которых видимость часто работала не так как нужно для функционального или даже хоть какого-нибудь программирования.
Что если разом избавиться от всего этого? Ведь именованные переменные не нужны. Еще в ревущие двадцатые годы логики придумали, как переписывать выражения так, чтоб никаких переменных не осталось. Тернер решил испытать такой радикальный подход: заменить окружения набором констант-комбинаторов [Turn79].
Трансляция лямбд в комбинаторы, например S
и K
, известна функциональным программистам в это время. Она есть как в книге Карри [Curr58], так и в книге Берджа [Burg75], хотя и не как пример способа имплементации ФЯ. Простейший алгоритм - только несколько строк кода.
Тернер называет множество плюсов такого подхода. Для исполнения получающегося в результате трансляции комбинаторного кода нужна простая машина. И эта простая машина делает довольно много. В отличие от SECD машины, она переписывает исполняемый код и работает как оптимизатор. Тернер имплементировал свертывание констант для имплементаций SASL [Turn12], но комбинаторный интерпретатор заменяет все константные выражения на их результаты при первом использовании [Turn79]. И не только константные, ведь оптимизация происходит во время выполнения. Индерекшены, которые оставляют после себя ленивые вычисления, "излечиваются" интерпретатором также автоматически. Это просто применение I
. Сборщик мусора может копировать меньше исполняя при копировании код программы. Первый вызов функции приводит к тому, что её тело инлайнится. Абстракция бесплатна. И эта бесплатность абстракции почти бесплатна и для имплементатора. Какие-то сотни строк кода против десятков тысяч во всяких сверхсложных трансформирующих код компиляторах, делающих то же для лямбд.
И раз абстракция бесплатна, Тернер приступает к переписыванию библиотечных функций так, как завещал Бердж [Burg71] [Burg72] и ссылается на его книгу [Burg75]. Объясняет то, что не делал этого раньше тем, что в SECD имплементации у такого подхода будет слишком большой оверхэд.
Тернер описывает правую свертку, впервые с современным названием foldr
def foldr op a = f
where
f x =
x = nil -> a;
op (hd x) (f (tl x))
и переписывает то, что раньше записывал как наборы рекурсивных уравнений
def sum = foldr plus 0
def product = foldr times 1
def all = foldr and true
def some = foldr or false
(В реальном коде на SASL76 нужен только один def
на файл, не понятно, почему Тернер пишет так в статье [Turn79]. То, что он пишет код в статье, в основном, без использования новых синтаксических фич SASL - более понятно, но об этом позже.)
Программист может смело добавлять бесплатные слои абстракции не только потому, что решена проблема производительности. Проблема генерации кода для того, что не используется тоже решена. Ведь оптимизируется только то, что вызывается во время исполнения. Никакой генерации массы специализированного кода, который никогда не будет вызван.
Думаем, сложно переоценить, насколько уютен для имплементатора ФЯ этот локальный оптимум, в котором Тернер впервые оказался в 1977-ом году. Вернувшись туда несколько позже, Тернер так уже и не выбрался из него, в поисках чего-то получше. Не смотря на то, что продолжал имплементировать ФЯ еще долгие годы. Только один раз он использовал другой, еще более минимальный подход для совсем уж неамбициозной имплементации.
Но если преобразование из ЛИ в комбинаторы - такой хороший способ имплементации ФЯ, то почему сегодня его можно встретить разве что как плагин для lambdabot
или утилиту pointfree
? Разумеется, не все так просто.
Даже если оптимизируется только то, что исполняется, результаты оптимизации могут быть слишком велики и может понадобиться заменить эти результаты на первоначальный неоптимизированный код в рантайме, во время сборки мусора, например.
Трансформация кода приводит к тому, что сложно восстановить из какого кода произошли те комбинаторы, в которых обнаружилась ошибка в рантайме. И как выглядел бы стек без этих трансформаций. Тернер пытается все это сделать, но считает, что скоро это будет не так и нужно. Почему? Тернер хочет добавить в SASL проверку типов. Уже 77-ой год, LCF/ML работает, проблема типизации ФЯ в принципе решена. Сейчас Тернер в своих воспоминаниях говорит, что у проверки тегов во время выполнения могут быть плюсы [Turn12] [Turn19], но в 77-ом он считает, что "почти все" программы которые выдают ошибки в рантайме после добавления проверки типов будут выдавать ошибки компиляции [Turn79]. Компилируется - работает!
Но, наверное, после того, как так удачно избавился от страданий с окружениями, сложно заставить себя снова начать страдать с ними уже ради тайпчекера. Так что типы в SASL не появятся, и вообще появятся в языках Тернера только ближе к середине 80-х годов.
Но даже с той самой трехстрочной трансляцией лямбд в комбинаторы не все так просто. Еще Тернеру и его предшественникам понадобилось решить ряд проблем. Если ограничиваться минимальным набором комбинаторов вроде S
и K
, то комбинаторный код будет намного больше, чем лямбды и быстро расти в зависимости от размера лямбд. Проблема проявляет себя даже на крошечных примерах, в результате ей занимались еще логики. И книга Карри [Curr58] содержит одно из решений, которое использовал Тернер. Нужно добавить два комбинатора B
и C
, они же (.)
и flip
. Версии комбинатора S
, в которых один из параметров принимает константу. И четыре правила оптимизации, которые преобразуют специальные случаи применения S
. Оптимизацию можно совместить с преобразованием из лямбд, так что плохой код огромного размера не существует на промежуточных этапах. Имплементация становится сложнее, но увеличивается только на несколько строк. И качество кода становится существенно лучше.
Еще один комбинатор, который нужно добавить - Y
и имплементировать его так, чтоб создавалась циклическая ссылка. Имплементация через S
и K
и правила редукции как в учебнике приводят к плохой производительности. Если имплементировать всю рекурсию как Y
, не создающий циклической ссылки, то можно управлять памятью только с помощью счетчиков ссылок, сборщик мусора не нужен. Но Тернер считает, что это только повредит производительности.
Наработок из книги Карри все еще недостаточно, чтобы получать такой компактный код, как у SECD. В реальном коде множество связанных переменных и по мере того, как Тернер удаляет их одну за одной, комбинаторный код растет:
-- убираем первую и из
S a1 b1
-- получаем
S(B S a2)b2
-- избавляемся от следующей
S(B S(B(B S)a3))b3
-- и еще одной
S(B S(B(B S)(B(B(B S))a4)))b4
Растет "как минимум квадратично".
Тернер решил проблему добавив еще комбинаторов и правил переписывания в оптимизатор [Turn79b].
Если добавить параметр к комбинатору S x y z = x z (y z)
, то получаем комбинатор S' a x y z = a(x z)(y z)
, который позволяет записывать предыдущий пример так:
S a1 b1
S' S a2 b2
S'(S' S) a3 b3
S'(S'(S' S)) a4 b4
Разумеется, аналогичные версии нужны и для B
и C
. Предложенные Тернером B'
комбинатор и соответствующее правило оптимизации на самом деле неправильные и делают комбинаторный код только хуже [SPJ87]. Но это будет обнаружено позже и исправлено не Тернером.
Уэлч (P. H. Welch) обратил внимание Тернера на то, что алгоритм для получения компактного кода уже придуман Абдали, одним из последних независимых изобретателей продолжений [Reyn93].
Камаль Абдали (Syed Kamal Abdali) в своей диссертации [Abda74] 74-го года и статье [Abda76] того же года, которая была опубликована только в 76-ом году, рассматривает другое решение проблемы, с которой столкнулся Тернер.
Абдали не удаляет связанные переменные из терма последовательно, одну за одной. Он делает это одним шагом. И добавляет не три комбинатора, а неограниченное количество. Точнее, три правила для генерации неограниченного количества комбинаторов K(n)
, I(n,m)
и B(n,m)
. Можно создавать и создавать такие все более крупные специализированные комбинаторы, пока у вас есть для них место. А что останется без специализаций - будет работать не с бинарными деревьями применений комбинаторов к комбинаторам, а с массивами. С понятными последствиями для компактности представления, производительности и распараллеливаемости. Абдали, правда, занимался формальной семантикой ЯП, а не их имплементацией, так что никаких компиляторов и интерпретаторов не написал. Ускорять и параллелизовать было нечего.
Как и в случае Тернера, внимание Абдали также обратили на то, что уже проделана серьезная работа по решению проблем, которыми он занимался. В случае Абдали это сделал лично Хаскель Карри. Карри указал ему на то, что публиковал собственный алгоритм который работает с несколькими переменными одновременно в 33-ем году. В книгу 58-го этот алгоритм просто не попал. Вероятно потому, что для логиков эта проблема была не так актуальна, как для программистов. Абдали все равно опубликовал алгоритм потому, что его версия проще. Какие же оправдания были у Тернера?
Один из имплементаторов ФЯ и будущих героев нашей истории Стюарт Рэй (Stuart Charles Wray) позднее считал, что подход Абдали позволил бы генерировать более компактный и быстрый код, чем подход Тернера [Wray86]. И даже сам Тернер был согласен с утверждениями о компактности [Turn79b]. Но необходимость в большом количестве комбинаторов и необходимость в сложно устроенных комбинаторах для тех случаев, которые не покрываются специализациями Тернер считал проблемой. Слишком сложной будет машина, которая исполняет комбинаторный код. Почему он считал это важным - отдельная история. Но когда эта история более-менее закончилась, и Тернер и другие имплементаторы ФЯ будут делать эти сложные комбинаторы уже другими способами. Идеи Абдали для имплементации ФЯ так и не используют [Wray86]. Это не тот путь, по которому пошли реальные имплементаторы ФЯ, которые хотели улучшить Тернеровский комбинаторный подход.
[Turn79]
Тернер сравнил имплементацию строгого SASL с помощью SECD, имплементацию ленивого SASL с помощью ленивой SECD и новую имплементацию SASL с помощью SK-машины на нескольких сотнях строк бенчмарков.
Бенчмарков? Да, мы не забыли упомянуть о такого рода исследованиях для PAL, LCF/ML и NPL. Их просто не было. Тернер один из немногих и из первых имплементаторов Эдинбургской программы, который этим занялся и даже что-то опубликовал.
Благодаря Тернеровским S' B' C'
комбинаторный код в памяти примерно в два раза меньше SECD-кода. Со сравнением производительности этого кода получилось уже не так хорошо.
Тернер пишет, что не может сравнивать скорость имплементаций непосредственно. Имплементации написаны на разных языках, для разных ОС и машин. В статье о комбинаторном интерпретаторе [Turn79] Тернер не пишет на каких языках и для каких ОС и машин. Про энергичный SECD SASL мы знаем, что он написан на BCPL [Turn12]. Но есть свидетельства, что обе имплементации ленивых SASLов на C для UNIX. И на прокрастинирующей SECD [Abra81] и комбинаторная [Turn83] [Bund84]. Поэтому странно, что Тернер сравнивает их между собой так же, как сравнивает их со строгим SASL.
Комбинаторной машине нужно больше шагов редукции, чем строгому SECD но Тернер считает, шаг комбинаторной занимает меньше времени. Поэтому Тернер применяет тот же способ оценки, какой применяют разработчики GHC и сегодня: сравнивает аллокации. На микробенчмарках в которых мало применений функций SK-машина аллоцирует раза в два больше, чем SECD, а на тех, в которых в основном только функции и применяются наоборот - SECD аллоцирует в два раза больше. Сравнение со строгой SECD Тернеру интереснее и он в итоге заявляет, что переписывание графов сочетает "безопасность нормального порядка" с "эффективностью аппликативного порядка"
[Turn79]. Под безопасностью тут понимается, что нормальный порядок доредуцирует до нормальной формы, а аппликативный - как повезет.
Сравнению с прокрастинирующей машиной уделяется меньше внимания, но отмечается, что если эффективность аппликативного порядка если и не достигается SK-машиной во всех случаях, то уж точно достигается лучше, чем прокрастинирующей SECD-машиной, которая аллоцирует в десять раз больше, чем строгая.
Но если первая имплементация ленивого SASL настолько хуже, почему Харви Абрамсон портировал именно её? Годы спустя Саймон Пейтон Джонс [SPJ82] имплементировал упрощенные версии всех трех машин на одном языке (BCPL). Специально для того, чтоб произвести замеры размеров кода и аллокаций в куче и на стеке. И разница между машинами оказалась гораздо меньше. Но нам интереснее сравнение реальных имплементаций, которое запланировано в отдельной главе.
Почему годы спустя? Потому, что статьи Тернера про улучшенный алгоритм трансляции из лямбд в комбинаторы и про имплементацию SASL с помощью комбинаторов были получены издательством соответственно в октябре и декабре 1977-го, а опубликованы только в 1979-ом году. В августе того же 1979-го года вышла ревизия руководства по SASL 76 для того, что Тернер называет ""комбинаторная" версия", а мы будем называть SASL 79. Имплементация изменилась существенно, но изменения в языке незначительны - добавлены только числа с плавающей точкой.
Если в 1976 более-менее законченный вид принял язык, и прочие версии были только редакциями, то в 1979 это произошло с библиотекой, версия библиотеки 83-го года, будет только редакцией.
Правда, эта версия 79-го года до нас не дошла, только отредактированная в июле 84-го [Turn83]. Так что, хотя большая часть изменений должны были быть сделаны до августа 79, какие-то из них появились на годы позже.
В стандартной прелюдии SASL появляется filter
с современным названием:
filter f () = ()
filter f (a:x) = f a -> a:filter f x; filter f x
А также foldr
, который написан как и в статье [Turn79] с вложенной функцией, у которой на один аргумент меньше, но с использованием группы уравнений:
foldr op r
= f
WHERE
f () = r
f (a:x) = op a(f x)
И некоторые функции написаны или переписаны как частичные применения foldr
:
some = foldr or FALSE
Но некоторые, как map
, так и остались рекурсивными уравнениями.
Конечно, не только foldr
используется для имплементации библиотечных функций. Функция member
, которую мы использовали выше для демонстрации паттерн-матчинга в SASL 76, теперь написана так, что ПМ уже не продемонстрируешь:
member x a = some(map(eq a)x)
Появляется и foldl
с современным названием, но не современного вида.
foldl op r () = r
foldl op r (a:x) = foldl op(op a r)x
Не тот порядок аргументов у op
что сейчас. Как у Берджа [Burg72], чтоб можно было писать reverse
так:
reverse = foldl cons ()
Но число и порядок аргументов уже не как у Берджа.
Функция length
имплементирована как нехвосторекурсивные уравнения, а не с помощью foldl
. Но foldl
ленивый, так что может это не так и странно.
I
и K
снова в библиотеке.
На SASL 79 заканчиваются интересные результаты Тернера как имплементатора, но основные его результаты как разработчика ФЯ еще впереди.
Я пытался изобрести функциональный эквивалент языка BASIC
Дэвид Тёрнер [Turn19]
Раз уж Тернер теперь работает в университете Кента, начав разрабатывать новый язык, он снова назвал его по месту работы - KRC (Kent Recursive Calculator). Но это последний язык Тернера, который он назвал по месту работы.
Как мы помним, остается еще много того, что Тернер должен был увидеть в NPL 75 и может позаимствовать оттуда. Чем Тернер и занялся.
Одним новым заимствованием из NPL стали гарды, выглядевшие в NPL 75 так:
f x <= r1 if p
<= r2 otherwise
Тернер сделал синтаксис легче
f x = r1 , p
= r2
Это, по всей видимости, рекорд легкости синтаксиса гардов.
И Тернер использовал эту очень легковесную синтаксически фичу для того, чтоб писать больше кода. Почему?
Возможно, Тернеру было несколько некомфортно делать все эти большие шаги в сторону современного вида уравнений с ПМ. И современный их вид - это не то, что имело широкое признание в узких кругах тех, кто вообще знал про паттерн-матчинг в 70-х. Может быть, Уоррен посчитал такие шаги разумными, но мало кто еще. И нет особых свидетельств того, что Тернер с Уорреном знали о том, как они могли бы быть друг с другом согласны.
К счастью, Тернер не стал запрещать перекрытие паттернов или делать гарды обязательными. Он стал дописывать [Turn81] гарды в некоторые примеры, которые он использовал для демонстрации краткости кода.
A 0 n = n + 1
A m 0 = A (m-1) i, m>0
A m n = A (m-1) (A m (n-1)), m>0&n>0
И явно отмечать, что эти гарды дописаны для того, чтоб уравнения можно было переставить в другом порядке. То есть стал писать как на SCRATCHPAD.
В ФП этот подход, конечно, не прижился. На что Пролог мог повлиять более чем одним способом. Так что, когда мы сегодня видим один из бесчисленных примеров функции Фибоначчи, то в нем нет гарда в третьем уравнении, а у Тернера какое-то время был:
fib 1 = 1
fib 2 = 1
fib n = fib (n-1) + fib (n-2), n>2
Но Тернер вскоре перестал писать эти необязательные гарды. Тем более, что сделать с их помощью переставляемыми уравнения с матчингом чего-то кроме чисел уже совсем не так легко.
Паттерн-матчинг в KRC приблизился к современному виду ближе, чем в каком-то другом ЯП 70-х. Если, конечно, забыть о том, что нет пользовательских конструкторов. Ну, по крайней мере максимально приблизился в деталях, если не в главном.
Другим новым заимствованием из NPL были конструкторы множеств, выглядевшие в NPL так:
<: f(x) : x in S & p(x) :>
Тернер называет их ЦФ-выражениями (ZF expressions). Их синтаксис Тернер тоже сделал легче:
{ f x | x <- s ; p x }
или
{ f x ; x <- s ; p x }
Тернер, судя по коду примеров [KRC81], начал с ;
, потом перешел на |
, а затем обратно на ;
, когда обнаружил конфликты парсинга с операцией "или". Но поддержка |
в языке осталась.
ЦФ-выражения работают со списками, а не с множествами, имплементированными как списки, как в NPL. Т.е. дубликаты элементов не удаляются крайне неэффективным способом. Но для конструирования таких множеств крайне неэффективным способом есть функция mkset
. Раз уж работа идет со списками, можно имплементировать стандартные списочные функции с помощью новой фичи, что и сделано [KRC81]:
filter f x = {a|a<-x;f a}
Но функция map
в прелюдии имплементирована не так. Кстати, обратите внимание, что больше никаких DEF
, даже и одного на целый файл.
Первоначально, filter
можно было записать проще:
filter f x = {a<-x;f a}
Просто повторять a
перед |
было не нужно, раз уж генератор единственный и к a
ничего не применяется. Но это упрощение для особых случаев было убрано из языка, хотя использующий его код остался в примерах [KRC81].
После этого изменения ЦФ-выражения стали, на первый взгляд, предельно близки к современным лист компрехеншонс. Если не считать мелких синтаксических деталей вроде ;
и того, что скобки не совпадают со скобками для списков, которые в KRC квадратные, как в LCF/ML и NPL, а не как в SASL 76/79. Но это только на первый взгляд.
Тернер не слышал о SETL до 80-х годов. Тернер уверен, что и Дарлингтон не слышал, и потому считает, что конструкторы множеств в NPL изобретены Дарлингтоном независимо [Turn19]. Но Дарлингтон не изобрел как сделать так, чтоб они хорошо работали. Вероятно, это можно посчитать доводом в пользу того, что Дарлингтон изобрел их независимо и от авторов SCRATCHPAD.
Обычный конструктор множества - это как проблема кромок. И если Дарлингтон пытался решать и то и другое трансформацией и решение не заработало, то Тернер решил, как решал Моррис - с помощью ленивых списков.
Но Тернер прилагает больше усилий, чтоб починить протокомпрехеншоны Дарлингтона, чем прилагают более поздние их имплементаторы:
krc> take 4 {[a,b]|a <- ["True","False"];b <- [1..]}?
[["True",1],["False",1],["True",2],["False",2]]
Результаты генераторов чередуются. В отличие от Хаскеля:
ghci> take 4 [(a,b)|a <- [True,False],b <- [1..]]
[(True,1),(True,2),(True,3),(True,4)]
False
никогда не появится. Как авторы языков спецификации пытались сделать больше неработающего сегодня кода с паттерн-матчингом работающим, так и Тернер пытался сделать больше неработающего сегодня кода с лист-компрехеншонами работающим.
Обратите, также, внимание на современную нотацию для получения списков чисел в KRC. [a,b..c]
тоже работает.
Но паттерн-матчинг слева от <-
не работает, как и в SCRATCHPAD и в NPL. i,j <- xs
означает не ПМ, а то же, что i <- xs; j <- xs
. В SETL же к этому времени можно было хотя бы разбирать туплы: {y : [x,y] in xs | x /= 0}
[Dewa79].
Как раз в то время, когда Тернер исправил конструкторы множеств, сделав из них ЦФ-выражения, из новой версии NPL их убрали. Тернер считает, что убрали потому, что со строгими списками они бесполезны. Но это говорит только о том, что после потери контакта с Эдинбургской программой Тернер не особенно внимательно следил за NPL. Конструкторы множеств появились в NPL потому, что там не было ФВП. И исчезли скорее всего потому, что ФВП там появились.
Но если в NPL конструкторы множеств были потому, что там не было ФВП, то зачем они в KRC, в котором ФВП есть?
Как и в случае с SASL 76, если Тернер что-то добавляет, то считает нужным что-то и отнять.
Или передумать и не отнимать. Раз есть гарды - нелинейные паттерны не так и нужны и больше не используются:
assoc ([a,b]:x) a' = b, a = a'
= assoc x a'
Бывший пользователь нелинейных паттернов, функция member
, обошлась и без гард.
member [] a = "FALSE"
member (a:x) b = a = b | member x b
Наверное, нелинейные паттерны не попали в KRC? Нет, они там есть. Почему же они не используются? Трудно сказать. Может быть их сначала не было, может быть Тернер только собирался их убрать. По той или иной причине, код написан так, как будто их нет.
Разумеется, другие удаления фич вполне состоялись. Еще как!
KRC - одно из самых смелых и радикальных высказываний Тернера как разработчика языков программирования. NPL 75 и SASL 76, получив уравнения с паттерн-матчингом, сохранили и какие-то конструкции-выражения, позаимствованные у обоекембриджцев. Так и не стали языками уравнений в чистом виде. Мы писали в прошлой главе про языки Эдинбургской программы как смешение ISWIM и NPL. Но такое описание портит то, что NPL сам по себе смешение ISWIM с языком уравнений.
Тернер, победив в SASL 76 let
и lambda
, не остановился на достигнутом. В KRC нет тернарного условного оператора. Понятно, что он не нужен - есть же гарды. Это довольно логично.
Что действительно впечатляет, так это то, что в KRC нет выражения where
. Да, ни let
, ни where
, ни лямбд. Не все так ужасно, как может показаться на первый взгляд: функции объявляются каррированными и их частичное применение делает ФП более-менее возможным, но, как, например, в POP-2 не особенно удобным.
map f [] = []
map f (a:x) = f a:map f x
f y x = x + y
{map (g y) [1..3]|y <- [1]} 1?
Все труды имплементатора, которые нужны для поддержки лямбд, let
и прочего нужны и для имплементации вот этого вот. Вложение областей видимости возможно, просто должно выглядеть ужасно. Потому, что не должно быть двух фич, которые делают одно и то же (если только это не нелинейные паттерны).
Так что ЦФ-выражения нужны в KRC не намного меньше, чем в NPL.
{x + y|y <- [1]; x <- [1..3]}
Разумеется, let
нет и в ЦФ-нотации.
KRC легко сочетает красоту одного из самых легких ФП-синтаксисов с неожиданным уродством, появляющимся, когда захочется воспользоваться возможностями, поддержка которых для ФЯ, казалось бы, сама собой разумеется. Понятно, даже и Тернеру, что декембриджизация зашла слишком далеко и куда-то не туда.
И Тернер сделал шаг назад, добавил ЦФ-нотацию в SASL, но не перенес в SASL более радикальные идеи из KRC.
Позднее Тернер все-таки придумал, как сделать язык с вложенными функциями в стиле уравнений, а не ISWIM. Не знаете ни одного современного языка в котором ушли от лямбд и let
? Да, это направление ухода от синтаксиса выражений к синтаксису уравнений было тупиковым. Но движение в тупик не было напрасным, ведь в этом тупике Тернер нашел свое ключевое синтаксическое изобретение, определившее вид современных ФЯ. Но это уже совсем другая история.
KRC имплементирован не с помощью SK-машины, а способом, который обычно предшествует каким-то "машинам" - переписыванием абстрактного синтаксиса в памяти [Hugh83]. В данном случае не дерева, а графа, так что имплементация не самая медленная из возможных, но одна из самых медленных. Преобразования при компиляции в SK-код и его оптимизации простые. Но не проще, чем их, преобразований, отсутствие.
Равнодушие к производительности, видимо, может мотивировать высокоуровневую имплементацию стандартной прелюдии не хуже чем "бесплатность" абстракции. По крайней мере функции в ней часто определены с помощью ФВП, таких как foldr
, которая называется просто fold
:
fold op s [] = s
fold op s (a:x) = op a (fold op s x)
Функции foldl
нет. Возможно, она не нужна, если не заботиться о производительности. Хотя сомнительно, что кто-то измерял производительность foldl
в прелюдии ленивого SASL. Потому, что это ленивый foldl
.
Тернер имплементировал KRC на BCPL для EMAS ОС на ICL 2960 с ноября 79 по октябрь 81-го [KRC2016]. Эта имплементация использовалась в Университете Кента для преподавания с 80-го до 86-го года и в Оксфорде в начале 80-х [Turn16].
Но почему вдруг такая простая имплементация и снова на BCPL, на котором Тернер ничего не имплементировал начиная с SASL 76. И почему для преподавания не использовался SASL 79, который сам появился как язык для преподавания? Об этих причинах Тернер ничего не рассказывает в своих воспоминаниях. Так что нам остается только разобраться, что это за EMAS. Поскольку у EMAS не особенно много пользователей, история операционной системы EMAS описывает и историю её использования в Университете Кента.
Университет Кента владел мэйнфреймом ICL 2960 c 1976 по 86. Это была младшая машина линейки со специальной версией ОС VME, которая нормально не работала. Падала чаще раза в день. К тому же, производитель компьютера решил еще и урезать ее функциональность в 79-ом году. В результате, в Университете Кента решили перейти на разрабатываемую в Эдинбурге ОС для этой линейки - EMAS (Edinburgh Multi Access System) [Eager]. Что и было сделано в декабре 79-го [Eage22]. В отличие от VME, для EMAS существовал компилятор языка, на котором Тернер умел писать код - BCPL.
Так что, когда заработал компьютер, который использовали для преподавания, Тернер проскочил в образовавшееся окно возможностей со своим быстро и просто имплементированным "функциональным Бейсиком". В 85-ом году, не задолго до перехода с ICL 2960 на машину, на которой можно было использовать UNIX и C, KRC был переписан как SK-интерпретатор на C Саймоном Крофтом (Simon Croft) [Turn16]. Все сходится!
Имплементация KRC на BCPL Тернера - первая его имплементация, код которой дошел до нас [KRC81]. В 2016 Тернер портировал её на C [KRC2016]. Эта имплементация компилируется и работает в момент написания этого текста и использовалась для того, чтоб подтвердить или опровергнуть некоторые наши гипотезы о KRC. Но использовать её для этого нужно с осторожностью. Например, Тернер зачем-то поменял при портировании индексацию списков. В KRC 81-го года она начинается с единицы, а в KRC 2016 - с нуля. Нельзя полностью исключить, что он и нелинейные паттерны имплементировал в 2016-ом году.
К концу 70-х Тернер создал уже целое семейство ленивых ФЯ с несколькими имплементациями и несколькими пользователями. Что же с функциональным программированием, появилось ли оно?
В 83-ем году более известный другими своими работами Саймон Пейтон Джонс решил, что ему известно очень мало программ даже "среднего размера" на функциональных языках. А таких программ на имплементированном Тернером ленивом SASL не известно вовсе. Так что Саймон Пейтон Джонс написал [SPJ85] на SASL генератор парсеров в 835 LOC. Для сравнения, его программа той же функциональности на BCPL имела размер в 1501 LOC.
И с годами, по-видимому, такие программы не стали доступны и известны даже тем, кому они должны были быть интересны. Потому, что даже в 87-ом году обсуждаемый генератор парсеров - самая большая программа в наборе бенчмарков [Hart88] для очередной имплементации SASL. Получается, что не смотря на намного большую распространенность и время жизни, не известно о существовании открытых программ на KRC и SASL 76-83 даже такого размера, как IDEA на SASL 75.
Ну, хотя-бы Тернер наконец защитил диссертацию о комбинаторной имплементации SASL, которая напечатана в 81-ом году и тоже не отсканирована, как и диссертация Вадсворта, с рассказа о которой мы начали эту главу.
Хотя SASL и KRC - самые успешные и важные для нашей истории ленивые языки на рубеже 70-х и 80-х, они не единственные ленивые языки этого времени. Упоминаемые нами в прошлой главе, язык уравнений О'Доннела и гибрид TEL с Прологом под названием FPL [Levi82] также ленивые языки. Но разработчики ленивых языков в это время совсем не так хорошо связаны как разработчики языков спецификации, не смотря на существенное пересечение этих групп. Так, О'Доннел даже в 1984 году пишет [O'Do84], что не знает никаких ленивых языков, кроме своего языка уравнений. Не знает даже о языке SASL, на который ссылается Бурсталл в статье, на которую О'Доннел ссылается сам.
Очередное подтверждение того, что стоит с осторожностью делать выводы о том, прочел ли ссылающийся на статью эту статью. И с еще большей осторожностью к выводам о том, что сославшийся на статью ознакомился с тем, на что ссылаются в этой статье.
Но что это за статья Бурсталла, в которой ссылаются на SASL?
Сейчас, когда мы закончили рассказывать историю всех составных частей для сборки функционального языка Эдинбургской программы, как он был определен в предисловии, пришло время рассказать о первой попытке собрать такой язык, а вместе с этим и подвести итоги 70-х годов.
A very high-level language such as HOPE pays penalties of inefficiency because it is remote from the machine level. It could be thought of as a specification language in which the specifications are 'walkable' (if not 'runnable')
Р. М. Бурсталл, Д. Б. МакКвин, Д. Т. Саннелла. HOPE: экспериментальный аппликативный язык [Burs80]
Первым к сборке ФЯ из подготовленных трудами Эдинбургской программы деталей приступил Дэвид Тернер еще в 1976. Но эта его попытка затянулась почти на десятилетие, так что лучше мы начнем с того, кто первый закончил такую попытку. А это сделал Бурсталл с двумя новыми соавторами.
В 1979-ом году Бурсталл сделал очередной программный доклад на конференции [Burs79]. Доклад назывался "Аппликативное программирование" и текст этого программного доклада не сохранился так же, как и текст предыдущего. Но сохранилась аннотация. В докладе Бурсталл рассказал о преимуществах и недостатках аппликативного программирования, которое "также называется непроцедурным или функциональным", происходит от чистого Лиспа и описано в книге Берджа [Burg75]. Бурсталл записал в это функциональное программирование Пролог и все логическое программирование вообще, а также анонсировал новый аппликативный язык - HOPE, с которым он в данный момент экспериментирует.
Язык назван в честь адреса группы экспериментального программирования, располагавшейся на Хоуп Парк Сквер (Hope Park Square) [Ryde82] [Ryde2002]. Здание группы экспериментального программирования располагалось рядом с парком и Хоуп - фамилия организатора осушения земли, на которой парк был устроен. Но HOPE последовательно записывается заглавными буквами. Возможно, что это еще и акроним? Мы не видели расшифровок в статьях того времени и в более поздних воспоминаниях авторов, но есть работа по истории ЯП [Pigo95], ссылающаяся на недоступные в электронном виде источники, в которой расшифровка есть. HOP означает "Higher Order Parameters", а E
, в таком случае, видимо, означает Extension? Звучит правдоподобно для названия ФВП расширения NPL.
Описания первых двух версий HOPE опубликованы как отчет Эдинбургского университета 80-го года и его редакция от февраля 81-го. Но эти отчеты недоступны, так что о развитии от HOPE 80 до HOPE 81 мы будем как обычно судить по статьям [Burs80] [Burs80b] и описаниям в приложениях к диссертациям [Ryde82] [Sann82].
Бурсталл работал над HOPE с двумя соавторами. Один был имплементатором языка, а другой - одним из первых пользователей.
Третий основной соавтор Бурсталла 70-х годов - Дэвид МакКвин (David MacQueen) - работал в Эдинбурге с мая 75-го года [MacQ15]. МакКвин - один из самых важных авторов и имплементаторов функциональных языков. И уж точно самый важный из тех, про которых (на момент написания этого текста) нет статьи в Википедии.
МакКвин закончил Стэнфордский университет в 68-ом, защитил диссертацию в МТИ в 72-ом. Работал научным сотрудником (Research Fellow) в Университете Эдинбурга 1975-79 [MacQueen].
Сначала МакКвин поработал над Эдинбургской имплементацией POP-2 для PDP-10 - WPOP [Slom89]. Но года с 78-го [MacQ15] или даже с 77-го [MacQ20] он стал работать вместе с Бурсталлом над HOPE, новой версией NPL [Feat79], следующей после той, которую мы тут называем NPL 79. Или, может быть, существующей с ней параллельно.
Дело в том, что NPL 79 начисто отсутствует в воспоминаниях МакКвина. И это самая неподходящая версия NPL чтоб вот так пропасть из памяти. Потому, что она оставила больше всего следов. Многие версии NPL не особенно запоминающиеся, но на NPL 79 написан какой-то код. И версия важная потому, что первая, в которой появляются АлгТД более-менее современного вида. Выпадение такого важного достижения из истории, конечно, не может остаться для неё без последствий. Поэтому, если у нас HOPE - первая завершившаяся попытка собрать ФЯ Эдинбургской программы из деталей отработанных в протоэдинбургских протоязыках, то в исторических работах МакКвина [MacQ15] [MacQ20] HOPE - это язык, впервые испытавший одну из важнейших таких деталей.
МакКвин помнит про один из последних NPL-ей (NPL 75 или NPL 77?), в которых еще были отдельные конструкторы, вместо привычного синтаксиса для объявления АлгТД. И рассказывает теперь, что "закрытые" АлгТД с BNF-образным синтаксисом появились впервые именно в HOPE [MacQ15] [MacQ20]. Возможно, что работа над NPL 79 и HOPE велась параллельно. NPL 79 был версией NPL, которую не забросили так быстро, как все прочие NPL до него. И писали на ней какой-то код только из-за отставания языка, поддерживаемого переписывателем, от языка, поддерживаемого интерпретатором. Дополнительный довод в пользу такого разветвления - то, что NPL 79 это последний интерпретатор Бурсталла. Интерпретатор следующей версии пишет в основном МакКвин. Если это разветвление NPL-линейки на две параллельные вообще было, оно было только репетицией гораздо более важного и длительного разделения NPL-линейки в 80-е.
Но никакого параллельного развития NPL и HOPE могло и не быть, и это просто история, которая должна научить нас с большей осторожностью полагаться на воспоминания о событиях, произошедших несколько десятилетий назад. Совсем не полагаться на которые мы, к сожалению, не можем.
Мартин Фезер в своей диссертации [Feat79] перечислял недостатки NPL. Основными недостатками он посчитал отсутствие средств для абстракции данных и отсутствие ФВП. Также, Фезер писал, что для более полного воспроизведения функциональности первого переписывателя Дарлингтона и Бурсталла в NPL нужно добавить мутабельность. Если не для непосредственного использования программистом, то как примитивы, которые добавляются в процессе трансформации. Фезер знает о работах Бурсталла и МакКвина над первыми двумя недостатками: чтоб получить HOPE они добавляют в NPL абстракцию данных и ФВП.
Дэвид Тернер рассказывает [Turn19] о бесполезности конструкторов множеств в NPL, которые конструируют строгие списки из строгих списков. Тернер не единственный, кто критиковал строгие списки как контрол-структуру для связывания функций. Одним из таких критиков был в описываемое время и наш старый знакомый Рейнольдс [Schw82]. Авторы HOPE, судя по всему, знакомы с этой критикой и сами критиковали NPL за большинство этих недостатков [Burs80b].
С какими языками из тех, в которых какие-то из этих проблем решены, они знакомы? Авторы HOPE ссылаются на ISWIM, Лисп, Пролог, ML, SASL, OBJ, SCRATCHPAD, SETL, "язык Берджа", т.е. псевдокод из книг Берджа, а не McG [Burs80]. Не беспокойтесь насчет некоторых особо радикальных инноваций из этих языков, они в HOPE не попали. Но, к сожалению, авторы HOPE не позаимствовали кое-что из того, что не помешало бы позаимствовать. Конечно, следует учитывать, что в статьях упоминается то, о чем узнали к моменту написания статьи, а не ко времени дизайна языка.
Авторы HOPE заявляют, что их цель - создать простой но мощный язык, который способствует написанию понятных и легко преобразуемых программ, с хорошими шансами избежать ошибок при их написании. По замыслу авторов, HOPE обладает мощностью Лиспа без его сложностей. Что авторы считали простым, но мощным? У них был набор идей на этот счет, и HOPE - эксперимент для их проверки.
HOPE - первый ФЯ в NPL линейке. Наконец, мы можем написать наш традиционный пример:
dec map : (alpha -> beta) # list(alpha) -> list(beta)
--- map(_, nil) <= nil
--- map(f, h::t) <= f(h) :: map(f, t)
map ((lambda x => x + y), [1, 2, 3]) where y == 1
Функции высшего порядка - одна из главных составляющих "простого, но мощного языка" с точки зрения авторов HOPE. В качестве примеров таких функций авторы приводят map
, который в HOPE называется *
и foldl
, который называется **
. Эта функция произносится как reduce
и будет иметь такое название в библиотеках будущий версий HOPE. Один из популярных сегодня вариантов названия левой свертки, который авторы HOPE заимствуют, по их словам, из APL. Обе функции, по заявлениям авторов [Sann82], в стандартной библиотеке и только пара представителей группы функций, которые широко используются в HOPE коде для того, чтоб сократить использование явной рекурсии настолько, насколько возможно. Исходный код стандартной библиотеки не дошел до нас, а дошедшего кода на HOPE недостаточно чтоб подтвердить такие заявления или опровергнуть их.
module list_iterators
pubconst *, **
typevar alpha, beta
dec * : (alpha->beta)#list alpha -> list beta
dec ** : (alpha#beta->beta)#(list alpha#beta)
-> beta
infix *, ** : 6
--- f * nil <= nil
--- f * (a::al) <= (f a)::(f * al)
--- g ** (nil,b) <= b
--- g ** (a::al,b) <= g ** (al,g(a,b))
end
Интересно, что это один из ранних случаев, когда про правую свертку даже не вспоминают, не то что не рассматривают её как основной вариант свертки. Последовательность аргументов сворачивающей функции как у Берджа.
В HOPE не попали конструкторы множеств из NPL. Авторы HOPE не то чтобы решили, что они совсем не нужны. Они даже писали, что собираются их имплементировать [Burs80]. Просто пока не дошли руки. Не нужны срочно. И мы полагаем что потому, что в отличие от NPL, в HOPE есть ФВП.
Тернер считает [Turn19], что авторы HOPE от конструкторов множеств отказались из-за их бесполезности при работе со строгими списками. Проблема гипотезы Тернера в том, что списки в HOPE ленивые.
Авторы HOPE считают, что ленивые списки - это основная полезная контрол-структура, которую дает ленивость и больше ничего и не нужно. Мечты Шварца [Schw82] о ленивом NPL пока что, в основном остались мечтами. В HOPE добавили только один ленивый конструктор lcons
, который конструирует ленивые списки, тип которых тот же, что и у строгих. И паттерн x :: xs
матчит не только энергичный ::
, но и lcons(x,xs)
.
Ленивый map
:
typevar alpha, beta
dec <*> : (alpha->beta)#list alpha -> list beta
infix <*> : 6
--- f <*> nil <= nil
--- f <*> (a::al) <= lcons(f(a),(f <*> al))
Полиморфизм, функции высшего порядка и ленивые списки позволяют писать настолько обобщенный код, насколько возможно, считают авторы HOPE. Использовать готовые комбинаторы вроде map и fold проще, чем циклы и рекурсию.
Авторы не посчитали, что в ФЯ лямбды не нужны, так что лямбды появились, но не как в LCF/ML, а более похожие на NPL. Т.е. с несколькими (если нужно) кейсами, не каррированные, с тяжелым синтаксисом
lambda true,p => p
| false,p => false
С этого, по видимому, начинается история двух видов лямбд в функциональных языках: один вид лямбд с каррингом, второй - без карринга, зато с несколькими кейсами ПМ, если нужно. В некоторых языках будут только первая разновидность, как в Haskell 98. В некоторых - только вторая, как в Standard ML, а в некоторых обе разновидности как различные конструкции, как в OCaml и в GHC Haskell после добавления \case
[GHC23].
Интересно, что разделители между кейсами в лямбдах HOPE не такие, как между уравнениями. В лямбдах тот разделитель кейсов и уравнений, который со временем станет самым популярным. В тех языках, которые произошли от NPL 79 с такими специальными разделителями, а не от NPL 75, в котором специальных разделителей для таких случаев нет, используются те же разделители строк, что обычно.
И, если посмотреть на код на HOPE, начинаешь подумывать, что немного борьбы с лямбдами бы не помешало.
let
и where
в HOPE это не конструкции для объявления функций как в ISWIM и LCF/ML, а конструкции только для матчинга как where
в NPL.
Нельзя писать:
let function(x) <= ...
По крайней мере так не пишут в дошедшем до нас коде. Пишут так:
let function == (lambda x => ...)
не смотря на то, что матчинг конструктора в let
cons(x,y) == ...
и декларация функции
--- func(x,y) <= ...
отличаются синтаксически.
Нет легкого синтаксиса для объявления каррированных функций как в LCF/ML и SASL. Остается только использовать лямбды:
typevar alpha,beta,tau
dec compose : (alpha->beta)#(beta->tau)
-> (alpha->tau)
--- compose(f,g) <= lambda x => f(g(x))
Вся эта тяжесть не от того, что на HOPE не писали кода в котором много использования первоклассных функций. Наоборот, на HOPE написали код в котором такого использования больше, чем в чем бы то ни было до того. Пример [Ryde82]:
dec monadic_signature : Signature(Tag alpha) ->
M_Signature(Set(Tag alpha),Set_Mor(Tag alpha))
--- monadic_signature(Opns,mor(_,arity,_),Sorts) <=
let C & cat(_,_,id,_) == cat_of_sets in
let omap == ! object part of functor
(lambda S => ! S is a set of variables
let indexed_set_of_terms ==
! set of terms indexed on operations
(lambda rho =>
let string(l1) == arity(rho) in
(lambda l => string(rho::l))
* lists(length(l1)-1)(S)) * Opns in
let set_of_terms ==
! either pinked variables or terms
! of depth one
(pink*S) U total_union(indexed_set_of_terms) in
set_of_terms) in
let mmap == ! morphism part of functor
(lambda mor(s,f,t) =>
let f1 == (lambda
pink(s) => pink(f(s))
| string(rho::l) =>
string(rho::(f*l))) in
mor(omap(s),f1,omap(t))) in
let Sigma == functor(omap,mmap) in
let sigma == ! the natural transformation
nat_transform(I(C),
(lambda S =>
mor(S,
(lambda x => pink(x)),
omap(S))),
Sigma) in
( Sigma, sigma )
И двое из трех программистов писавших этот код - Бурсталл и Саннелла - авторы HOPE. Но не имплементаторы. Так что, либо им хотелось вот это все писать, либо к тому времени, когда возникли какие-то пожелания по итогам первого опыта использования, основные имплементаторы уже над этой имплементацией не работали, либо имплементаторов HOPE - МакКвина и Леви - не так-то просто было заставить что-то имплементировать.
Но можно. Упоминания и примеры использования некоторых фич отсутствуют в материалах, опубликованных в 80-ом году [Burs80] [Burs80b]. Но присутствуют в материалах 81-82гг. [Sann82] [Ryde82]. Можно предположить, что отсутствуют не просто потому, что их не посчитали важным упомянуть, а потому, что они появились в HOPE не сразу. Были добавлены из-за того, что код, который писали первые пользователи HOPE, состоял в основном из ручного перекладывания и распаковывания словарей:
let C & cat(_,_,id,_) == cat_of_sets in ...
Первая фича - паттерны, матчащие все что угодно. В LCF/ML они записывались ()
[Gord79], но в HOPE имеют современный вид: _
. В Прологе такие паттерны современного вида появились, по видимому [Warr78], раньше.
Вторая фича - @
-паттерны, они же as
-паттерны. В HOPE - "многоуровневые паттерны" с &
вместо современного @
. Это больше похоже на серьезную инновацию.
HOPE часто называют чисто-функциональным языком (Бурсталл называет такие языки "аппликативными"). Оператор присваивания в нем отсутствует. Авторы считают, что это существенное упрощение языка, одна из основных идей, которую они хотели проверить. Отсутствует в HOPE и ввод-вывод. Второе это больше недоработка, чем принципиальное решение. Ленивыми списки сделали, в числе прочего и для того, чтоб организовать ввод-вывод сохранив чистофункциональность языка.
Как же HOPE обходится без всего этого? Пожелания Фезера о добавлении мутабельности не были воплощены в жизнь? Ну, не совсем.
HOPE обходится без всего этого по той же причине, по которой Гордон посчитал, что мутабельные ссылки в LCF/ML не нужны [Gord79]. Да, как в LCF/ML можно имплементировать функции на императивном языке. В случае HOPE этот язык - POP-2. Вот такое вот упрощение языка отсутствием присваивания. В HOPE есть стандартные функции с побочными эффектами, например выводящие текст в терминал и генерирующие новые имена, разные при каждом вызове. Саннелла пишет, что технически все это делает HOPE неаппликативным языком [Sann82]. Так что этот эксперимент с ссылочной прозрачностью пока что не увенчался успехом.
Поскольку HOPE не транслируется в POP-2, как LCF/ML транслируется в LISP, интероп обходится не так дешево, как в LCF/ML и, по видимому, все библиотечные функции HOPE не написаны на POP-2, в отличие от LCF/ML, в котором почти все функции стандартной библиотеки написаны на Лиспе. Впрочем, код стандартной библиотеки HOPE до нас не дошел.
Не то чтобы на этом закончился период не очень приспособленных для исполнения чистых языков исполняемой спецификации, но начался период, когда в эти языки уже добавляют лазейки для создания эффектов из практических соображений. Но, пока что, не придумали работающей системы управления эффектами. Начался период языков номинально чисто-функциональных, но на практике не так и отличающихся от LCF/ML, который несколько ограничил изменяемость всего как в PAL, но не более того.
В главе об LCF/ML мы рассказывали о том, что Милнер считал, что если ФЯ типизировать, то нужны и полиморфизм и вывод типов. И Тернер и авторы HOPE с этим согласны. Пока Тернер собирался писать тайпчекер для SASL, МакКвин, руководствуясь советами Милнера и Гордона, имплементировал его для HOPE, по-видимому, второго языка с выводом типов. Авторы HOPE, правда, не отвергают аннотации типов так бескомпромиссно как Милнер.
Аннотировать типы каждого имени, как требовалось в NPL, уже не нужно. Общий для языков с уравнениями способ получать код, похожий на псевдокод 60-х, просто задвигая аннотации типов от него подальше, на этом уходит из употребления в Эдинбургской программе. Но аннотировать типы некоторых имен все еще нужно.
Обязательные аннотации типов топлевел-функций в HOPE сохраняются, и они не связаны с мутабельностью, как в случае LCF/ML. Эти аннотации нужны для решения проблем с тем, с чем Милнер вообще решил не связываться - перегрузкой.
Перегрузка досталась HOPE из NPL 79 [Feat79] где она, по-видимому, работала (но не в трансформаторе программ) и, в условиях необходимости аннотировать типы всего, не имела очевидных отрицательных последствий. Интересно, что перегрузка появилась не для арифметики. В NPL были только натуральные числа Пеано. В HOPE, как и в LCF/ML, не было числовых типов кроме одного для целых чисел. В это время единственным "Эдинбургским" ФЯ не только с целыми, а еще и с числами с плавающей точкой был SASL 79 и он был "динамически типизирован". Так что, статически разрешаемая перегрузка оператора *
в Эдинбургских ФЯ появилась для map
раньше, чем для умножения чисел с плавающей точкой.
Перегрузка имени по типу в HOPE разрешается с помощью алгоритма Вальца (Waltz), разработанного для компьютерного зрения [MacQ20]. Это звучит как неожиданное применение, но только потому, что в компьютерном зрении начали решать задачи удовлетворения ограничений раньше [Burg90]. Вальц сокращает пространство поиска исключая такие метки для вершин графа, которые несовместимы с соседними вершинами. Это лучше переиспользует проделанную уже работу по сравнению с "биениями бэктрекинга", перевычисляющего то, что можно бы и не перевычислять.
Необходимость держать в памяти и обходить граф и заставляет ограничить его размер, введя обязательные аннотации типов не только для перегруженных функций, а вообще всех функций на топлевеле. Но программистов так просто не перехитрить! По дошедшим до нас отрывкам кода на HOPE видно, что они пишут гигантские, по сравнению с прочими ФЯ упоминавшимися в этой истории, топлевельные функции со множеством локальных, которые объявлять может и не очень удобно, как мы выяснили в предыдущем параграфе, но для которых хотя-бы не требуется аннотировать типы. Пролог и KRC явно более успешно сопротивлялись написанию на них гигантских функций/процедур.
Так или иначе, но авторы HOPE посчитали имплементацию перегрузки сложной, а работу алгоритма все равно недостаточно быстрой. В результате, их отношение к перегрузке стало гораздо более скептическим, но не до такой степени, как у авторов LCF/ML и его более непосредственных наследников. В языках непосредственно происходящих от HOPE перегрузка в той или иной степени сохранится.
Естественно, поскольку статическая проверка типа для POP-2 в это время - нерешенная задача, если вы пишете функцию на POP-2, которая не соответствует своей сигнатуре на HOPE - программа просто ведет себя неопределенно [Sann82].
Как уже бывало с предыдущими версиями NPL, в мелочах изменилось все, но не обязательно стало лучше.
Сигнатуры функций, выглядевшие в NPL 79 так [Feat79]:
+++ append(list A, list A) <= list A
В HOPE выглядят как в языках описания спецификаций, ну или в "типизированном" ISWIM-псевдокоде [Burs80]:
dec append : list A # list A -> list A
В NPL 79 сигнатуры типов функций похожи на определяющие функции уравнения, но вид туплов и списков отличается от вида их типов и в NPL. Так что, может это идея и родственная более поздним о том, что конструкторы и конструкторы типов должны выглядеть одинаково, но другая.
Кстати, и конструкторы и конструкторы типов туплов изменились между NPL и HOPE. Но и в списках и в туплах элементы разделяются запятыми, в отличие от LCF/ML. Позднее МакКвин объяснял [MacQ14] разделитель элементов списков ;
в ML тем, что парсинг методом Пратта осложняет использование "многоцелевых" разделителей. Но сам-то МакКвин, имплементируя парсер HOPE тем же методом, не поленился, справился!
Еще один важный компонент "простого, но мощного языка" - алгебраические типы данных. Авторы HOPE считают, что для пользователя языка должно быть легко определять и использовать свои типы данных. Чтоб он не поленился использовать тип age
вместо типа integer
и избежать ошибок. И легко - это АлгТД и паттерн матчинг, а не энкодинг через примитивные типы и использование с помощью всяких предикатов и геттеров как в LCF/ML. Паттерн-матчинг проверяется на полноту покрытия - избегаем еще больше ошибок. Все это вместе делает HOPE первым языком, про который говорили "компилируется - работает". Авторы пишут, что обнаружили, что довольно просто писать программы, работающие правильно при первом запуске.
В HOPE типы данных объявляются в стиле BNF/Хоара, почти как в NPL 79. Почти, потому что, разумеется, не обошлось без обычного для NPL-серии изменения пары деталей. Вместо <=
теперь ==
, а вместо ;
в объявлениях взаимно рекурсивных типов ключевое слово with
[Burs80] [Sann82].
typevar alpha
data list alpha == nil ++ alpha :: list alpha
Паттерн-матчинг может и похож на современный на первый взгляд, особенно с добавлением таких стандартных сейчас фич как _
-паттерны и @
-паттерны. Но все еще работает как в NPL, а не как у Тернера или в Прологе. Порядок паттернов все еще не имеет значения.
В NPL 79 алгебраические типы использовались и для того, чтоб дать название какому-нибудь композитному типу [Feat79]:
DATA instream <= in(list list char)
DATA word <= wo (list alphanumeric)
DATA telegram <= te(list word)
DATA statistics <= st(num, truval)
DATA message <= me(telegram, statistics)
В HOPE появились и синонимы для типов как deftype
в LCF/ML, но не совсем:
type Right_Obj_Comma_Mor(o1 ,m1) == Comma_Mor(o1,m1,o1,m1,Num,Num)
в отличие от LCF/ML, синонимы в HOPE параметризованные как и АлгТД.
Не понятно, как это согласуется с идеями авторов HOPE, излагаемыми выше, о том, что структуры данных нужно оборачивать конструкторами разных типов, чтоб их не перепутать. Видимо, удобство им и тут дороже всяких принципиальных подходов и негибких идей.
NPL не относился к языкам описания абстрактных типов данных непосредственно. В нем отсутствовала конструкция для группировки и сокрытия функций и конструкторов. Так что, для участников исследовательской программы, занимающейся АТД, NPL - это демонстрация того, как можно имплементировать исполнение их едва исполняющихся спецификаций. В отличие от него, HOPE - это язык описания абстрактных типов данных без всяких натяжек.
В HOPE появились простые непараметризованные модули, с помощью которых можно скрывать некоторые функции и конструкторы. Это довольно обычное явление для языков спецификации. Как мы помним, многие разработчики языков с уравнениями, описывающими АТД, хотели параметризовать эти АТД, но не смогли или не успели это сделать в 70-е. Модули - одно из названий обычных в этих языках конструкций для объединения деклараций функций и/или типов. Примечательно только то, что в HOPE конструкция впервые в ФЯ под современным названием.
Та ранняя разновидность этих конструкций, которая попала в CLU и LCF/ML менее типична, чем та, что попала в HOPE. Она построена вокруг типа, а такой подход, вскоре после отделения этой ветки от программы исследования АТД, сочли непрактичным и позволили группировать функции многих типов и без обязательных конструкторов [Lisk93]. Неудобно и нет смысла группировать конструкторы значений типа и прочие функции этого типа с помощью одной конструкции. И HOPE использует две разные языковые конструкции для этого. Еще одна из множества инноваций, которые опоздали в LCF/ML совсем чуть-чуть.
Интересно, что синтаксис модулей в HOPE не выглядят как синтаксис теорий из Clear, объединение с которым планировалось, но так и не состоялось. По непонятной причине модули выглядят почти как в языке программирования MODULA [Wirt76], на который авторы не ссылаются. Именно MODULA, а не намного более известный язык MODULA-2. В меньшей степени похожи, но все еще похожи они на синтаксис модулей в языке описания спецификаций SPECIAL [Robi76], на который авторы HOPE ссылаются [Sann82].
Модули HOPE напоминают модули Хаскеля своими отдельными, как и аннотации типов, аннотациями экспортов.
module ordered_trees
pubtype otree
pubconst empty, insert, flatten
...
end
но в списке импортов перечисляются модули, а не функции. Механизма для импорта только части публичных функций и типов модуля, по видимому, нет.
module tree_sort
pubconst sort
uses ordered_trees, list_iterators
...
end
Разработка и имплементация Clear существенно продвинулась с 77-го года, но ожидаемое сближение с HOPE пока не состоялось. Это не мешает авторам этих языков продолжать утверждать что на самом-то деле они довольно похожи, как они утверждали во времена Clear 77 и NPL 75. Почему они это делают? Потому, что на это сходство опирается предлагаемый авторами Clear метод его использования.
Предполагается записывать сначала абстрактную спецификацию и затем инкрементально конкретизировать её, пока спецификация не станет исполняемой, то есть программой [Sann82]. Это соответствует идеям Вирта и Дейкстры, но противоположно направлению в котором предлагал двигаться Гуттаг [Gutt78]. Который считал, что программисту написать программу проще, чем спецификацию. Чтоб спецификация на Clear стала исполняемой, её нужно сделать во-первых "анархической". Саннелла называет "анархической" спецификацию в которой нет аксиом (уравнений) для конструкторов типов данных. Во-вторых, у всех уравнений нужно сделать простые левые части. Так что речь идет об исполняемости как у AFFIRM, не как в OBJ или у О'Доннела. Строгое разделение на функции, которые нельзя использовать слева от =
и конструкторы, которые можно.
Не подумайте только, что "исполняемая спецификация" так просто исполнится. Какой-то экстракции в HOPE из Clear нет. Бурсталл и Гоген планируют сделать полуавтоматический переписыватель спецификаций на Clear в будущем. Но пока что нужно переписать код вручную и на практике это не так легко.
Саннелла пишет, что некоторые Clear спецификации - это просто HOPE программы, записанные "немного отличающейся нотацией" [Sann82]. В этом утверждении есть доля правды. Но только доля. Clear похож на HOPE в достаточной степени для того, чтоб Саннелла использовал в имплементации Clear модифицированный парсер и тайпчекер, написанные МакКвином для имплементации HOPE. С другой стороны видно, что модифицировано в парсере не так мало:
proc Reverse(X:Triv) =
enrich List(X) by
opns reverse : list -> list
eqns reverse(nil) = nil
reverse(a::l) = append(reverse(l),a::nil) enden
Еще сильнее отличается синтаксис деклараций типов данных. В Clear, как и принято в языках описания спецификаций, они декларируются как прочие функции. В итоге надо прийти к простым конструкторам, но можно начинать не с них [Ryde82].
proc List(X : Triv) =
enrich X by
data sorts list
opns nil : list
(_ :: _) : element,list -> list
end
theory Triv:
constant Triv = sorts element
end
constant Bool_Lists = List(Bool[element is bool]) end
Сравните с компактными BNF-образными декларациями АлгТД в HOPE.
Но главное отличие не во множестве мелких деталей из-за которых при переписывании из Clear в HOPE и наоборот потребовалось бы править практически каждую строку кода. Это были бы довольно простые механические правки. Главное отличие в параметризации. Вернее, в параметризациях.
Обратите внимание на тип reverse
. У типа списка list
нет параметра. Зато параметр есть у модуля List(X)
в котором список объявлен. В Clear нет параметрического полиморфизма как в HOPE. Есть параметризованные модули. Которые придется использовать для написания обобщенного кода.
Но параметризованных модулей нет в HOPE. Что толку от постепенной трансформации спецификации в программу, если программу придется потом переписывать в принципиально другом стиле?
Саннелла допускает, что параметрический полиморфизм в Clear может появиться, но главные ожидаемые изменения - это параметризованные модули для HOPE. Бурсталл и МакКвин обсуждали Clear-образные параметризованные модули для HOPE в 78-80-гг, но так ничего и не имплементировали. Для HOPE все закончилось пропозалом параметризованных модулей, который не отсканирован и не выложен в интернет. МакКвин сделал по нему доклад на конференции в 81-ом году.
Интересно, что язык модулей для HOPE старается выглядеть другим языком даже в мелочах. Так аннотации типов для параметризованных модулей не отдельные декларации как в HOPE, а аннотации на месте, как в LCF/ML. Также, есть легкий способ объявлять каррированные параметризованные модули, в отличие от легкого способа объявлять каррированные функции:
structure Sorting(L : LIST)(P : POSET) : SORTING(P,L) ... end
или даже
structure Sorting : SORTING(P0,L0) ... end
Декларации типов функций отправляются в интерфейсы (сигнатуры) модулей. В интерфейсе (сигнатуре) модуля можно записать АлгТД [MacQ20]:
interface LIST (BOOL)
data list a == nil ++ cons (a, list a)
dec null : list a −> bool
end
а в имплементации потом указать, что пользуетесь непосредственной имплементацией:
structure List : LIST (Bool)
data-rep list is free
−−− null (nil) <= true
−−− null (cons(x, l)) <= false
end
Один из последних приветов из эпохи, когда АлгТД еще был абстракцией, для которой, конечно же, нужно писать серьезную, низкоуровневую имплементацию. Но иногда или временно можно согласится и на такую несерьезную вещь как ссылка на один из объектов кучи с тегом и другими ссылками. Из эпохи, когда АТД было нормально перепутать с АТД.
Довольны ли авторы HOPE тем, что у них получилось? Обладает ли язык мощностью Лиспа без его сложностей? Чем HOPE точно обладает, так это знакомым видом ФЯ. На первый взгляд. При более внимательном рассмотрении видны странности и отличия. Например, нечувствительный к порядку уравнений паттерн-матчинг и перегрузка, требующая обязательных аннотаций. Но большая часть важных деталей уже собрана в единое целое, на рабочем столе осталась только пара не понадобившихся деталей. Если бы мы писали историю идей, то на этом бы предыстория ФЯ завершилась и началась история. Но мы пишем историю имплементаций, и тут до окончания предыстории еще далеко.
Понятно, что HOPE не похож на современные ФЯ в деталях потому, что ни одна из версий HOPE не дожила до наших дней достаточно хорошо сохранившись, чтоб детали HOPE стали деталями современных ФЯ.
И это довольно ожидаемый результат для очередного варианта NPL. Как обычно, через пару лет авторы NPL бросят очередной NPL и сделают новый, поменяв детали и название, чтобы потом поменять еще раз и еще, пока n-ый вариант NPL, называющийся не NPL, не станет современным ФЯ.
Но не все так просто, пока большая часть авторов NPL/HOPE всем этим занимались, другая группа разработчиков и имплементаторов ФЯ поддерживала HOPE примерно в одном и том же виде, сохраняя все узнаваемые детали и странности. Пока не перестала. В отличие от предыдущих эфемерных NPL-ей, каждому из которых уже через пару лет на смену приходил следующий, HOPE просуществовал гораздо дольше. Разделение NPL-линейки, консервация одной из ветвей и её последующая гибель произошли благодаря автору Фортрана и популярности Пролога в Японии. Разочарование автора Фортрана в "фортранах" и популярность Пролога в Японии запустили череду странных решений и невероятных последствий, которая повлияла как на то, что HOPE не был таким же короткоживущим, как предыдущие версии NPL, так и на то, что HOPE недостаточно долгоживущая версия, чтоб дожить до наших дней. И повлияла не только на HOPE. Но это уже другая история.
А пока что у авторов HOPE много идей о том, как HOPE переделать. Авторы HOPE недовольны перегрузкой. Она замедляет компиляцию и её сложно имплементировать. К тому-же, ad-hoc полиморфизм хочется сделать менее ad-hoc: нужно работать над еще одной проблемой, с которой Милнер решил вовсе не связываться - ограниченным полиморфизмом. Авторы HOPE планируют описывать, какие операции можно применять к значениям типов-параметров otree(alpha[<])
или использовать именованные группы ограничений, как теории в Clear.
Судя по всему, такого проработанного плана как для параметризации модулей, не было. Только общие идеи. Считали, что нужно смотреть на то, как это сделано в CLU. Тем более, что там в 76-79гг. уже придумали как проверять ограничения во время компиляции и в 80-ом эффективно (по крайней мере, по мнению авторов CLU) имплементировали такую разновидность перегрузки [Lisk93].
Если сначала авторы HOPE были не уверены, хотят ли они только один вид параметризации с ограничениями, то со временем решили, что нужно два вида, отдельно для языка "уравнений", отдельно для языка модулей. По крайней мере, в следующем их языке так и было сделано.
Но многое в HOPE еще рано переделывать. Надо, для начала, доделать. Многие планы авторов пока что не реализованы. Главные проблемы HOPE связаны с имплементацией.
МакКвин имплементировал HOPE на POP-2 для PDP-10 как компилятор в инструкции для стековой машины, имплементированной как интерпретатор. Да, интерпретатор тоже на POP-2 [Burs80]. Код компилятора не сохранился, но пишут, что его размер был примерно 7 тыс. строк [Moor82]. МакКвин написал парсер методом Пратта, как Ньюи написал парсер LCF/ML [MacQ14] [MacQ15]. Написал тайпчекер, руководствуясь советами Милнера и Гордона.
МакКвин пишет, что первая имплементация HOPE была сделана в 79-80гг. [MacQ22]. Есть основания предположить, что основная работа по имплементации была проделана до 80-го года. МакКвин пишет [MacQueen], что работал в Эдинбурге до 79-го года. В статье 80-го года все авторы HOPE включая и МакКвина записаны в Департамент Компьютерных Наук Эдинбургского Университета, куда Бурсталл и др. перешли работать из бывшей Группы Экспериментального Программирования в конце 79-го. Но связываться с МакКвином уже предлагают через Институте Информационных наук Университета Южной Калифорнии в Лос-Анджелесе, где Гуттаг и Мюссер делали AFFIRM. На странице МакКвина в LinkedIn [MacQueen] в момент написания этого текста это место работы не указано, между окончанием работы в Эдинбурге в 79-ом и началом работы в Лабораториях Белла в 81-ом просто пропуск. С окончанием его работы в бывшей Группе Экспериментального Программирования его работа над имплементацией ФЯ не закончилась. Даже во время написания этого текста он работает над имплементацией очередного ФЯ на GitHub. Но основная его работа над первой имплементацией HOPE, по-видимому, продолжалась до ухода из Эдинбургского Университета. Судя по ссылкам в диссертации Райдхерда [Ryde82], отчет с описанием HOPE мог выйти еще в 79-ом году, но не вышел. Так что большая часть работы была проделана в одном департаменте, а отчет вышел в другом, когда бывшие участники бывшей Группы Экспериментального Программирования уже работали в Департаменте Компьютерных Наук. И советы по имплементации проверки типов МакКвин получал от Милнера и Гордона еще не работая с ними в одном департаменте.
Другой разработчик HOPE, упоминающийся в первой статье [Burs80] - Майкл Леви (Michael Robert Levy), защитивший диссертацию в Университете Уотерлу в 78-ом. Что именно он имплементировал, правда, авторы HOPE не пишут. Бурсталл и/или Саннелла умели писать на POP-2. И от них могло потребоваться что-то имплементировать, если принять, что МакКвин закончил работать в Эдинбурге в 79-ом, а Леви пребывал в Эдинбурге временно. Но об их вкладе нет даже таких расплывчатых упоминаний, как о работе Леви.
Авторы HOPE собирались сделать, но не сделали оптимизирующую компиляцию паттерн-матчинга, исключающую лишние проверки. Хотя у МакКвина уже были какие-то идеи о том, как это можно было сделать [MacQ22]. Не имплементировали параметризованные модули, потоковый ввод-вывод, конструкторы множеств. Не написали интерпретатор на чем-нибудь побыстрее POP-2. Эдинбургская система для трансформации программ не была обновлена для поддержки кода на HOPE.
В отличие от имплементации LCF/ML, дожившей почти что до наших дней, первая имплементация HOPE вскоре была заброшена. Практически сразу же после того как закончить работу над ней, МакКвин уже приступил к написанию новой имплементации HOPE на другом языке.
Другой соавтор Бурсталла, работавшим над HOPE, был первым пользователем этого языка. Дональд Саннелла (Donald Sannella) получил степень бакалавра от Йельского университета в 77-ом и степень магистра от университета Калифорнии в Беркли. Чтоб защитить диссертацию в 82-ом году в Эдинбургском Университете [Sann14] он имплементировал CLEAR. Дважды.
Еще в 77-ом году Бурсталл стал писать код на NPL чтоб разобраться с новым для него понятием теории категорий - копределом [Burs80b]. Этот код мог быть даже больше, чем самая большая дошедшая до нас программа на NPL 79, но это не очень высокая планка. Конечно, без ФВП с теорией категорий особенно не поразбираешься, так что код был со временем переписан Бурсталлом, Райдхердом (David Eric Rydeheard) и Саннеллой на HOPE. Райдхерд и Саннелла развивали код, пока он не вырос до примерно тысячи строк [Ryde82] и стал набором ТК-абстракций, который был использован Саннеллой для имплементации CLEAR [Sann82]. Существенная часть этого кода дошла до нас как примеры в диссертации Райдхерда [Ryde82]. Код с перекладыванием лямбд в/из АлгТД выше - как раз оттуда. Райдхерд называет эту тысячу строк "большой программой".
Имплементация CLEAR с помощью этого ТК-инструментария, правда, столкнулась со сложностями. Абстрактный, высокоуровневый код работал слишком медленно. Пара примеров спецификаций по 8 строк каждая требовали 2 и 4 минуты работы [Sann82]. Так что Саннелла написал менее абстрактный код. Для того чтоб выяснить, насколько быстрее результат можно получить, если не писать на ФЯ в ФП-стиле. И для того чтобы получить "программу, которой можно пользоваться". Разница получилась существенной. Те спецификации, который использующий ТК-абстракции код обрабатывал за полчаса, теперь обрабатывались в 1000 раз быстрее [Ryde82]. Те, что требовали минут пять - заработали только в 100 раз быстрее [Sann82]. Более серьезные примеры заставляли абстрактный код удерживать достаточно памяти, чтоб большая часть времени тратилась на сохранение страниц на диск и чтение с диска.
Ну, нельзя сказать, что писать в ФП стиле на ФЯ так просто даже и сегодня. К сожалению, Тернер не воспользовался возможностью продемонстрировать заявленную им "бесплатность" ФВП в SASL 79, переписав и запустив эту "большую" тысячестрочную программу.
В результате трудов Саннеллы, на HOPE написана часть имплементации CLEAR размером в 1700 строк [Sann82]. Парсер и тайпчекер на POP-2 на основе кода имплементации HOPE, написанного МакКвином. Этот код используется из кода на HOPE с помощью машинерии для вызова функций на POP-2. Весьма вероятно, что это одна из добавленных позднее фич и, вместе с @
и _
паттернами определяет разницу между HOPE 80 и HOPE 81. Потому, что не упоминается в ранних материалах по HOPE. Но установить это точно по доступным в электронном виде материалам нельзя.
Как продемонстрировал Уоррен, если не писать на Прологе в стиле логического программирования - можно добиться неплохой производительности. К сожалению, не писать на ФЯ в ФП стиле все еще недостаточно для хорошей производительности. Саннелла отмечает, что работа кода на POP-2 составляет какие-то единицы процентов по сравнению с кодом на HOPE. Не смотря на то, что код на POP-2 это, например, тайпчекер с разрешением перегрузки, который имплементаторы и пользователи HOPE считают медленной, проблемной частью имплементации.
Самая крупная спецификация, которую проверяет CLEAR - спецификация вывода типов из статьи Милнера [Miln78]. 270 строк на CLEAR, проверяется 15 минут.
Да, с производительностью все плохо, но Саннелла очень доволен HOPE как языком программирования. Считает, что писать код на HOPE легче, чем на всех остальных известных Саннелле языков, называет это "величайшим триумфом HOPE". Список известных ему языков не приводится. Саннелла считает, что принцип "компилируется - работает" подтверждается на практике. Ленивость Саннелле при имплементации CLEAR не пригодилась.
Раз уж работа над CLEAR теперь велась в Департаменте Компьютерных Наук, где работали Милнер с Гордоном и где разработали LCF/ML, Саннелла написал экспорт CLEAR теорий в PPλ для работы с ними в LCF. При этом Саннелла приобрел опыт программирования на LCF/ML и сравнил его с HOPE. Языки он посчитал очень похожими. Фича ML, которой ему не хватало в HOPE - исключения [Sann82]. Очередной тревожный звоночек, не предвещающий ничего хорошего для тех, кто почему-то ждет, что следующий язык от авторов HOPE будет чисто функциональным.
Как мы помним, наш старый знакомый, соавтор NPL Джон Дарлингтон в 77-ом году перешел из Эдинбургского Университета работать в Имперский колледж Лондона.
И летом 82-го года в Имперском колледже Лондона написан компилятор подмножества HOPE на HOPE [Moor82]. Или, правильнее сказать, был написан из Имперского колледжа Лондона. Ранее там интересовались новой имплементацией POP-2, и можно было бы предположить, что в Лондоне есть все что нужно для использования и развития HOPE, но нет. Интерпретатор HOPE, который используется для написания компилятора HOPE на HOPE, запускают на эдинбургском компьютере. По крайней мере до осени 82-го года. Возможно, эдинбургскую имплементацию HOPE не так просто использовать не на том компьютере, на котором её разрабатывают. Возможно, в Лондоне так и не появилась машина, на которой работал POP-2. Или она уже не работала в 82-ом году. Что обычно и происходило с этими машинами в это время. Но это другая история.
Лондонский компилятор разделяет код - написанный вручную парсер рекурсивным спуском - с системой трансформации программ. Ну разумеется, Дарлингтон пишет систему трансформации программ, теперь программ на HOPE.
Мур утверждает, что, насколько ему известно, других компиляторов в "аппликативном" стиле еще не написали. Видимо он, в отличие от Бурсталла [Burs79], не считает Пролог аппликативным языком. Надо заметить, что "аппликативность" условная. В HOPE нет ввода-вывода, так что для сохранения сгенерированного кода в файл вызывается функция на POP-2.
Компилятор - определенно исследовательский проект, он задуман продемонстрировать, что на таком языке как HOPE можно писать значительные программы. Удалось ли это продемонстрировать?
В начале нашей работы мы определили HOPE как первый язык, соответствующий нашему определению ФЯ. И определили компилятор ФЯ, написанный на ФЯ как ключевой момент в истории имплементаций ФЯ. Имели ли мы в виду этот компилятор подмножества HOPE на HOPE? Нет, по двум причинам.
Насколько значительная это программа? Три тысячи строк. Авторы считают такую программу "большой". Напомним, что компилятор Уоррена в это время больше 6KLOC. Эти три тысячи строк на HOPE неплохо выглядят по сравнению с кодом, написанным на эдинбургских прото-ФЯ в 70-е. Но главным образом потому, что код продолжали писать даже когда было уже понятно, что в использовании имплементаций ФЯ 70-х годов для разработки "значительных программ" нет особого смысла.
Три тысячи строк кода - это подозрительно мало для компилятора даже подмножества HOPE. И кода так мало не из-за выразительности и краткости функционального языка. Кода мало потому, что компилятор делает не так много работы, как можно ожидать от компилятора в код обычной машины. Потому, что он транслирует один высокоуровневый язык в другой высокоуровневый язык - Compiler Target Language [Moor82]. Который не так и отличается от HOPE, поддерживая все те же конструкторы алгебраических типов и правила перезаписи [Reev86]. Такой вот код на HOPE
data tree(alpha) == tip(alpha) ++ node(tree(alpha) # alpha # tree(alpha))
dec size : tree(alpha) -> NUM
--- size(tip(i)) <= 1
--- size(node(t1, i, t2)) <= plus (1, plus(size(t1), size(t2)))
соответствует такому на CTL:
CONSTRUCTOR tip($1), node ($1, $2, $3)
REWRITEABLE size($1)
SEQ_DO
SNAPSHOT_PACKET($1: OPERATOR, OPERAND())
SEQ_ALT
IS_CONSTRUCTOR($1)
ALT
$1 IS tip($T1)
REWRITE_$PBP(!1)
$1 IS node($T1, _, $T2)
SEQ_DO
GENERATE_PACKET(&1: size($T1))
GENERATE_PACKET(&2: size($T2))
GENERATE PACKET(&3: plus(&1, &2))
REWRITE_$PBP(plus(!1, &3))
END_SEQ_DO
END_ALT
TRUE
DO
UPDATE_PACKET($1:
PENDING DEMANDS = --1,
SIGNAL_SET = ++ (CONTROL_FLOW($PBP,
WHEN_CONSTRUCTOR)
)
)
RESTORE_$PBP(PENDING_SIGNALS = 1)
END_DO
END_SEQ_ALT
END_SEQ_DO
Предполагается, что в будущем CTL будет исполняться компьютером - параллельным переписывателем графов, а пока что исполняется его эмулятором, написанном на Паскале.
Более серьезная проблема - компилятор работает слишком медленно для того, чтоб его можно было использовать. Программа из двадцати строк кода компилируется за 5-10 минут, из них полторы минуты работает код на HOPE, полторы минуты - сборщик мусора, а все остальное время страницы памяти читаются с диска и пишутся на диск.
Опыт Мура с ленивостью противоположен опыту Саннеллы. Ленивость критична для того, чтоб компилятор вообще работал. Компиляция происходит в пять проходов: лексер, парсер, проверка типов и два прохода кодогенератора. Стадии компиляции имплементированы как 5 функций и первоначально обменивались полностью материализованными в памяти структурами, которые занимали слишком много памяти в случае "нетривиальных" программ, так что функции стали принимать и возвращать ленивые списки. Сделать это изменение было несложно, что по мнению Мура должно продемонстрировать плюсы ФП.
Мур, как и Саннелла, доволен самим языком - писать код на ФЯ легко и приятно! В Лондоне не унывают из-за медленной работы. Все-таки компилятор - исследовательский проект, производительность не главная цель. Для начала, они надеются имплементировать более эффективный эмулятор, который позволил бы бутстрап и достаточно быстро работающий для практического использования компилятор HOPE на HOPE. И у них есть планы как решить проблемы с производительностью.
Но об этом позже. Сначала попробуем оценить эти проблемы с производительностью. Компиляция со скоростью 3 строки в минуту выглядит плохо, но что выглядело бы хорошо в эти годы? Насколько HOPE и прочие ФЯ 70-х медленнее прочих высокоуровневых языков?
В 80-ом году авторы HOPE пишут [Burs80], что интерпретатор этого языка исполняет программы в 9 раз медленнее, чем интерпретатор Лиспа и 23 раза медленнее, чем работает код, сгенерированный компилятором Лиспа. В 82-ом Саннелла пишет [Sann82], что тот же интерпретатор HOPE в 3 раза медленнее интерпретатора и в 50 раз медленнее компилятора Лиспа. Имплементация Лиспа в обоих случаях одна и та же, но не указано одна и та же версия этой имплементации или нет. Что за программы они сравнивают - авторы HOPE не пишут. Производительность LCF/ML на той же машине вовсе не известна. И это довольно нормально для этого времени, авторы и пользователи имплементаций почти всех языков, о которых мы рассказывали в этой истории, не уделяют бенчмаркам особого внимания. Почти всех. Исключение? Уоррен.
И все равно неожиданно, что Лисп не быстрее Пролога в несколько раз.
Дэвид Уоррен, Прикладная логика: её использование и имплементация как инструмента для программирования. [Warr78]
В своей диссертации [Warr78] Уоррен сравнивает производительность кода своего компилятора Пролога (Prolog-10) и своего интерпретатора Пролога (Prolog-10I) с Марсельским интерпретатором (Prolog M) и компиляторами Лиспа и POP-2 на нескольких микробенчмарках.
nreverse | qsort | d times10 | d div10 | d log10 | d ops8 | palin25 | dbquery | |
---|---|---|---|---|---|---|---|---|
Prolog-10 | 1.55 | 1.71 | 1.00 | 1.00 | 1.00 | 1.00 | 2.03 | 1.00 |
Lisp | 1.00 | 1.00 | 1.74 | 2.62 | 1.14 | 1.31 | 1.00 | |
Pop-2 | 5.87 | 3.06 | 3.73 | 5.41 | 4.46 | 2.34 | 1.62 | |
Prolog M | 33.4 | 29.0 | 28.8 | 30.8 | 32.1 | 27.3 | 36.0 | 53.9 |
Prolog-10I | 33.5 | 30.7 | 25.4 | 28.7 | 25.6 | 28.4 | 30.5 | 48.0 |
Prolog-10
░▒▓▓
Lisp
▒▓░▒▓
Pop-2
░░░▒▒▓▓░░░▒▒▓▓
Prolog M
░░░░░░░░░░░░░░░░░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓░░░░░░░░░░░░░░░░░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓
Prolog-10I
░░░░░░░░░░░░░░░░░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓░░░░░░░░░░░░░░░░▒▒▒▒▒▒▒▒▒▒▒▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓
И график только для компиляторов:
Prolog-10
░░░░▒▒▒▒▒▒▒▒▒▓▓▓▓▓░░░▒▒▒▒▓▓▓▓▓▓▓
Lisp
░░░▒▒▒▒▒▒▓▓▓▓▓▓▓▓░░░░░░░░▒▒▒▒▓▓▓▓▓▓▓▓▓
Pop-2
░░░░░░░░░░░░░░░░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓░░░░░░░░░░░░░░░░░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓
Здесь и далее результаты бенчмарков безразмерные. Это отношения времени работы ко времени работы самого быстрого варианта. Соответственно, чем меньше - тем лучше.
nreverse
- это наивный разворот списка наивной конкатенацией. qsort
- тот самый "квиксорт". d times10
, d div10
, d log10
, d ops8
- процедура для символьного вычисления производных применяется к разным выражениям. palin25
- пример для сравнения логических переменных с мутабельными ячейками. И dbquery
это запрос для "базы данных" определенной как прологовые предикаты против попарного сравнения элементов в массивах POP-2, чему такой запрос примерно соответствует. Большинство бенчмарков не совсем уж однострочники, а маленькие программы примерно на страницу текста на самом многословном языке. И это обычно не Пролог.
Интересно, что сравнимая с компилируемым Лиспом производительность достижима для аппликативного языка. Но не для POP-2. Имплементация POP-2 определенно оставляет желать лучшего. Особенно, если посмотреть на код. Когда такой вот код
:-mode qsort(+,-,+).
:-mode partition(+,+,-,-).
qsort([X,..L],R,R0) :-
partition(L,X,L1,L2),
qsort(L2,R1,R0),
qsort(L1,R,[X,..R1]).
qsort([],R,R).
partition([X,..L],Y,[X,..L1],L2) :- X =< Y, !,
partition(L,Y,L1,L2).
partition([X,..L],Y,L1,[X,..L2]) :-
partition(L,Y,L1,L2).
partition([],_,[],[]).
работает в два раз быстрее, чем такой:
FUNCTION QSORT LIST;
VARS Y Z Q QQV QQW QQS;
0;
L2:IF NULL(LIST) OR NULL(TL(LIST)) THEN GOTO SPLIT CLOSE;
NIL->QQS; NIL->Y; NIL->Z;
HD(LIST)->QQW;
L1:HD(LIST)->QQV; TL(LIST)->LIST;
IF QQW>QQV THEN QQV::QQS->QQS
ELSEIF QQW<QQV THEN QQV::Z->Z
ELSE QQV::Y->Y
CLOSE;
IF NULL(LIST) THEN Z;Y;1; QQS->LIST; GOTO L2 ELSE GOTO L1 CLOSE;
SPLIT: ->Q; IF Q=0 THEN LIST EXIT
->Y;
IF Q=1 THEN ->Z; LIST<>Y;2; Z->LIST; GOTO L2 CLOSE;
Y<>LIST->LIST;
GOTO SPLIT
END;
это выглядит не очень хорошо для POP-2.
Эта имплементация Лиспа (более старая версия той, с которой сравнивали авторы HOPE) не самая лучшая, но из более-менее производительных. Поэтому подозрительно, что Уоррен вот так взял и догнал компилятор Лиспа. Результат двадцатилетней работы над компиляцией Лиспа. Насколько хорошо вообще можно скомпилировать Лисп? В этом сравнении скоростей просто нет никаких языков, про которые мы можем уверенно сказать, что они хорошо компилируются и демонстрируют как быстро можно было бы работать на этой машине в принципе.
Довольно безопасно предположить, что бенчмарки подобраны так, чтоб компилятор Уоррена выглядел лучше. Так, измерения для Лиспа и POP-2 включают время сборщика мусора. И сборщик мусора Уорреновской имплементации не работал при исполнении всех этих программ вообще. Конечно, Уоррен поступил бы более добросовестно, если бы он включил в набор бенчмарков и такие, которые потребовали бы работы сборщика мусора. Но это существенное преимущество имплементации, когда можно сделать столько всего, аллоцируя на стеке.
Разумеется, один лиспер - Клаудио Гутьеррес (Claudio Gutierrez) - попытался Уоррена разоблачить [Guti82]. Гутьеррес не улучшал бенчмарки Уоррена, написанные на Лиспе. Он написал собственные на Прологе.
Гутьеррес прошелся по всем пунктам, которые мы рассматривали в главе о причинах хорошей производительности Пролога Уоррена. Использовал списки вместо конструкторов, сделав представление в памяти структур данных не лучше, а хуже, чем у Лиспа и добавив лишней работы для ПМ. Не стал аннотировать параметры, не дав компилятору Уоррена более рационально аллоцировать и генерировать более быстрый код, оптимизировать хвостовую рекурсию.
Это, конечно, не все что можно было сделать. Гутьеррес использовал прологовскую базу данных в качестве мутабельных ссылок, а она слишком медленная для такого использования. Но это еще не все, компилятор Уоррена не компилирует предикаты, которые добавляются в базу данных во время исполнения, так что Гутьеррес таким использованием еще и обеспечил вызовы интерпретатора в цикле. Он, также, не ставил каты (!
), которые помогают оптимизировать хвостовые вызовы.
И в результате всего этого лиспер мог бы просто уничтожить Пролог, если бы не стал измерять, в основном, скорость вывода текста. Так возможность была упущена. Не вышло даже отставания в десять раз. Гутьеррес мог бы заподозрить, что что-то не так из-за небольшой разницы между производительностью скомпилированного и интерпретируемого Лиспа, но нет.
В итоге, после ответа [O'Ke83] из Эдинбурга, в котором все это было исправлено, компилятор Уоррена стал выглядеть еще лучше, чем после первоначальных бенчмарков Уоррена.
Такая богатая культура написания и критики бенчмарков напоминает современную. Основное отличие от которой - ожидание ответов годами. Потому, что дискуссия идет в публикациях.
К сожалению, функциональной части Эдинбургской программы эта культура была чужда, и оставалась чуждой большую часть 80-х.
К счастью, мы все-таки можем составить какое-то представление о производительности других упоминавшихся в этой истории имплементаций. Замеры были сделаны на других машинах, на еще более микро микробенчмарках, чем у Уоррена. Но все-таки были сделаны.
fib 20 | primes 300 | 7queens | 8queens | insort 100 | tak | |
---|---|---|---|---|---|---|
LCF/ML (LISP) | 100 | 145 | 425 | 18.8 | 172 | |
SASL (LSECD) | 67.4 | 100 | 425 | 15.0 | ||
Miranda (SKI) | 157 | 61.5 | 422 | |||
LISP int. | 45.7 | 39.0 | 120 | 8.00 | 42.2 | |
LISP comp. | 2.39 | 5.50 | 13.0 | 36.2 | 1.00 | 1.67 |
C | 1.00 | 1.00 | 1.00 | 1.00 | 1.00 | |
Pascal | 2.00 | 2.50 | 2.92 | 1.44 |
LCF/ML (LISP)
░░░░░░░░░░░░░░░░░░░░░░░░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓
SASL (LSECD)
░░░░░░░░░░░░░░░░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓
Miranda (SKI)
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓
LISP int.
░░░░░░░░░░░▒▒▒▒▒▒▒▒▒▒▓▓▓▓▓▓▓▓▓▓▓
LISP comp.
░▒▓
C
▒
Таблица собрана из двух статей [Augu84] [Augu89]. Не беспокойтесь о поздних датах публикации этих статей. 80-е не были так бедны достижениями имплементаторов как 70-е.
fib 20
- это вычисление 20-го числа Фибоначчи наивным рекурсивным способом. primes 300
- вычисление простых чисел меньше 300 нерешетом неэратосфена из руководства SASL [Abra81]. 8queens
- задача расстановки восьми ферзей. 7queens
- то же, но ферзей семь потому, что расставлять 8 слишком долго. insort 100
десять сортировок вставками списка из ста элементов. tak
- это любимый бенчмарк лисперов - функция Такеучи, измеряет скорость рекурсивного вызова, как и fib
, но работает быстрее в ленивом языке, чем в энергичном.
Форк первой имплементации LCF/ML. Тоже транслирует в Лисп, который интерпретируется, но в другой Лисп, не тот, в который компилировали Милнер и др. и с которым сравнивали Уоррен и авторы HOPE. Этот Лисп похуже, но несущественно. Точнее сказать сложно, на одной машине эти Лиспы не работали.
Это Сент-Эндрюсовская имплементация SASL c помощью "прокрастинирующей" SECD, которую начал писать Тернер, а потом дорабатывал Кэмпбелл. И он, по видимому, отлично справился с доработками. Потому, что интерпретатор работает гораздо быстрее, чем можно было ожидать после чтения статьи Тернера. В своей статье о комбинаторной интерпретации [Turn79] Тернер писал, что не мог сравнить их на одной машине, но ленивая SECD имплементация аллоцирует в 5-20 раз больше, чем комбинаторная и в 10 раз больше, чем строгая SECD. С предполагаемыми серьезными последствиями для производительности. Но вот прокрастинирующая SECD работает на одном компьютере с комбинаторным интерпретатором, а существенного отставания не видно. Или комбинаторный интерпретатор вовсе не является прорывным изобретением, которое ускорило интерпретацию ленивых языков на порядок и выровняло их скорость с интерпретаторами строгих языков, или те, кто измеряли скорость просто перепутали имплементации SASL. Это вполне возможно, статьи не про скорость имплементаций SASL. Они в этом сравнении только для того, чтоб имплементация авторов статьи хорошо выглядела. Но особых оснований считать, что они что-то перепутали нет. Кроме того, их университет отсутствует в списке пользователей комбинаторного SASL [Turn19]. Да, и некоторых авторов имплементаций ФЯ в эти годы есть списки пользователей этих имплементаций. Легко умещающиеся на одну страницу. А те, у кого такого списка нет - обычно являются единственными пользователями собственной имплементации.
Комбинаторные интерпретаторы в этом соревновании представляет не SASL, а более поздний интерпретатор Тернера. Который должен быть быстрее того, который Тернер сравнивал с SECD-машинами в своей статье. Например потому, что в нем исправлены проблемы с неправильным B'
. Установить это точно нам не удалось, замеры производительности комбинаторного SASL мы не нашли, но это довольно правдоподобно.
Может быть Тернер просто плохой имплементатор? Это проверить можно. Вот результаты комбинаторной имплементации KRC Саймона Крофта [Thom90], которая пришла на смену Тернеровскому интерпретатору KRC в Университете Кента:
fib 20 | primes 300 | |
---|---|---|
KRC | 79.3 | 23.8 |
C (VMS) | 1.00 | 1.00 |
Измерения сделаны на машине из той же линейки, что и машина на которой запускали бенчмарки SASL. Модель более дешевая, в полтора раза медленнее. И ОС отличается и компилятор C, по всей видимости, хуже. Не смотря на все эти отличия видно, что и SKI-интерпретатор, написанный не Тернером все равно не демонстрирует на порядок более высокой скорости, чем ленивая SECD.
LISP int.
- это интерпретатор Лиспа, в который транслируется измеряемый тут LCF/ML. Можно было бы ожидать, что разница с LCF/ML будет поменьше.
LISP comp.
- это компилятор того же самого Лиспа. Теперь его можно сравнить с языками, производительность которых понятнее: Си и Паскалем. И, хотя скомпилированный Лисп существенно быстрее того, что смогли сделать герои этой истории до сих пор, проблемы видны. И, судя по результатам бенчмарков, чем сильнее программа отличается от однострочника, тем хуже результат.
В 60-е годы исследователи Обоекембриджской программы попытались создать практичную имплементацию ФЯ. Эта попытка завершилась частичным успехом - созданием непрактичной имплементации ФЯ. С тех пор прошло еще одно десятилетие, но практичной имплементации ФЯ как не было - так и нет. Исследователи Эдинбургской программы не ставили перед собой грандиозных целей своих предшественников. Они гораздо лучше представляли себе, что такое "ФЯ". И, может быть поэтому, делали языки для скриптования и обучения программированию. Языки кое-как исполняемой спецификации и вовсе неисполняемые.
За десятилетие от PAL до HOPE изобретены, хотя-бы в первом приближении, все недостающие элементы, составляющие современный ФЯ. Но бурное десятилетие истории идей неприятно контрастирует с десятилетием стагнации истории имплементаций.
Конечно, производительность PAL неизвестна, но нет оснований считать, что производительность этой SECD-имплементации может существенно отличаться от производительности SECD-имплементации в конце семидесятых.
Новинка 70-х - SK-машина - не дает, на практике, никакого прогресса.
И даже использовать наработки других исследовательских программ не удалось. Трансляция ФЯ в Лисп почему-то производит Лисп, который исполняется медленнее чем написанный вручную как раз настолько чтоб и на этом направлении никаких изменений к лучшему не было.
Единственная идея, которая улучшила производительность получающихся программ - это вызов кода на более практичных, но менее функциональных языках 60-х.
Итоги 70-х вызывают массу вопросов. Почему не появилось компиляторов ФЯ в нативный код? Были ли для этого какие-то объективные препятствия или ФЯ просто не так повезло с имплементаторами, как Прологу? Может важно то, что Пролог - язык первого порядка? Почему тогда для Лиспа есть компиляторы не хуже? Почему код на Лиспе, в который транслируется ML, исполняется настолько медленнее? Почему он не компилируется, а интерпретируется? Почему типизированный язык транслируется в Лисп, а не в какой-нибудь типизированный язык со сборщиком мусора? Почему LCF и через десять лет после него HOPE разрабатываются на компьютерах из одной линейки? Что не так с развитием аппаратного обеспечения? Почему на том компьютере, на котором запускают бенчмарки в 83-ем году нет POP-2, HOPE, компилятора Пролога Уоррена и даже того же самого Лиспа, с которым Уоррен сравнивал свой нативный компилятор и в который транслировали оригинальный LCF/ML?
Пришло время разобраться с этими загадками. И в следующей главе мы начнем с компиляции ФЯ с помощью Лиспа.
Эдинбургская исследовательская программа объединяет не особенно значительное число исследователей и потому вполне естественно ожидать, что они будут использовать множество наработок других исследовательских программ. Которые, так уж вышло, полезны и для имплементации функциональных языков Эдинбургской программы. Итак, что они могут использовать для того, чтоб создать компилятор ФЯ?
В 70-е вывод типов имплементируют только в Эдинбурге. И имплементаторам ЯП все равно нужно имплементировать вывод типов самим - проверка типов делается до каких-то трансформаций.
Над паттерн-матчингом работает больше исследователей, но эти паттерн-матчинги, в основном, делают больше и медленнее, чем авторам ФЯ нужно. Как в языке О'Доннела, например.
Есть, правда, компилятор Уоррена. Нельзя ли компилировать ФЯ в Пролог? Авторы первых компиляторов ФЯ не стали этого делать, но разработчики Пролога об этом думали. Уоррен сформулировал [Warr81] свои идеи об этом в конфронтационном ключе. Заявил о производности и следующей из этого ненужности лямбды. А процесс трансформации лямбд в предикаты формально не описал. Ван Эмден и другие позднее представили эти идеи как технику трансляции ФЯ в Пролог [Emde90], но даже первая статья Уоррена вышла слишком поздно. К этому времени первые имплементаторы компиляторов ФЯ уже определились с теми способами, какими они будут ФЯ имплементировать.
Наиболее очевидная часть функционального языка Эдинбургской программы, которую можно позаимствовать и использовать в готовом виде - это, собственно, сам функциональный язык в более широком смысле - язык с первоклассными функциями. Язык без паттерн-матчинга и вывода полиморфных типов, в который будет транслироваться язык, в котором они есть.
POP-2 - один из таких языков, но, как мы видели, обладает не особенно хорошей производительностью и, по всей видимости, страдает от недостатка труда, вложенного в его совершенствование. Для того, чтоб строить на фундаменте, уже наработанном исследователями и имплементаторами, воспользоваться успехами более раннего и богатого направления, есть более подходящий кандидат - LISP.
Первый компилятор Лиспа начали писать еще в 1957-ом году Роберт Брайтон (Robert Brayton) и помогавший ему Дэвид Парк. Да, тот самый Девид Парк, который после этого будет так успешно имплементировать CPL. Брайтон и Парк писали компилятор LISP 1 на ассемблере. Параллельно Клайм Мэлинг (Klim Maling) писал компилятор Лиспа на Лиспе. Но проект написания компилятора на Лиспе был заброшен из-за очевидного для лисперов того времени превосходства в производительности будущего компилятора на ассемблере. Но в 1960-ом Брайтон ушел из МТИ и без него разобраться в компиляторе на ассемблере и развивать его лисперы уже не смогли [Blai70]. Какие выводы сделали Лисперы из этого опыта, который МаКарти кратко описывает как "неудачный" [McCa78]? Ну, не то чтобы у такого подхода было много работающих на практике альтернатив в то время.
Но для написания на ассемблере компилятора альтернатива все-таки была. Не позднее 1962, более известные другой своей работой, Тимоти Харт (Timothy P. Hart) и Майкл Левин (Michael Levin) написали компилятор LISP 1.5, который считается первым компилятором, скомпилировавшим самого себя [Hart62] [McCa62]. Этот компилятор Лиспа на Лиспе лисперы развивать смогли.
Компилятор Харта и Левина производит код, работающий в 40 раз быстрее интерпретатора [Hart62]. Или в от 10 до 100 раз, в зависимости от программы [McCa62]. В зависимости от каких именно свойств программы? Это мы еще рассмотрим подробнее. Бутстрап занимает всего 5 минут, при том, что большую часть времени большая часть компилятора еще интерпретируется.
И если уже в 62-ом году можно было компилировать язык с первоклассными функциями, то почему же не было компилятора Эдинбургского ФЯ даже уже в 1980? Лисп - это же ФЯ с первоклассными функциями, да?
Так же думал и Дэвид Тернер, когда в 72-ом году начал изучать Лисп. Но он быстро заметил что код делает не то, что он ожидал бы от языка, в котором есть лямбда [Turn12]. Тернер вчитался в документацию [McCa62] и понял, что...
Проблему впервые обнаружил Джеймс Слагл (James R. Slagle), работая с первой имплементацией Лиспа. МакКарти, в своей истории Лиспа даже приводит псевдокод, соответствующий коду Слагла, при отладке которого лисперы впервые поняли, что что-то работает не так надо:
testr[x, p, f, u] <- if p[x] then f[x] else if atom[x] then u[] else
testr[cdr[x], p, f, λ:testr[car[x], p, f, u]].
Да, разумеется, как обычно, это псевдокод, соответствующий коду. Не публиковать же в статье все эти скобки.
Слагл думал, что написал что-то вроде такого:
data Lisp = Atom String | Cons Lisp Lisp deriving (Show, Eq)
testr x p f u | p x = f x
testr (Atom _) p f u = u ()
testr (Cons h t) p f u = testr t p f $ \() -> testr h p f u
но на самом деле получилось у него что-то вроде этого:
testr x p f u | p x = f x
testr h@(Atom _) p f u = u h -- <==
testr (Cons h t) p f u = testr t p f $ \h -> testr h p f u
МакКарти первоначально считал, что это баг в имплементации интерпретатора, и первый имплементатор Лиспа Стив Рассел (Stephen Russell) его быстро исправит. Но оказалось, что интерпретатор делает все, как и задумывал МакКарти. Баг был в идее МакКарти. Конструкция LAMBDA
, придуманная МакКарти, не имплементирует лямбду из ЛИ, в литературу по которому МакКарти не вникал, а просто позаимствовал название [McCa78]. Придуманная Джоном МакКарти (John McCarthy) конструкция - это средство метапрограммирования, создает описание кода функции, которую интерпретатор может выполнить, используя окружение в месте вызова интерпретатора. В описании кода функции есть свободная переменная с именем x
, интерпретатор идет по списку пар и ищет в нем первый ключ "x"
. И находит пару, которая в коде Слагла соответствует параметру функции. Это динамическая видимость. Для функционального программирования же нужна лексическая видимость. Нужно чтоб лямбда конструировала замыкание на окружение в месте вычисления лямбда-выражения. Интерпретатор искал не в том списке пар. Но другого у него и нет.
Рассел, более известный другой своей работой, нашел ошибку и, не без помощи Патрика Фишера (Patrick Fischer), придумал как ее исправить [McCa78]. Код на Хаскеле выше работает потому, что замыкания образуют стек для обхода дерева. Так что Рассел добавил в Лисп замыкания.
eval[(FUNCTION, f); b] = (FUNARG, f, b)
apply[(FUNARG, f, b); x; a] = apply[f; x; b]
Удивительно, что описание конструирования и использования замыкания выглядят как уравнения с паттерн-матчингом, в отличие от обычных описаний на псевдолиспе.
Исправления ошибки, однако, не произошло. Ошибка была классифицирована как фича, а исправление как другая фича, которую можно и не использовать. Видимость по умолчанию не поменялась и конструирование замыканий сделано явным. Не настолько явным, как в POP-2. Ручная работа, которую Поплстоун не хотел автоматизировать, делается в первых Лиспах автоматически. Так что аргумент Поплстоуна для объяснения лисперских решений неприменим.
"Исправление" в Лиспах было сделано не позднее 1960-го [McCa60b], так что прошло не очень много времени для формирования традиции и для написания кода, работоспособность которого нужно сохранять.
Трудно объяснить динамическую видимость по умолчанию и производительностью. Как мы увидим в дальнейшем, лисперов очень беспокоила "глубина" окружения. Они хотели сделать представление окружения в памяти настолько плоским, насколько возможно. И если в наивной имплементации статической видимости она определяется глубиной вложения блоков, то в наивной имплементации динамической возможно придется ползти через ассоциативный список размером со стек вызовов, чтоб из глубин рекурсии прочесть значение переменной определенной до первого вызова [Bake78].
Казалось бы, не важно, что конструировать замыкания нужно явно, если этот код на Лиспе все равно генерируется. Но это явное конструирование замыканий только одно из проявлений равнодушного отношения лисперов тех времен к функциональному программированию. Другим, более важным для компиляции ФЯ трансляцией в Лисп, было то, что наработки по компиляции Лиспа и улучшению производительности обычно не распространяются на эти замыкания.
В 1964-ом году началась разработка нового компилятора Лиспа для нового компьютера. Новый компьютер - той самой линейки, компьютер из которой станет не таким уж и новым новым компьютером в Эдинбурге через десять лет. А от нового компилятора произойдут и важная для нашей истории имплементация Лиспа в МТИ - MacLisp - и та, с которой Милнер познакомится в Стенфорде и будет использовать для имплементации LCF и ML в Эдинбурге - Stanford Lisp.
В 1966-ом году имплементатор этого нового компилятора Ричард Гринблатт (Richard D. Greenblatt) решал ту же проблему окружений, имплементированных как поиск имен в списках пар, что Тернер решал десятилетием позже. И решил, заменив основной способ имплементации окружений с ассоциативного списка на стек [Whit77]. И, более того, даже не такой как в ALGOL 60, в котором есть статическая цепь и дисплеи для имплементации передачи функций в функции. Предполагается, что вы пользуетесь функциями в новом Лиспе даже не как процедурами в ALGOL, а только как в BCPL.
В это время Лисп уже разделился на диалекты. Но имплементаторы BBN-Lisp, начала второго из двух основных семейств Лиспов, Дэниел Бобров (Daniel G. Bobrow) и Дэниел Мерфи (Daniel L. Murphy) позаимствовали Гринблаттовскую идею о имплементации окружения с помощью стека. При этом ошибочно [Weiz68] считая, что такое решение полностью равномощно ассоциативному списку [Bobr67].
Лисперы придумали несколько способов имплементации окружения. Медленных и неправильных, побыстрее и тоже неправильных. И неправильных по-разному, в зависимости от того, интерпретируется код или скомпилирован. Придумали даже правильный но медленный способ. И программист на Лиспе мог и даже должен был постоянно выбирать вручную какие из этих изобретений использовать. Вот только как имплементировать окружения и правильно и быстро пока что придумать не удалось. Так закончилась первая неудачная попытка сделать Лисп функциональным языком.
Компилятор старается имитировать действия интерпретатора настолько точно, насколько это возможно, чтобы интерпретируемый и компилируемый код вели себя одинаково. Примечательно, что для Лисп-систем такое свойство является скорее исключением, чем нормой. Джулиан Паджет, Три необыкновенных Лиспа. [Padg88]
Второе дыхание FUNARG-проблемология получила после контакта лисперов с Ландином и Берджем.
Одна из основных работ по теме этого периода - статья Мозеса [Mose70]. В ней Мозес вспоминает про работу Ландина в МТИ в 1967 и его ограниченное влияние на лисперов. Основное внимание уделяется описанию разных способов имплементации окружений в Лиспе и их развесистости в памяти. Костыльно-грабельный ландшафт, который Мозес обозревает вместе со своим читателем, выглядит мрачно. Вывод, однако, Мозес делает неожиданный. Он не считает проблемой все эти разновидности быстрых неработающих и медленных работающих связываний переменных, которых в других языках, "к сожалению" нет. Тем более, лисперам особо и не надо возвращать никакие замыкания.
Лисперам, который представляет главный пользователь MacLisp Мозес может и не надо.
Это, однако, не все лисперы МТИ. Есть еще разработчики высокоуровневых языков поверх Лиспа. Им надо самим имплементировать свои работающие окружения как структуры в куче [Stee96]. В MacLisp такие фичи для них не добавляют. Это обосновывается идеей, которая в наше время ассоциируется совсем не с Лиспом: не нужно платить за то, чем не пользуешься.
И если разработчикам высокоуровневых языков, разрабатывающихся в МТИ как правильная альтернатива резолюционизму, помощи от имплементаторов MacLisp ждать не стоит, нужно надеяться только на себя, то ради кого разработчики MacLisp стараются? Кто не будет платить, потому что не использует высокоуровневые фичи? Мы еще рассмотрим работу этих более важных пользователей MacLisp подробнее. Эти работы будут действительно полезны для функционального программирования, хотя не самым непосредственным образом и это не то, чего планировали достичь авторы этих работ.
Только в 75-ом году в MacLisp добавили фичу PROGV
[Whit77], более-менее похожую на ручное замыкание из POP-2. Но насколько эффективно ее можно было бы использовать для имплементации ФЯ - неизвестно. Потому, что в Stanford Lisp - версии LISP 1.6, отколовшейся от МТИ-компилятора вместе с МакКарти, отколовшимся от МТИ, ничего такого не появилось. Если кто вдруг захочет возвращать функции - ему остается только интерпретатор, и поиски имен переменных в ассоциативном списке.
Именно эту версию Лиспа использовали в Эдинбурге и это объясняет то, что код на LCF/ML 70-х не компилировался, а интерпретировался. И объясняет производительность этого интерпретатора по сравнению с интерпретатором Лиспа.
Таким образом, едва ли можно говорить о преимуществе Пролога как языка первого порядка для имплементатора компилятора. Имплементация Лиспа, с которой он сравнивается более-менее компилирует только подмножество Лиспа первого порядка.
Итак, влияние Ландина и ISWIM/PAL в МИТ оказалось ограниченным и флагманская имплементация Лиспа продолжила неколебимо следовать прежним антифункциональным курсом. К счастью, такой недружественный к функциональному программированию Лисп уже не единственный Лисп. Появились уже имплементации, у создателей которых было другое мнение о нужности возвращения функций из функций.
Весной 1968 более известный другой своей работой Джозеф Вейценбаум (Joseph Weizenbaum) написал одну из первых статей [Weiz68], объясняющих FUNARG-проблему. В ней он упомянул и заблуждения [Bobr66] имплементаторов BBN-LISP о том, что они могут решить FUNARG-проблему с помощью стека как в Алголе. Это может и лучше, чем стек как в MacLisp, но все еще недостаточно.
Упомянутые имплементаторы не стали упорствовать в своих заблуждениях, открыли для себя возвращение функций из функций и ссылаются на статью Берджа [Burg71] про это, продолжили работать над решением проблемы. И в 1970 году уже знакомый нам Дэниел Бобров с пришедшей на смену Мерфи Элис Хартли (Alice K. Hartley), придумали [Teit2008] более подходящий для этого нелинейный, древовидный стек. Стек, фрагменты которого не освобождаются, пока на них кто-то продолжает ссылаться, что определяется с помощью счетчика ссылок или даже сборщика мусора. Скорее всего с помощью сборщика мусора. Потому, что с помощью одного только счетчика ссылок не удалось сделать все что хотелось бы. Заодно, сборщик мусора может и компактифицировать слишком развесистый стек.
Казалось бы, лисперы не должны быть недовольны использованием сборщика мусора, но тут не все так просто и к этому мы еще вернемся.
Статья об этом стеке [Bobr73] была отправлена в издательство в 72-ом и напечатана в 73-ем. Новым соавтором Боброва стал Бен Уэджбрейт (Ben Wegbreit), который до того занимался имплементацией бэктрекинга. Хартли не упоминается в статье, но указана в анонсе [Teit78] новой фичи как имплементатор.
Этот новый стек Боброва, Хартли и Уэджбрейта обычно называется спагетти-стеком, но это название не единственное и не используется в первой статье [Bobr73]. Но такое название самое популярное в среде лисперов. Не смотря на то, что в отличие от многих других (вроде кактус-стека) оно происходит от очень плохой визуальной метафоры. Спагетти не разветвляются, что очень сложно не заметить. Возможно, что название дано критиком и спагетти в нем означают только нежелательную запутанность, как в спагетти-коде.
Изобретатели спагетти-стека ожидали, что если использовать язык со спагетти-стеком как язык со стеком обычным, то работать все будет не намного медленнее [Stee96]. Но этот способ неуплаты за неиспользуемое лисперы МТИ все равно посчитали нежелательным, и решили ничего такого не делать [Whit79].
Если же эти возможности использовать для возвращения функций, бэктрекинга или корутин, то уже не все так хорошо. Исследователи, работающие в МТИ над высокоуровневыми языками на базе Лиспа, раскритиковали [Stee77m] спагетти-стек за ряд недостатков. Корутины и возвращаемые из функций функции удерживают больше стека, чем нужно. Это также не особенно быстрый способ имплементировать рекурсию. Но все равно имплементировали что-то похожее своими силами в МакЛисповой куче [Stee96].
Между изобретением спагетти-стека и его использованием для имплементации Лиспа прошло немало времени [Teit74]. Элис Хартли закончила имплементацию в бывшем BBN-LISP (который к этому времени уже назывался Interlisp) в 1975-году [Teit78].
Разница в поддержке функционального программирования имплементацией - не единственное и не главное отличие между двумя основными ветвями развития Лиспов. MacLisp был более традиционным средством разработки, интерпретатором и компилятором работающими с текстом, написанным в текстовом редакторе. Основные усилия разработчиков были направлены на качество генерируемого кода (если вы, конечно, не генерируете его из кода функционального). У разработчиков Interlisp были другие приоритеты. Программирование на Interlisp предполагало то, что сегодня ассоциируется с совсем другим языком, разрабатывающимся в это время по соседству. А именно манипулирование супом из объектов в памяти. Сохранение и загрузку образов этого супа из нечеловекочитаемых файлов. В добавок к этому, манипулирование должно было происходить с помощью текстовых команд, которые среда исправляет так, как считает нужным. Весь фокус имплементаторов был на инструментарии для этого.
Статьи Берджа оказали некоторое влияние на имплементацию Interlisp, но главного успеха в продвижении ФП в среде лисперов Бердж добился в другом месте. К сожалению, в не самом важном месте для развития Лиспа. Разумеется, этим местом была уже известная нам лаборатория IBM в Йорктаун Хайтс.
Ван Эмден вспоминает [Emde06], что в Йорктаун Хайтс Лисп влачил периферийное, а иногда даже подпольное существование. Использовался он для имплементации уже знакомой нам системы компьютерной алгебры SCRATCHPAD. Но у Фреда Блэра (Fred W. Blair) были более амбициозные планы, касающиеся Лиспа. МакКарти сделал Лисп на 85% правильно, объяснял Ван Эмдену Блэр, осталось исправить оставшиеся 15, добавить в Лисп первоклассные функции. Об этих планах Блэр рассказывал, по видимому, в 71-ом году, когда Ван Эмден посещал лабораторию IBM. И годы после этого планы все оставались планами. И Ван Эмден считает в своих воспоминаниях само собой разумеющимся, что проект Блэра вскоре помер. Этого, однако, не произошло.
Разработка Lisp/370 по настоящему началась только в 74-ом году [Padg88]. Встречаются ссылки на отчет Блэра 76-го года, но до нас дошла только версия 79-го [Blai79], описывающая раннюю версию LISP 1.8+0.3i. Да, номер версии - комплексное число.
Блэр, сам один из авторов обзоров разнообразных лисповых переменных и областей видимости [Blai70], и относившийся к этому разнообразию хуже Мозеса, все-таки разнообразие по большому счету сохранил. Не полностью, отличия в работе областей видимости в одном и том же коде между компилятором и интерпретатором он посчитал нежелательными. Также Блэр поменял умолчания. Лямбда по умолчанию создает лексические замыкание. Имплементация основана [Padg88] на спагетти-стеке Боброва и его развитии Стилом [Stee77m]. Блэр ссылается на работы обоекембриджской программы и работы Милнера и Гордона по описанию семантики Лиспа. Описывает семантику своей имплементации с помощью SECD-машины. Описывает данные с помощью нотации Ландина.
Сначала Lisp/370 был внутренней разработкой для разработки внутренней разработки, но со временем стал продуктом, за который нужно было платить IBM $1500 в месяц. После двух лет можно было перестать платить, так что Lisp/370 обошелся бы своему пользователю не больше чем какие-то $36000 [Whit77] ($182000 в 2023-ем году). Это был если и не первый компилятор ФЯ в широком смысле, то по всей видимости первый, который продавался. Сложнее сказать, покупался ли. Но это серьезно помешало бы использовать его как бэкенд для компилятора эдинбургского ФЯ. Какой-нибудь MacLisp и прочие университетские разработки того времени разрабатывались на государственные деньги и были общественным достоянием. Более серьезной проблемой было то, что Lisp/370 работал на машинах, которых не было у большинства лисперов и у имплементаторов эдинбургских ФЯ. Почему так получилось мы разберемся позже. И портировать этот Лисп на те машины, которые у них были, было бы сложно из-за плохой портируемости имплементации, написанной в значительной степени на ассемблере. Да и IBM было не особенно интересно это делать.
Не смотря на такую изолированность этого Лиспа от истории ФЯ, он на нее все-таки повлиял. Над этим ФП-лиспом поработали Джон Уайт (Jon L. White) и Ричард Гэбриел (Richard P. Gabriel) [Whit77] [Stee96] и этот опыт позднее сделал их сторонниками функционализации Лиспа.
Как мы уже отмечали, Коэн необоснованно строг к изобретателям и имплементаторам Пролога. Их переход от идеи до компилятора рекордно быстрый. Лисперы же смогли наконец скомпилировать лямбду только почти через двадцать лет. Конечно, справедливости ради нужно сказать, что они не очень-то и хотели. Успеют ли имплементаторы эдинбургских ФЯ скомпилировать лямбду хотя-бы за это же время? В конце 70-х время у них еще есть!
Более-менее поддерживающие функциональное программирование варианты Лиспа опоздали к началу разработки LCF/ML. Но, до того, как некоторые проблемы стали очевиднее и до того, как появились средства получше была пара лет, окно возможностей для того, чтоб скомпилировать ML или HOPE например в Interlisp. Это не было сделано. Имплементаторы эдинбургских ФЯ для своих попыток имплементации ФЯ с помощью Лиспа выбирали антифункциональное MacLisp-семейство. Может быть это просто случайность, вопрос моды, и если бы Милнер поработал в Калифорнии на пару лет позже, то он привез бы в Эдинбург оттуда моду на Interlisp. И LCF/ML был бы компилятором с самого начала. Да, современный Лисп происходит из MacLisp семейства, а Interlisp умер вскоре после того, как ФЯ стали компилировать через Лисп. Но можно ли было предвидеть это в 70-е? Может это какой-то эффект отбора и мы знаем о функциональном программировании потому, что Лисп был выбран правильно, а про AFFIRM транслирующийся в Interlisp, например, не слышали? Насколько важным для истории ФП было выбрать правильный Лисп? Рано или поздно мы это выясним.
Не смотря на то, что началась с прибытия Ландина в МТИ, вторая волна функционализации Лиспа породила имплементации, которые были еще новыми и даже экспериментальными десятилетие спустя, в конце 70-х. Как раз вовремя, чтоб их затмили результаты волны третьей.
Иногда здравомыслящему дизайнеру удается проанализировать накопившийся набор идей, отбросить менее важные и получить новый, небольшой и чистый дизайн.
Это не наш случай. Мы на самом деле пытались создать нечто сложное, но обнаружили, что случайно получили нечто, отвечающее всем нашим целям, но гораздо более простое, чем мы собирались сделать.
Джеральд Сассман, Гай Стил [Stee98]
Итак, вторая волна функционализации Лиспа бессильно разбилась о неприступную крепость МТИ, никак не поколебав уверенности тамошних лисперов в том, что ФП им ненужно. Но в то же самое время функциональное программирование уже завоевывало их изнутри. После того, как они сами переизобрели его, отсекая все лишнее от своих экспериментальных антирезолюционистских противологических языков.
Как мы еще помним, в 1970 гости Эдинбурга из МТИ, одним из которых был Джеральд Сассман (Gerald Jay Sussman), убеждали не связываться с резолюционизмом. Что же они предлагали вместо этого? Много чего, но ничего работающего.
В 1969 Карл Хьюит спроектировал "крайне амбициозный" язык Planner с паттерн матчингом и бэктрекингом. Этот язык так никогда не имплементировали полностью, но спроектировали и имплементировали язык для его имплементации - MDL. MDL хотя бы использовался для того, чтоб имплементировать ранние прототипы CLU [Lisk93] и игры Zork. Не такие амбициозные проекты как Planner. Почему бы не транслировать Planner в Лисп? Из-за отсутствия в нем в то время структур данных кроме пар и динамической видимости.
В 1971 Сассман с коллегами имплементировал подмножество Planner под названием Micro-Planner. Правда, имплементировали они его неправильно. Micro-Planner не использовал для матчинга алгоритм унификации. Просто как-то матчил. И формального описания языка не было, и трудно было сказать как матчить надо было. Но Сассман считал, что в ряде сложных случаев он матчил не так. Как Сассман впоследствии рассказывал новому важному герою нашей истории Гаю Стилу (Guy Lewis Steele, Jr.), первой корректной имплементацией Micro-Planner был Пролог. Эти проблемы с практичной альтернативой резолюционизма, однако, не поколебали уверенности МТИ в превосходстве практичности над резолюционизмом. Практичность решили еще усилить.
В 1972, не в силах преодолеть ограничения Micro-Planner, Сассман с коллегами решили, по крайней мере, преодолеть бэктрекинг. Бэктрекинг - рассуждает Сассман - это просто переусложненный способ выразить обычный перебор с помощью вложенных циклов. Вот пусть пользователь языка и пишет вложенные циклы. Так ему будет понятнее, что он делает и нужно ли ему это делать. Так появился следующий антипролог МТИ - Conniver. Этот язык был достаточно простым, чтоб имплементировать его на основе MacLisp. Но не без собственного, похожего на Interlisp-идеи стека в куче.
Дальше эти более явные средства перебора стали совершенствовать. И мы уже знакомы с вершиной этой эволюции средств перебора. С восстания против них началась история ленивых ФЯ. Это акторы, решающие проблему кромок в языке Planner-73, позднее названном PLASMA (PLAnner-like System Modeled on Actors) [Stee98]. Да, это все пока выглядит как история идущая куда-то не туда, в сторону от истории ФП. Не беспокойтесь, все еще можно исправить. Остался еще один шаг, последний антипролог МТИ.
Создания этого антипролога не произошло бы, если б Сассман не был должен читать курс, который заставлял его думать о лямбда исчислении, а Стил не пытался бы понять акторы [Stee75]. Стил учился в МТИ и совмещал учебу с работой, поддержкой MacLisp. Как разработчик MacLisp, он интересовался конструкциями, которые разработчики антипрологов организуют в куче. Чтоб сделать более-менее нормальную видимость, ведь MacLisp им с этим не помогает. PLASMA была актуальным антипрологом в это время и Стил привык к тому, как работают акторы, но не то чтобы понял. Потому, что не мог никому объяснить как они работают. И лучший способ что-то понимать - это, конечно же, написать игрушечный интерпретатор. К чему Сассман со Стилом и приступили. Почему они писали один интерпретатор и для лямбд и для акторов? Акторы требовали видимости как в Алголе, ну или как в лямбда-исчислении.
Первоначально интерпретатор был с вызовами по имени, как и полагается для интерпретатора ЛИ. Но Стил с Сассманом столкнулись с проблемой: итерация громоздила в памяти кучи санков, пропорциональных числу итераций. Решение было найдено, но слишком поздно. Только после того, как интерпретатор переделали на вызов по значению, а ЛИ расширили условным оператором. Энергичным этот язык так и остался. А вот чего в нем не осталось, так это акторов. Потому, что в ходе имплементации Стил с Сассманом обнаружили, что лямбды и акторы имплементированы одинаково. Поэтому они решили, что акторы - то же самое, что и замыкания. Так что Стил с Сассманом просто выкинули все наработки многолетней борьбы с Прологом, кроме одной - статической видимости. Лучше бы оставили еще одну, но об этом позже.
Так замкнулся круг борьбы с резолюционизмом: от Лиспа борцы пришли к Лиспу, но только с работающими лямбдами. ООП как в SmallTalk было серьезной заявкой на абсолютный антипролог, спору нет. Но ничто не сделает Пролог настолько ненужным, как функциональное программирование.
Язык назвали в духе предыдущих - Schemer. Но в ОС на которой Schemer разрабатывался было ограничение на длину имен в 6 символов. В это ограничение не влезали ни Planner ни Conniver. Но только используемое для имени команды урезанное название Schemer было настоящим словом, а не сочетанием согласных вроде PLNR
. Так что язык стали называть SCHEME. Перове описание языка вышло как отчет [Stee75] в конце 1975.
Акторы оказались не нужны в Схеме из-за того, что это просто функции, которые ничего не возвращают, а продолжают вызывать другие функции [Stee98]. И не то чтобы Стил собирался что-то возвращать из остальных. Он не собирался останавливаться на наивном интерпретаторе, а хотел компилировать функции в производительный код [Stee77]. Было бы неплохо, если б и циклы оказались ненужными.
Я в каком-то смысле обескуражен. Во многих смыслах, я бы сказал.
Эдсгер Дейкстра
Нет-нет! <..> Нет-нет-нет! <..> Что! Что? Что?
Адриан Ван Вейнгаарден
Первый способ компилировать функции в производительный код был изобретен Адрианом Ван Вейнгаарденом, автором Алголов и научруком Дейкстры и Ван Эмдена. И в сентябре 1964 Ван Вейнгаарден представил [Wijn66] свои идеи на рабочей конференции IFIP в Бадене близ Вены. Представил, правда, как способ транслировать производительный код в функции, что вызвало не самую хорошую реакцию у аудитории.
В докладе он неформально описал трансляцию из алголоподобного языка в меньший язык, в котором остаются только конструкции, семантику которых, по мнению Ван Вейнгаардена, будет легче описать. Процедуры он считает конструкцией, которую описывать легко. Именно процедуры, даже функции преобразовывает в процедуры добавлением параметра. А goto
и метки описывать сложно, так что он и их преобразовывает в вызовы процедур и их декларации.
Интересно, что этот доклад слушал Стрейчи, который позднее будет вместе с Вадсвортом несколько лет ломать голову над тем, как транслировать goto
в лямбды. Пока они не переизобрели то, о чем Стрейчи должен был бы узнать из этого доклада. О CPS-преобразовании. Там же присутствовал МакКарти, который тоже не смог позднее помочь Локвуду Моррису переизобрести продолжения. На конференции присутствовал и Ландин. Но Ландин не подал виду, что Локвуд Моррис изобрел что-то уже известное Ландину, когда Моррис докладывал о своих результатах в Лондонском колледже королевы Марии.
Эту полную непроницаемость аудитории для идей Ван Вейнгаардена Рейнольдс в своей работе [Reyn93] называет главной загадкой истории продолжений. Рейнольдс не надеется, что загадка будет когда-то разгадана со всей определенностью, хотя можно и строить предположения. Ну что же, давайте строить предположения. Для строительства предположений есть материал: сохранился не только доклад, но и стенограмма его обсуждения. И, как точно подметил Рейнольдс, "последовавшая за докладом дискуссия выявила глубокие философские различия между ван Вейнгаарденом и остальными исследователями".
Вейнгаарден, судя по всему, потерял Стрейчи, когда сказал, что транслирует функции в процедуры. Когда началось обсуждение, Стрейчи заявил, что Вейнгаарден "убрал из языка все, про что люди думают, что это важно". И "оставил то, что все считают неважным". Стрейчи недоволен, что Вейнгаарден заменяет функции на процедуры, он бы хотел чтоб было наоборот, потому что функции - более изученные математические объекты. Вейнгаарден отвечает, что это вопрос "вкуса", ему процедуры понятнее. Возможно, Стрейчи и не слушал остальной доклад потому, что не сделал никаких комментариев по более важной для нашей истории его части.
Примерно та же история и с МакКарти, он в основном дискутирует что более фундаментально - числа или строки.
Комментарий Самельсона из той же серии, но ближе к важной части доклада. Самельсон спрашивает, почему goto
заменены процедурами, а не наоборот. goto
выглядит для него более фундаментальной конструкцией. Вейнгаарден отвечает, что не знает, как описать семантику goto
.
Дейкстра спрашивает, доказал ли Вейнгаарден корректность трансформации. Вейнгаарден отвечает что нет, но выглядит так, что должно работать. Из других вопросов понятно, что Дейкстра почему-то решил, что трансформация заменяет вызовы функций на goto
а не наоборот. В результате трансформации остаются только goto
. В принципе, не самое плохое направление мысли для имплементатора ЯП, но не в этом случае. Вейнгаарден уверяет, что нет, наоборот, никаких goto
не остается.
Доклад слушают имплементаторы языков и их, конечно, интересуют вопросы имплементации. Но нельзя сказать, что они сами сделали важные для имплементатора выводы из доклада. Видимо узнать в чем-то молоток, когда нужно забивать гвозди - сложнее, чем видеть везде гвозди, когда в руках молоток.
Горн спрашивает, будет ли трансляция осуществляться какой-то программой, видимо, как часть процесса компиляции. Вейнгаарден отвечает, что это разделение на два языка с производными конструкциями и без делается в основном для упрощения спецификации языка. Далее Горн сомневается в том, что такие преобразования сохранят смысл программы, но видимо не совсем понимает, что делает Вейнгаарден и Вейнгаарден не совсем понимает в чем заключаются его претензии.
МакИлрой говорит, что ему в принципе нравится выделение минимального языка. Особенно с точки зрения имплементатора языка. Но ему не нравится, что goto
заменен на процедуры. И, значит, нужно тратить память, чтоб "поддерживать всю историю вычисления". Вейнгаарден отвечает, что МакИлрой должно быть имеет в виду конкретную имплементацию процедур. Но после преобразования процедуры никогда не возвращают, только вызывают следующую перед своим окончанием. А значит можно использовать имплементацию, которая ничего не делает для того, чтоб можно было возвращаться из процедуры. А в этом, по мнению Вейнгаардена и есть вся сложность имплементации процедур. Процедуры, нужные после трансформации проще. Они то же самое, что и goto
.
Таким образом, Вейнгаарден явно проговаривает главную, самую важную для имплементатора идею в обсуждении, хотя сам доклад сфокусирован на другом. Ничего слушателям самим и не нужно было додумывать.
На этом стенограмма обсуждения заканчивается. Реакция МакИлроя и прочих имплементаторов на переформулированную идею либо не сохранилась, либо ее и не было. Последнее вполне возможно. Рейнольдс, когда работал над историей продолжений, запрашивал комментарий у МакИлроя. Дуглас МакИлрой (Douglas McIlroy) в письме Рейнольдсу [Reyn93] вспоминает, что так и не увидел никаких практических примеров, только трюк для демонстрации того, что и так известно.
Но идеи Вейнгаардена не были просто проигнорированы. Результат доклада был гораздо хуже.
МакИлрой вспоминает [Reyn93], как на следующий день во время утреннего перерыва Дейкстра записывал на салфетке свои идеи структурных команд для выхода из цикла. Он не сделал из доклада Вейнгаардена вывод о том, что goto
можно использовать для имплементации функций. Он сделал вывод, что goto
не нужен. И он не собирался заменять goto
эффективно имплементированными функциями, он собирался заменять goto
конструкциями, которые для имплементации функций использовать в общем случае нельзя. В последующие годы Дейкстра сформулировал мем, носители которого будут уничтожать любую возможность использовать какой-то язык как целевой для компиляции ФЯ с помощью CPS, даже не зная о существовании продолжений и давно найдя для этого другие обоснования. Война с продолжениями идет так успешно, что даже низкоуровневые VM вроде LLVM и WebAssembly сделаны непригодными для эффективной компиляции в них ФЯ.
Как и компиляция с помощью CPS-трансформации, противофункциональная идея Дейкстры берет все возможное от того факта, что процедура и goto
- это одно и то же. Если бы формулировка была "procedures considered harmful", знакомящиеся с ней могли бы засомневаться: так ли плохи процедуры на самом деле? Но процедура выступает в своей наиболее неприглядной форме - goto
. И сомнения в оправданности борьбы возникают гораздо реже. Поскольку ни Дейкстра, ни его последователи особо не интересовались и не интересуются тем, с чем они на самом деле борются, полноценной поддержки ФП взамен исключаемой обычно не добавляется. План Дейкстры не существует, но действует.
Ну, по крайней мере, как мы помним, Хоар придумал АлгТД современного вида как борьбу с голыми указателями, по его замыслу, аналогичную борьбе с goto
. Не самый прямой и даже объяснимый эффект, но определенно положительный.
Было ли изобретение Вейнгаардена жертвой самого недопонятого из всех недопониманий этой главы? Или у имплементаторов ЯП были более серьезные основания отмахнуться от него как от простого курьеза, ничего не дающего на практике? Но что плохого в эффективных функциях Вейнгаардена? Что тут не любить?
Начнем с того, что алгоритм Вейнгаардена содержит ошибку [Reyn93]. Что справедливо подозревают его оппоненты во время обсуждения, хотя и не могут на эти ошибки указать. Но это проблема не особенно значительная. В первой половине 71-го года Джеймс Моррис, конечно же изобрел продолжения независимо, в этот раз независимо и от другого Морриса, который в свою очередь изобрел их независимо от него. Моррис описал CPS-трансформацию подробнее и исправил ошибку обработки вложенных блоков. Также он не транслирует функции в процедуры - столкновение со Стрейчи вполне можно было избежать. Но Джеймс Моррис, в отличие от многих других изобретателей продолжений, был разоблачен. Это все уже изобретено, в публикации отказано. Опубликовано было только письмо в редакцию [Morr72], в котором Моррис кратко излагает собственную идею, для реализации которой он и переоткрыл CPS-преобразование. Также он называет все это бесполезным на практике. И это, судя по всему, нормальная реакция имплементатора того времени на "быстрые" функции Вейнгаардена. То, что по замыслу Вейнгаардена делает их быстрыми, также делает их крайне непривлекательными и даже и бесполезными с точки зрения имплементатора ЯП.
Моррис придумал CPS-преобразования во время экспериментов по энкодингу структур данных в лямбда-исчислении. И применил для обхода ограничений языков на возвращение значений из функций. ALGOL 60 не может возвращать из функций многое из того, что функции могут принимать через параметры. Можно передать функции несколько параметров, но нельзя вернуть несколько результатов. Нельзя вернуть массив или функцию, но и то и другое можно передавать в алголовскую функцию. И CPS-преобразование для Морриса - это трюк, который позволяет преобразовать псевдокод на псевдоалголе, который все это возвращает, в "работающий" код на ALGOL 60, который все это только принимает. Разумеется, возвращение всего этого в Алголе запрещено не просто так. Моррис это понимает, но про CPS-преобразование в это время пишут и то, что, скажем так, не способствует серьезному отношению имплементатора ЯП к этой идее. В качестве примера рассмотрим работу очередного независимого изобретателя продолжений.
В январе 1972 Майкл Фишер (Michael J. Fischer) сделал доклад [Fisc93] на конференции. Фишер впервые [Reyn93] доказал, что CPS-преобразование сохраняет смысл преобразуемого кода. То, чем интересовался Дейкстра во время обсуждения доклада Вейнгаардена за почти восемь лет то этого. Но крайне маловероятно, что новый доклад понравился бы Дейкстре. Для чего Фишер применил CPS-преобразование? Есть две стратегии управления памятью, рассказывает Фишер. В одном случае память удерживается пока нужна, что определяется сборщиком мусора. В другом случае память удерживается до возврата из функции. С помощью стека. Не все корректные лямбда-выражения можно корректно вычислить при использовании второго метода управления памятью. Он освобождает память раньше времени. Но мощность обоих методов одинаковая, утверждает Фишер. Ну, это явно неверное утверждение. Но погодите, Фишер технически прав, и эта его правота полностью бесполезна на практике. Следите за руками: Фишер может преобразовать любое лямбда-выражение вот так и после этого, при его вычислении с помощью стека никогда не окажется, что что-то нужное для вычисления освобождено. Да, потому, что вообще ничего никогда не освобождается. CPS-преобразование, функции никогда ничего не возвращают. Это звучит как шутка, но нет. Все серьезно. Одна из целей этой работы показать, что управления памятью как в MacLisp "достаточно" для "всего". И дальше таких идей от лисперов будет только больше.
Разумеется, имплементатор ЯП этого времени не хочет, чтоб функции никогда ничего не возвращали. Он не так давно, можно сказать, только что изобрел способ автоматического управления памятью, который намного быстрее сборки мусора и хочет использовать его побольше. Да, функция - это важнейшее средство управления памятью [Dijk60] [Stee77]. "Улучшение" функций ценой потери такого важного средства для него неприемлемо.
Правда, Моррис пишет, что трюк бесполезен только для существующих имплементаций. Может быть можно как определить что нужно освободить перед хвостовым вызовом, предполагает Моррис. Смогут ли Сассман и Стил решить эту проблему?
Итак, популяризация ФП в среде лисперов по настоящему началась, когда Джеральд Сассман и Гай Стил переоткрыли ФП заново. Под влиянием того же, под влиянием чего это сделали Обоекембриджцы: ALGOL 60 и лямбда-исчисления. Но, в этот раз ФП изобрели, отрезая лишнее от объектно-ориентированного антипролога. Наконец-то на ФП обратили внимание в самом мейнстриме Лиспа, а не в каком-то ответвлении.
На протяжении десятилетий одни лисперы объясняли другим лисперам, что такое FUNARG-проблема и как её (не) решать. Объясняли что функции, с которыми можно обращаться как с объектами - не то же самое, что представления функций, с которыми можно обращаться как с объектами. Но настоящий золотой век FUNARG-проблемологии начался со статей "Lambda The Ultimate".
В серии публикаций Сассман и Стил описывают Схему - "full-funarg" диалект Лиспа с ключевыми фичами для ФП в самом широком толковании: лексической видимостью, замыканиями для решения FUNARG-проблемы, оптимизацией хвостовых вызовов.
В статьях третьей волны обсуждения FUNARG-проблем [Stee75] [Stee77] [Bake78] смелее критикуется динамическая видимость и отличия в работе скомпилированного кода от кода, исполняющегося в интерпретаторе. И критика не ограничивается тем, что без лексической видимости и первоклассных функции язык не будет иметь близкую к лямбда-исчислению семантику. Динамическая видимость критикуется с позиций производительности.
Лисперы защищали динамическую видимость из-за того, что считали, что лексическая видимость не может быть имплементирована так же эффективно. Сассман и Стил утверждают, что это заблуждение. Лексическая видимость может быть имплементирована эффективнее, чем динамическая. И поддержка опциональной динамической видимости замедляет работу имплементаций окружений вроде спагетти-стека. К тому же, лексическая видимость и оптимизация хвостового вызова не независимы, динамическая видимость не позволяет оптимизировать хвостовой вызов [Stee77]. И Сассман со Стилом считают, что им есть что продемонстрировать для подтверждения их убеждений.
CPS-трансформация, преобразующая все вызовы в хвостовые и их имплементация как переход к метке - центральная идея, используемая Стилом для имплементации ФЯ. Первый компилятор Схемы под названием CHEAPY, написанный им, делает практически только это. Это пруф-оф-концепт, половина - код из отчета [Stee76] ноября 76-го года "LAMBDA: The ultimate declarative", другая половина - простой транслятор без оптимизаций в другой язык.
CHEAPY показал, что компилировать ФЯ таким образом можно, и Стил приступил к более амбициозному проекту. Для защиты своей магистерской диссертации, он не позднее мая 77-го написал первую версию второго, гораздо более сложного компилятора Схемы под названием RABBIT. Научруком был Джеральд Сассман. На этом развитие компилятора не закончилось, он был изменен для поддержки второй версии Схемы, а его оптимизатор был полностью переписан. Эта версия RABBIT описана в отчете [Stee78], вышедшем в мае 1978.
Да, у этого компилятора, уже есть оптимизатор. Но это все еще прототип компилятора, точнее даже прототип части компилятора. Стил не писал кодогенератор и рантайм, использовав как бэкенд компилятор MacLisp. Схема транслировалась в очень небольшое и низкоуровневое подмножество Лиспа. Разумеется, использовать лисповые функции и объявления переменных нельзя, они работают медленно и неправильно. Точнее нельзя использовать функции как функции. Лисповая функция используется как "модуль". Функции Схемы становятся метками и командами перехода к ним GO
внутри Лисповой процедуры PROG
. В MacLisp был goto
. Но в стандартном и лучше всего имплементированном на PDP-10 системном языке BLISS, аналоге BCPL, goto
не было [Stee76b]. До автора этого языка к этому времени уже дошли идеи Дейкстры. Ну, разработчики MacLisp особое внимание уделяли качеству генерируемого кода, так что это не самый плохой выбор для бэкенда. И можно, наверное, надеяться, что у флагманской имплементации языка, который использует сборку мусора уже двадцать лет, будет неплохой сборщик мусора. Мы к этому еще вернемся.
Для компилятора планировался фронтенд, транслирующий какой-нибудь алголоподобный язык в Схему. К этому времени описатели семантики ЯП наработали много идей по представлению разных, в том числе и императивных конструкций в ЛИ. И Сассман и Стил считают серьезным преимуществом корректной имплементации ЛИ то, что все это можно использовать для имплементации такого фронтенда. Тем более, что такой энкодинг императивных конструкций не приводит к неприемлемому росту размера кода. Поскольку имплементация функций эффективная - поддержка циклов в ядре языка не нужна. Получающиеся в результате трансляции цикла лямбды будут скомпилированы в тот же goto
. Транслятор из алголоподобного языка не был сделан, но подход опробован трансляцией Схемы в её подмножество, которое используется как промежуточный язык. Сассман и Стил сделали обзор этих трансляций в отчете "Lambda: The Ultimate Imperative" [Stee76b], вышедшем в марте 76-го года.
И главная, наиболее интересная для истории ФЯ часть RABBIT - трансформации этого промежуточного языка, расширенного лямбда-исчисления. Ну или двух языков: до и после CPS-преобразования. Все эти подмножества Схем никак статически не проверяются и не разделяются все равно.
Небольшой набор преобразований транслирует это расширенное ЛИ в императивный код. Как утверждает Стил, такой код, который ожидается от традиционного компилятора. Большое количество традиционных техник оптимизации - частные случаи небольшого количества оптимизаций, имплементированных в RABBIT. Это не случайность, а правильный выбор базиса. CPS-преобразование разрешает важные проблемы компиляции естественным образом: промежуточные значения становятся явными аргументами, также становится явным и порядок вычислений.
Компилятор как бы примиряет подходы Бурсталла и Уоррена. Компиляция не требует ручного управления, и тем не менее размах трансформации должен был производить впечатление в свое время. Трансформационная часть пайплайна - десять проходов:
- Три прохода предварительного анализа. Поиск мутаций, сайд эффектов и тривиальных выражений - того, что можно оставить вычислить MacLISP.
- Оптимизации, которые данные этих анализов используют.
- CPS-преобразование.
- Четыре прохода анализа окружений и замыканий.
- Генерация кода на малом подмножестве MacLISP.
Лексическая видимость означает, что получить доступ к переменным окружения может только или код в этом замыкании, или код, получивший их через параметр. Так что представление окружения не должно быть какого-то стандартного формата. Это абстрактные данные, с которыми работают только через интерфейс "вызов функции". Внутренне представление может быть выбрано наиболее подходящим для каждого случая. И RABBIT выбирает разместить окружение в регистрах, на стеке или в куче.
Да, осуществлены мечты Морриса о том, чтоб имплементация размещала и освобождала память с помощью стека корректно, даже если функции ничего никогда не возвращают. Если бы кто-нибудь написал транслятор ALGOL 60 в Схему, то RABBIT генерировал бы код для ALGOL 60, который все окружения размещает в регистрах или на стеке, как и полагается компилятору Алгола. Ничего не понадобилось бы размещать в куче и обходить сборщиком мусора. Для имплементации с помощью RABBIT языка, который можно имплементировать без сборщика мусора, сборщик мусора и не понадобится. Не смотря на все эти лямбды в промежуточном коде. Быстрые функции не заставляют на самом деле жертвовать управлением памятью с помощью стека.
Те окружения, которые все же надо размещать в куче из-за возвращения функций из функций, например, не представляют ничего интересного. Стил упоминает, что они могли бы быть имплементированы с помощью недавно добавленных в MacLisp непрерывных структур в памяти вроде массивов и рекордов. Структур не являющихся парами, которыми обычно был ограничен Лисп. Это было сделано в PLASMA, но это не сделано в RABBIT, окружения в памяти представлены как списки. Разумеется в них не нужно искать имена. Это же лексическая видимость, все положения переменных в списках известны статически.
RABBIT и CHEAPY в основном написаны на Схеме и транслируют сами себя в MacLisp. CHEAPY - меньше 10 страниц кода (примерно 700 строк, если судить по распечатке кода RABBIT), "мог бы быть написан" за день работы. Мог бы, но едва ли Стилу дали бы поработать над ним целый день.
Окончательная версия RABBIT от 15 мая 1978 это 3290 LOC [RABBIT]. Ну, по сравнению с функциональными Эдинбургскими достижениями - неплохо. На первую версию RABBIT для магистерской диссертации ушел один человеко-месяц. На переписывание оптимизатора и получение той версии, которая описывается в отчете - еще два. Но эти два человеко-месяца работы были проделаны в течение восьми календарных месяцев. Каждую неделю Сассман тратил на разработку компилятора не более десяти часов. Потому, что его машинное время было ограничено и он мог запустить разрабатываемый им компилятор только один или два раза за ночь.
Надо полагать, что именно так и прочим героям этой истории удавалось за годы написать только единицы тысяч строк кода.
У читателя может возникнуть вполне закономерный вопрос: почему RABBIT не поучаствовал в ФП-бенчмарках прошлой главы? Дело в том, что нам мало что известно о производительности генерируемого им кода. В отличие от Уоррена, который с удовольствием сравнивал свой компилятор с другими, Стил сравнивает RABBIT только с интерпретатором Схемы и с самим собой. И то сравнению особого внимания не уделяется. Неизвестно на каком коде сравнивается. Надеемся, что на более-менее серьезном по меркам этого времени - самом RABBIT.
С самим собой - в смысле с оптимизатором и без. Компиляция с оптимизацией занимает в два раза больше времени. Без учета времени в сборщике мусора - в 1.5 раз. Для работы RABBIT нужен примерно один мегабайт памяти.
Но оптимизированный код быстрее только в 1.2 раза. Оказывается, компилировать Схему мешает ряд проблем. Мешает отсутствие типов. Мешает то, что нельзя рассчитывать на ссылки без нуллов. Мешает то, что в Схеме все мутабельное как в PAL. Значительная часть анализа перед оптимизатором нужна для того, чтоб выявить какие объявления просто объявления констант как let
в ML. Сассман и Стил жалеют [Stee98], что не позаимствовали из Хьюитовской PLASMA мутабельную ячейку как специальный объект. И даже те оптимизации, которые удается сделать, основаны на шатком фундаменте. Они корректны только при правильном употреблении CATCH
.
Ну, возможно при компиляции с помощью RABBIT какого-нибудь ФЯ Эдинбургской программы можно смягчить часть проблем. Но ФЯ Эдинбургской программы не компилировали с помощью RABBIT. Может потому, что, как пишет Стил, трансляция в MacLisp затрудняет практическое использование RABBIT? Должны быть более серьезные трудности. Разработчики Эдинбургских ФЯ определенно знали о нем и часто ссылались на описание. RABBIT быстро и полностью затмил для них компилятор Уоррена.
С интерпретатором RABBIT расправляется, но не очень легко. Оптимизированный код в 30 раз быстрее интерпретатора. Выглядит не очень хорошо. И нет никаких сравнений с MacLisp или с какими-нибудь "традиционными компиляторами". Заявлено, что RABBIT должен генерировать "сравнимый" код, но сравнения нет. Не очень хороший признак.
И в 80-е годы, когда бенчмарки стали популярнее, никто не сравнивал RABBIT с другими имплементациями ФЯ или "традиционными компиляторами". Так же как и компиляторы POP-2 и компилятор Уоррена, этот кролик не смог спастись и найти новый дом. Что же случилось?
И, кстати, требования к памяти в один мегабайт это много?
[Abda74]: Abdali, Syed Kamal. "A combinatory logic model of programming languages." PhD diss., University of Wisconsin--Madison, 1974.
[Abda76]: Abdali, S. K. (1976). An Abstraction Algorithm for Combinatory Logic. The Journal of Symbolic Logic, 41(1), 222. doi:10.2307/2272961
[Abra81]: Harvey Abramson, D. A. Turner, SASL Reference Manual. TM-81-26, October 1981 https://www.cs.ubc.ca/sites/default/files/tr/1981/TM-81-26.pdf
[Acke28]: Ackermann, W. (1928). Zum Hilbertschen Aufbau der reellen Zahlen. Mathematische Annalen, 99(1), 118–133. doi:10.1007/bf01459088
[Aiel74]: Aiello, Jack Michael. An Investigation of Current Language Support for the Data Requirements of Structured Programming. Massachusetts Institute of Technology, Project MAC, 1974. https://ia802901.us.archive.org/34/items/bitsavers_mitlcstmMI_32305830/MIT-LCS-TM-051_text.pdf
[ATLAS]: London Atlas http://www.chilton-computing.org.uk/acl/technology/atlas/p010.htm
[Atki78]: Atkinson, M. P., & Jordan, M. J. (1978). An effective program development environment for BCPL on a small computer. Software: Practice and Experience, 8(3), 265–275. doi:10.1002/spe.4380080304
[Augu84]: Lennart Augustsson, A compiler for lazy ML. LFP '84: Proceedings of the 1984 ACM Symposium on LISP and functional programming August 1984 Pages 218–227 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 127–141 DOI:10.1093/comjnl/32.2.127
[Augu21]: The Haskell Interlude 02: Lennart Augustsson https://www.buzzsprout.com/1817535/9286902
[Baba79]: Babaoglu O, Joy W, Porcar J. Design and implementation of the Berkeley virtual memory extensions to the UNIX operating system. Department of Electrical Engineering and Computer Science, University of California, Berkeley. 1979 Dec 10.
[Baba81]: Babaoglu, Özalp and William N. Joy. “Converting a swap-based system to do paging in an architecture lacking page-referenced bits.” TR 81-474 October 1981.
[Back59]: J. W. Backus. The Syntax and Semantics of the Proposed International Algebraic Language of the Zurich ACM-GAMM Conference. Proceedings of the International Conference on Information Processing, UNESCO, 1959, pp.125-132. Typewritten preprint.
[Back63]: J. W. Backus, F. L. Bauer, J. Green, C. Katz, J. McCarthy, A. J. Perlis, H. Rutishauser, K. Samelson, B. Vauquois, J. H. Wegstein, A. van Wijngaarden, M. Woodger, and P. Naur. 1963. Revised report on the algorithm language ALGOL 60. Commun. ACM 6, 1 (Jan. 1963), 1–17. doi:10.1145/366193.366201
[Bake78]: Henry G. Baker. 1978. Shallow binding in Lisp 1.5. Commun. ACM 21, 7 (July 1978), 565–569. https://doi.org/10.1145/359545.359566
[Bare92]: Barendregt, Henk P. "Lambda calculi with types." (1992).
[Barr63]: Barron, D.W., Buxton, J.N., Hartley, D.F., Nixon, E., and Strachey, C. The main features of CPL. Computer Journal 6(2) (1963) 134–143.
[Barr68]: Barron, David William. Recursive Techniques in Programming (1968)
[Bask80]: Forest Baskett, Andreas Bechtolsheim, Bill Nowicki and John Seamons, The SUN Workstation. March 1980
[BBC73]: The Lighthill debate on Artificial Intelligence https://www.youtube.com/watch?v=03p2CADwGF8&t=1682s
[Bech82]: Andreas Bechtolsheim, Forest Baskett, Vaughan Pratt. The SUN Workstation Architecture. Technical Report No. 229, March 1982
[Bell98]: Bell G, Strecker WD. Retrospective: what have we learned from the PDP-11—what we have learned from VAX and Alpha. In 25 years of the international symposia on Computer Architecture (selected papers) 1998 Aug 1 (pp. 6-10). doi:10.1145/285930.285934
[Blai70]: Fred W. Blair. Structure of the Lisp Compiler. IBM Research, Yorktown Heights, circa 1970. https://www.softwarepreservation.org/projects/LISP/ibm/Blair-StructureOfLispCompiler.pdf
[Blai79]: Blair, F. W. "The Definition of LISP 1.8+0.3i." IBM Thomas J Watson Research Center, Internal Report (1979). https://www.softwarepreservation.org/projects/LISP/ibm/Blair-Definition_of_LISP1_8_0_3i-1979.pdf
[Boye75]: Robert S. Boyer and J Strother Moore. 1975. Proving Theorems about LISP Functions. J. ACM 22, 1 (Jan. 1975), 129–144. doi:10.1145/321864.321875
[Bobr66]: Bobrow, Daniel G., D. Lucille Darley, Daniel L. Murphy, Cynthia Ann Solomon and Warren Teitelman. “THE BBN-LISP SYSTEM.” (1966).
[Bobr67]: Daniel G. Bobrow and Daniel L. Murphy. 1967. Structure of a LISP system using two-level storage. Commun. ACM 10, 3 (March 1967), 155–159. https://doi.org/10.1145/363162.363185
[Bobr73]: Daniel G. Bobrow and Ben Wegbreit. 1973. A model and stack implementation of multiple environments. Commun. ACM 16, 10 (Oct. 1973), 591–603. doi:10.1145/362375.362379
[Brat86]: Bratko, Ivan. Prolog programming for artificial intelligence. 1986.
[Broo14]: Stephen Brookes, Peter W. O'Hearn, and Uday Reddy. 2014. The essence of Reynolds. SIGPLAN Not. 49, 1 (January 2014), 251–255. doi:10.1145/2578855.2537851
[Bund84]: Bundy, Alan, ed. "Catalogue of artificial intelligence tools." (1984).
[Bund21]: Alan Bundy, The early years of AI in Edinburgh https://www.youtube.com/watch?v=VSdnsfGcz_A
[Burg64]: W. H. Burge. 1964. The evaluation, classification and interpretation of expressions. In Proceedings of the 1964 19th ACM national conference (ACM '64). Association for Computing Machinery, New York, NY, USA, 11.401–11.4022. doi:10.1145/800257.808888
[Burg66]: William H. Burge. 1966. A reprogramming machine. Commun. ACM 9, 2 (Feb. 1966), 60–66. doi:10.1145/365170.365174
[Burg71]: W. H. Burge. 1971. Some examples of the use of function-producing functions. In Proceedings of the second ACM symposium on Symbolic and algebraic manipulation (SYMSAC '71). Association for Computing Machinery, New York, NY, USA, 238–241. doi:10.1145/800204.806292
[Burg72]: Burge, W. H. (1972). Combinatory Programming and Combinatorial Analysis. IBM Journal of Research and Development, 16(5), 450–461. doi:10.1147/rd.165.0450
[Burg75]: Burge, William H. "Recursive programming techniques." (1975).
[Burg75b]: Burge, W. H. (1975). Stream Processing Functions. IBM Journal of Research and Development, 19(1), 12–25. doi:10.1147/rd.191.0012
[Burg89]: Burge, W.H., Watt, S.M. (1989). Infinite structures in scratchpad II. In: Davenport, J.H. (eds) Eurocal '87. EUROCAL 1987. Lecture Notes in Computer Science, vol 378. Springer, Berlin, Heidelberg. doi:10.1007/3-540-51517-8_103
[Burg90]: Burg, Jennifer J. "Constraint-based programming: A survey." (1990).
https://core.ac.uk/download/pdf/236248615.pdf
[Burs04]: ROD BURSTALL and VICTOR LESSER, Robin Popplestone https://www-robotics.cs.umass.edu/ARCHIVE/remembrance.html
[Burs67]: Burstall, R. M., & Fox, L. (1967). Advances in Programming and Non-Numerical Computation. The Mathematical Gazette, 51(377), 277. doi:10.2307/3613292
[Burs68]: Burstall, Rodney Martineau, John Stuart Collins, and Robin John Popplestone. POP-2 papers. Edinburgh & London: Oliver & Boyd, 1968.
[Burs69]: Burstall, Rod M. "Proving properties of programs by structural induction." The Computer Journal 12.1 (1969): 41-48. doi:10.1093/comjnl/12.1.41
[Burs70]: Burstall, R. M. Formal description of program structure and semantics in first—order logic. Machine Intelligence 5 (Meltzer & Michie, Eds.). American Elsevier, New York 79 (1970): 98.
[Burs71]: Burstall, R. M., J. S. Collins, R. J. Popplestone. Programming in POP-2 (1971).
[Burs72]: Burstall, Rodney M. "Some techniques for proving correctness of programs which alter data structures." Machine intelligence 7, no. 23-50 (1972): 3.
[Burs77]: R. M. Burstall and J. A. Goguen. 1977. Putting theories together to make specifications. In Proceedings of the 5th international joint conference on Artificial intelligence - Volume 2 (IJCAI'77). Morgan Kaufmann Publishers Inc., San Francisco, CA, USA, 1045–1058.
[Burs77b]: R. M. Burstall. Design considerations for a functional programming language. In Infotech State of the Art Conference, Copenhagen, Denmark, 1977.
[Burs79]: Burstall, Rod M.. “Applicative programming.” International Conference on Software Engineering (1979).
[Burs80]: R. M. Burstall, D. B. MacQueen, and D. T. Sannella. 1980. HOPE: An experimental applicative language. In Proceedings of the 1980 ACM conference on LISP and functional programming (LFP '80). Association for Computing Machinery, New York, NY, USA, 136–143. DOI:10.1145/800087.802799
[Burs80b]: Burstall, R. M. (1980). Electronic category theory. Lecture Notes in Computer Science, 22–39. doi:10.1007/bfb0022493
[Burs92]: Burstall, R.M. (1992). Computing: Yet Another Reality Construction. In: Floyd, C., Züllighoven, H., Budde, R., Keil-Slawik, R. (eds) Software Development and Reality Construction. Springer, Berlin, Heidelberg. doi:10.1007/978-3-642-76817-0_6
[Burs2000]: Burstall, R. Christopher Strachey—Understanding Programming Languages. Higher-Order and Symbolic Computation 13, 51–55 (2000). doi:10.1023/a:1010052305354
[Burs2006]: Burstall, R. (2006). My Friend Joseph Goguen. In: Futatsugi, K., Jouannaud, JP., Meseguer, J. (eds) Algebra, Meaning, and Computation. Lecture Notes in Computer Science, vol 4060. Springer, Berlin, Heidelberg. doi:10.1007/11780274_2
[Burstall]: Rod Burstall's home page https://web.archive.org/web/20181021215651/http://homepages.inf.ed.ac.uk/rburstall/
[Böhm72]: Böhm, C., & Dezani, M. (1972). A CUCH-machine: The automatic treatment of bound variables. International Journal of Computer & Information Sciences, 1(2), 171–191. doi:10.1007/bf00995737
[Cadi72]: J. M. Cadiou and Zohar Manna. 1972. Recursive definitions of partial functions and their computations. In Proceedings of ACM conference on Proving assertions about programs. Association for Computing Machinery, New York, NY, USA, 58–65. doi:10.1145/800235.807072
[Camp85]: Campbell-Kelly, Martin. “Christopher Strachey, 1916-1975: A Biographical Note.” Annals of the History of Computing 7 (1985): 19-42.
[Card2006]: Cardone, Felice and Roger Hindley. “History of Lambda-calculus and Combinatory Logic.” (2006).
[Chio2001]: S. Chiou et al., "A Marriage of Convenience: The Founding of the MIT Artificial Intelligence Laboratory"
[Coel82]: Coelho, H., Cotta JC, and L. M. Pereira. "How to Solve it in Prolog, July 1982." Laboratório Nacional de Engenhara Civil, Lisbon, Portugal.
[Cohe88]: Jacques Cohen. 1988. A view of the origins and development of Prolog. Commun. ACM 31, 1 (Jan. 1988), 26–36. doi:10.1145/35043.35045
[Colm96]: Alain Colmerauer and Philippe Roussel. 1996. The birth of Prolog. History of programming languages---II. Association for Computing Machinery, New York, NY, USA, 331–367. doi:10.1145/234286.1057820
[Coro83]: Corovessis, Jiannis. A parallel implementation of SASL. University of St. Andrews (United Kingdom), 1983.
[Coul68]: Coulouris, George, T. J. Goodey, R. W. Hill, R. W. Keeling and D. Levin. “The London CPL1 compiler.” Comput. J. 11 (1968): 26-30.
[Coul]: George Coulouris http://www.eecs.qmul.ac.uk/~gc/
[Crev93]: Crevier, Daniel, AI: the tumultuous history of the search for artificial intelligence, 1993.
[Curr58]: Haskell B. Curry, Robert Feys, William Craig. "Combinatory Logic: Volume I" (1958).
[Darl72]: Darlington, John. "A semantic approach to automatic program improvement." (1972).
[Darl73]: J. Darlington and R. M. Burstall. 1973. A system which automatically improves programs. In Proceedings of the 3rd international joint conference on Artificial intelligence (IJCAI'73). Morgan Kaufmann Publishers Inc., San Francisco, CA, USA, 479–485.
[Darl75]: R. M. Burstall and John Darlington. 1975. Some transformations for developing recursive programs. In Proceedings of the international conference on Reliable software. Association for Computing Machinery, New York, NY, USA, 465–472. doi:10.1145/800027.808470
[Darl76]: Darlington, J., & Burstall, R. M. (1976). A system which automatically improves programs. Acta Informatica, 6(1). doi:10.1007/bf00263742
[Darl77]: Burstall, R. M., Darlington, J. (1977). A Transformation System for Developing Recursive Programs. Journal of the ACM, 24(1), 44–67. doi:10.1145/321992.321996
[Darl81]: Darlington, J. (1981). An experimental program transformation and synthesis system. Artificial Intelligence, 16(1), 1–46. doi:10.1016/0004-3702(81)90014-x
[Davi76]: D. J. M. Davies, POP-10 User's Manual, 29 May 1976. http://www.cs.otago.ac.nz/staffpriv/ok/pop2.d/POP-10.pdf
[Dewa79]: Dewar, Robert BK. The SETL programming language. Bell Laboratories, 1979.
[Dijk60]: Dijkstra, E. W. (1960). Recursive Programming. Numerische Mathematik, 2(1), 312–318. doi:10.1007/bf01386232
[Dijk62]: DIJKSTRA, E. W. (1962). "An ALGOL60 Translator for the X1" Automatic Programming Bulletin, No. 13.
[Dugg96]: Dominic Duggan and Constantinos Sourelis. 1996. Mixin modules. SIGPLAN Not. 31, 6 (June 15, 1996), 262–273. doi:10.1145/232629.232654
[Dunn70]: Raymond D. Dunn. "POP-2/4100 Users' Manual". School of Artificial Intelligence. University of Edinburgh (February 1970)
[Eager]: Bob Eager, More on the ICL 2900 Series http://www.tavi.co.uk/icl/bob.htm
[Eage22]: Bob Eager, Edinburgh Multi Access System (EMAS) https://www.youtube.com/watch?v=khnu7R3Pffo
[Emde76]: M. H. Van Emden and R. A. Kowalski. 1976. The Semantics of Predicate Logic as a Programming Language. J. ACM 23, 4 (Oct. 1976), 733–742. doi:10.1145/321978.321991
[Emde90]: Cheng, Mantis HM, Maarten H. van Emden, and B. E. Richards. "On Warren's method for functional programming in logic." In ICLP, pp. 546-560. 1990.
[Emde06]: Maarten van Emden, The Early Days of Logic Programming: A Personal Perspective https://dtai.cs.kuleuven.be/projects/ALP/newsletter/aug06/nav/articles/article4/article.html
[Emde19]: Emden, Maarten van. “Reflecting Back on the Lighthill Affair.” IEEE Annals of the History of Computing 41 (2019): 119-123.
[Emer91]: Emerson W. Pugh, Lyle R. Johnson, and John H. Palmer. IBM's 360 and Early 370 Systems. Cambridge, Mass.: MIT Press, 1991
[Earl73]: Earley, J. (1973). Relational level data structures for programming languages. Acta Informatica, 2(4), 293–309. doi:10.1007/bf00289502
[Edinburgh]: https://www.ed.ac.uk/informatics/about/history-school-of-informatics/brief-history-of-the-school-of-informatics
[Evan68]: Evans Jr, Arthur. "Pal—a language designed for teaching programming linguistics." In Proceedings of the 1968 23rd ACM national conference, pp. 395-403. 1968.
[Evan68b]: A. Evans. PAL - A Reference Manual and a Primer. Department of Electrical Engineering, Massachusetts Institute of Technology, February 1968, 185 pages.
[EventML]: EventML https://nuprl.org/software/
[Fate71]: Richard J. Fateman. 1971. The user-level semantic matching capability in MACSYMA. In Proceedings of the second ACM symposium on Symbolic and algebraic manipulation (SYMSAC '71). Association for Computing Machinery, New York, NY, USA, 311–323. doi:10.1145/800204.806300
[Fate73]: R. J. Fateman. 1973. Reply to an editorial. SIGSAM Bull., 25 (March 1973), 9–11. doi:10.1145/1086803.1086804
[Fate81]: Fateman RJ. Views on transportability of lisp and lisp-based systems. InProceedings of the fourth ACM symposium on Symbolic and algebraic computation 1981 Aug 5 (pp. 137-141).
[Feat79]: Feather, Martin S. “A system for developing programs by transformation.” (1979).
[Fisc93]: Fischer, M. J. (1993). Lambda-calculus schemata. LISP and Symbolic Computation, 6(3-4), 259–287. doi:10.1007/bf01019461
[Fode81]: Foderaro JK, Fateman RJ. Characterization of VAX Macsyma. InProceedings of the fourth ACM symposium on Symbolic and algebraic computation 1981 Aug 5 (pp. 14-19).
[Fode83]: Foderaro JK, Sklower KL, Layer K. The FRANZ Lisp Manual. Regents of the University of California; 1983 Jun.
[Fox66]: FOX, Leslie (ed.). Advances in programming and non-numerical computation. 1966 ISBN:978-0-08-011356-2, 0080113567
[Franz]: History of Franz Inc. https://franz.com/about/company.history.lhtml
[Frie76]: Friedman, Daniel P., and David S. Wise. CONS should not evaluate its arguments. Computer Science Department, Indiana University, 1976. https://help.luddy.indiana.edu/techreports/TRNNN.cgi?trnum=TR44
[Frie76b]: Friedman, Daniel P. and David S. Wise. “CONS Should Not Evaluate its Arguments.” International Colloquium on Automata, Languages and Programming (1976).
[Full76]: Samuel H. Fuller. 1976. Price/performance comparison of C.mmp and the PDP-10. In Proceedings of the 3rd annual symposium on Computer architecture (ISCA '76). Association for Computing Machinery, New York, NY, USA, 195–202. doi:10.1145/800110.803580
[Gabb98]: Gabbay, Dov M., Christopher John Hogger, and John Alan Robinson, eds. Handbook of logic in artificial intelligence and logic programming: Volume 5: Logic programming. Clarendon Press, 1998.
[GEDANK]: GEDANKEN. Scanned source listing. https://www.softwarepreservation.org/projects/GEDANKEN/Reynolds-GEDANKEN-MakeTranslator.pdf
[GEDANKb]: GEDANKEN. Scanned execution listing https://www.softwarepreservation.org/projects/GEDANKEN/Reynolds-GEDANKEN-Test_Ch_II_Run.pdf
[GHC23]: GHC User’s Guide https://downloads.haskell.org/ghc/latest/docs/users_guide/
[Gilm63]: GILMORE, P. C. (1963). "An Abstract Computer with a LISP-like Machine Language without a Label Operator," in Computer Programming and Formal Systems, ed. Braffort, P., and Hirschberg, D., Amsterdam, North Holland Publishing Co.
[Gira72]: Girard, Jean-Yves. "Interprétation fonctionnelle et élimination des coupures de l'arithmétique d'ordre supérieur." PhD diss., Éditeur inconnu, 1972.
[Gogu79]: Goguen, J.A. (1979). Some design principles and theory for OBJ-0, a language to express and execute algebraic specifications of programs. In: Blum, E.K., Paul, M., Takasu, S. (eds) Mathematical Studies of Information Processing. Lecture Notes in Computer Science, vol 75. Springer, Berlin, Heidelberg. doi:10.1007/3-540-09541-1_36
[Gogu82]: Joseph Goguen and Jose Meseguer. 1982. Rapid prototyping: in the OBJ executable specification language. SIGSOFT Softw. Eng. Notes 7, 5 (December 1982), 75–84. doi:10.1145/1006258.1006273
[Gogu85]: Futatsugi, K., Goguen, J. A., Jouannaud, J.-P., & Meseguer, J. (1985). Principles of OBJ2. Proceedings of the 12th ACM SIGACT-SIGPLAN Symposium on Principles of Programming Languages - POPL ’85. doi:10.1145/318593.318610
[Gogu88]: Goguen, Joseph. Higher order functions considered unnecessary for higher order programming. SRI International, Computer Science Laboratory, 1988.
[Gogu2000]: Goguen, J. A., Winkler, T., Meseguer, J., Futatsugi, K., & Jouannaud, J.-P. (2000). Introducing OBJ. Software Engineering with OBJ, 3–167. doi:10.1007/978-1-4757-6541-0_1
[Gold70]: Golden, Jeffrey P. "A User's Guide to the AI Group LISCOM LISP Complier: Interim Rep
Идея классная, прочитал с интересом. Но текст очень сложный, как код на Хаскеле) Много вводных слов, сложноподчиненных предложений, прилагательных. Жду продолжения! Надеюсь, когда текста будет много, можно будет отдать редактору - и получится шикарная статья (книга?)