Функциональное программирование - это инженерная ветвь конструктивной математики. Олег Нижников.
Мы не ставим перед собой цели дать определение функциональному программированию. Мы, однако же, вынуждены определить какие-то практические рамки для нашего исследования. Выбрать историю чего именно мы пишем, и выбрать что-то обозримое. И многие определения ФП просто непрактичны с этой точки зрения. История языков с первоклассными функциями в наши дни фактически равна истории языков программирования, ведь даже редкие языки в которых их до сих пор нет - вроде 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. Возможно потому, что авторы увлеклись эквивалентной конструкцией, которую считали одной из основных фич обоекембриджских языков и хотели использовать только ее. Эта фича -
where
Что делать, если лямбд нет, а хочешь написать
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
конструкция.
let
В 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-е, придуманная не Гогеном.
У уравнений бывают гарды (
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))
работает быстрее при нормальном порядке редукции, чем при аппликативном. Можно порадоваться, что речь хотя-бы зашла о сколько-нибудь реальной задаче.
same-fringe ?
yes)))))))))))]
Хьюит [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-преобразование разрешает важные проблемы компиляции естественным образом: промежуточные значения становятся явными аргументами, также становится явным и порядок вычислений.
Компилятор как бы примиряет подходы Бурсталла и Уоррена. Компиляция не требует ручного управления, и тем не менее размах трансформации должен был производить впечатление в свое время. Трансформационная часть пайплайна - десять проходов:
Лексическая видимость означает, что получить доступ к переменным окружения может только или код в этом замыкании, или код, получивший их через параметр. Так что представление окружения не должно быть какого-то стандартного формата. Это абстрактные данные, с которыми работают только через интерфейс “вызов функции”. Внутренне представление может быть выбрано наиболее подходящим для каждого случая. И 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 и компилятор Уоррена, этот кролик не смог спастись и найти новый дом. Что же случилось?
И, кстати, требования к памяти в один мегабайт это много?
В 60-е и 70-е один человек или небольшая команда могли спроектировать и имплементировать язык с достаточной производительностью для того, чтобы быть полезным.
Г. Стил, Р. Гэбриел, Эволюция Лиспа [Stee96].
И тут наши игры стали оборачиваться серьезными последствиями, учитывая стоимость памяти в [три с половиной миллиона долларов].
Дж. Мозес [Mose08].
Давайте выясним, много ли памяти требовал RABBIT в 1978. Единица компиляции RABBIT - это функция. Для того, чтоб скомпилировать самую большую функцию своего собственного исходного кода RABBIT только-только хватало 256K слов на PDP-10. Слово на PDP-10 - это 36 бит. Поэтому “примерно” мегабайт.
И да, 256K это все адресное пространство для этого компьютера с 18бит указателями. Так что рекомендуемая Стилом память является также и максимально доступной на этой машине. Рекомендуемая память - не результат того, что кто-то попробовал разные размеры и решил что начиная с мегабайта все начинает сносно работать. Это размер функций в исходниках ограничивали так, чтоб они умещались в памяти и компилятор хоть как-то работал.
Но это еще не все, RABBIT занимает больше одного адресного пространства. Его исполняющийся код находится при этом в отдельном адресном пространстве и на компьютере примерно два мегабайта памяти. Но насколько полно RABBIT использует второй мегабайт мы не знаем.
Для имплементации, в которой почти все - указатель на набор указателей в куче конечно важнее, сколько в эту память умещается указателей. В каждом слове может быть два указателя по 18 бит. Так что на более современной машине с 32-бит указателями структуры, которые RABBIT строит в куче заняли бы скорее два мегабайта.
Стил пишет, что не очень-то и старался компилятор оптимизировать и может можно написать его так, чтоб он требовал меньше памяти. Но как мы увидим дальше, последующие компиляторы ФЯ требовали только больше и больше памяти. RABBIT работает на PDP-10 с примерно двумя мегабайтами физической памяти. И на 32-х битной машине это будет скорее 3Мб. Забегая вперед, первый компилятор ML для компиляции другой имплементации ML требует по крайней мере 4Мб на 32бит машине.
Итак, компилятор ФЯ - слишком тяжелая программа для массовых персональных компьютеров вплоть до середины 90-х, если не дольше. Так что возможности отдельного индивидуума в 70-е сильно преувеличены Стилом и Гэбриелем. Посмотрим, какие немассовые компьютеры были доступны пользователям и имплементаторам ФЯ, которых поддерживали могущественные организации.
Я спросил, может ли Atlas управлять автомобилем у одного из разработчиков. Тот ответил, что Atlas “может делать все что угодно”.
Поплстоун, Р. Дж. Ранняя разработка POP [Popplestone]
Первыми машинами, для которых пытались написать компилятор ФЯ были Atlas [Lavi12] в Лондоне и прототип Atlas 2, называемый Titan в Кембридже. Как мы помним, для этих машин писали компиляторы CPL. Группа Экспериментального Программирования имела какое-то время доступ к Atlas в Манчестере по телефону. До того, как Эдинбургский университет получил свой компьютер [Edinburgh]. К тому же компьютеру в Манчестере имел ранее доступ и Поплстоун, но тогда, когда еще не знал как его можно использовать [Popplestone]. Но можно сказать, что и Стрейчи с Парком не знали как Atlas можно использовать, а как - нельзя. Интересно, что скорость Atlas была сопоставима со скоростью той машины, на которой компилятор ФЯ впервые более-менее заработал. В 1962, когда заработал первый серийный Atlas, это означало претензии на самый быстрый компьютер в мире.
Манчестер и Кембридж участвовали в разработке машин и не покупали их, а Лондон заплатил за свою машину два миллиона фунтов. Что примерно соответствует 56 миллионов долларов 2023-го. Специально для этого компьютера было построено двухэтажное здание и его работа означала множество других расходов, так что более полное представление о том, насколько дорого обошлось бы его использование можно составить по рыночной цене на машинное время. Один час Atlas продавался за 750 фунтов - 21 тысячу долларов 2023-го года.
Разумеется, шанс воспользоваться такой дорогой машиной не очень часто выпадал имплементаторам ФЯ, но нельзя сказать, что в этот раз он был упущен. Дело в том, что с памятью все обстояло совсем не так хорошо, как с количеством операций в секунду. В 48бит слово Atlas умещалось два 24бит указателя. Так что адресного пространства хватило бы первым компиляторам ФЯ с запасом. Вот только такое адресное пространство Atlas и Titan никогда не понадобилось. В 1966 - год завершения работы над CPL - у Atlas в Лондоне было 32K слов физической памяти и еще 96K слов свопа на четырех барабанах. У более бюджетного Titan в Кембридже не было виртуальной памяти вообще, но со временем его память была увеличена с 32K до 64K и потом и до 128K слов, до того, как Titan был окончательно выключен в октябре 73-го. Хуже того, имплементация CPL началась еще до получения TITAN на EDSAC 2 [Rich13] с 16K слов памяти [Wilk92].
Так что можно довольно уверенно заключить, что амбициозные версии CPL, мечты о ML-ях за десятилетие до ML, не могли быть имплементированы на этих машинах. Замена не сумевшего написать компилятор CPL Стрейчи на не сумевшего написать компилятор Лиспа Парка не могла ничего изменить. Как не могла бы изменить и замена Парка на кого-нибудь сумевшего написать какой-нибудь компилятор. Даже знавшие гораздо больше об имплементации ФЯ разработчики компиляторов 70-х и 80-х не смогли уместить компилятор ФЯ, компилирующий компилятор ФЯ в память на 256 тысяч указателей. Тем более на 64 тысячи.
Но решительное заключение о реальности менее амбициозных CPL-ей делать пока рано. Мы выяснили, что имплементаторы CPL не могли сделать то, что хотели. Но имплементация CPL не продвинулась достаточно далеко, чтоб определить, что они сделать могли. К счастью, как раз такой язык: почти CPL, почти амбициозный и почти ФЯ был почти имплементирован. И его историю, удобно проводящую нижнюю границу пространства возможностей для имплементаторов ФЯ, мы еще рассмотрим подробнее. Но не слишком подробно.
В 1974 [Emde06] Эдинбург получил компьютер из той же линейки, что и компьютеры на которых RABBIT работал в МТИ: PDP-10, он же DECsystem-10 с KI10 ЦП, 256K слов памяти и 512K слов на диске для свопа [Gord79] c ОС TOPS-10. И дело даже не в в два раза меньшей физической памяти. Три эдинбургские группы имплементаторов ЯП были одними из многих обычных пользователей этой машины, не сконфигурированной для исполнения больших программ. Каждый процесс на ней получал не больше 75K физической памяти, все остальное отправлялось на диск.
Из этих 75K LCF вместе с интерпретатором ML занимал 60K [Gord79]. Имплементация POP-2 занимала 19K [Feat79], интерпретатор NPL 11K [Feat79] - 12K [Darl81]
система трансформации Дарлингтона 6K [Darl81], система Фезера 36K [Feat79]. Интерпретатор HOPE, видимо, вместе с POP-2 66K [Sann82] - 70K [Burs80].
Интересна разница в том как заявляются требования. Если Стил вообще не особенно заботится о размере кода, только о том, сколько памяти нужно на деревья в куче, которыми этот код манипулирует, то обычный эдинбургский пользователь PDP-10 рад, что хотя-бы код влез в память, а уж какой-нибудь однострочник он худо-бедно обработает. Компиляторы ФЯ он не компилирует. По крайней мере до 80-х. В восьмидесятые Clear требует минимум 110K слов памяти из которых 66K интерпретаторы POP-2 и HOPE [Sann82]. Но это другое время и другая машина, лимиты для пользователей могли изменить.
В такой тяжелой ситуации интерпретатор - способ оптимизировать использование памяти. По сравнению с компилятором вроде RABBIT. Но что же тогда с компилятором Уоррена? Для бутстрапа ограничение могло бы быть проблемой. Для исполняемого Марсельским интерпретатором компилятора Уоррена 75K слов едва хватает, сам Марсельский интерпретатор занимает 25K из них.
После бутстрапа ситуация улучшилась. Код и рантайм компилятора Уоррена 25K слов, а интерпретатора 14K [Warr78]. Компилятор Уоррена генерирует достаточно компактный код. Но главное - компилятор Уоррена может использовать аннотации чтоб меньше аллоцировать и память необходимая для компиляции редко превышает 5K слов [Warr77] [Warr78], в десять раз меньше, чем в случае Марсельского интерпретатора. Но почему такая разница с требованиями RABBIT?
Компилятор Уоррена компилирует одно правило за раз, а не целую процедуру, как компиляторы ФЯ. Другими словами, всегда компилирует однострочники. Много однострочников, но не слишком много, потому что в памяти еще должна умещаться таблица глобальных символов. Такая небольшая единица компиляции может быть в разы или даже десятки раз меньше, чем функция - единица компиляции для компилятора Стила. Кроме того, даже с этими однострочниками компилятор Уоррена не производит каких-то амбициозных трансформаций. Команды виртуальной машины примерно соответствуют конструкциям в коде:
clause2: uskeld(O,cons) qsort(cons(
uvarl(O,global,0) X,
uvarl(1,local,0) L),
uvar(1,local,1) R,
uvar(2,local,2) RO
init(1,2)
localinit(3,5)
neck(5,2) ):-
call( partition) partition(
local(O) L,
global(O) X,
local(3) L1,
local(4) L2),
call(qsort) qsort(
local(4) L2,
global(1) R1,
local(2) RO),
call(qsort) qsort(
local(3) L1,
local(2) R,
label1 cons(X,R1))
foot(3)
label1: fn(cons)
var(O)
var(1)
clause3: uatom(O,nil) qsort(nil,
uvar(1,local,0) R,
uref(2,local,0) R,
neckfoot(0,3) ).
И команды - просто макросы, генерирующие по паре строк на ассемблере.
В результате компилятор компилирует себя примерно за 5 минут. Эти системные требования и скорость Уоррен называет “приемлемыми”, но не особенно доволен. Можно было бы и лучше сделать! Утверждает, что производительность сравнима с макроассемблером, поставляемым с PDP-10.
Можно предположить, что для какого-нибудь нетипизированного языка уравнений, вроде KRC, Уоррен мог бы написать компилятор с похожими системными требованиями. Но компилятор полноразмерного Эдинбургского ФЯ таким нетребовательным не сделать.
Но вернемся к ограничениям памяти для пользователя. Эдинбург вовсе не был каким-то особенно несчастным местом. Существование Stanford LISP объясняется тем, что почти всем желающим использовать MACLISP-образный Лисп нужна имплементация, требующая меньше памяти чем MACLISP, пусть даже и хуже. “Очень немногие” места могли себе позволить использовать компилятор MACLISP, требовавший от 65K слов [Whit77]. МТИ использовал на PDP-10 собственную ОС ITS, лучше приспособленную для запуска больших программ. С 1973 MACLISP мог работать на TOPS-10, но не особенно хорошо из-за особенностей работы с памятью в стандартной ОС PDP-10. Стенфорд смог начать использовать его только в 1976-ом году.
Теперь ясно, что место разработки RABBIT - очень особенное место, где довольно обычный мэйнфрейм используют необычным образом, как рабочую станцию [Chio2001]. Там работают над средой для использования программ требующих много памяти. Программ вроде компиляторов ФЯ. Давайте выясним, что им удалось сделать и как это можно использовать для имплементации ФЯ Эдинбургской программы.
Финансируй людей, а не проекты!
Джозеф Ликлайдер
Ликлайдер давал нам деньги одним большим шматком.
Марвин Мински [Crev93]
Лаборатория ИИ МТИ больше известна неработающими роботами, неработающими программами для работы с естественными языками и неработающим распознаванием изображений [Crev93]. Конечно, не сразу стало понятно, что все это не будет работать.
С 58-го года ИИ группа МТИ, основанная МакКарти и Марвином Мински (Marvin Lee Minsky), занималась разработкой и имплементацией Лиспа. Также МакКарти имел отношение к разработке системы разделения времени - крайне успешной идее, сделавшей работу за компьютером более-менее интерактивной. Что, как мы помним, вдохновило Мики на создание группы экспериментального программирования, имело такое решающее значение для Поплстоуна и произвело такое сильное впечатление на Ван Эмдена. Все ждали новых успехов и летом 63-го ARPA (Advanced Research Projects Agency) стартовала проект MAC грантом в $2,220,000 (22 миллиона 300тыс. долларов в 2023). Это было только началом, долгие годы после этого проект MAC получал три миллиона каждый год (т.е. 21-30 миллионов долларов в 2023). Ну, может быть не все ждали успехов. МакКарти ушел из МТИ перед тем, как все это счастье на них свалилось [Crev93].
Идея Ликлайдера (Joseph Carl Robnett Licklider) заключалась в выращивании кадров. Деньги ARPA выделяются не на конкретные проекты, а на лабораторию, в которой делают что хотят. Предполагалось, что если то чем они занимаются не вырастет во что-то полезное, то хотя бы те, кто работал в лаборатории смогут делать что-то полезное.
Так что Мински хотел чтоб студенты работали над разными проблемами, а не развивали одно и то же направление. Поэтому, когда один из тысячи цветов начал расцветать, понадобились специальные усилия для того, что Мински позволил это [Mose08], чтоб проект не повторил судьбу слишком высоких колосьев из рассказа о Фрасибуле. Так что этот цветок расцвел, и только один этот цветок. Остальным, кто хотел заниматься другими работающими - как выяснилось позднее - вещами не удалось перехитрить Мински и он их успешно разгромил.
Для чего из перечисленных работ над пониманием естественного языка, распознавания образов и роботами может понадобиться запускать требующие много памяти программы на Лиспе? Да, в мини-MAC в Эдинбурге занимающиеся роботами и распознаванием образов писали на сравнимом с Лиспом языке POP-2. Но их принуждало это делать устройство эдинбургской системы разделения времени. Их коллег из МТИ принудить использовать Лисп было сложнее. Занимавшийся в МТИ компьютерным зрением Бертольд Хорн (Berthold Horn) вспоминает [Crev93], что в 60-е Лисп имел игрушечную имплементацию и только в 70-е стал полезным инструментом. Так что остается только работа с естественными языками. Но это пользователи антипрологов Хьюита и Сассмана. И мы уже знаем, что это не самые важные пользователи. У имплементаторов MACLISP есть пользователи поважнее и нет времени имплементировать пожелания антипрологистов. Но что в ИИ Лаборатории важнее ИИ? Ну, как гласит расхожее среди разработчиков неработающего ИИ выражение, которое обычно приписывается МакКарти: когда что-то начинает работать, никто не хочет больше называть это ИИ.
Эта заработавшая программа, сегодня уже не считающаяся ИИ по причине своей работоспособности, занималась трансформацией деревьев, построенных в куче из объектов и ссылок на них. Поэтому ее системные требования оказались похожи на системные требования первых компиляторов ФЯ. Так что человеко-годы труда лисперов, которые создавали доступную среду в которой она могла работать, создали заодно такую среду и для первых компиляторов ФЯ. И история этой программы поможет нам обозначить пространство возможностей для первых компиляторов ФЯ. Понять, на каких платформах мог бы работать компилятор ФЯ, насколько они были редкими и как дорого бы обошлись. Эта программа - система компьютерной алгебры MACSYMA (project MAC’s SYmbolic MAnipulator).
Тогда я еще не знал, что моя и другие группы потратят следующие два десятилетия на улучшение скорости работы и расхода памяти Лиспа.
Дж. Мозес, Macsyma: личная история [Mose08].
Шли годы и вопрос все задавался: “Почём Лисп?”
Дж. Уайт, Лисп: программа - это данные: историческая перспектива на MACLISP [Whit77]
Главным проектом в группе/Лаборатории ИИ МТИ, как и в Йорктаун Хайтс, была система компьютерной алгебры на Лиспе. Все началось с программы символьного интегрирования SAINT (Symbolic Automatic INTegrator) Джеймса Слагла, того самого, который обнаружил, что видимость переменных работает как-то неправильно. Но продолжилась программой SIN того самого Джоэла Мозеса (Joel Moses) [Mose08], который писал [Mose70], что правильно работающая видимость не так и нужна.
Поскольку Слагл защитил диссертацию по символьному интегратору в 61-ом году, Мински считал [Mose08], что еще одна диссертация на эту тему не нужна. И в 63-ем году не дал Мозесу работать над улучшением интегратора. Мозес стал работать над другой темой. Но, к счастью, в 65-ом году выяснилось, что и эту проблему уже кто-то решил. Так что Мински разрешил таки Мозесу работать над усовершенствованным интегратором, что он и делал до 67-го года.
Еще одной развилкой было решение Мозеса использовать Лисп. Сначала он хотел использовать ассемблер, но Слагл писал на Лиспе и Мозесу Лисп показался приятным языком. Программа-то все равно исследовательская. Что может пойти не так? Мозес использовал и уже готовый код на Лиспе, но трудно сказать, повлияло ли это на решение использовать Лисп.
Разработчики SCRATCHPAD в Йорктаун Хайтс посчитали хорошей идеей писать такую систему на Лиспе в числе прочего и из-за решения Мозеса. И использовали код его интегратора. Вот только обстоятельства в МТИ изменились и имплементаторы MACSYMA стали не особенно уверены, что идея хорошая. Что же случилось? То, что еще долго не будет угрожать разработчикам SCRATCHPAD. У MACSYMA появились пользователи.
Узнав о планах разработки систем компьютерной алгебры, Уильям Мартин (William Martin) объединил ряд мелких проектов в МТИ, прячущихся ниже радара Мински, в систему компьютерной алгебры. Вскоре, в июле 1969 разработку этой системы поддержало ARPA, решившее все-таки поддержать проект, а не людей в этот раз. Что привело к практически полному подчинению ИИ Лаборатории этому проекту. Уже с 68-го года изменения в MACLISP были в основном для нужд MACSYMA [Mose74] [Stee82]. В 71-ом году Мартин покинул проект и Мозес стал его руководителем на всю оставшуюся историю MACSYMA в МТИ. В 1972 разработчики MACSYMA стали фактическими хозяевами MACLISP [Stee96]. Опасения Мински вырастить слишком большой проект полностью оправдались.
Если разработчики MACSYMA были не вполне довольны Лиспом, то Лисперы МТИ были недовольны тем, как Лисп используется для имплементации MACSYMA. Мозес не хотел делать ИИ как полагалось его делать в МТИ - использовать какой-то обобщенный инструментарий для поиска, вроде разрабатывающихся в МТИ антипрологов. Мозес хотел делать очень специализированные имплементации, использовать поиск как можно меньше [Mose08]. Даже “практичный” антипрологовый поиск все еще непрактичен для его предполагаемого пользователя.
Лисперы позднее в своих воспоминаниях [Stee96] будут уверять, что работа над Лиспом в МТИ была не только ради MACSYMA, но в 70-е они вынуждены были уверять в обратном. Даже в описании RABBIT [Stee78] Стил упоминает обсуждения полезности своей работы для MACSYMA. Поэтому Мозес так уверенно объясняет, что лисперам нужно [Mose70], а в воспоминаниях лисперов о событиях вокруг MACSYMA часто заметна горечь.
Раз Лисп был не самым подходящим языком для разработки MACSYMA - то теперь лисперам нужно было его сделать подходящим. Разработчикам MACSYMA нужна была быстрая арифметика и массивы. В это время это было не особенно совместимо с динамическим языком в котором все значения - ссылки на объекты в куче. Такие же проблемы, как мы помним, были и у POP-2, но у его пользователей в Эдинбурге были не те ресурсы, чтоб что-то с этим сделать. Но в МТИ ситуация была другой.
Пользуясь новой поддержкой ARPA разработчики MACSYMA наняли программиста Джеффри Голдена (Jeffrey P. Golden), который занялся улучшением компилятора MACLISP. Прототип первого компилятора Лиспа, который специализирует код с аннотациями, работающий с числами назывался LISCOM [Gold70]. Этот компилятор произошел от того самого компилятора Гринблатта и Нельсона, которые заменили поиски имен в списках на стек. Работу над LISCOM начал более известный другими своими работами Уитфилд Диффи, но его вскоре сменил Голден, и Диффи отправился в Стенфорд [Stee96]. Там он работал над версией MACLISP если и не для бедных, то не для таких богатых как в МТИ и учил Робина Милнера писать на Лиспе. Голден же довел LISCOM до более-менее рабочего состояния в 71-ом году, на момент написания отчета [Gold70] 70-го года работал только первый из двух проходов, собирающий информацию для кодогенерации. После прототипа пришло время рабочего альтернативного компилятора NCOMPLR для MACLISP. Голден, Уайт и Эрик Розен (Eric Rosen) развивали его с 1972-го параллельно с основным, более быстрым компилятором COMPLR без этой оптимизации [Moon74]. COMPLR также произошел от компилятора Гринблатта-Нельсона и был полностью заменен на NCOMPLR только в 1975 [MACLISP]. И если в 73-ем арифметика хорошо компилировалась и можно уже было смело переходить от написания хэллоуворлдов к вычислению факториалов, то поддержка массивов чисел появилась в NCOMPLR только к лету 75-го [MACLISP]. Поддержка объектов кучи со многими полями - в 76-ом году [Stee82].
Непрерывные объекты в памяти, в которых больше пары полей и быстро работающие массивы ссылок в сочетании с быстрой арифметикой для вычисления индексов полезны для эффективной имплементации окружений в ФЯ. Или, по крайней мере могут быть полезны. Стил не успел воспользоваться ими для имплементации RABBIT, только написал, что надо бы ими воспользоваться. Также, быстрая арифметика пригодится для того, чтоб хорошо выглядеть в основном бенчмарке для ФЯ 80-х - вычислении чисел Фибоначчи.
Насколько наработки авторов MACLISP по специализации полиморфного кода были полезны для имплементаторов ФЯ, у которых схожие проблемы если не из-за динамической типизации, то из-за параметрического полиморфизма на универсальном представлении? Мы разберемся с этим подробнее, когда будем писать о таких решениях для ФЯ. По крайней мере, MACLISP продемонстрировал возможность. Насколько убедительной была демонстрация, и насколько обоснованным было это убеждение - другой вопрос.
Насколько быстро работал скомпилированный NCOMPLR код? Гэбриел и Стил вспоминают о нем как об образце хорошей производительности для имплементаторов лиспа на годы [Stee96]. Но это может больше говорить об успехах имплементации Лиспа в эти годы. Неплохо было бы сравнить с какими-то имплементациями языков, у которых точно должна быть приличная производительность. И с этим не все хорошо. Мозес заявляет [Mose74], что тесты показали: код, генерируемый NCOMPLR лучше, чем генерируемый DEC PDP-10 FORTRAN. Утверждения Стила скромнее: скорость только “соперничающая” с FORTRAN [Stee77b]. Проблема в том, что такие заявления ссылаются на один единственный микробенчмарк, опубликованный Ричардом Фейтманом (Richard Fateman) в 73-ем году [Fate73]. Микробенчмарк представлял из себя вычисление кубических корней тысячи чисел с плавающей точкой. Массивы не используются - в 73-ем оптимизаций для массивов еще не было. Версия на фортране справлялась за 2.22 сек, версия на Лиспе с GOTO за 1.81 сек, версия с рекурсией за 2.20 сек. Пример на языке MACSYMA работает с той же скоростью, что и на MACLISP. Оцените его высокоуровневость:
MAIN():=(FOR I : 1.0 STEP 1.0 THRU 1000.0 DO CRT(I))$
CRT(X):=BLOCK([TEST,VAL],
DECIARE([TEST,VAL,X],FLOAT),
VAL:X,
A, TEST:(VAL^3-X)/(3.O*VAL^2),
VAL:VAL-TEST,
IF (ABS(TEST/VAL) > 1.0E-7) THEN GO (A),
RETURN(VAL))$
На этот микробенчмарк - и только на него - десятки ссылок. Даже в описании оптимизации арифметики в MACLISP [Stee77b] нет никаких измерений, заявления о хорошей производительности ссылаются на все тот же микробенчмарк. Более интересные замеры производительности будут в 80-е, но таких известных сравнений с Фортраном нет, так что ссылаться продолжили и в 80-е и в 90-е. Но уже с комментариями, что сравнимую с Фортраном производительность больше никогда не удавалось воспроизвести [Stee82] [Stee96].
Нам не приходилось видеть, чтоб лисперы допускали то, что никакого превосходства над Фортраном никогда и не было, просто Фейтман что-то не то померил. Стил и Гэбриел пишут два десятилетия спустя, что эта славная победа LISP над FORTRAN просто заставила разработчиков компилятора Фортрана совершенствовать его до тех пор, пока компилятору Лиспа не стало трудно с ним соревноваться. Никакими ссылками история этого эпического противостояния, правда, не подтверждена.
Много усилий тратилось на не настолько интересные для имплементаторов ФЯ вещи вроде длинной арифметики. Длинная арифметика была встроена в рантайм и написана на ассемблере, в MACLISP нет средств использования кода на низкоуровневом языке кроме встраивания в рантайм. Отсюда, видимо, происходит длинная арифметика из коробки в Haskell.
Многие требования авторов MACSYMA не нуждались в какой-то исследовательской работе и происходили просто из той самой игрушечности Лиспа 60-х. Даже флагманская имплементация Лиспа такая как MACLISP была просто не готова к размерам практически полезных программ. Скомпилированная часть MACSYMA загружалась в интерпретатор около часа. Но это можно было исправить: оптимизации сократили это время до каких-то пары минут [Whit78].
Но одних только этих усилий по улучшению MACLISP было недостаточно. Для MACSYMA сначала удвоили память в собственном PDP-10 Лаборатории ИИ, впервые получив эту необычную систему, у которой больше памяти чем в максимальной конфигурации, продаваемой DEC. Теперь код программы мог быть загружен в память и для программы все еще была доступна память размером с адресное пространство. В конце 60-х это обошлось в $400,000 (три с половиной миллиона долларов 2023-го). Через три года, в 1972 разработчики MACSYMA получили еще один PDP-10 такой конфигурации. А в 74-ом году еще один PDP-10 новой модели с более быстрым процессором, появившемся к этому времени. В 1975 году свежая версия центрального процессора PDP-10 KL-10 с 2Мб памяти стоила примерно как один тот первый апгрейд памяти - $638,000 ($3.6M в долларах 2023) [Full76].
Пользователи MACSYMA не покупали собственные машины этой экзотической конфигурации, а работали с системой через ARPANET с мая 72-го [Mose74]. И по заверениям [Mose08] разработчиков MACSYMA, в 72-ом году узел, на котором она работала, был одним из самых популярных в ARPANET. За первые два года системой пользовались сотни людей. Половина пользователей были из МТИ [Mose74], другая половина была пользователями, которые решили судьбу Лиспа, а также много других, часто довольно неожиданных судеб. Но об этом позже.
MACSYMA продолжала расти, с 73-го года она загружалась только частями и её разработчики ожидали, что её ядро перестанет помещаться в адресное пространство по крайней мере с 75-го года [Mose08]. Так что возникает вопрос: если ИИ Лаборатории МТИ хватало средств на недешевые модификации мэйнфреймов 60-х, как же так получилось, что и в 78-ом году RABBIT все еще запускают на них? Почему MACSYMA десять лет кое-как работает на этих машинах вместо того, чтоб начать хорошо работать на более новых компьютерах без ограничений вроде адресного пространства в 256K слов? И что случилось с RABBIT вскоре после его создания? А заодно с POP-2 и компилятором Уоррена? Почему все это отсутствует в первых ФП-бенчмарках 80-х? Во время разработки RABBIT ИИ Лаборатория МТИ - это явно не только особое место, оно еще и проходит особенный период своей истории.
Дело в том, что мы, описывая Эдинбургский период истории ФЯ, не упомянули одного важного обстоятельства. Все это время история ФЯ разворачивается в постапокалиптическом сеттинге. Апокалипсиса было даже два, просто совпавших по времени, но не связанных причинно. Один был быстрым и сокрушительным ударом, второй - медленным угасанием, растянувшимся на десятилетие.
Что, вы не слышали о Лайтхилле? О том, кто хочет с нами расправиться?
Мартен ван Эмден, Вспоминая историю с Лайтхиллом [Emde19].
ДРЮ МАКДЕРМОТ: И он меня спрашивает: “ядерная зима” - это когда урезают финансирование ядерного оружия?
(НИКТО НЕ СМЕЕТСЯ)
Дрю МакДермот и др., Темные века ИИ: панельная дискуссия на AAAI-84 [McDe84]
Ван Эмден впервые услышал фамилию “Лайтхилл” в ноябре 72-го. Вскоре после того, как начал работать в Отделе Машинного Интеллекта [Emde19]. Но события, которые привели к нападению Лайтхилла на машинный интеллект начались гораздо раньше. Организатор мини-MAC Мики начал воспроизведение большого MAC в Эдинбурге с разработки системы разделения времени, а продолжил разработкой ИИ. Для этого он уговорил работать в Эдинбурге Ричарда Грегори (Richard Langton Gregory) и Кристофера Лонге-Хиггинса (Christopher Longuet-Higgins). Группа Экспериментального Программирования стала частью новой структуры - Отдела Машинного Интеллекта. Но отношения между основателями отдела не задались. Гегори покинул Эдинбург уже в 1970 [Howe07], так что сложные отношения продолжались в основном между Мики и Лонге-Хиггинсом. Основной причиной конфликтов, по воспоминаниям Ван Эмдена, было желание каждого из них спихнуть административные обязанности на другого. Но существовали и разногласия о машинном и не только разуме [Emde19]. Со временем эти трения привлекли внимание тех, кто выделял на них средства - SRC (Science Research Council). В рамках выяснения, что же происходит в Эдинбурге с разработкой машинного разума, прикладному математику Джеймсу Лайтхиллу (Michael James Lighthill), не имеющему вроде бы никакого отношения к ИИ, поручили написать обзор успехов этой области [Howe07]. И он написал [Ligh72], что успехов особых нет, да и есть основания считать, что и не будет.
Ван Эмден, правда, сомневается, что Лайтхилл не имел совсем уж никакого отношения к ИИ. Особенно после того, как узнал из интервью Фримена Дайсона (Freeman Dyson) какие хорошие друзья они с Лайтхиллом и Лонге-Хиггинсом еще со школы [Emde19].
В докладе Лайтхилл пишет, что не все, что делается под зонтиком “ИИ” одинаково плохо. Но подозрительно, что как-то так получается, что то, над чем работает Мики (роботы) - из самого худшего, а Лонге-Хиггинс (моделирование ЦНС) - из самого лучшего. Похоже, правда, что в результате пострадали все машинисты разума, и не только в Великобритании.
Проблему того, что кое-что в ИИ все же работает Лайтхилл решает просто: выписыванием всего работающего из ИИ. Более-менее работающее имеет смысл вывести из под зонтика “ИИ” и интегрировать с родственными областями и областями применимости. И не то чтобы авторам всего работающего требовалась такая подсказка. Если от ассоциации с ИИ больше нет пользы - такая идея носится в воздухе. Например, компьютерная алгебра и доказатели дистанцировались от ИИ. Понятен и увод Пролога Уорреном от марсельской работы с естественным языком (очень плохо по мнению Лайтхилла) к парсерам и компиляторам (достаточно успешно).
Языки с автоматическим управлением памятью - имеют право на существование, разрешает Лайтхилл. И Эдинбургские ФЯ появились достаточно поздно, чтоб легко дистанцироваться от ИИ. Не смотря на то, что Группа Экспериментального Программирования была частью Департамента Машинного Интеллекта и статьи Бурсталла про ПМ и уравнения выходили в сборнике “Машинный Интеллект”. Так что Эдинбургские ФЯ появились в главном Британском ИИ-центре, но их авторы предпочитали упоминать ИИ примерно так же часто, как упоминали его мы в этой истории. До начала этой главы. Для Пролога дистанцироваться от ИИ было несколько сложнее. А для Лиспа - практически невозможно. К сожалению, не все видели между этими языками какую-то принципиальную разницу.
Только в проекте MAC не понадобилось дистанцироваться от ИИ Лаборатории потому, что Мински отделился со своей ИИ лабораторией от проекта MAC сам, в 1970. Рассчитывая, что так лаборатория получит больше ресурсов [Chio2001]. Но, скорее всего, просчитался.
Но даже если согласиться с Ван Эмденом, что Лайтхилл предвзят - эта предвзятость выражается в основном в том, что он не критикует то, что можно было и покритиковать. Проблема в том, что дела с имплементацией ИИ в это время действительно идут неважно. И ИИ-деятелям трудно что-то противопоставить главному возражению Лайтхилла. Всем этим придуманным ими перебирателям хоть бектрекингом, хоть циклами, хоть корутинами, надо целую вечность перебирать в случае хоть сколько-нибудь реальной задачи. И уменьшить это пространство перебора они могут только с помощью специфических знаний предметной области, так что от универсального подхода и самого смысла объединять это все в какой-то “ИИ” мало что остается.
Существует ответ МакКарти Лайтхиллу в формате рецензии на его отчет. МакКарти пишет, что проблема с большим пространством поиска была очевидна разработчикам ИИ с самого начала. И только некоторые из них короткое время слишком оптимистично смотрели на то, насколько эвристики “общего назначения” тут могут помочь [McCa74]. Мы же тут сделаем предположение, что Лайтхилл не пытался научить разработчиков ИИ чему-то новому, а писал для тех, кто платил разработчикам ИИ деньги. И вот для них эта проблема могла быть не такой очевидной.
Интересно, что МакКарти сохраняет оптимизм, пишет, что Лайтхилл “промахнулся”, безуспешно попытался остановить разработку ИИ. И зачем-то рассказывает, что проиграет в споре 250 фунтов, если до 78-го года компьютерная программа не обыграет в шахматы международного мастера, с которым он поспорил [McCa74]. Этот спор МакКарти, конечно, проиграет. Но ему и прочим ИИ-исследователям предстоят потери гораздо большие и существенно раньше.
Лайтхилл определенно не промахнулся, выстрелив в британскую школу ИИ. Но, возможно, что первая ИИ-зима в США началась и не с событий в Эдинбурге. Существуют и другие версии начала, например с недовольства результатами работ по распознаванию речи в Университете Карнеги — Меллона [Crev93].
В 74-ом году ARPA урезало финансирование основным ИИ-лабораториям США, в том числе и финансирование ИИ Лаборатории МТИ и разработки MACSYMA. Но пользователи MACSYMA хотели продолжать ей пользоваться. И пользователи MACSYMA могли себе это позволить. Так что следующие восемь лет все финансирование разработки MACSYMA, MACLISP и будущих, более важных для истории ФЯ имплементаций MACLISP-образных диалектов Лиспа поступало от MACSYMA-консорциума. Который состоял из Министерства Энергетики (DoE, до 77-го года Управление по энергетическим исследованиям и разработкам), NASA, ВМФ США и Schlumberger. Большая часть средств шла на исправление багов и новые фичи системы компьютерной алгебры, но так же и на улучшение компилятора [Mose08]. Теперь, чтоб получать деньги надо было делать не неработающих роботов, а работающую MACSYMA!
Но жизнь найдет путь, так что тот же Стил получал от ВМФ США деньги на RABBIT при том, что его отношение к MACSYMA ограничивается благодарностью человеку, с которым Стил обсуждал, как можно применить RABBIT на пользу MACSYMA (никак). Все стало делаться во имя MACSYMA и именем её. Не беспокойтесь, в 80-е МТИ и выходцы из него отомстят за все эти унижения. И если и не добьются того, чтоб MACSYMA нигде не работала, то по крайней мере сделают её тем, чем она является сегодня. Но, к счастью, только после того, как многое полезное для работы компиляторов ФЯ будет уже сделано.
SRC урезал финансирование ИИ еще раньше. Правда, Эдинбург пострадал меньше, чем другие, более слабые центры ИИ Великобритании. Наступившая ИИ-зима определенно не несла ничего хорошего для Мики [Emde19] и роботов. Но возможно, что это даже пошло на пользу развития ФП. Так Бурсталл занялся более важными для ФЯ делами, вместо того, чтоб досыпать роботу детали игрушечной машины, как на видео [BBC73].
Можно предположить, что ослабление Мики могло повлиять на то, что мини-MAC стал использовать построенную вокруг PDP-10 экосистему, родственную той, что использовалась в большом MAC. На первый взгляд, это хорошо для использования наработок многих других ИИ-лабораторий для имплементации ФЯ. Но, как будто мало одной ИИ-зимы, назревает еще одна катастрофа. Построенная вокруг PDP-10 экосистема обречена.
Большинство значительных ИИ-лабораторий 60-х повторяли ИИ Лабораторию МТИ. Поэтому, чтобы разобраться как погибли их наработки, нужно понять причины ряда решений в МТИ 60-х.
Как вышло, что МТИ стал использовать мэйнфреймы линейки PDP-6/10, а не существенно более популярные мэйнфреймы IBM, тем более, что разработка Лиспа началась на них? Одной из причин была задержка релиза новой линейки IBM [Chio2001]. Главной причиной - отношения между МТИ и IBM испортились из-за патентного спора [Stee96] [Chio2001]. МТИ претендовал на изобретение ферритовой памяти и требовал IBM платить по два цента за каждый бит. При этом в 1965 производство ферритовой памяти обходилось IBM в 1-3 цента за бит в зависимости от скорости её работы. И IBM так не хотел платить, что разрабатывал несколько новых видов памяти для замены ферритовой. Ни один из них не был практичным или не успевал стать практичным достаточно быстро. Но Джей Райт Форрестер (Jay Wright Forrester), которому IBM демонстрировал свои разработки, этого не знал. И МТИ, ошибочно считая, что IBM вот-вот сделает их патент ненужным, в феврале 64-го согласился на разовый платеж в 13 миллионов долларов (129 миллионов в 2023). На тот момент это была рекордная сумма на урегулирование патентного спора, но не испугавшись неработающих изобретений, МТИ мог бы получить намного больше [Emer91]. Почувствовав себя обманутым, МТИ решил не покупать новые машины IBM. И совсем не очевидно, что МТИ действительно причинил какой-то заметный вред IBM, а не просто отморозил уши назло им. По крайней мере уши разработчиков MACSYMA, которым на машине IBM нужно бы было её умещать не в 2^18
, а в 2^24
адресное пространство [Emer91].
Почему не IBM понятно. А почему именно выбрали именно DEC PDP-10? DEC основали выходцы из МТИ, где они были еще и участниками клуба железнодорожного моделизма. Поезда управлялись с помощью компьютера, так что МакКарти и Мински использовали клуб для того, чтоб искать программистов для ИИ Лаборатории. Так они нашли Гринблатта, например. На основателей DEC в МТИ пока еще не обиделись. Пока еще.
Другая причина скорее всего была той же, что и до того причина выбора IBM. IBM подарил МТИ компьютер 704 в 1957 году, а DEC компьютер PDP-1 в 1960. И так же, как МТИ покупал после этого более новые машины IBM, так же и ИИ Лаборатория приобрела PDP-6, а затем и его более новую, надежную и популярную версию PDP-10 [Chio2001].
Еще одной причиной было то, что PDP-6/10, по мнению лисперов, хорошо подходил для имплементации Лиспа. Из-за инструкций для работы со стеком и того, что cons
- ячейка умещалась в одно слово. Правда, зато практически полезная программа на Лиспе не помещалась в его адресное пространство. Так что все-таки мог бы подходить и лучше.
Лисперы утверждают даже, что разработчики PDP-6 прислушивались к пожеланиям лисперов [Stee96]. Что не так очевидно. Например, у Atlas тоже два указателя помещаются в одно слово. И делать из него “Лисп-машину” точно не собирались. А когда лисперы действительно спроектировали компьютер для Лиспа - у него с PDP-6 было мало общего. Лучше бы лисперы повлияли на адресное пространство, если могли. Все таки, когда выпускаешь машину у которой физическая память размером с адресное пространство, должна возникать какая-то тревога, что система не выдержит проверки временем.
Разумеется, пользователи PDP-10 ждали новой версии, поддерживающей большее адресное пространство. Между релизами PDP-6 и PDP-10 прошло два года. Но ни через два, ни через четыре, ни даже через восемь лет эта долгожданная версия все не появлялась. В чем же проблема? Как мы уже упоминали выше, мэйнфреймы DEC не были популярны. DEC была ведущим производителем другой разновидности компьютеров - миникомпьютеров. Так что в DEC решили сконцентрировать усилия на успешных линейках машин. Но, надо полагать, если донести такие решения до клиентов и потенциальных клиентов - они могут воздержаться от апгрейда или покупки PDP-10. Поэтому решение было донесено только в 1983-ем году. Конечно, просто отмалчиваться десять лет трудно. Мозес вспоминает [Mose08], что в 1976 на встрече с представителями Лаборатории ИИ МТИ вице-президент DEC Гордон Белл (Gordon Bell) обещал новую, более дешевую версию PDP-10 в 1978. Это обещание не было исполнено. Очередной удар в спину МТИ от производителя железа.
DEC продолжал продавать апгрейды и новые системы и даже, в конце концов, расширение адресного пространства. Но для его использования нужно было писать весь рантайм и кодогенератор заново, как для новой платформы, так что это мало кого привлекало. Версию Stanford LISP [Stee96] с такой поддержкой - Rutgers ELISP сделали в 1980, и Стенфорд даже купил апгрейд в 1983. В год окончания производства системы. Да, только в 1983-ем году DEC официально сообщил о том, что продолжения линейки PDP-6/10 не будет.
И наработки ИИ Лабораторий, которые её использовали нельзя было просто так взять и портировать. Огромные по тем временам лисповые рантаймы написаны на ассемблере [Stee96] [Mose08]. И не только лисповые.
Уже в момент покупки PDP-10 Эдинбургским университетом у платформы не было будущего. Так что не только RABBIT и MACLISP, а и POP-2 обречен, первая имплементация HOPE обречена. Но хуже всего ситуация складывалась для компилятора Уоррена. Два указателя в слове не оказалось особо критичным для имплементации Лиспа, но эта и другие особенности PDP-10 были критичны для того чтоб сама идея Бойера и Мура об имплементации Пролога, которую использовала имплементация Уоррена, была практичной. И целый способ имплементации Пролога - разделение структуры - практически перестал использоваться и сменился на новый стандарт де-факто: копирование структуры [Li96] [Korn22].
Линейка PDP-6/10 оказалась для Лиспа, POP-2, Пролога смертельной ловушкой, из которой что-то все-таки спаслось, но от которой вся эта ИИ-культура МТИ, Стенфорда и Эдинбурга так уже и не оправилась.
В каком-то смысле, более бедные лаборатории лучше подготовились к восьмидесятым годам. Так Сент-Эндрюс в середине 70-х не смог заменить свой мэйнфрейм IBM на другой, новый мэйнфрейм. Хотя рассчитывали на это, даже писали интерпретатор SASL на микрокоде этого пока еще не купленного мэйнфрейма [Somm77]. Вместо мэйнфрейма они получили миникомпьютер из линейки, развитие которой DEC решил приоритизировать. Адресное пространство в 64K ограничивало их не намного сильнее, чем обычные ограничения одного пользователя на PDP-10. И это-то адресное пространство DEC намеревался увеличить в ближайшее время.
Но компиляторы Лиспа и Пролога хотя-бы работали на PDP-10. А что делать тем, кто хочет разрабатывать более требовательные к ресурсам компиляторы ФЯ? Что будут делать авторы ФЯ, правда, мало кому интересно. Гораздо важнее - что делать разработчикам MACSYMA? Она, как мы помним, едва помещается в адресное пространство в 75-ом году.
У лисперов МТИ было три основных плана бегства с PDP-10. Один хуже другого.
Программа приобрела репутацию чудовищно сложной, запутанной до такой степени, что ни один человек не утверждал, что понимает ее. И ни один человек не мог модифицировать ее не боясь повредить без возможности исправить.
Бернард Гринберг
Поскольку проблема с памятью была осознана еще до начала ИИ-зимы первые направления бегства были намечены еще как экспансия. Правда, не в область популярных систем, а в область больших бюджетов.
Лаборатория ИИ была только частью проекта MAC. В другой части продолжалась разработка систем разделения времени. Лаборатория ИИ разработала собственную ОС потому, что эта работа их коллег по проекту MAC продвигалась не очень быстро, да еще и по мнению ИИ Лаборатории куда-то не туда [Chio2001]. С 64-го года проект MAC был вместе с General Electric и Bell Labs разработчиком системы MULTICS для больших компьютеров с виртуальной памятью. И в начале семидесятых такая машина появилась в МТИ - Honeywell 6180. Версия 6080 с аппаратной защитой памяти, один из первых образцов [Honeywell72]. “Honeywell” потому, что эта компания подобрала проект заброшенный General Electric.
Осуществить перенос MACSYMA на MULTICS хотел еще Мартин, но разработку MACLISP для MULTICS начал только в конце 71-го года Дэвид Рид (David P. Reed). В 1973-74 годах работы велись под руководством Дэвида Муна (David Moon). MACLISP на MULTICS заработал в 73-ем году [Whit77]. Имплементация не могла быть портирована, за исключением кода компилятора, написанного на Лиспе. Но даже часть этого кода была воспроизведена потому, что никто уже не понимал как он устроен и работает [Gree96]. Ни сам язык MACLISP, ни устройство имплементации не было документировано. Но для воспроизведения нужно было все это прояснить. Результатом этой программной археологии стал мануал MACLISP [Moon74], который лисперы называют “Moonual” [Stee96]. Разобраться в устройстве (N)COMPLR было еще сложнее. В 78-ом году LCP, компилятор для MULTICS, был в полтора раза короче. Часть трюков были просто не нужны на MULTICS. Например потому, что там размер указателя был не 18бит и мог вмещать целые числа полезного размера. Но значительное количество трюков просто посчитали практически неизвлекаемыми из кода NCOMPLR и утраченными. Сопровождающие код компилятора для MULTICS, как и Стил в описании RABBIT рекомендовали [Gree96] читать о том, как надо делать оптимизирующие компиляторы не в коде NCOMPLR, а в книге [Wulf73] Вульфа об имплементации BLISS-11.
Не смотря на то, что портировать MACSYMA удалось [Whit77], это направление бегства привело в тупик. Машина была “непопулярна” [Fate81] [Stee96], производитель гордился двумя сотнями заказов и способностью поставить 25 машин за квартал. Это для всей линейки из которой Macsyma работала только на топовой модели [Honeywell72]. Система была непопулярна по сравнению с мэйнфреймами IBM, но и PDP-10 не был популярным. Более важным было то, что система была не особенно популярна среди пользователей Macsyma, которых почему-то не привлекала перспектива смены платформы на ту, которая на десятичный порядок дороже. И с 74-го года к их мнению надо было прислушиваться еще более чутко.
Honeywell 6180 была одной из первых машин, на которой бы мог работать компилятор ФЯ. Если бы в 73-ем году лисперы из проекта MAC умели и хотели делать компиляторы ФЯ. Машина, на которой будущий первый компилятор ML смог бы как-то компилировать, например, другой компилятор ML (4Мб памяти, 8Мб быстрого хранилища для свопа) обошлась в какие-то семь миллионов долларов (51.5 миллион долларов 2023) [Honeywell72]. И мы знаем, что мог бы, потому что на машине этой серии работал один из первых компиляторов ФЯ. Но было это в 80-е годы, когда было много гораздо более доступных альтернатив. Лаборатория, в которой это произошло, получила машину в 1979 [MULTICS1]. Эх, почему имплементаторам ФЯ дали поработать на компьютерах за 50 миллионов в 60-х, а не в 70-х? В 70-х от этого могла бы быть какая-то польза. Для имплементаторов ФЯ, конечно.
Но MACLISP на MULTICS мог бы и не дожить до того момента, когда LCP скомпилировал компилятор ФЯ, если бы на Лиспе не написали вторую программу с пользователями.
После того как спасение MACSYMA с помощью MACLISP на MULTICS не нашло понимания у её пользователей, имплементация Лиспа влачила жалкое, полузаброшенное существование. Его ведущие разработчики в МТИ вроде Муна оставили его ради другого проекта. Разработкой теперь занимался Бернард Гринберг (Bernard S. Greenberg), который больше пытался расшифровать и описать NCOMPLR, чтоб перенести из него еще что-нибудь в LCP [Gree96]. Honeywell не сопровождал его, никто не распространял никакие программы написанные на Лиспе для MULTICS.
Но в 78-ом году Гринберг и Брюс Эдвардс (Bruce Edwards) начали писать на нем EMACS, который стал за пару лет популярным у пользователей MULTICS. EMACS стал продуктом Honeywell, поставляемым вместе с ОС и компания вынуждена была поддерживать разработку имплементации Лиспа [Gree96b].
Второе направление было еще более амбициозным и еще менее уместным в наступающие тяжелые времена. Лидером направления был имплементатор MacLisp Ричард Гринблатт. Позднее к нему присоединились еще один имплементатор MacLisp Дэвид Мун, и многие другие лисперы Лаборатории ИИ МТИ, в том числе более известный другими своими работами Ричард Столлман (Richard Stallman).
Лисп-машины становились все более популярным проектом в МТИ и оттягивали все больше исследователей и программистов с других направлений [Stee96]. Написание эффективной имплементации Лиспа на обычном железе произвело на лисперов тяжелое впечатление. И опыт с имплементацией и реимплементацией MACLISP совсем не внушило лисперам уверенность в способности делать это снова и снова. У большинства отняло всякое желание это когда-нибудь делать. Какого же желания после этого оставалось хоть отбавляй? Правильно, разрабатывать собственное железо.
Железо предполагалось не таким уж особенным, тогда 32битные машины с изменяемым микрокодом становились все более обычными, но в обычных машинах было недостаточно быстродействующей памяти для микрокода, который лисперы планировали писать для имплементации лиспа. Этот микрокод служил для проверок тегов в указателях, а также для имплементации некоторых фич, из которых самая важная для этой истории - “невидимые” указатели.
“Невидимые” указатели - это системы из указателей на указатели (на указатели и т.д.) на значения, которые для программиста видны как указатели на значения.
Ладно, но как быть с урезанием финансирования, желанием пользователей Macsyma работать на доступных машинах? Проще простого: разрабатывать собственное дешевое железо.
Этим дешевым железом должен был стать не микропроцессор. Лисп-машина будущего строилась на элементной базе прошлого. Может быть они не знали про успехи VLSI? Сомнительно. Похоже, что они ожидали гораздо больших успехов, чем были реально достигнуты. Например, что скоро появятся твердотельные жесткие диски на полупроводниках.
Под влиянием работ в Palo Alto Research Center (Xerox PARC) было решено, что лисп-машина будет персональным компьютером. Но персональным миникомпьютером, а не микрокомпьютером [Gree74]. Т.е. “персональные” компьютеры все равно смонтированы в 19-дюймовых стойках, стоят в машинном зале, требуют специального охлаждения, в офисе только терминал.
Что же подразумевается под “персональностью”? То, что машины однопользовательские и к ним нельзя подключиться через ARPANet [Gree77]. Да, да, как мы помним, Лаборатория ИИ была спасена пользователями, которые работали на компьютерах лаборатории через ARPANet. Кроме того, (D)ARPA была и будет далее заинтересована в машинах для ARPANet. Ну что ж, этим займутся другие.
В 74-ом году Лисп-машинисты планировали, что машина будет стоить $70K ($436,855 в 2023). И, чтоб уложиться в эту сумму, собирались экономить на памяти. Да, главной целью создания Лисп-машин было выполнение программ на Лиспе, которым нужны мегабайты памяти. Но лисперы пока что предпочли считать, что нужны мегабайты адресного пространства, а память можно организовать в три уровня: 200-400Кб ферритовой памяти, 1-2Мб быстрого жесткого диска и 12Мб медленного [Gree74].
К 77-му году, когда была наконец построена первая лисп-машина с памятью, планы немного поменялись на $80K ($406,166 в 2023) машину с 4Кб быстрого кеша для стека, 256Кб ферритовой/полупроводниковой памяти (в 5 раз медленнее) и 64Мб (все адресное пространство) на диске (еще в 25000 раз медленнее).
Но, разумеется, идея экономии на памяти оказалась не такой уж хорошей. Начало было многообещающим: в 77-м на первой лисп-машине работают одновременно две программы - MACSYMA и программа Вудса (William Woods) LUNAR - каждая из которых умещалась в адресное пространство PDP-10 только с помощью всяких ухищрений для частичной загрузки. По “предварительным” результатам “значительная часть” Macsyma работает на машине с 256Кб ферритовой памяти как на PDP-10 KA-10 [Gree77]. Однако же, к марту 1980 уже считалось, что и вдвое большей памяти недостаточно: MACSYMA на лисп-машине с 512Кб работала в 5 раз медленнее чем на PDP-10 KL-10.
На 1Мб производительность посчитали хорошей, так что планы снова изменились (причем не только настоящие но и прошлые, в 1980-ом лисперы пишут, что проект Лисп-машин стартовал с предположения о том, что 512Кб основной памяти будет достаточно, и это не то, что писали в отчете 74-го года). Решили что надо больше 800Kб памяти и на нескольких машинах уже поставили 1Мб. К счастью, память дешевела, так что скоро можно и 4Mб делать [Whit80].
А что еще подешевело к 80-му? Правильно, микропроцессоры, и пока лисперы собирали прототипы Лисп-машин, другие люди собирали прототипы реальных персональных компьютеров на порядок дешевле, но об этом ниже.
Как же такое экзотическое железо как Лисп-машина могло повлиять на историю ФП? Непосредственно - очень незначительно. Да, разработчики ФЯ в МТИ (бывало и такое!) использовали Лисп-машины. Да, единственный имплементатор ФЯ, который пользовался машиной с Multics имел доступ также и к Лисп-машине, в числе прочего. Но, в отличие от машин с Multics, лисп-машины заработали примерно тогда же, когда и те, на которых велась основная разработка ФЯ. Реальный вклад лисп-машинизма в том, что он обескровил направления, которые могли бы быть полезными для имплементаторов ФЯ. Уменьшил влияние лисповой исследовательской программы на ФП. Например, Лисп-машина определила какой компилятор эдинбургского ФЯ скомпилирует себя первым. Разумеется, не тот, который пытались заставить работать на Лисп-машине.
Втайне Уэст боялся VAX.
Т. Киддер. Душа новой машины [Kidd81].
Третьим направлением бегства МТИ-лисперов с PDP-10 был VAX-11. Внезапное для лисперов (но не для всех) 32бит продолжение PDP-11, которое DEC сделала вместо ожидаемого лисперами продолжения PDP-10.
PDP-10 и Honeywell 6180, которые использовала или собиралась использовать ИИ-Лаб-культура 60-70-х были мэйнфреймами. Большими компьютерами, требующими специальных помещений, десятков тыс. долларов в месяц на поддержку, стоящими порядка сотен тыс. - миллионов долларов 68-го года. PDP-11 же и VAX-11 были миникомпьютерами, самыми небольшими и дешевыми компьютерами общего назначения, на момент появления это класса в 68-ом году.
PDP-11 был популярной и распространенной машиной, де факто стандартным 16бит миникомпьютером. Довольно быстро, впрочем, оказалось что 16бит адресное пространство было ошибкой DEC и серьезным ограничением. Быстро - по меркам DEC, которая рассчитывала начать разрабатывать замену гораздо позже, чем через шесть лет [Bell98].
Вокруг PDP-11 существовала другая известная программистская культура, несущая отпечаток относительной нищеты и суровых ограничений: UNIX. Если минсковиты неиронично сравнивали себя с космической программой “Аполлон” [Crev93], то эта культура выросла из персонального проекта программистов Bell Labs, замученных сначала Multics, а потом отсутствием доступа к ней [Salu94]. Теперь перед ними должно было распахнуться бескрайнее адресное пространство.
Проект с кодовым названием STAR стартовал в DEC в 75 году, первая имплементация VAX (Virtual Address eXtension) - VAX-11/780 была представлена в 78-ом. В октябре 80-ого выйдет менее производительная, но более компактная и бюджетная версия - VAX-11/750. VAX-11 будет самым популярным 32бит миникомпьютером [Bell98], основным направлением в DEC и главным успехом.
Насколько “мини” был этот миникомпьютер? Ну, не настолько, чтоб мощность вентиляторов системы охлаждения не измерялась в лошадиных силах. Корпус с ЦП 1.53м x 1.17м x 0.77м (ВШГ) и весит полторы тонны. Обычно соединяется с корпусами других устройств и вся сборка имеет габариты 1.53м x 2.51м x 0.77м. Все это требовало помещение с кондиционером на 42000 БТЕ/час.
Машины, которые покупали лаборатории и университеты обычно имели 256-512Кб памяти, но ее можно было расширить до 2Мб в первый год продаж и до 8Мб позднее. Кроме того, большие программы могли работать благодаря страничной памяти. На предыдущем миникомпьютере DEC PDP-11 программа могла загружаться в физическую память только полностью.
У покупателей обычно были уже совместимые устройства для PDP-11 (жесткие диски, стриммеры, терминалы). Конфигурация, которую купили Bell Labs (512Кб и без всего, что можно было использовать из старого) стоила в феврале 78-го $241,255 ($1,180,534 в 2023), но DEC продала Bell Labs со скидкой за $200 242 ($979,845 в 2023) [Lond78].
Понятно, что для какой-то заметной распространенности и доступности все это слишком дорого и велико, но уже в следующем году после начала проекта STAR в Motorola стартовал проект MACSS - Motorola’s Advanced Computer System on Silicon - миникомпьютер в одном чипе. Первый процессор семейства MC68000 анонсирован в 79 и доступен в 80. С 16бит шиной памяти но 32бит регистрами и совместим с будущими полностью 32бит моделями. Адресное пространство 24бит, 16мб для данных и 16мб для кода. Поддержка виртуальной памяти пока только в отдельном чипе [Heer80].
Так что в 80-ом году Форест Баскетт (Forest Baskett) в Стэнфорде обнаружил, что машина с 256кб памяти, поддержкой виртуальной памяти и ЦП сопоставимым по производительности с VAX-11/780 может быть собрана из микросхем стоимостью в тысячу долларов ($3,734 в 2023 году). Стоимость собранной рабочей станции оценили в $8,000 ($29,870 в 2023) [Bask80]. За два десятка лет от часа машинного времени до целой машины с сопоставимой производительностью за сумму того же порядка.
Работу над рабочей станцией Баскетт, Андреас Бехтольсхайм (Andreas Bechtolsheim), Майк Нильсен (Mike Nielsen) и Джон Симонс (John Seamons) начали в мае 80-го под руководством более известного другими своими работами Вона Пратта (Vaughan Pratt). И 30 ноября 1980 была готова первая итерация прототипа, смонтированная накруткой [Bech82].
Если про VAX-11/780 имплементаторы ФЯ по крайней мере будут думать, что на этой машине будут нормально работать первые компиляторы ФЯ и почти вся разработка компиляторов ФЯ в первой половине 80-х будет на и для VAX-11, то от этого прототипа произойдут машины, на которых они действительно будут работать во второй половине 80-х годов. VAX-11 и MC68000 не совместимы, но, по словам имплементаторов компиляторов, достаточно похожи чтоб модификация кодогенератора требовала большого труда. И на этих машинах будет работать одна и та же ОС.
Некоторые более современные фичи языков программирования, такие как “ко-рутины” и “замыкания”.
Дж. Уайт, NIL: перспектива [Whit79]
В 1978-ом [Stee82] в МТИ начали разработку расширенной версии MacLisp на новых дешевых машинах с большим адресным пространством (VAX-11, будущих микрокомпьютерах) для разработки больших программ на Лиспе, в первую очередь для MACSYMA [Whit79] [Mose08]. Её назвали NIL, что означало New Implementation of Lisp или NIL Is Lisp. [Whit79] Основными авторами на первом этапе были опытный имплементатор Лиспов Джон Уайт, Рик Брайан (Rick Bryan) и Боб Кернс (Bob Kerns) [Rees2010] [Weinreb].
Это было как раз то, что хотел Macsyma-консорциум [Mose08], так что все финансирование с разработки MacLisp было переброшено на NIL [Stee82] [Stee96].
Для истории ФЯ нужно отметить, что второстепенной целью было опробовать в Лиспе всякие новомодные фичи из ФЯ вроде “замыканий” и “лексической видимости” [Whit79] и в проекте участвовал автор компилятора RABBIT Гай Стил. У которого появился союзник в МТИ в деле функционализации Лиспа. Джон Уайт в 77-ом году поработал [Whit77] в IBM Research над тем самым функциональным лиспом Фреда Блэра. И, видимо, теперь считал, что это не такая плохая идея [Stee96]. Так отголоски обоекембриджской программы снова дошли до лисперов МТИ, но только после того как они переизобрели ФП самостоятельно.
Лексическая видимость, впрочем, теперь и в дальнейшем ее сторонники представляли не как функционализацию Лиспа, а как приведение области видимости в порядок. Прорывная идея заключалась в том, что область видимости в Лиспе должна работать одинаково независимо от того интерпретируется код или скомпилирован [Whit79].
Код писали на MacLisp на PDP-10 со слоем совместимости, который делал MacLisp похожим на NIL - техника бутстрапа, которую будут и дальше применять многие имплементаторы лиспов и схем [Whit79].
Рассчитывали не повторять ошибки прошлого с написанием кучи кода на ассемблере, сделать портабельную имплементацию, основанную на виртуальной машине [Whit79].
Пока что все выглядит хорошо. Что же в этот раз было не так?
Возможно, цель написать Лисп на Лиспе была излишне амбициозной. Тут речь не только о компиляторе. Решение не писать рантайм на ассемблере было хорошим. Проблема была в том, что рантайм решили писать не на языке системного программирования для этой платформы, а на Лиспе [Whit79].
И ошибки все равно повторили, стали писать кучу кода на ассемблере [Shivers] [Stee96]. А что делать? Macsyma нужна быстрая длинная арифметика [Weinreb].
От проекта уже в том же 78-ом отделился дочерний проект по имплементации NIL для еще более карикатурно недоступной и нераспространенной платформы чем Multics - суперкомпьютера S-1 Mark IIA, разрабатываемого в Ливерморской лаборатории (Lawrence Livermore Laboratory). И Стил оказался как раз в нем. Вы, наверное, недоумеваете, уж как это-то могло оказать влияние на развитие ФП? Всему свое время.
S-1 NIL должен был быть следующим шагом после MacLisp по оптимизации арифметики и работе с массивами. Стил собирался использовать как свой опыт с RABBIT, так и передовые наработки по аллокации регистров и т.д. из мейнстримных компиляторов [Stee82] [Stee96], и под передовыми наработками подразумевался компилятор BLISS-11 [Wulf73].
Различий между этими имплементациями становилось все больше, как в языке (в VAX NIL решили добавить ООП), так и в рантайме (теги на S-1, группировка в памяти однотипных объектов кучи на VAX) [Stee82].
Но самой большой проблемой было то, что лисперы МТИ не любили Macsyma, не любили VAX. Не любили очень эмоционально и годы спустя придумывали странные истории [Stee96] о том, как это правильно - не делать имплементацию Лиспа для VAX-11. Например, почему помер InterLisp? Потому, что сделали его имплементацию для VAX, очевидно же. Лисперы МТИ не хотели работать над NIL. Они хотели работать над Лисп-машинами [Stee82] [Rees2010] [Weinreb]. Для истории ФЯ нужно отметить, что из-за этого на проекте поработал и приобрел опыт будущий важный имплементатор ФЯ.
С 79-го года имплементаторы писали, что NIL будет готов где-то через полгода [Whit79], но ни в 79, ни в 80 его не закончили.
Наступили 80-е, а лисперы МТИ, основных, самые успешные и опытные имплементаторы Лиспов в 70-е, так и не создали компилятора Лиспа, поддерживающего ФП фичи для перспективных машин. Не появилось бы вообще никакого, если бы один из разработчиков Macsyma, давно уже покинувший МТИ, не начал войну за наследство ИИ-лаборатории.
Ричард Фейтман, тот самый разработчик Macsyma, который когда-то сравнивал MacLisp с Фортраном, с 1974-го работал в Университете Калифорнии в Беркли [Franz]. Анонс VAX-11 Фейтман встретил с энтузиазмом: в отличии от оставшихся в МТИ лисперов он считал, что это как раз то, что нужно для использования Macsyma [Fate81]. Он вообще по многим вопросом придерживался мнений, сильно отличающихся от мнений лисперов МТИ. Одним из ключевых было то, что Macsyma - это не какая-то собственность МТИ, а общественное достояние, ведь она сделана на деньги ARPA и Минэнерго [Stee96]. В МТИ могут сколько угодно придумывать оправдания для неспособности или нежелания сделать то, что хочет Macsyma-консорциум. Фейтман в это время сделает в точности то, что консорциум хочет. Это не так просто, но Фейтман не боится сложностей.
В это время в Беркли не было какой-то значимой производной МТИ ИИ и.т.д культуры. Было довольно чахлое ответвление UNIX культуры. Кен Томпсон провел там лето в 1976, установил UNIX на PDP-11 и начал писать компилятор Паскаля для него [McKus]. С тех пор в Беркли ни шатко ни валко занимались дописыванием этого компилятора (в 80-е он еще пригодится имплементаторам ФЯ), а Бил Джой (Bill Joy) собирал набор написанных в университете утилит. Не проблема, с чего-то надо начинать.
Еще одно непопулярное у лисперов МТИ мнение Фейтмана: написание (переписывание) имплементации Лиспа на языке C лучше, чем подход NIL. Язык C, на тот момент новый и мало кем используемый, уже продемонстрировал свои возможности для системного программирования и разработки языков. Поэтому разработчики нового Лиспа для VAX решили, что никакие модификации его для их целей не потребуются. Для сравнения, Хейвенс (W. Havens) из Университета Висконсина писал VAX Interlisp на Паскале и Паскаль пришлось модифицировать, менять типизацию указателей.
Основным преимуществом C Фейтман и его единомышленники считали один портабельный компилятор, определяющий язык [Fate81]. Никаких проблем с несовпадением имплементаций, комитетами по стандартизации и так далее!
Университетский PDP-11/70 c UNIX UNIX ver 6 [Fate81] конечно же имеет слишком мало памяти, но разрабатывать какой-никакой Лисп можно уже на ней. И студенты Фейтмана приступили к разработке. Начали с Лиспа для PDP-11, написанного в Гарварде. Расширили для совместимости с MacLisp. Основными контрибьюторами в первоначальную систему были Майк Карри (Mike Curry), Джон Бридлов (John Breedlove) и Джеф Левински (Jeff Levinsky). Билл Роуэн (Bill Rowan) написал сборщик мусора и поддержку массивов. Кип Хикман (Kipp Hickman) и Чарльз Костер (Charles Koester) - поддержку плоских структур. Основными разработчиками имплементации далее были Джон Фодераро (John Foderaro), работавший над компилятором, Кейт Скловер (Keith Sklower), занимавшийся длинной арифметикой и оптимизациями и Кевин Лэйер (Kevin Layer), который позднее будет писать MC68K бэкенд [Fode83] [Franz].
Заметная часть имплементации написана на Лиспе. Рантайм почти полностью написали на C, но не обошлось без пары страниц ассемблера. Компромисс между портируемостью и производительностью.
Основное предназначение имплементации - поддержка MACSYMA. Так что имплементаторы не особо стараются имплементировать фичи, которые для MACSYMA не нужны. Также не особенно работают над оптимизатором считая, что это можно компенсировать с помощью FFI. Писать часть кода на Фортране, Си и т.д.
Вызывать фортран код из PDP-10 Maclisp невозможно (Multics Maclisp есть FFI), на Лисп-машинах в это время нет компилятора Фортрана, готовых библиотек для HPC и прочих полезных вещей. На популярных платформах все это, конечно, есть. Библиотеки для FFT работает в 100 раз быстрее, чем написанная на Лиспе, так зачем тратить силы на оптимизации [Fate81]? Первая версия этого Лиспа, названного Franz Lisp заработала на PDP-11 весной 78-го [Fate81].
Конечно, PDP-11 недостаточно чтоб запускать MACSYMA. Не проблема, Фейтман достанет VAX-11. Пока студенты Фейтмана занимались имплементацией Лиспа, он и еще 13 преподавателей составили заявку на грант NSF, который позволил, с добавлением из фондов департамента, приобрести VAX. Машину привезли осенью 1978. Сначала на ней работала ОС DEC VMS, но департамент хотел использовать UNIX, к которому привыкли на PDP-11 [McKus]. Конечно, для использования наработок студентов Фейтмана тоже нужен был UNIX. И для того, чтоб запускать большие программы вроде MACSYMA VMS не подходила. В VMS каждому процессу дается память одинаковая для всех процессов - резидентный набор. Страницы, которые не являются частью никаких резидентных наборов используются как дисковый кэш. Даже если работает один процесс - из его резидентного набора все равно отбирается память если много неработающих процессов. Разумеется это неудобно, процессы сильно отличаются по потреблению памяти. Также у процессов меняется поведение по запрашиванию памяти, Лисп-система во время сборки мусора сильно отличается таким поведением от времени работы мутатора. Это будет частично исправлено в VMS 2.1, но пока что этой версии системы не было [Baba81]. Не проблема, Фейтман достанет 32бит UNIX.
И действительно, вскоре после получения компьютера Фейтман раздобыл копию 32/V UNIX, написанного Джоном Рейзером (John Reiser) и Томом Лондоном (Tom London) в Bell Labs [McKus]. На момент анонса VAX-11 в Bell Labs уже был порт на более ранний 32бит миникомпьютер Interdata 8/32, и писать бэкенд для VAX-11 для так называемого “портабельного компилятора” Джонсона начали еще в середине декабря 1977. К тому времени, как им привезли железо 3-го марта 78-го, компилятор был уже такой степени готовности, что смог скомпилировать себя и ядро UNIX.
19 мая ОС заработала [Lond78].
Но хотя 32/V и предоставлял Version 7 UNIX окружение на VAX, система за исключением машинно-зависимых частей кода оказалась похожа на систему для PDP-11. Аппаратная поддержка памяти на VAX не использовалась ни для чего кроме эмуляции PDP-11 сегментов. Поведение идентичное тому, что на PDP-11: аллокация для процессов непрерывных участков реальной памяти, которая сбрасывается в своп только целиком. Для выполнения процесса он должен быть загружен полностью, соответственно размер реальной памяти все еще предел для одного процесса [Baba81] [McKus]. Все еще хуже, чем на VMS!
Но для Franz Lisp этого уже достаточно. После получения VAX-11 прототип имплементации был перенесен на неё с PDP-11 и доработан до полноценной имплементации которая может собирать MACSYMA. Franz Lisp работает на VAX-11 UNIX с января 79 [Fate81].
Для MACSYMA этого недостаточно. На машине первоначально 0.5Мб [Baba81] или 1Мб [McKus] памяти, нет возможности загрузить её всю целиком в реальную память. Не проблема, Фейтман организует поддержку страничной памяти.
Фейтман связался с профессором Доменико Феррари (Domenico Ferrari), преподавателем факультета систем в Беркли чтоб привлечь его группу к написанию системы виртуальной памяти для UNIX. Основной целью проекта называется предоставление большое адресное пространство для пользовательских процессов, а именно для лисп-программ таких как MACSYMA и для программ обработки изображений [Baba79]. Этим занялся его студент Озалп Бабаоглу (Ozalp Babaoglu). После имплементации прототипа Бабаоглу стал работать совместно с Биллом Джоем, который тогда поддерживал берклиевский дистрибутив UNIX. Билл Джой помог с интеграцией менеджера памяти в 32/V и отладкой [McKus]. Модификация Version 7 UNIX в систему со страничной памятью началась поздней весной 79 и первая версия была готова для использования в сентябре 79 [Baba81].
Лисп является не только одним из основных бенчмарков для системы. Поддержка имплементации Лиспа закладывается в сам менеджер памяти. Добавлен системный вызов, чтоб перед началом сборки мусора можно было запросить более подходящую политику пейджинга [Baba81]. Система по умолчанию использует политику “дольше всего не использовалась”. Но во время сборки Franz Lisp посещает так много страниц, что такая политика не годится. Поэтому системе посылается сигнал и она меняет ее на FIFO с удвоенной скоростью замены страниц. После окончания сборки системе посылается сигнал и она помещает все страницы лисповой программы в список свободных и возвращается к политике по умолчанию [Fode81].
В декабре 1979 готов 3 BSD - дистрибутив UNIX с ядром с поддержкой страничной памяти и портированными программами, разработанными для 2 BSD. Например компилятором Паскаля, который начинал Кен Томпсон во время визита в Беркли [McKus]. Распространение системы началось в январе 1980, к весне распространено больше 50 копий. Популярна у тех, кто запускает большие программы, а не использует как обычную систему разделения времени [Baba81]. Сопоставимая поддержка больших программ появилась в Bell Labs UNIX только в System V Release 2 Version 4 в 84 году [Quar86]. Фейтман доволен результатами, Macsyma на VAX работает с 80-го года [Fode81]. В 81-ом “Vaxima” работает на VAX-11/780 c 2.5Мб физической памяти. Сегмент только для чтения, разделяемый между всеми пользователями 1.8Мб, объекты в динамической памяти 1.4Мб - 3.3Мб. В 81-ом году в Franz Lisp 15,000 строк на Си, немного ассемблера для аллокатора, сборщика мусора, длинных целых. Из-за того что в Си недостаточно инструментов для управления регистрами, пришлось передавать/возвращать на стеке во многих случаях, когда можно бы было в регистрах. Также лисперы недовольны медленным сишным соглашением о вызове. Но были довольны тем, как быстро удалось написать систему не сильно медленнее написанной на ассемблере. Со сборкой мусора все еще проблемы, даже поддержки от ОС недостаточно [Fode81], но к этому мы еще вернемся.
Лисперы МТИ не любят Franz LISP, хоть и признают, что это де факто стандартный Лисп на VAX. Пишут, что он написан “быстро и грязно” [Stee82] и что Franz Lisp не интересен потому, что “ничего не предлагает кроме портируемости” [Stee96].
3BSD произвел хорошее впечатление на (D)ARPA и они выбрали берклиевский UNIX для использования как единую ОС для своих проектов, а значит для поддержки и финансирования [McKus]. Для начала, контракт на 18 месяцев с апреля 1980 на добавление фич, нужных для (D)ARPA. Билл Джой - руководитель проекта. В октябре 1980 выходит 4 BSD - основная система, на которой будут разрабатывать компиляторы ФЯ в 80-е годы. Дистрибутив включает Pascal и Franz Lisp, через год работает на ~500 машинах [McKus]. 3BSD и 4BSD де факто стандартный выбор UNIX на VAX, использовался даже в самой Bell AT&T. И многие, получив от них лицензию, даже не утруждали себя получением ленты с 32V [Quar86]. В (D)ARPA довольны дистрибутивом, заключают новый двухгодичный контракт, увеличивают финансирование в пять раз. И увеличив финансирование тут, в кое-каком другом месте они его существенно уменьшили, но это уже другая история.
Franz LISP входит в дистрибутив 3BSD и 4BSD. Статья про то, что страничная память имплементировалась для того, чтоб запускать Macsyma поставлялась с ним вместе с прочей документацией. Но чем больше лет проходило с тех пор тем меньше упоминаний о роли Macsyma и Лиспа в истории BSD и тем более расплывчатыми становятся оставшиеся. Лисперы исчезают из истории, как товарищи с фотографий со Сталиным. На момент написания этого текста, например, статья про BSD на Википедии почти полностью свободна от какого-то упоминания Лиспа, кроме скриншота на котором Franz LISP по какой-то причине.
По всей видимости, ранние историки BSD смело упоминали Лисп потому, что это было что-то важное и передовое, но чем дальше, тем страннее очередному автору очередного исторического очерка было видеть и тем сложнее оставить упоминания Лиспа. Который превратился в какую-то затянувшуюся шутку. И лисперов, которые стали известны как те, кто утверждают, что все изобрели и все могут сделать, но ничего не делают.
Да, как мы уже видели и еще увидим, лисперы часто присваивают себе достижения, которые от невнятной идеи лиспера до практически работающего воплощения доводились другими. Но этого достижения у них не отнять. Они сыграли важную роль в том, что UNIX стал ОС для рабочих станций, а не какой-нибудь нишевой системой для управления АТС. Но почему Лисп превратился в эту самую затянувшуюся шутку, с которой UNIX культуре некомфортно иметь что-то общее? Пока что лисперам, вроде бы, удалось спастись с тонущего PDP-10 и спасти главное из того ценного, что было сделано (но не компилятор с поддержкой ФП-фич). Но война за наследство ИИ-лаборатории только началась. И если МТИ проиграл первую битву, это еще не значит, что он не может сделать так, чтоб все проиграли войну.
Итак, мы выяснили, что имплементаторы Лиспов не были особенно заинтересованы в поддержке функционального программирования. И стели делать что-то для этого только в конце 70-х. И даже когда стали делать, то не сделали ничего подходящего для того, чтоб взять и использовать как бэкенд для компиляции ФЯ. Причины для этого были разными, но результат один: в 70-е имплементаторы эдинбургских ФЯ могли использовать Stanford LISP на PDP-10, а в 80-е они вступили имея возможность использовать Franz LISP на VAX-11. Ни та ни другая имплементация, фактически, не имела для поддержки ФП ничего кроме сборщика мусора. Тут возникает вопрос: если имплементации Лиспа дают только сборщик мусора, то почему бы не рассмотреть использование каких-то других языков со сборщиками мусора? Зачем платить производительностью за поддержку в имплементации Лиспа ненужных ФЯ “динамических” фич? Сегодня относительно успешно имплементируют ФЯ трансляцией в мейнстримные языки со сборщиком мусора. Но десятилетия наработок в области эффективной имплементации скриптов все еще не дают ФЯ, транслируемому в JavaScript, догнать ФЯ, транслируемый в JVM. Но существовало ли в 70-е годы что-то соответствующее Java в этом сравнении? Мейнстримные языки со сборкой мусора могут быть существенно более современным явлением. Давайте выясним.
Описание ALGOL 60 стало достойной демонстрацией языка. Хорошо организованное, маняще неполное, слегка двусмысленное, трудное для чтения, краткое, оно было идеальным холстом для языка, который обладал теми же свойствами. Как и Библия, оно было задумано не только для чтения, но и для толкования.
Алан Перлис, Американская сторона разработки ALGOL [Perl78]
После Парижского заседания на Петера Наура снизошел Святой Дух.
Фриц Бауэр, Европейская сторона разработки ALGOL [Naur78]
Вернемся практически в самое начало нашей истории, когда работы над имплементацией CPL в Лондоне и том самом Кембридже завершились крахом. Выяснилось, что ожиданиям нового высокоуровневого языка в Кембридже и Королевском Институте Радиолокации не суждено сбыться. После этого мы отправились в другой Кембридж. Там сделали работающую, урезанную версию CPL практически полностью свободную от всех претензий на высокоуровневость. И потому не интересную для истории ФЯ. Понятно, что практически вся наша история ФЯ до сих пор была посвящена тому, что произошло от другого языка сделанного там, свободного от практически всех претензий на практичность. Что же происходило в том самом Кембридже и Институте Радиолокации? Похоже, что ни один из вариантов продолжения CPL от другого Кембриджа их полностью не устроил. Так что они поучаствовали в эксперименте, который определил, как много мечт о CPL могут быть воплощены в относительно практичном языке. Попробовали двигаться к компилятору ФЯ не как в Эдинбургской программе от ФЯ-скрипта к языку общего назначения, а от языка общего назначения к добавлению в него ФП фич. Как много ФП можно себе позволить с компилятором, который работает на обычной машине, а не на специально сконструированной без особых оглядок на дороговизну как в МТИ?
После гибели CPL, в Кембридже и Радиолокационном институте обратили внимание на другой язык, произошедший от ALGOL. Как и CPL. И общее происхождение только одно из многих общих свойств. Авторы CPL не без оснований заявляют [Camp85] о влиянии CPL на этот язык, как об одном из основных результатов их трудов. Язык, впрочем, начал разрабатываться достаточно давно, чтоб были основания предполагать его влияние на CPL.
Насколько ALGOL 60 хорош как основа для получения ФЯ? Да, мы уже рассказывали об одной неудачной попытке это сделать и скоро расскажем об еще одной. Но причина этих неудач не в ALGOL 60. Посмотрим на пример из учебника по ALGOL 60 [Ruti67]. В нем объявляется такая вот ФВП:
REAL PROCEDURE sum (p, q, term);
VALUE p, q;
INTEGER p, q; REAL PROCEDURE term;
BEGIN
REAL s;
INTEGER k;
s := 0;
FOR k := p STEP 1 UNTIL q DO s := s + term(k);
sum := s
END sum
А так она использована с объявлением локальной процедуры, которая замыкается на какие-то значения a и b выше по области видимости:
REAL PROCEDURE scalp(p);
INTEGER p;
scalp := a[p] x b[p];
z := sum(1, n, scalp);
Что напоминает высокоуровневые обоекембриджские языки, но отличается экстремально тяжелым синтаксисом и тем, что все эти замыкания и передачи работают в одну сторону - вниз. Не только для функций, вернуть массив тоже не получится. Это, конечно не ФЯ, но очень амбициозный проект для 50-х годов. Память управляется автоматически и без сборщика мусора. Ну, пока кто-нибудь не сломает всю эту автоматику CPS-трансформацией, как Моррис. Имплементировать ALGOL 60 было не так легко, и обычно он не был имплементирован полностью [Naur78]. Но интересные для нашей истории фичи были имплементированы и довольно рано, в 1960-ом году [Dijk62]! ALGOL 60 - это не просто набор пожеланий, как CPL. И значит было известно из опыта, что можно и не отступать от набора пожеланий CPL так далеко, как отступили в BCPL. Но можно ли было отступать еще меньше?
Конечно, есть и другая, существенно менее интересная чем “работающий только вниз ISWIM с тяжелым синтаксисом”, перспектива на ALGOL 60. Что это - Паскаль. Но действительно ли это Паскаль?
С одной стороны, это даже меньше, чем Паскаль. Описанный в репорте ALGOL 60 - не то, что мы бы сегодня ожидали от языка программирования. Это скорее что-то вроде языка формул калькулятора. Средств для описания структур данных в нем нет. Так что авторам имплементаций Алгола приходилось разрабатывать свои средства, и делать ALGOL 60 с расширениями. Как AED-0 [Ross61], на котором Ричардс написал первый компилятор BCPL для бутстрапа [Rich2000], как имплементация ALGOL 60 от Королевского Института Радиолокации [Wood66], как SIMULA.
С другой стороны - это больше, чем Паскаль. И ALGOL 60 не так просто имплементировать. В 1964 Дональд Кнут опубликовал [Knut64] код для проверки того, имплементирует ли компилятор ALGOL 60 основные фичи языка. И насколько эффективно.
BEGIN REAL PROCEDURE A(k, x1, x2, x3, x4, x5);
VALUE k; INTEGER k;
BEGIN REAL PROCEDURE B;
BEGIN k := k - 1;
B := A := A(k, B, x1, x2, x3, x4)
END;
IF k <= 0 THEN A := x4 + x5 ELSE B
END;
outreal(A(10, 1, -1, -1, 1, 0))
END;
Да, как мы уже выяснили, работающая лексическая видимость это не то, на поддержку чего можно всегда рассчитывать в 60-е и 70-е годы. Код выглядит немного странно, но это всего лишь Паскаль с другим синтаксисом аннотации типов. Нет! Тест Кнута проверяет не только лексическую видимость. На Паскале этот код будет выглядеть так [Rosetta1]:
program manorboy(output);
function zero: integer; begin zero := 0 end;
function one: integer; begin one := 1 end;
function negone: integer; begin negone := -1 end;
function A(
k: integer;
function x1: integer;
function x2: integer;
function x3: integer;
function x4: integer;
function x5: integer
): integer;
function B: integer;
begin k := k - 1;
B := A(k, B, x1, x2, x3, x4)
end;
begin if k <= 0 then A := x4 + x5 else A := B
end;
begin writeln(A(10, one, negone, negone, one, zero))
end.
Да, в ALGOL 60 способ передачи аргументов по умолчанию - вызов по имени. Единственный аргумент из примера, который передается по значению декларирован как таковой: VALUE k
. Это определенно не самое распространенное решение. Но интереснее то, как алголисты его использовали для имитации конструкции несколько более распространенной.
В ALGOL 60 нет лямбд. На первый взгляд это кажется нормальным. Их и в CPL/ISWIM обычно не планировали. В этой истории мы уже повидали несколько функциональных языков без лямбд, и в ALGOL 60 есть вложенные функции и функциональные параметры для передачи функций вниз. Но алголистам этого мало и они имитируют лямбды с помощью вызова по имени. Обычно программисты поступают наоборот и имитируют вызов по имени с помощью лямбд.
Чтоб паскалеобразность не мешала пониманию совсем не паскалеобразной семантики, рассмотрим пример на языке, в котором вызов по имени привычнее. Допустим нам нужно воспроизвести такую вот ФВП:
bar f = do
f 1
f 2
которая используется так:
> bar (\x -> print x)
1
2
Для этого алголисты использовали технику, называющуюся “устройство Йенсена” (Jensen’s device), в честь одного из имплементаторов Алгола Йорна Йенсена (Jørn Jensen).
Заводим для каждой связанной переменной параметр функции и пишем в него нужные значения перед каждым “вызовом” “функции”:
foo a f = do
writeIORef a 1
f
writeIORef a 2
f
Получаем тот же результат:
> x <- newIORef 0
> foo x (print =<< readIORef x)
1
2
Но какой ценой? Отказ от имплементации лямбд ничего не дает. Разумеется, имплементировать передачу в функции функций и санков для вызова по имени одинаково трудно. Сложная для имплементации часть лямбды - свободные переменные. И в ALGOL 60 есть “лямбды” без конструкции связывания переменных. Все переменные в них свободные. Это не только не упрощает имплементации, но и в сочетании с другими фичами не запрещает использование “лямбд” в качестве лямбд. Но, может быть, так и было задумано? Да, обычно в ISWIM-ах не было лямбд, но в реально имплементированных ISWIM-ах лямбды как раз обычно были. И Йенсен очень хорошо позиционирован для того, чтоб продвинуть лямбду в Алгол, если он хочет. Он работает над компилятором вместе с основным автором Алгола и редактора репорта Петером Науром (Peter Naur). Но нет, вместо обычной лямбды - странный фокус. Алголисты вообще про лямбду-то знают?
Корректная статическая видимость Алгола в сочетании с этим странным фокусом, по всей видимости, и породили мнение о том, что авторы Алгола не знали о лямбда-исчислении, но (недо)открыли его независимо. Статическая видимость необходима для того, чтоб откладывание вычисления параметров правильно работало.
Следовательно, предполагает Бердж [Burg75], а вслед за ним и Тернер [Turn12], имеет место конвергенция подходов “как надо делать по теории” и “как надо делать чтоб работало”. Что должно подтверждать некоторую фундаментальную правильность подходов “по теории”. Во что, конечно, хочется верить.
Как мы уже знаем, лисперы действительно столкнулись со сложностями, когда имплементировали видимость не как в ЛИ. С другой стороны, два десятка лет после открытия этих проблем лисперы чаще решали, что сойдет и так.
Но насколько правдоподобно то, что авторы Алгола совсем не знали ЛИ?
МакКарти уже понял что такое лямбда и как она должна работать [McCa60b] к тому времени как ALGOL 60 репорт был готов. Но в первые годы разработки ALGOL 60 МакКарти видимо знал про лямбды гораздо меньше, если судить по коду в первом отчете [McCa58] Лаборатории ИИ МТИ, в котором map
применяется так, как будто написан с помощью техники Йенсена:
maplist (cdr(J),L,(L = K -> diff(L), L != K -> copy (L)))
Но Рутисхаузер утверждает, что техника Йенсена изобретена на два года позднее, в 1960 [Ruti67]. Возможно, МакКарти мог поучаствовать в обсуждениях только после того, как основные решения были приняты. И к тем участникам обсуждений, кто должен был знать ЛИ в это время можно добавить еще и Ландина. Хотя тогда он еще не состоял в комитете.
Есть серьезные основания сомневаться в идеях Берджа о том, что ALGOL 60 не проектировали со знанием ЛИ. И для странностей Алголов есть другое объяснение. Комитетчики совсем не ладили друг с другом и придерживались очень разных взглядов на то, что нужно добавлять, а что нет. Многие странности языка и неоднозначности его описания были определены тем, как проходили линии фронта на момент перемирия 1960-го года.
Бауэр (Friedrich Ludwig Bauer) пишет [Naur78], что Самельсон (Klaus Samelson) и “другие” пытались добавить в Алгол прямую и логичную как в лямбда-исчислении передачу функций через аргументы. Неудачно. Алан Перлис (Alan Jay Perlis), по словам Бауэра, заявлял, что понимает, что вызов по имени - скрытое добавление в язык фич для передачи функций в функции, которые не удалось добавить явно. Бауэр жалуется, что Наур просто не оставил адекватных заметок об этих попытках “функционализации”. Наур пишет, что в вопросах передачи функций через параметры не было не только общего согласия, но и общего понимания. Что полностью понимал о чем говорится в предложениях по передаче функций в функции только Перлис. Похоже, что не только он, но вполне возможно, что понимали не все.
Было бы чрезмерным упрощением разделять авторов ALGOL на про-ФП и анти-ФП фракции. Что “про”, а что “анти-ФП” в те далекие времена было не совсем очевидно. И из нашего времени многие позиции выглядят противоречиво. По крайней мере, на первый взгляд. Например, те, кто были за лямбды - были против рекурсии.
Попытка американской части комитета (видимо МакКарти) добавить рекурсию с аннотацией recursive
оказалась неудачной, предложение было отклонено с небольшим перевесом. Но рекурсию удалось добавить Ван Вейнгаардену с Дейкстрой в результате “Амстердамского заговора”. В репорте обнаруживались или даже закладывались некоторые умолчания и утверждения, которые по отдельности не привлекали внимания тех, кого они не должны были привлекать, пока все вместе они не сложились в поддержку рекурсии. Когда противник уже измотан и потерял волю к борьбе с этой фичей. Наур вспоминает, что последнюю, решающую деталь, сконструированную так, чтоб Бауэр не обратил на неё внимание, предложил добавить Ван Вейнгаарден только при поддержке Дейкстры [Naur78], но Дейкстра приписывает исполнение хитрого плана себе [Dijk2001].
Может быть, такая борьба комитетчиков даже полезна. В конце концов, авторы CPL так и не закончили его описание. Ведь для них оно было только рутинным документированием того, что прочие авторы согласны со Стрейчи. Другое дело - описание ALGOL. Каждая строчка в нем - памятник чьей-то победы. Свидетельство того, как кто-то был переигран и уничтожен.
С другой стороны, успехи борьбы за то, чтоб ALGOL 60 был функциональным языком, были очень ограниченными. Хак Йенсена - это, скорее всего не то, что вы хотели бы получить вместо лямбды. Да и с рекурсией все вышло не особенно хорошо. Рекурсия имплементирована не самым практичным для ФП образом и занимает стек в любом случае. Но в это время еще не знали как имплементировать рекурсию практично. ALGOL 60 репорт написан за четыре года до того самого выступления Вейнгаардена [Wijn66]. Возможно, поэтому у рекурсии и были противники среди тех, кто продвигал ФЯ фичи. Но не обязательно. Во время обсуждения доклада Самельсон отнесся без особого энтузиазма к идеям Вейнгаардена [Wijn66].
Косвенным подтверждением истории про Самельсона, который хотел добавить в Алгол лямбды еще в 1960 можно считать то, как он изменил Алгол, получив такую возможность.
Говорят, что один ирландец на вопрос, как добраться до какого-то отдаленного места, ответил, что если вы действительно хотели бы попасть в это место, то не стоило бы начинать путь отсюда. <..> Начинать с ALGOL 60 было ошибкой.
Чарльз Линдси [Lind93]
На выпуске репорта об ALGOL 60 разработка Алгола не закончилась, и вскоре линии фронтов стали двигаться вновь. На этот раз основной конфликт определить намного легче. Не все авторы хотели, чтоб новый Алгол был таким же новаторским и амбициозным каким был в свое время старый. Хотели, чтоб был просто таким же. ALGOL 60 был недоделан, не было средств объявлять типы данных вроде рекордов. Рекорды нужно добавить. В ALGOL 60 были недостатки вроде вызова по имени - это нужно убрать. Минимальные изменения ALGOL 60 описывались в репорте-кандидате Хоара (рекорды) и Никлауса Вирта (Niklaus Wirth) (все остальное) [Wirt66].
Понятно, что этот подход, в основном, был направлен против функционализации Алгола. Но и члены этой партии продвигали некоторые изменения, которые делали Алгол более похожим на ФЯ.
В прошлый раз победил тот, кто первым написал хорошую заготовку для репорта. Так что победа партии умеренной модификации Алгола была весьма вероятна. В октябре 1965 в Сен-Пьер-де-Шартрёз минимальный инкремент Алгола был представлен как два документа от Хоара и Вирта. И комитет решил, что это примерно то, что нужно. Победное шествие, правда, натолкнулось на неожиданное препятствие.
Наш старый знакомый, непонятный изобретатель CPS-преобразования Адриан Ван Вейнгаарден, тоже подготовил заготовку репорта. Репорт вовсе не был готов, но комитету понравилось, как Ван Вейнгаарден описывает язык. Так что комитет решил: нужен язык Хоара и Вирта, описанный Ван Вейнгаарденом.
Из собственных взглядов на то, каким должен быть новый Алгол у Ван Вейнгаардена пока что были только идея “ортогональности” и то, что новый Алгол, называемый сначала Algol X, должен быть языком выражений. Последнее делает язык более похожим на ФЯ, у первого будут неоднозначные последствия для “функционализации”.
История добавления типов данных в CPL плохо задокументирована. Добавление типов данных в Алгол - совсем другое дело. Почему мы вдруг вспомнили про историю типов данных в CPL? Потому, что это скорее всего одна и та же история.
Джон МакКарти был автором первого предложения [McCa64] о том, как могут выглядеть типы данных для Алгола. Для этого он, еще в 1964, приспособил уже знакомую нам его идею [McCa61] о суммах и произведениях. Стековая дисциплина Алгола не особенно способствовала развесистым деревьям в памяти, так что в его примерах только плоские структуры вроде точек. Декларирующий типы программист должен придумать и описать имена конструкторов, селекторов и даже функций, возвращающих число-тег суммы:
CARTESIAN typename, ((field1, type1),...(fieldn, typen)), constructorname
UNION typename, ((cons1, sel1, type1),...(consn, seln, typen)), gettag
Да, никакой конструкции для деконструкции МакКарти не предложил. Нужно сравнивать числа-теги:
typename x := cons1(y)
type1 r := IF gettag(x) = 1 THEN sel1(x) ELSE y
Эту идею летом 65-го развил Хоар. Не только добавив побольше ключевых слов.
RECORD CLASS cons(head, tail);
BEGIN INTEGER head;
REFERENCE tail(cons)
END;
Хоар вводит ссылки. Рекорды Хоара совсем не плоские. Наоборот, это средства создания деревьев в куче. Тип рекорда и его конструктор имеют одно и то же имя:
BEGIN REFERENCE list (cons);
list := cons(1,NULL);
tail(list) := cons(0,NULL);
END
Удивительно, но синтаксис для юнионов не такой тяжелый:
UNION list(cons, nil);
Юнион тут - “неопределенная ссылка”.
Легкость декларации компенсируется синтаксисом для деконструкции юниона с невероятными составными ключевыми словами:
IF x IS A cons THEN ... head(x) ...
OR IF IT IS A nil THEN ...
Хоар упустил возможность изобрести нотацию Бурсталла, при том что сам уже предлагал [Hoar64] для Алгола конструкцию такого вида:
CASE x OF (
resIfx1 ELSE
resIfx2 ELSE
resIFx3
)
Но для чисел, а не тегов юнионов. Идея настолько не предусматривает ничего кроме чисел, что даже самые примитивные “паттерны” в виде констант пока что отсутствуют, есть только порядок результатов.
Тут типы данных для Алгола сильнее всего сближаются с обсуждаемыми позднее [Stra67] типами данных для CPL. Этот обмен идеями мог осуществить Ландин, который принимал участие в проектировании обоих языков. Никаких признаний или даже претензий на заимствование в ту или иную сторону мы, правда, не видели. Да, у пишущих истории CPL есть мнение [Camp85], что новый Алгол происходит от CPL, хотя и без конкретных упоминаний заимствования типов данных. Историки Алгола CPL не вспоминают.
Хоар упоминает CPL в этом предложении фичи для Алгола, но не как язык, из которого он позаимствовал типы данных. Хоар пишет о ссылках как концепции из CPL, которую он предлагает не заимствовать. Уже в 65-ом году Хоар противник первоклассных ссылок и продвигает ссылочные типы. Продвижение которых и через десять лет после этого будет слишком смелым и опережающим свое время. Но в 70-е Хоар будет продвигать уже не МакКартиевские раздельные суммы и произведения, а суммы произведений. Это будет сближение с работами Бурсталла, а пока что пройдено сближение с CPL, дальше расхождения в типах данных с новым Алголом будут нарастать.
Разумеется, комитет Алгола 68, не принял это предложение Хоара в неизменном виде. Вместо ссылочных типов новый Алгол получил плоские типы, которые можно размещать и на стеке с явными первоклассными ссылками. Которыми можно имплементировать список с некоторым выбором представления так:
MODE LIST = UNION(NULL,CONS);
MODE NULL = VOID;
MODE CONS = REF STRUCT(INT h, LIST t);
┌───┬───┐ ┌───┬───┬───┐
│001│ ├──►│ 1 │000│ │
└───┴───┘ └───┴───┴───┘
или так:
MODE LIST = REF UNION(NULL,CONS);
MODE NULL = VOID;
MODE CONS = STRUCT(INT h, LIST t);
┌───┐ ┌───┬───┬───┐ ┌───┬───┬───┐
│ ├──►│001│ 1 │ ├──►│000│ │ │
└───┘ └───┴───┴───┘ └───┴───┴───┘
Есть и еще одно важное отличие этих типов данных попавших в Алгол от того, что хотел продвинуть Хоар. Эти MODE
декларации аналог type
в Хаскеле, а Хоаровский RECORD CLASS
- аналог data
. MODE
можно подставить в другой MODE
и все продолжит работать, типизация структурная и ближе к первоначальной идее МакКарти:
MODE LIST = REF UNION(VOID,STRUCT(INT h, LIST t));
MODE NULL = VOID;
MODE CONS = STRUCT(INT h, LIST t);
Хоар такого не хотел.
Вы, вероятно, не захотите использовать юнионы для объявления списков таким образом не только из-за неэффективного представления МакКартиевских данных в памяти (о котором мы уже писали в главах про LCF/ML и непосредственную имплементацию) но и потому, что новый Алгол пока что не имеет удобной конструкции для их разбора, пусть даже и с тяжелым синтаксисом как IF Хоара. Вместо удобной есть вот такая:
CASE nullvar, consvar ::= l IN
nullvar,
t of consvar
ESAC
Что тут происходит? Что-то вроде такого:
data Cons = Cons{ h :: Int, t :: List }
data List = Null () | NotNull Cons
case l of
Null null -> do
nullVar <- newIORef null
return nullVar
NotNull cons -> do
consvar <- newIORef cons
return . t =<< readIORef consvar
Да, вместо паттерна тут тоже только порядок. Что, возможно, даже хуже, чем вы ожидаете. Потому, что еще один автор нового Алгола, более известный другими своими работами, Ёнэда (Yoneda Nobuo) настоял, чтоб декларации UNION(NULL,CONS)
и UNION(CONS,NULL)
объявляли один и тот же тип. Порядок типов в юнионе не имеет значения. Одно хорошо - неправильный порядок вариантов в этом CASE
не пройдет проверки типов. Так что пока лучше использовать рекорды и null
, как собирались и в CPL. Не очень-то похоже на ФЯ. Юнионы даже хотели заменить на ссылку на любой юнион, но Хоар их отстоял.
Почему мечты Хоара потерпели крах? Это результат других планировавшихся изменений Алгола, идеи Ван Вейнгаардена об “ортогональности” и практических соображений. Если убрать из Алгола вызов по имени - нужно найти какую-то замену. Нельзя просто постоянно копировать структуры, передающиеся по значению. Нужно добавить передачу аргументов по ссылке. По замыслу Хоара и Вирта ссылочные типы - рекорды и юнионы - и способ передачи параметров - это отдельные фичи, никак не связанные. Но “ортогональность” Ван Вейнгаардена, что-то вроде минимальности в сочетании с первоклассностью, требует все это делать с помощью одной фичи - первоклассных ссылок. Видимо, решение делать все типы ссылочными тоже соответствовало бы “ортогональному” подходу. Но во-первых, из практических соображений авторы Алгола не хотели размещать все в куче. Наоборот, хотели иметь возможность размещать все на стеке, хотя куча со сборкой мусора и была добавлена. Но если вы переживаете за прочие функциональные фичи после такого-то решения вопроса о ссылочных типах, то у вас есть все основания. Во-вторых, это не устроило бы самих Хоара с Виртом. Рекорды Хоара должны были быть единственным ссылочным типом, передача параметра по ссылке должна была быть отдельной фичей. Хоар с Виртом были против “ортогональности” в принципе и называли свой подход “диагональным”. Не повезло! Описывать все это в репорте хотел только Ван Вейнгаарден. Вернее, руководить своими студентами и аспирантами, которые описывают. Так что все больше и больше в Algol X было таким, как хотел Ван Вейнгаарден. Если кто-то предлагал какую-то фичу, которую Ван Вейнгаарден видеть в Алголе не хотел, он жаловался, что описание языка - тяжелый труд, что добавлять это уже поздно, но если так уж нужно предлагающий может сам описывать синтаксис и семантику. Изменения, которые Ван Вейнгаардену нравились он, конечно же, вносил быстро и с удовольствием. “Кто первый подготовит заготовку для репорта - тот и выиграл” - неверный урок из истории ALGOL 60. Выиграл тот, кто хотел делать скучную работу по редактированию репорта. Просто в случае ALGOL 60 это был один и тот же человек - Петер Наур.
Время шло. Ван Вейнгаарден все не дописывал кандидат-репорт до конца. Партия минимального изменения потеряла темп и Алгол догнали авторы серьезных изменений. Вирт потерял интерес к Algol X и имплементировал их совместное с Хоаром предложение как ALGOL W, который позднее развился в язык Pascal. Algol X же разовьется совсем не в Паскаль. Партия минимального изменения упустила свой шанс и проиграла. С осени 66-го года Ван Вейнгаарден работал над репортом один, без Вирта и Хоара. Началось время существенного изменения.
Погодите-ка, если Ван Вейнгаарден так усилился, пытался ли он использовать свой подход к описанию языков из того доклада про CPS, сделать в Алголе работающую рекурсию? Похоже что нет. Он усилился потому, что описывал язык не как рассказывал в том докладе. И сам, по всей видимости, потерял интерес к этому своему изобретению.
На заседании комитета в октябре 66-го в Варшаве МакКарти предложил ad-hoc перегрузку операторов. Но самое важное для нашей истории изменение там предложил Клаус Самельсон. Передачи параметров по ссылке достаточно для того, чтоб заменить вызов по имени для многих. Но, конечно, не для тех, кто использовал Устройство Йенсена. Сторонники “функционализации” попытались обменять свою частичную победу из ALGOL 60 на победу более полную. Самельсон предложил добавить в Algol X лямбды [Same65].
Вместо техники Йенсена применяющейся как здесь:
REAL PROCEDURE traps(a, b, n, f, x);
VALUE a, b, n; REAL a, b, f, x; INTEGER n;
BEGIN REAL s; INTEGER i;
x := a;
s := f/2;
FOR i := 1 STEP 1 UNTIL n - 1 DO
BEGIN x := x + (b - a)/n;
s := s + f END;
x := b;
traps := (b - a)/n * (s + f/2)
END
traps(0, 1, n, exp(sin(w * t)), t)
Теперь можно, наконец, писать нормальную ФВП:
REAL PROCEDURE traps(a, b, n, f);
VALUE a, b, n; REAL a, b; INTEGER n; REAL PROCEDURE f;
BEGIN REAL s; INTEGER i;
s := (f(a) + f(b))/2;
FOR i := 1 STEP 1 UNTIL n - 1 DO
s := s + f(a + i * (b - a)/2);
traps := (b - a)/n * s
END
traps(0, 1, n, REAL t : exp(sin(w * t)))
В предложении самельсона 65-го года лямбды точно не изобретены независимо, ЛИ упоминается явно. Но типы функций все еще неполные, как в ALGOL 60. И, конечно, из принципа “ортогональности” следует, что в языке должны быть лямбды и функциональные переменные, раз уж в нем есть функциональные параметры. К следующему заседанию комитета в Зандворте в мае 67-го Ван Вейнгаарден включил и перегрузку и лямбды в язык.
Для того, чтоб эти нововведения больше походили на Алгол, в Algol X добавили фичу, которой критики Алгола будут особенно недовольны - неявные приведения типов. Например, ссылки приводятся к значениям, а значения к лямбдам. Алголисты так вошли во вкус, что вместо специальных скобок добавили приведение элемента массива к массиву из одного элемента, для того чтоб (1)
мог быть таким же нормальным литералом для массива как ()
и (1,2)
. Как и МакКартиевские перегрузки операторов и структурные типы, это делало вывод типов в Алголе не особенно реалистичным, хотя этого, вроде бы, никто и не собирался делать.
Сходство с ALGOL 60, впрочем, уже перестало интересовать алголистов. Даже основной автор ALGOL 60 написал предложение [Naur64] о том, что функция должна возвращать значение последнего блока, а никаких присваиваний к одноименной переменной быть не должно. Алголисты сделали следующий логичный шаг и сделали Новый Алгол языком выражений, это была одна из первых идей Ван Вейнгаардена о том, каким должен быть новый Алгол. Еще 64-го года.
Наур сделал еще одно популярное предложение [Naur66] от октября 65-го о том, что аннотации типов и способов передачи аргумента
PROCEDURE P(a, b, c); VALUE b; INTEGER a, b; REAL b;
должны быть в списке параметров:
PROCEDURE P(INTEGER a, INTEGER VALUE b, REAL c);
как, например, в CPL. По крайней мере, CPL в начале 66-го года [Stra66b]. Но еще в 63-ем в CPL псевдокоде [Barr63] типы уже были в списке параметров, хотя способы передачи параметров декларировались отдельно:
ROUTINE Work [REAL a, b, c, INDEX d, LABEL e]
VALUE a, e; REF c; SUBST b, d
Это нововведение приняла даже партия минимального изменения Алгола.
Синтаксис ALGOL 60 со множеством длинных ключевых слов подсократили и облегчили. Но легкость синтаксиса не навязывалась. У конструкций сделали и легкие и тяжелые синтаксические формы.
Наконец, в феврале 68 Ван Вейнгаарден распространил черновик репорта, который дошел даже до комитетчиков, которые старались не знать как там идут дела с описанием нового Алгола. В ужасе, они увидели там вместо Алгола язык, на котором можно писать что-то такое:
MODE LIST = REF NODE;
STRUCT NODE = (INT head, LIST tail);
OP >> = (INT x, LIST xs)LIST: HEAP NODE := (x,xs);
PRIO >> = 9;
LIST nil = NIL;
PROC map = (PROC(INT)INT f, LIST xs)LIST:
(xs IS NIL | NIL | f(head OF xs) >> map(f,tail OF xs));
INT y = 1;
map((INT x)INT: x + y, 1 >> (2 >> (3 >> nil)))
Удивительно, но язык, похожий на ALGOL 60 примерно в той же степени, что и CPL, все равно назвали Алгол. Хотя, после различий между SASL и SASL, такое уже не должно удивлять. По крайней мере авторы языка сами дописали год к названию, получив ALGOL 68.
Конечно, бросается в глаза отсутствие параметризованных типов, но в остальном язык выглядит скорее как очередной модный и молодежный язык с элементами ФП начала двадцать первого века. Так что, не смотря на то, что долгое время Паскаль от партии ограниченной модификации Алгола был популярнее, понятно кто в конечном итоге оказался на верной стороне истории. И рассказ о параметризованных типах еще впереди, алголисты продвинулись тут значительно дальше смутных идей авторов CPL.
Да, функции всегда определяются с помощью лямбд. Ортогональность! Обратите внимание на то, что все объявления по умолчанию - это объявления констант. У обоекембриджских языков, как мы помним, с этим не все хорошо, это больше похоже на будущие эдинбургские языки. Функции не обязаны быть константами, можно объявить и мутабельную ссылку на функцию. В некоторых случаях даже нужно. Например, чтоб разбить взаимную рекурсию в том случае, если компилятор ALGOL 68 её не поддерживает. И компиляторы ALGOL 68 что-то не поддерживают не так и редко.
Думаем, уже из этого примера понятно, почему авторам первого компилятора Пролога и первого компилятора ML нравился ALGOL 68. Понятно, почему алголисты считают, что ML и даже эдинбургские ФЯ вообще произошли от ALGOL 68. Эта гипотеза выглядит очень правдоподобной. Установить происхождение ML от CPL можно только читая малодоступный обоекембриджский самиздат и подмечая какие-то необычные детали. И по воспоминаниям участников Эдинбургской Программы.
Понятны и претензии обоекембриджцев на влияние и даже происхождение ALGOL 68 от CPL. Какой из этих языков повлиял на какой и как - не так просто определить. Особенно потому, что участие Ландина в проектировании этих языков не особенно хорошо документировано.
Известно, что Ландин был в “ближнем круге” ван Вейнгаардена вместе с Ёнэдой, командой имплементаторов Герхарда Гооса (Gerhard Goos) из Мюнхена и другой командой имплементаторов из Брюсселя и получал черновики репорта еще в 67-ом. Если учесть такой-то состав “ближнего круга”, а также то, что Гоос работал в одном университете с Самельсоном и Бауэром, неожиданная метаморфоза Алгола уже не выглядит такой уж неожиданной.
Но странно, что при всем при этом одно важное обстоятельство вскрылось только в Июне 1968 на встрече комитета в Тиррении. Там Ландин продемонстрировал, что функциональное программирование на ALGOL 68, каким его планировали сделать на тот момент, невозможно:
PROC curryplus = (REAL u)PROC(REAL)REAL: (REAL v)REAL: u+v;
PROC(REAL)REAL addthree = curryplus (3);
addthree(5)
возвращаемая из функции curryplus
функция addthree
ссылается на окружение, время жизни которого закончилось вместе с этим возвращением. Лямбды и тут неправильные. И алголисты не готовы к типам, значения которых не могут лежать на стеке. Поэтому возвращение функций, ссылающихся на окружение из которого они возвращаются, было запрещено. Так что МакКарти поучаствовал в создании как минимум двух языков с, по крайней мере, двумя разновидностями неправильных лямбд.
Но все эти функциональные проблемы затмила реакция комитетчиков не из “ближнего круга”. Интересно, что еще сильнее чем язык, комитетчиков возмутил способ его описания. Хотя Ван Вейнгаарден получил в свое время контроль над репортом потому, что комитетчики хотели, чтоб новый Алгол описывали именно так.
После распространения черновика весной 68-го из комитета ушли Наур и Вирт, еще несколько комитетчиков, включая Хоара и Дейкстру, доработали в комитете до завершения первой редакции, но написали особое мнение [Dijk70] в котором назвали репорт слишком большим, а язык устаревшим.
Почему они считали его устаревшим? Это неочевидно из самого особого мнения. Но если учесть хорошее отношение Хоара к SIMULA и прочие работы и идеи подписантов особого мнения, то можно предположить, что из-за отсутствия каких-то конструкций для группировки функций. Вроде классов и, в недалеком будущем, модулей и АТД. По крайней мере, отсутствие модулей будет одной из основных проблем, которые авторы ALGOL 68 будут решать в 70-е.
Действительно ли репорт большой? Ну, даже если не считать все цитаты и одну иллюстрацию из «Винни-Пуха» Милна, зачем-то включенных в репорт, ALGOL 68 больше, чем SML 97. Но меньше, чем Haskell 98. Правда, в последствии репорт стал больше, проектирование языка не закончено в 60-е годы. Комитет планировал выпустить репорт и дать время имплементаторам его использовать. А после, уже с участием имплементаторов, написать окончательный репорт, к которому можно только добавлять фичи. Но не убирать и менять фичи из “окончательного” репорта. С момента выбора названия они опасались, что в 68-ом году он не будет готов. Так и вышло, первое издание репорта [Wijn69] было закончено в 1969.
Более десяти тысяч успешных компиляций было выполнено вычислительной службой Королевского радарного института.
Филип Вудвард, Практический опыт ALGOL 68 [Wood72]
Конференция пытающихся имплементировать Алгол 68 состоялась в июле следующего, 1970 года в Мюнхене. Над имплементацией работали еще во время написания репорта. Две команды имплементаторов из Мюнхена и Брюсселя, как мы помним, пристально следили за работой Ван Вейнгаардена. Не дожидаясь очередных сюрпризов на плановых заседаниях комитета на одном из очередных морских или лыжных курортов. Но, неожиданно для всех участников [Lind93] [Bond2001], гонку выиграли не они.
Первый компилятор написала команда имплементаторов из Королевского Института Радиолокации в Молверне: Иан Карри (Ian F. Currie), Сьюзен Бонд (Susan G. Bond) и Джон Морисон (John D. Morison). Перед докладом [Curr70] выступил с краткой историей проекта их руководитель, начальник отдела Филип Вудвард. Тот самый Вудвард, который рассказывал на летней школе 63-го года по “нечисленным вычислениям” [Fox66] про CPL и диалект ALGOL 60, который произвел впечатление на Поплстоуна [Popplestone].
В 68-ом году в Радиолокационном институте начался переход на новые машины. И хотелось еще более современный и выразительный язык, чем Алгол 60. В 63-ем Вудвард с энтузиазмом ожидал CPL, который, по его рассказам, должен был “объединить все полезные средства LISP и ALGOL, да еще и добавить новых”. В 68-ом было уже ясно, что разработка CPL провалилась. Но не время впадать в отчаянье. Как раз заканчивают описание почти такого же амбициозного языка - ALGOL 68. Успеют ли в институте имплементировать компилятор ALGOL 68 за два года, до полного перехода на новые машины, или лучше снова имплементировать ALGOL 60? (Портировать тот компилятор, про который Вудвард рассказывал в 63-ем невозможно - он написан на ассемблере). Решение было сложным. Но если говорить о выборе языка, на который они хотели бы перейти с Алгола 60, то тут выбора (уже?) не было.
В результате Вудвард и др. решили, что напишут новый компилятор примерно за год. И писали его с января 69-го. Компилятор написан на расширенном подмножестве Алгол 60, который им все же пришлось имплементировать для новой машины, ICL 1907F. Для нее еще не было ничего, кроме посредственного компилятора Фортрана. В апреле 70-го компилятор подмножества Алгола 68 уже был готов для пользователей в институте, в октябре 70-го запланирован полный переход на новые компьютеры.
Да, имплементировано только подмножество Алгола 68, которое разработчики из Молверна называют ALGOL 68-R. Но это не частичная имплементация из тех что были для CPL, когда почти ничего не было имплементировано. В данном случае было имплементировано почти все. Изменения в языке нужны для того, чтоб код можно было компилировать в один проход. Мы практики, объясняет Вудвард, для нас важна высокая скорость компиляции. Для этого пришлось пожертвовать несколькими фичами вроде автоматического преобразования обычных обращений по имени в лямбды и сделать декларации перед использованием обязательными. Такое объявление типов - расширение языка:
MODE X;
MODE Y = STRUCT (INT a, REF X x);
MODE X = STRUCT (REAL r, REF Y y)
Взаимная рекурсия двух функций разбивается уже имеющимися средствами:
PROC p1,q1;
PROC p = VOID: ( ... ;q1; ... );
PROC q = VOID: ( ... ;p1; ... );
p1 := p; q1 := q; ...
Что, конечно, было бы не особенно приятно делать в ФЯ.
Для компиляции нужно по крайней мере 96Кб (32K 24-бит. слов). Из них 60Кб - код самого компилятора. Разработчики считают, что это довольно много.
Можно использовать рантайм с компактифицирующим сборщиком мусора, но для желающих писать программы не использующие кучу - всего оверхеда связанного со сборкой мусора можно избежать. Производительность генерируемого кода для авторов тоже важна.
Возможно, Стрейчи и др. все-таки стоило доделать описание CPL. Может быть, в Радиолокационном Институте имплементировали бы какое-то более интересное его подмножество, чем имплементировал Ричардс.
Но Вудвард, конечно, не единственный и даже не первый герой нашей истории, безрезультатно ожидавший CPL. CPL ждал еще один наш старый знакомый - Морис Уилкс. И, конечно же, в кембриджской компьютерной лаборатории под его началом тоже обнаружили новый язык с похожими на CPL возможностями.
В 1970, в том году, когда первый компилятор Алгола 68 уже передали первым пользователям, более известный другими своими проектами Стивен Борн (Stephen Richard Bourne) и Майкл Гай (Michael J. T. Guy) начали имплементацию языка под названием Z70 [Birr77]. Z70 был языком выражений для калькулятора с двумя типами: целыми числами и функциями, небольшим приблизительным подмножеством ALGOL 68. Z70 развивался как ISWIM, от минимального языка выражений, до языка совсем не минимального. Но если ISWIM долгое время рос только на бумаге, то Z70 рос как имплементация. Да еще и на том компьютере, для которого в свое время делали CPL. Имплементирован на том самом Titan с помощью компилятора компиляторов PSYCO. Можно ли имплементировать Z70, который еще больше? Раз за разом оказывалось, что можно! Шло время, и Z70 включал все больше фич Алгола 68, а все большую часть компилятора Z70 писали на Z70. К середине 72-го года Z70 был все еще подмножеством, но уже узнаваемым диалектом Алгола 68 и его переименовали в Algol68C.
В том же 72-ом году было принято решение писать портабельный компилятор Алгол 68, написанный на Алгол 68. Гай в этом проекте принимает уже не особенно активное участие. Первая версия была написана на подмножестве языка,доступном имплементации бывшего Z70. Портируемость достигалась использованием виртуальной машины ZCODE, как достигал BCPL, транслирующийся в OCODE.
Этот компилятор не был однопроходным, стадий компиляции у него было больше, чем у типичного компилятора Лиспа, но не так много, как у RABBIT, конечно. Компилятор в код ВМ компилировал в три стадии: парсер, тайпчекер и генератор ZCODE. ZCODE транслируется в нативный код нужной машины в один проход.
Компилятор, к сожалению, реализовывал гораздо менее интересное для нашей истории подмножество Алгола 68: только не требующую сборки мусора часть языка.
В сентябре 72 Борн начал переписывать парсер на Алгол 68. С января 73-го новый участник проекта - аспирант Эндрю Биррелл (Andrew Birrell) - пишет тайпчекер и работает над системой раздельной компиляции. К июню 73-го парсер и тайпчекер работают достаточно хорошо, чтоб распарсить и тайпчекнуть себя. Биррелл, еще один аспирант Уолкер (I. Walker) и научный сотрудник Эндрюс (A. Andrews) на основе кодогенератора Z70 пишут новый кодогенератор и транслятор ZCODE для Titan. Наконец, в конце сентября 1973 кембриджский компилятор Алгола 68 благополучно компилирует себя на Titan, за неделю до окончательного прекращения его работы.
Ура? Мечта Мориса Уилкса и других кембриджцев сбылась? По крайней мере, в урезанном виде. После апгрейда машины с увеличением памяти в четыре раза [Lavi12]. И слишком поздно. Да, это не воплощение самых смелых фантазий о CPL, не ML и даже не ФЯ вовсе. Но демонстрирует, что BCPL-пораженчество не было единственным практически возможным результатом обоекембриджской программы. Практичный неФЯ мог бы быть в большей степени ФЯ, чем сделал Ричардс.
Но мы уже выяснили, что компилятор полноценного ФЯ был неосуществим в то время и на том железе, которое было в распоряжении имплементаторов CPL. Но что насчет 70-х и более нового железа? CPL на рубеже 60-х и 70-х мертв, а ALGOL 68 пока еще нет. Dum spiro spero! Сегодня нам известны мейнстримные языки, такие как Java и С#, в которых изначально не было полноценных лямбд и параметризованных типов, но со временем они были добавлены. Могли ли их добавить в ALGOL 68?
Такие пожелания появились практически сразу же после конференции имплементаторов. Как оказалось, имплементировать Алгол 68 не так сложно, как ожидали его авторы. Так почему-бы не сделать язык еще более амбициозным? Тем более, что партия минимальной модификации и все сочувствующие окончательно разгромлены.
Первое заседание комитета, который готовил второй, окончательный репорт состоялось через неделю после конференции имплементаторов, в июле 1970, в Абе-ла-Нёв [Lind93]. На нем Ханс Бекич (Hans Bekić) начал свою компанию за полноценные лямбды в Алголе 68. Но не стоит удивляться, что в начале 70-х кто-то, о ком мы пока еще не писали, вдруг озаботился возможностью возвращать функции из функций. Ханс Бекич работал с Ландином в колледже королевы Марии в 1968–69 [Jone99]. Работал в Венской лаборатории IBM, цитировал [Beki84] Бурсталла, разрабатывал META IV в 1973-75 вместе с Джонсом [Jone78]. И таким образом был частью той социальной сети вокруг Берджа и Йорктаун Хайтс. Тех узких кругов, в которых возвращение функций из функций было хорошо известно.
Проблема в том, что полноценные лямбды, размещающие замыкание в куче, а не на стеке делают сборку мусора гораздо важнее. Авторы Алгола 68 предпочитают его видеть в первую очередь стековым языком, имплементация которого может и не включать сборщик мусора. Так что у таких идей найдутся противники как среди старого поколения комитетчиков, которые в свое время не пропустили ссылочные типы Хоара, так и среди новых комитетчиков-имплементаторов.
Но, как мы уже выяснили, идеи о возвращении функций стали занимать умы как раз в это время. От возвращения функций в 70-м отмахнуться уже чуть сложнее, чем в 68-м. Поэтому против полноценных лямбд выдвинули два контрпропозала. Что посчитали главной проблемой полноценных лямбд Бекича [ALGOL72b]? То, что компилятор должен как-то отличать лямбды, замыкание которых нужно размещать в куче от тех, которым требуется только дисплей и стек. И все объявления процедур в Алголе 68 лямбды, так что отличать придется часто. Компилятор Стила будет решать эту проблему в конце 70-х. И не то чтобы комитетчики опасались, что не смогут решить её в начале 70-х, но считали, что это может заметно замедлить и усложнить компиляцию. Вполне разумное предположение, если учесть системные требования RABBIT. При этом комитетчики считают, что если задача будет решена и замыкания на стеке будут создавать только функции, которым это необходимо, то не придется платить за это тем, кто таких функций не пишет. Так что оба контрпропозала были направлены на облегчение решения именно этой задачи.
Первый контрпропозал - окружения Бома. Хендрик Бом (Hendrik Boom) предложил явно обозначать область видимости замыкания аннотацией glob
:
MODE M = PROC(REAL)REAL;
OP O = (GLOB M f, g)M: (REAL X)REAL f(g(x));
M cs = cos O sin;
Второй контрпропозал - частичная параметризация. Сначала так называли декларацию каррированных функций. Вместо многословия нескольких лямбд (X x)PROC (X)X:(X y)X: x+y
просто (X x)(X y)X: x+y
.
MODE F = PROC(REAL)REAL;
PROC compose = (F f)(F g)(REAL x)REAL: f(g(x));
F g = compose(cos)(sin);
F h = g(1.0);
Окружения каррированных функций отправляются в кучу.
В обоих случаях компилятору очень легко определить где размещать окружения.
Но, конечно, без параметризованных типов функциональное программирование хорошо не пойдет. И как раз такой пропозал выдвинул Радиолокационный Институт весной 71-го [ALGOL71]. Типы в Алголе 68 называются “modes” и эта фича получила название “modals”.
В ранней версии сами типы еще не были параметризованы. Предлагалось объявлять абстрактные типы без правой части и потом работать с ссылками на этот тип:
MODE X;
PROC sort = (REF[] X vec, PROC(REF X, REF X)BOOL cmp): ...
Фича демонстрируется на ФВП:
MODE X, LIST = STRUCT(REF X value, REF LIST next);
PROC apply = (REF LIST l, PROC(REF X) f):
(REF LIST ll := l; REF LIST empty = nil;
WHILE ll ISNT empty DO (f(value OF ll); ll := next OF ll)
);
Тут MODE X, LIST = ...
не какой-то новый синтаксис, это просто краткая запись для MODE X; MODE LIST = ...
В обобщенном коде со значениями такого абстрактного синонима, конечно, ничего нельзя сделать, можно только оперировать ссылками на него. Иначе такой код не скомпилировать отдельно от используемых потом конкретных типов. Обычная идея об “универсальном представлении”.
MODE INTLIST = STRUCT(REF INT value, REF INTLIST next),
BOOLIST = STRUCT(REF BOOL value, REF BOOLIST next);
INTLIST il := ...; BOOLIST bl := ...;
apply(il, (REF INT i): (i := -i));
apply(bl, (REF BOOL b): (b := NOT b));
Совсем не удивительно, что разработчики кембриджской имплементации без сборщика мусора стали противниками [Bour72] этого предложения разработчиков имплементации со сборщиком мусора. Кембриджцы обеспокоены сложностью имплементации и считают, что полагаться на работоспособность универсального представления на компьютерах будущего опасно: в будущем ссылка на значение одного типа может отличаться от ссылки на значение другого.
Пропозал развивался и в 72-ом году “modal”, который называют также и “explicit free mode” становится параметром [ALGOL72b]:
PROC sort = (MODE X, REF[]X a, PROC(REF X, REF X)BOOL swap)VOID: ...
параметризованная типом процедура используется так:
sort(INT, row, swap)
и раскрывается в
(MODE X = INT, REF[]X r = row, PROC(REF X, REF X)BOOL swp = swap ... sort(r,swp))
планируют разрешить частичную параметризацию для аргументов-типов такого вида:
PROC gensort = (MODE X)(REF[]X a, ...
PROC(REF[]INT, ...) intsort = gensort(INT);
даже если соответствующий пропозал для обычных параметров не пройдет. Теперь можно параметризовать и типы-синонимы:
MODAL LIST = (MODE Y) STRUCT(REF Y a, REF LIST(Y) next);
с похожим на лямбду синтаксисом. “Modals” называют функциями времени компиляции.
Появилось и предложение по улучшению конструкции для разбора композитных типов
[ALGOL72] [ALGOL72b]:
CASE xs IN
(NULL): ... ,
(CONS l): ... l ...
ESAC
Позже, чем похожая конструкция описана у Бурсталла [Burs69] (1968). Но раньше, чем похожая конструкция описанная Хоаром [Hoar75], ссылающимся на Бурсталла (октябрь 73). Раньше, чем похожая конструкция появилась в неисполняемом языке описания спецификаций META IV [Jone78] над которым работал Бекич (1973-75). Раньше, чем такая конструкция появилась в описании SCRATCHPAD [Jenk74] (апрель 74). Конструкция могла быть предложена МакКарти или Ландином. МакКарти слушал доклад Бурсталла [Burs69], но мы не видели, чтоб МакКарти когда-нибудь использовал её. И ни Ландин, ни МакКарти не принимали активного участия в доделывании Алгола 68. Может быть, конструкция была предложена Хоаром, который писал о ней позже и уже мог знать в это время. Но не принимал участия в доделывании Алгола 68 вовсе. Но скорее всего, если кто и предложил её - то Бекич, который на следующий год спроектировал язык с такой конструкцией и мог познакомится с ней в Йорктаун Хайтс. Или увидеть в статье Ландина и Бурсталла, в которой она вероятно использовалась и на которую Бекич ссылается в своих работах [Beki84]. “Вероятно” тут потому, что статья не отсканирована, и судить о её содержании можно только по косвенным признакам. Не исключено, даже не смотря на все это столпотворение людей, которые её видели, что конструкция была изобретена независимо.
Разумеется, у конструкции для разбора юнионов была и версия с облегченным синтаксисом, так что map
на Алгол 68 теперь можно написать так:
PROC map = (PROC(INT)INT f, LIST xs)LIST: (xs|
(NULL): null,
(CONS l): f(h OF l) >> map(f,t OF l));
К сожалению, “паттерн матчинг” одноуровневый, как в трактовке идеи Бурсталла Хоаром. И в системе МакКарти это еще хуже, чем в случае сумм произведений как у Хоара. Там хотя-бы можно связать хвост и голову в паттерне, а не доставать их из рекорда селекторами.
Итак, в 1972 Алгол 68 был близок к тому, чтоб стать “Эдинбургским ФЯ” до появления Эдинбургских ФЯ. Да, типы данных - не суммы произведений. Да, не ссылочные типы с понятными последствиями для полиморфизма. Но это детали и провести границу было бы труднее. Было бы. Такой Алгол 68 не состоялся.
На очередном заседании комитета в апреле 72 в Фонтенбло по всем требующим сборку мусора ФП планам был нанесен сокрушительный удар [ALGOL72a]. Параметризованные типы отклонили. Ареол первых имплементаторов Алгола 68 не помог. Отклонили и полноценные лямбды Бекича и обе их альтернативы. Линдси вспоминает, что полноценные лямбды вполне могли быть приняты. Но не были. Активность Ханса Бекича по отстаиванию полноценных лямбд произвела впечатление на его коллег по алголостроению и хорошо им запомнилась, если судить по его некрологу [Lind83]. Но её не было достаточно для победы.
Конструкции для разбора юнионов, правда, пережили разгром в Фонтенбло в неопределенном статусе и была принята в сентябре 72 в Вене [ALGOL73]. Так что имплементация map
из примера выше работает. В сентябре 1973 в Лос-Анджелесе был принят окончательный ALGOL 68 репорт [Wijn77]. Опубликован в марте 75-го.
Окончательность репорта, правда, не означала полного краха функционализации Алгола 68. Из него больше ничего нельзя убрать или существенно переделать, но можно добавить. Это даже проще, процесс для принятия расширений Алгола 68 не такой сложный. Конечно, лямбды Бекича уже не принять: нельзя менять как работают лямбды. Но частичная параметризация и параметризованные типы становятся такими пропозалами расширений.
Параметризованным типам новый процесс принятия не особенно помог. Алголистам не особенно нравилось, что можно работать только со ссылками на типы-параметры. Хотя это ограничение было с самого начала, со временем эта ссылочная версия “модов” стала называться “ограниченными модами”. Но и добавлять гипотетические неограниченные “моды”, то есть фичу, которая не позволяет раздельную компиляцию они тоже не хотели [Lind74b].
Но надежду решить более сложную проблему фунарга алголисты не потеряли. Частичная параметризация была переработана [Lind74a]. Это больше не способ объявлять каррированные лямбды. Фича теперь работает в месте применения, никак не влияя на объявление:
PROC compose = (PROC(REAL)REAL f, g, REAL x)REAL: f(g(x));
PROC (REAL)REAL cossin = compose(cos,sin, )
И прямо совместима с окончательным Алголом 68. Все лямбды продолжают компилироваться как раньше, частичное применение должно копировать данные со стека в кучу и создавать функцию, которая перед вызовом частично примененной функции вернет все что нужно из кучи на стек. Это, конечно, менее эффективно для пользователей, передающих функции вверх, но передающий их только вниз не платит за возможность передавать вверх.
В январе 75-го в Бостоне был собран специальный подкомитет по частичной параметризации из Бекича, Линдси и еще нескольких имплементаторов. В августе 75-го в Мюнхене расширение было принято. Описание добавлений в репорт опубликовали [Lind76] в начале 76-го. Алгол 68 все-таки стал, хотя-бы на бумаге, неудобным ФЯ без параметрических типов. Чем-то вроде POP-2, но на десяток лет позже.
Еще одно принятое по новому процессу расширение - модули. И в компилятор Радиолокационного института [Bond77] и в Кембриджский компилятор [Birr77] добавили модули довольно рано, для поддержки раздельной компиляции. В радиолокационном Алголе они были даже параметризованными. Стандартные модули начали обсуждать в Фонтенбло в 72, рассмотрели несколько пропозалов [Schu74] [Curr76] [Lind76b] [ALGOL78]. Некоторые идеи оттуда могли повлиять на работу с пространствами имен в ФЯ. Например оттуда в OCaml могла попасть квалификация для выражения вида Module.(name1 + name2)
. Но это могло быть и изобретено независимо. Модули приняли в Яблонне в августе 78 [Lind78], на чем процесс расширения Алгола 68 и закончился.
Комитет, пишущий Алгол-репорт - так называемая “Рабочая Группа 2.1 IFIP по алгоритмическим языкам и исчислениям” - существует и сегодня [IFIP21], но, к сожалению, больше не выпускал никаких Алголов. В этом комитете побывали Дэвид Тернер, Конор МакБрайд, Олег Киселев, Ричард Берд и многие другие выдающиеся мыслители. Так что больно думать о том, какие потенциальные Алголы мы потеряли.
Но не все смирились с гибелью мечты о функциональном алголе 72-го года. Бауэр и Самельсон в том же роковом году решили делать в Мюнхене свой функциональный Алгол [Baue76].
Конечно же, функции в Алгол 77 могут возвращать функции [Baue81] и в нем даже планировались аннотации ленивости [Baue79c]. С 73-го года этот язык стал языком CIP-L (Computer-aided Intuition-guided Programming) для системы трансформации CIP-S как S-0/NPL/Hope был в Эдинбурге и Лондоне языком для систем Дарлингтона и Фезера. CIP-L не синоним Алгола 77, в Мюнхене делали еще и ФП-Паскаль. Более-менее активная работа началась только в 75.
Чтоб направлять и проверять трансформации в Алгол добавили параметризованные АТД со спецификациями как в CLEAR Бурсталла и Гогена. Можно сказать, что проект и был повторением работ Бурсталла и других с той разницей, что вместо CPL/ISWIM был ALGOL 68. Это не было какой-то конвергенцией с переизобретением того же самого независимо. Бауэр и другие явно ссылаются на работы Дарлингтона по трансформации кода и работы Бурсталла, Циллеса, Гогена, Гуттага над языками спецификации [Baue76] [Baue79b].
TYPE Stack ≡ (MODE U) STACK U, nullstack, isnull, top, rest, append:
mode STACK U,
FUNCT STACK U nullstack,
FUNCT(STACK U) bool isnull,
FUNCT(STACK U s : ¬ isnull(s)) U top,
FUNCT(STACK U s : ¬ isnull(s)) STACK U rest,
FUNCT(STACK U, U) STACK U append,
LAW A : ¬ isnull(s) => append(rest(s), top(s)) = s,
LAW R : rest(append(s,x)) = s,
LAW T : top(append(s,x)) = x,
LAW E : isnull(nullstack),
LAW NE: ¬ isnull(append(s,x)) ENDOFTYPE
не смотря на очевидно исполняемый пример спецификации [Baue79], имплементации у Бауэра не отмирают, их нужно писать
[ MODE STACK U ≡ EMPTY | (STACK U trunk, U item),
FUNCT nullstack ≡ STACK U: ()
FUNCT isnull ≡ (STACK U s) BOOL: s = nullstack,
FUNCT top ≡ (STACK U s : ¬ isnull(s)) U: item OF s,
FUNCT rest ≡ (STACK U s : ¬ isnull(s)) STACK U: trunk OF s,
FUNCT append ≡ (STACK U s, U s) STACK U: (s, x) ]
Обратите внимание на то, что типы должны быть ссылочными, разрешена рекурсия без явных ссылок. Облегченный синтаксис для декларации типов данных, но первоначально была еще система МакКарти, со ссылкой на него. Но вскоре ей на смену пришли суммы произведений как у Хоара, со ссылкой на него [Baue81]
MODE LIST ≡ ATOMIC{nil} | cons(M head, LIST tail)
Если эдинбургские коллеги Бауэра в конце концов пришли к языку в котором и типы и модули параметризованы, то Алгол 77 начал как язык в котором и типы (modes) и модули (types) параметризованы, но стал языком, в котором параметризованы только модули.
Не смотря на цитирование Бурсталла, добавление в язык АлгТД и уравнений для спецификаций, никакого паттерн-матчинга, только многоветочный if
IF l IS cons THEN ... head OF l ... tail OF l
| l IS nil THEN ...
Но Бауэр позаимствовал выражение where
.
Разумеется, все это богатство ссылочных фич Алгола 77 не было имплементировано в 70-е [Part84]. А что было имплементировано? И что можно было использовать для имплементации эдинбургских ФЯ?
Параметризованные типы (ну или “моды”) были имплементированы в диалекте Алгола 68, называющемся Mary [Conr74]. Но Mary это не более свободный в использовании кучи язык, а наоборот более низкоуровневый и системный, с более явными ссылками. И параметризованные типы там жертвуют раздельной компиляцией, а не принимают универсальное представление:
MODE LIST(M) = (REF LIST(M) NEXT, M VALUE);
LIST(INT) LI := (NIL,2);
LIST(REAL) LR := (LR,5.0);
по крайней мере некоторые их разновидности. Это имплементировано не позднее лета 1974.
С поддержкой “ссылочных” фич в имплементациях Алгола 68 же все обстояло не особенно хорошо. Предложение Радиолокационного института про параметрические типы не было имплементировано даже Радиолокационным Интститутом и никто не имплементировал в 70-е годы частичную параметризацию [Lind93]. Сегодня имплементация, в которой это расширение поддерживается существует - Algol68g - но имплементировано оно неправильно.
Первоначальный Алгол 68 69-го года никогда не был полностью имплементирован. Но окончательный Алгол 68 75-го года был имплементирован один раз полностью. Но большинство имплементаций было частичными. Конечно, окончательный Алгол 68 не стал ФЯ, но если есть хорошо работающий сборщик мусора и хороший генератор кода для актуальных платформ, то можно использовать все это для имплементации ФЯ эдинбургской программы. Но есть ли все это?
Начинались работы над многими имплементациями Алгола 68 [Hunt77], но только две более-менее законченные имплементации обеспечивали хорошую производительность генерируемого кода. Уже знакомые нам компиляторы Радиолокационного Института и Кембриджа [Curr70] [Wich76] [Hunt77] [ALGOL80]. Например, Algol68C может быть быстрее BCPL. Получается, что сколько не жертвуй высокоуровневыми фичами, при плохом генераторе кода от этого много толку не будет.
Радиолокационный институт в 76-ом году начал работы над ALGOL 68RS - портируемым компилятором с ВМ, написанным на подмножестве Алгола 68. Летом 77-го года он уже работал [Bond77]. Бутстрап был, разумеется, осуществлен с помощью ALGOL 68-R. Но в Радиолокационном институте делали компиляторы для тех машин, которые использовали там [ALGOL81]. Разработчики ФЯ их не использовали. В 81-82 годах для ALGOL 68RS написали бэкенд для ненужного уже разработчикам ФЯ Multics. Только в начале 83-го года начались работы над генераторами кода для VAX и Motorola 68K [Finn83].
В Кембридже, компилятор Algol68C заработал на новом компьютере IBM370/165 в конце декабря 73-го. Борн руководил его разработкой до начала 75-го. В 77-ом компилятор - это программа из 18 тыс. строк на Алголе 68 [Birr77].
Следующей машиной после IBM370, на которой заработал компилятор была ICL 4130, та самая, которую использовали в Эдинбурге до PDP-10. По крайней мере в 77-ом он как-то работал. Когда в Эдинбурге эту машину уже не использовали.
Университет Эссекса портировал [Gard77] кембриджский Algol68C на PDP-10. Работы велись с осени 75 до весны 77. В лучших традициях таких проектов 70-х наработали за этот период только семь человеко-месяцев.
Транслятор из ZCODE в нативный написали на BCPL и переписали на Algol68C после бутстрапа.
Считают, что важно добиться того, чтоб код компилятора умещался в 35K слов (~140Кб), что удалось. И маленькая программа должна компилироваться секунд за шесть. Это не удалось, компилируется более 10 секунд.
Но в 77-ом году Algol68C еще считается недоделанным. Весной 77 первый релиз ожидают летом того же года, но релиз будет выпущен существенно позже. В январе 80-го [ALGOL80].
И к релизу сборщик мусора все еще не заработал. Сборщика мусора в Algol68C нет не потому, что так и задумано. Разработчики компилятора обещают его в версии 2, работы над которой уже идут. Но если бы кто-то начал работы над компилятором ФЯ, который использует Algol68C как бэкенд, то это было бы ошибкой. Сборщик мусора так никогда не доделают и версия 2 никогда не выйдет. Последней версией будет 1.3.
Algol68C работал на VAX c 81-го года [ALGOL81], и хотя его невозможно было использовать как бэкенд и рантайм для ФЯ, его использовали в 80-е для написания имплементации ФЯ.
Из требований Algol68C можно составить некоторое представление о том, сколько памяти в конце 70-х может позволить себе требовать компилятор. Которым кто-то будет пользоваться на “обычной” машине, а не собранной специально под программы требующие гигантскую память, как RABBIT.
Первый релиз Algol68C на IBM 360/370 требовал минимум 180Кб и 200-220Кб для более реалистичных программ. Версия для PDP-10 требует минимум в 70K слов (~280Кб). Описатели этих требований обычно избегают указания конкретных размеров таких “реалистичных программ”, но есть и исключения. В требованиях Algol68C для Telefunken TR440/TR445 говориться, что может потребоваться 50K слов (325Кб) для компиляции “больших” программ в 30 страниц [ALGOL80]. Судя по числу строк в распечатках программ на других языках, которые нам приходилось видеть до сих пор, это 1-2 тысячи строк. Надо полагать, что это для случая без раздельной компиляции. Потому, что компилятор они как-то компилируют.
Опциональность сборщика мусора в ALGOL 68-R/ALGOL 68RS и полное его отсутствие в Algol68C, а также успешное сопротивление фичам, которые сделали бы использование сборщика мусора более важным, от ссылочных типов и до полноценных лямбд, делает Алгол 68 не Java 70-х, которая при годилась бы для имплементации ФЯ, а чем-то вроде языка D 70-х. Разумеется, Алголом 68 и D список этих языков не исчерпывается. Даже Лисп, а точнее MacLISP пытался быть таким языком в 70-е. Тут мы имеем дело с интересным, но трагичным явлением, которое мы будем называть “язык семидесятых”. Трагизм явления не только в неудачах вроде Алгола 68, но и в успехах вроде C++.
Авторы “языка семидесятых” уже знакомы со многими фичами, которые можно себе позволить в языке со сборщиком мусора. Но обязательный сборщик мусора не могут себе позволить авторы языка. Так что одни фичи приобретают странные формы и особенности, а другие отсутствуют, но не из-за незнания о них. Они сознательно запрещены потому, что авторы недостаточно смелые и неразборчивые для того, чтоб добавить эту фичу в странной форме и с особенностями. Если, конечно, авторы недостаточно смелые.
Ограниченная или неправильная первоклассность функций в одних языках 70-х могла мотивировать запрет на первоклассность функций в других языках 70-х. Так, в требованиях к языку, которым позднее станет Ada, запрет на первоклассность функций прописан явно [IRON77].
Да, авторы некоторых языков не могут себе позволить сборщик мусора и сегодня. Но сегодня это скорее исключение. В 70-е это было правилом. И редкими исключениями были первые примеры того, что мы будем называть “язык восьмидесятых”. Давайте посмотрим, какие языки в 70-е решили полагаться на наличие сборщика мусора, сделать его обязательным. Поищем настоящую Java 70-х.
ФЯ всегда были “языками восьмидесятых”, хотя их неудачливые имплементаторы в 60-е этого и не знали. Но не все “языки восьмидесятых” - это ФЯ. Многие ФЯ в гораздо меньшей степени, чем Алгол 68. В том числе и из-за плохого отношения их авторов к Алголу 68 [Lisk93]. И плохое отношение к Алголу 68 довольно распространенный сентимент. Не беда, ФЯ можно сделать из любого “языка восьмидесятых” просто добавлением фронтенда.
Первым таким языком был, по всей видимости, еще один произошедший непосредственно от ALGOL 60 язык - SIMULA. Точнее поздние его версии с обязательным сборщиком мусора вроде SIMULA 67.
Для пользователей IBM 370 компилятор SIMULA 67 был готов в мае 72-го [Holm98]. Пользователи PDP-10 получили его позже. Но читатели, думаем, уже привыкли к тому, что это платформа второго сорта для всего, кроме Лиспа.
Компилятор SIMULA 67 для PDP-10 написали в Стокгольме. Он заработал в сентябре 74-го и первый публичный релиз состоялся только в январе 75-го. Да, в случае SIMULA 67 номер года в названии создает не намного менее обманчивое впечатление о том, когда его можно было использовать, чем номер года в названии ALGOL 68.
Для VAX компилятора SIMULA, правда, не будет. Но гипотетические имплементаторы ФЯ, которые могли бы решиться использовать компилятор SIMULA как основу их компилятора, этого еще не могли знать. Была проблема очевидная уже тогда - плохая производительность [Wich76] [Stro93].
Но SIMULA 67 все-таки была полезна для развития ФП и повлияла на историю. Своим отсутствием на VAX мотивировала автора первого компилятора ML писать компилятор ML.
Другой ранний “язык восьмидесятых” - это уже знакомый нам по истории абстрактных типов язык CLU. Но до 77 он не может быть альтернативой компиляции через Лисп, так как сам транслировался в MDL - Лисп для антипрологов Хьюита. И практичный компилятор, который по оценке авторов был готов для использования, появился только в 1980 на PDP-10 и “позднее” на VAX и Motorola 68K [Lisk93]. Опять слишком поздно.
С прочими “языками восьмидесятых”, которые начали делать в 70-х, на рубеже десятилетий все еще хуже. Наш будущий герой Дэвид Мэттьюз (David Charles James Matthews) в своей диссертации [Matt83] подготовил краткий обзор таких языков в начале 80-х. Russell - еще не имплементирован. Cedar Mesa - Mesa со сборщиком мусора, еще не имплементирован. Alphard - не имплементирован.
Да, транслировать эдинбургский ФЯ в Лисп - еще и не такая плохая идея. Все остальное готово в лучшем случае в той же степени или еще меньше. И похоже, что только трансляция в Алгол 68 производила бы код быстрее лиспового. Если б еще Algol 68RS был для имеющихся у разработчиков ФЯ машин. Если б только доделали вовремя сборщик мусора для Algol68C.
Все эти победы (отсутствия) сборщика мусора над Бекичем, “языками восьмидесятых” и ФП, конечно, не должны быть ни для кого сюрпризом. Ны уже ознакомились с плачевной ситуацией с памятью в то время. Сегодня мы знаем, что со временем сборщики мусора начали работать нормально, хотя и не без проблем. Но видно ли из 1980-го года хотя-бы отблески этого счастливого времени где-то в далеком будущем? Конечно, к описываемому времени сборщики мусора как-то существуют уже два десятка лет. Но мы уже видели, каким жалким и условным бывает такое существование на примере лямбд. Символично, что после ухода из проекта Борна, руководить разработкой Algol68C стал сапожник без сапог, более известный как раз статьей о сборщике мусора - Крис Чейни (Chris J. Cheney). Давайте выясним, насколько плохо шла сборка мусора, когда функционального программирования не было.
Как только мы определились со сборкой мусора, ее реальную реализацию можно было отложить, потому что мы писали только игрушечные программы.
Джон МакКарти, История Лиспа. [McCa78]
Первые лисперы не хотели управлять памятью вручную, но выбирали между счетчиками ссылок и сборщиком мусора. Выбрали сборщик потому, что хотели, чтоб cons
-ячейка умещалась в одно слово. На IBM 704 это 36 бит. Адрес - 15 бит. Две группы по три бита - слишком мало для счетчика [McCa78]. Довольно удачное стечение обстоятельств для функционального программирования, которое не редко создает циклические ссылки. Но это удачное стечение обстоятельств, вероятно, не было критичным для появления ФЯ. Например, авторы SIMULA со временем отказались от подсчета ссылок в пользу сборки мусора потому, что проводили эксперименты и сравнивали производительность сочетания подсчета ссылок со сборщиком мусора для сборки циклов в куче и компактифицирующего сборщика. По крайней мере вспоминают про это [Dahl78]. Но, наверное, была какая-то польза от того, что лисперы начали работать над сборкой мусора раньше.
Сделав выбор между счетчиком ссылок и сборщиком мусора, лисперы отложили имплементацию сборщика мусора потому, что писали игрушечные программы, и сборка мусора пока что не требовалась.
Ко времени первой публичной демонстрации Лиспа в 60-ом или 61-ом году сборщик уже работал. И был продемонстрирован, хотя это и не планировалось. Во время репетиции он не включался, но репетиция заняла достаточно памяти, чтоб сборщик мусора начал работать во время демонстрации и работал все оставшееся время выделенное для нее, печатая статистику [McCa78]. Трудно сказать, почему лисперы не хотели показывать свое главное, по мнению Тернера [Turn12], достижение. Но, к счастью, все кончилось хорошо.
В выводе статистики сборки сборщик мусора уже гордо именуется сборщиком мусора, но в первой статье о Лиспе [McCa60] его еще стесняются так называть. МакКарти там слишком серьезный, но это скомпенсировано другими лисперами в других статьях.
МакКарти пишет, что процесс “рекламации” занимает “несколько секунд”. Это так, когда памяти 8192 слов. Но ко времени демонстрации сборщика это уже 32768 слов и процесс занимает по крайней мере четыре раза по “несколько” секунд. Не очень хорошо. Главная проблема [Wilk92] сборщика МакКарти: даже если живых объектов почти и не осталось к моменту сборки - все равно нужно обходить всю кучу, время работы сборщика зависит от размера кучи.
И сторонники подсчета ссылок практически с самого начала критикуют [Coll60] метод МакКарти за то, что время работы сборщика не зависит от того, сколько памяти освобождается. Но то, что время работы не зависит от того, сколько памяти освобождается - это даже хорошо. Плохо в нем то, что время работы не зависит только от количества живых объектов.
Получается, что рост памяти, которому мы так радовались на протяжении этой главы, несет не только новые возможности, но и новые вызовы. До следующей сборки проходит все больше времени, в которое можно уместить уже не только демонстрацию (но не вместе с её репетицией), но уже и какую-то более интересную работу. Но и перерыв в работе становится все более мучительным.
Конечно, для функционального программирования совсем не подходит такой взгляд на проблему. ФП код аллоцирует достаточно много, чтоб быстро занять память. ФП может извлекать пользу из роста памяти, если время сборки не растет так быстро как память. Но лисперы МТИ пока не собираются функционально программировать. И если отказаться от ФП - можно придерживаться стековой дисциплины, избежать аллокации массы короткоживущих объектов в куче. Так что Гринблатт и другие делают MacLISP, который использует преимущественно стек. И если аллоцировать в куче поменьше, переиспользовать структуры данных, изменяя их на месте, то можно сохранять эту иллюзию решения, отодвигать сборку в будущее, а там может и работа уже закончена и можно не собирать вовсе.
Такой подход маклисперов делает маловажной для них и другую проблему сборщика МакКарти, которая для ФЯ очень важна. Раз уж работа функциональной программы сводится к аллокации в такой значительной степени - аллокация должна быть быстрой. И получение памяти из списка свободных ячеек - не самый быстрый способ. Маклиспер просто аллоцирует на стеке - это быстро. Второй недостаток сборщика МакКарти особенно обостряется в сочетании с третьим.
Первоначальные аллокатор и сборщик МакКарти не имеют этого недостатка потому, что оперируют только парами. Так что каждая ячейка в списке свободных подходит. Сегодня кажется само собой разумеющимся, что в языке со сборщиком мусора можно аллоцировать не только объекты с двумя полями, но когда-то было иначе. В статье про ленивый эвалуатор [Hend76] как раз про такую кучу только для пар и говорится как про само собой разумеющуюся. Еще в начале 80-х это была важная и распространенная разновидность сборщиков [Cohe81].
Но, если вы не хотите в своем ФЯ использовать типы данных с представлением в памяти, как у композитных типов МакКарти, то вам нужен другой сборщик. И лучше этого не хотеть.
Конечно, со временем объекты кучи разных размеров захочет получить главный заказчик MACLISP-фич Мозес. И его поддержат менее важные заказчики - создатели Антипрологов Хьюит и Сассман [Feni71]. Один из авторов PAL Эванс ознакомит их с работами имплементатора PAL Ричардса и работами над сборщиками мусора для Алгола 68. Но не с эдинбургскими работами над такими сборщиками. А Поплстоун претендует [Popp2002] на написания одного из первых таких сборщиков. Но первый контакт маклисперов с разработчиками сборщиков для Алгола скорее всего состоялся раньше и по другой причине, но об этом позже.
Так вот, для интересующихся аллокацией объектов разных размеров сборщик МакКарти еще хуже. Поиск подходящего куска памяти требует больше времени. Память фрагментируется и используется менее эффективно. Уоррен считал, что сборщик мусора определенно создает больше проблем, если нужно аллоцировать объекты разных размеров. И потому для его имплементации Пролога еще важнее, чем для Лиспа, больше использовать стек и меньше использовать сборщик мусора [Warr77].
Можно предположить, что у имплементатора ФЯ нет никакой надежды дождаться помощи от маклисперов и более естественный его союзник - имплементатор объектно-ориентированного языка, который в первое время почему-то не хотел использовать стек, где это возможно. Мы уже выяснили, что у этого нежелания были серьезные негативные последствия в виде непрактичности “языка восьмидесятых” SIMULA 67 и плохого впечатления от языков со сборкой мусора вообще. Но у этого есть и положительная сторона. Для разработчиков таких ООЯ тоже важна скорость аллокации и нет надежды сделать все что нужно до исчерпания памяти.
Предположить можно, это логично. Но такая логика не может предсказать действия маклисперов, которые изобрели подходящий для ФЯ сборщик мусора. Сборщик, полностью или частично решающий все проблемы сборщика МакКарти, они изобрели, чтоб решить совсем другую проблему.
Памяти нужно все больше, и физическая память растет недостаточно быстро. Лисперы готовятся к переезду на машину с виртуальной памятью - пишут MacLISP для Multics. И это обостряет еще одну проблему сборщика МакКарти: нелокальность ссылок. Освободившиеся cons
-ячейки находятся все дальше друг от друга. Пары, с которыми ведется работа в данный момент, занимают одну страницу с парами, которые не скоро понадобятся и содержат ссылки на пары в других страницах. Переход к ним заставит читать данные с диска!
Мински изобретает решение проблемы: нужно обходить только живые пары в куче и сериализовать их в файл. После этого файл можно десериализовать в память и куча станет удобной компактной кучкой. Да, в наши дни сохранение сессии REPL в файл не особенно ассоциируется со сборкой мусора, но лисперы считают это изобретением копирующего сборщика мусора.
Память тем временем еще выросла. Уже можно копировать прямо в неё, предлагают [Feni69] Роберт Фенихель (Robert Fenichel) и Джером Иохельсон (Jerome Yochelson). В старые времена, рассказывают они, сборка мусора была нужна для переиспользования небольшого адресного пространства. Сегодня, в ревущем 1968-ом адресное пространство практически безгранично. На Multics в него поместятся миллиарды cons
-ячеек (выделено Фенихелем и Иохельсоном). Лисп-система может работать “почти бесконечно”. Но, конечно, производительность будет деградировать из-за нелокальности ссылок. Поэтому, делают вывод Фенихель и Иохельсон, современный сборщик мусора не ищет свободное место, а дефрагментирует кучу. Сборку надо начинать не при исчерпании свободного места, а реагировать на замедление работы. Ведь сборка никогда не необходима, а только желательна чтоб ускорять работу, когда фрагментация зашла слишком далеко. И уж конечно сканировать всю кучу, как придумал МакКарти, практически неосуществимо. Обходится только живая её часть. И небольшая живая часть может быть еще один ориентир для начала сборки. Например, можно ориентироваться на короткий стек. Короткий стек, вероятно, удерживает мало объектов - самое время копировать. Копирование из памяти в память означает, что нужно в два раз больше памяти, но это не беда, памяти полно. Но полно ли?
Можно только позавидовать маклисперам, которые писать в ФП стиле не собираются, ведь даже первый компилятор ФЯ, написанный на ФЯ и использующий их сборщик мусора, аллоцирует с такой скоростью, что “почти бесконечность” 32бит адресного пространства закончится до конца рабочего дня. Даже на совсем не быстрых машинах того времени.
Само изобретение Фенихеля и Иохельсона и делает эту скорость возможной, да и вообще делает возможным функциональное программирование. Для ФП очень полезно, что можно аллоцировать простым прибавлением числа, как при аллокации на стеке, а не выискивая подходящий свободный кусок памяти, как нужно делать при использовании сборщика МакКарти. И, конечно, при такой-то скорости аллокаций короткоживущих объектов в ФП очень полезно то, что нужно обходить только долгоживущие. Типичный ФП код аллоцирует гораздо больше короткоживущих объектов, чем долгоживущих. Это означает, что добавляя память, можно сделать цену сборки меньше и меньше. Копирование живых объектов занимает примерно столько же времени, а освобождает все больше памяти.
Изобретение, правда, требует некоторой доработки напильником практичности. Алгоритм придуманный лисперами не совсем подходит для практического использования из-за того, что требует аллоцировать память на стеке во время обхода объектов кучи. Фенихель и Иохельсон пишут, что можно этого избежать с помощью разворота указателей [Scho67], хотя сами этого не делают. Но проблему можно решить проще и сделать алгоритм быстрее. Наш знакомый алголист из предыдущей главы Крис Чейни придумал в 1970-ом году другой алгоритм копирования [Chen70]. Он обходит граф объектов кучи в ширину, используя уже скопированные объекты в качестве очереди для их обхода. Да, обход в ширину производит не такую хорошую локальность ссылок, как обход в глубину, но комбинированный обход, копирующий детей объекта вместе с ним, в основном решает эту проблему. Но борьба сторонников этих двух разных обходов будет продолжаться еще какое-то время.
Получившийся практический полезный копирующий сборщик обычно называют именем Чейни. Лисперы называют его “алгоритм Мински-Фенихеля-Иохельсона-Чейни-Арнборга” [Bake77]. Арнборг (Stefan Arnborg) - это имплементатор SIMULA 67 для PDP-10, который использовал в своей имплементации и несколько доработал этот сборщик. Сам Чейни-то, как мы помним, оказался слишком практичным, чтоб имплементировать сборщик для своего Algol68C.
Арнборг как раз из тех имплементаторов ООЯ, которые приветствуют [Arnb72] перенос всей сложности из аллокатора в сборщик мусора, но мало что придумали для этого раньше лисперов, не очень этим интересовавшихся в описываемое время.
В статье Фенихеля и Иохельсона очень нехватает экспериментов и измерений, проверяющих их идеи, но этого не хватает во многих статьях о которых мы еще расскажем в этой главе. Маклисперы пишут в основном о том, что планируют сделать, а не о том, что сделано. Но так или иначе идеи были проверены разными имплементациями Лиспа. В MULTICS MACLISP сделали копирующий сборщик [Moon74]. Это направление бегства с PDP-10 было быстро заброшено и мало что написано про результаты. Но впечатление о том, с чем столкнулись Лисперы МТИ, можно составить по более позднему опыту имплементаторов Interlisp на VAX [Bate82]. И они, испытав сборщик Чейни, сомневаются в его применимости для машин с виртуальной памятью. Если куча не умещается в физическую память - секунды сборки становятся минутами.
Алголисты больше пишут о том, что сделано и публикуют какие-то измерения. Уже летом 71-го они проделали какие-то опыты, которые показали, что, возможно, копирующий сборщик не очень хорошая идея, когда надо работать на машине с виртуальной памятью и физической памяти не много. Статья [Baec72b] опубликована в конце 72-го.
Возможно, что эта разность подходов повлияла на то, что к началу 80-х лисперы опубликовали больше всего материалов по сборке мусора, а алголисты заняли только второе место, по оценке Коэна в его обзоре сборщиков [Cohe81].
Да, копирующий сборщик приводит кучу в состояние, желательное на машине с виртуальной памятью. Но приводит крайне неудачным для машины с виртуальной памятью путем. В процессе сборки нужно пройтись по значительной части кучи, поднять в физическую память с диска множество страниц и, чтоб освободить для них место, задвинуть на диск множество страниц из физической памяти. При большом количестве долгоживущих объектов это не намного лучше подхода МакКарти и может быть даже хуже. Потому, что копирующий сборщик требует в два раза больше памяти для сборки.
С тем, для чего его придумали лисперы копирующий сборщик не справляется. Но он не полностью бесполезен для функционального программирования. Если вся куча находится в физической памяти, то он работает достаточно хорошо, хотя все еще делает много ненужной работы, обходя долгоживущие объекты чаще чем нужно. Но делает меньше ненужной работы, чем сборщик МакКарти. Не говоря о прочих его преимуществах, важных для имплементации ФЯ. Так что нужно просто подождать лет 15 до того времени, когда физической памяти будет достаточно. Но в МТИ, как мы помним, хотят запускать большие программы на Лиспе уже в начале 70-х. При этом надеются, что смогут для этого использовать машины с большим адресным пространством и диском, но небольшой быстрой памятью [Gree74]. И у лисперов МТИ есть кое-какие идеи о том, как можно исправить копирующий сборщик.
Следование ссылкам переадресации займет некоторое время.
К. Хьюит, Х. Либерман [Lieb80]
Не обязательно делать паузу на всю сборку всей кучи. Можно делать сборку параллельно, как предложил Стил [Stee75b] или хотя-бы инкрементально, как предложил МТИ-лиспер Бейкер (Henry G. Baker) [Bake77]. Сборщик Бейкера тоже копирующий, и проблема двойного потребления памяти при сборке еще усугубляется: сборка идет всегда и сборщику нужно тянуть в физическую память с диска все эти ненужные мутатору страницы. Компактность рабочей области хуже, рабочий набор больше. Есть и другие проблемы, особенно для ФЯ. Алгоритм Бейкера - из тех, которые заставляют больше платить за чтение объекта по ссылке, а не за изменение ссылки. Для доступа к объекту в куче нужно перейти по ссылке два раза. Естественно, для ФЯ предпочтительнее, когда нужно больше платить за изменение. Лисперы не собираются имплементировать этот метод сборки для Multics MacLISP, или какой-нибудь другой обычной машины. Только для Лисп-машин [Gree77]. И в 70-е все только собираются и собираются.
Но самое главное - это то, что принципиальная проблема все равно не решается. Сборщик делает ненужную работу, регулярно обходя граф долгоживущих объектов, на смерть которых нет особой надежды. Если объект кучи быстро не помер - он проживет долго. Но копирующий сборщик Бейкера или Чейни может быть частью более сложного сборщика, который действительно будет работать.
Если работать с одной большой кучей слишком тяжело, то почему бы не работать с множеством маленьких кучек? Алголист Беккер (H. D. Baecker) предложил [Baec72] добавить в Алгол 68 фичу из PL/I - возможность для программиста выбирать в какую арену аллоцировать. В отличие от PL/I, в Алголе 68 с этими аренами работал бы сборщик мусора. Явная работа программиста с аренами, конечно, нормальное решение для “языка семидесятых”, но для ФЯ хотелось бы чего-то более автоматического и менее явного.
Арнольд Рошфилд (Arnold Rochfeld) из Эдинбургского Университета предложил [Roch71] собирать копирующим сборщиком общую кучу (т.е. слишком много) и окружение блока кода (т.е. слишком мало) при выходе из этого блока (т.е. слишком часто). Автор подозревает, что работать сборщик будет слишком часто и предлагает аннотации для блоков, которые бы указывали, нужно ли собирать при выходе из такого блока. Статья получена издательством в октябре 69-го, но опубликована только в декабре 71-го года.
Придумать, что нужно собирать мусор только в части кучи легко. Сложнее придумать, как это сделать, если между этой частью кучи и оставшейся могут быть ссылки. Для кого решение этой проблемы важнее всего в МТИ? Правильно, для антипрологосторителя Хьюита, которому так просто не обойтись стеком для всего короткоживущего, как прочим пользователям MacLISP-диалектов. Питер Бишоп (Peter B. Bishop), пишущий в МТИ диссертацию под руководством Хьюита, ссылается на эти работы и предлагает [Bish75] два решения. Можно делать ссылки между разными кучами двойными, ссылками на ссылки из одной кучи в другую. Можно поддерживать для сборщика мусора список ссылок на поля или объекты, в которых есть ссылки из одной кучи в другую. Второй вариант, конечно, лучше для ФЯ, но Бишоп, разумеется, выбирает первый. Да что не так с лисперами МТИ и этими их ссылками на ссылки? Почему они так хотят биться о память? Во всем этом виноваты Лисп-машины. Помните про поддержку этих ссылок на ссылки в них? Поддержку сборки мусора в части кучи или в одной из многих куч тоже планируют делать для Лисп-машин [Gree74] [Gree77]. Не игнорировать же фичу Лисп-машины? Надо пользоваться. Лисперы в МТИ, как мы помним, теперь хотят делать что-то только для Лисп-машин.
Но 70-е годы подошли к концу и только в последний год десятилетия, 26 апреля 1980 выходит отчет [Lieb80] Хьюита и Генри Либермана (Henry Lieberman) в котором наконец-то обозначена концепция сборщика мусора, который обладает хорошей локальностью и быстро освобождает много места. То, что и нужно для функционального программирования.
Маклисперы и Уоррен решают проблему аллокации короткоживущих объектов с помощью стека. Но, пишут Хьюит и Либерман, стековая дисциплина не совместима с решением FUNARG-проблемы и объектно-ориентированным программированием. Они предполагают, что не смотря на эту несовместимость, подавляющее большинство объектов в ООЯ и ФЯ все равно короткоживущие. Важно, чтоб размещение короткоживущих объектов в имплементациях таких языков было быстрым. Цель Либермана и Хьюита - сделать так, чтоб аллокация временных структур не была сильно медленнее аллокации на стеке, чтоб программист не наказывался рантаймом за написание более понятных программ. Как мы помним, ООП - это ответ Хьюита на Пролог и логическое программирование, а FUNARG-проблемой и ФП вообще к этому времени заинтересовался его коллега Сассман. Хьюит хочет освободиться от ограничений стековой дисциплины, но сохранить разумную эффективность.
И у Либермана с Хьюитом есть идея, как уменьшить стоимость работы сборщика с короткоживущими объектами настолько, что сборщик мусора будет соперничать со стеком.
Кучу надо разделять на части по возрасту объектов. Не нужно тратить время на бессмысленный обход объектов, которые уже пережили несколько сборок и, судя по всему, помирать в ближайшее время не собираются. Вместо этого нужно сначала работать с теми частями кучи, в которых располагаются молодые объекты. Обход такой области и копирование из нее быстро закончится, ведь большинство объектов долго не живут. Много места освобождается очень быстро.
Так что больше нет куч A и B и структур для обозначения ссылок из A в B и из B в A. Есть куча 1 со старыми объектами и куча 2 с молодыми и ссылки молодых на старые - самые обычные. Их не нужно никак отслеживать и обозначать.
На этом общепризнанные сегодня идеи заканчиваются и начинаются своеобразные. Некоторое своеобразие довольно безобидное. Например, поколения нумеруются не в том порядке, в каком их нумеруют сегодня. Но много необычных идей совсем не так безобидны. Поколения нумеруются не в том порядке, в каком объекты движутся по ним, переживая сборки, а в порядке создания поколений. Да, поколения постоянно создаются, их должно быть много. Как минимум, два самых молодых поколения должны содержать небольшую долю долгоживущих объектов. Потому, что для сборки самого молодого поколения n нужно обходить еще и поколение n-1. Дело в том, что если ссылки между поколениями “назад” во времени теперь самые обычные, вместо последовательно двойных, то для учета ссылок “вперед” во времени строятся структуры еще хуже, чем решил строить Бишоп. Казалось бы, куда еще хуже? Но хуже всегда может быть. Чтоб можно было ограничится обходом только одного поколения более старого, чем собираемое, все ссылки “вперед” могут быть только в соседнее поколение. N поколений означают (n-1)-кратную максимальную косвенность таких ссылок. Не только чтение обходится недешево, но и изменение ссылок может требовать создания множества объектов во множестве поколений для многократной переадресации.
Но и это еще не все. Либерман с Хьюитом хотят использовать еще и специальную технику, которая увеличивает число таких ссылок. Сегодня есть такая техника уменьшения числа ссылок между поколениями - повышение. Новый объект, на который ссылается старый объект помещают в поколение старого объекта. Хьюит и Либерман планируют делать наоборот, понижать старый объект, перемещая его в молодое поколение, сборку которого он, конечно, переживет. И все ссылки на него из остальных старых объектов теперь превратились во множество скачков по памяти. Справедливости ради, в отчете пишут, что, возможно, стоит рассмотреть и повышение, но как и в случае со способами обозначить ссылки между поколениями у Бишопа, выбирают не то что нужно.
Все это пока что только концепция. Хьюит с Либерманом не проверили на опыте гипотезу поколений о том, что большинство объектов умирают молодыми, не проверили насколько хорошо работают их идеи об организации ссылок между поколениями. Ну, хотя-бы сформулировали ясную цель для имплементаторов ФЯ.
Хьюит и Либерман “говорили с Дэвидом Муном”, который имплементирует сборщик мусора для Лисп-машин. Имплементация такого сборщика шла. Как шла и имплементация инкрементального сборщика Бейкера без поколений. Но в 70-е так никуда и не пришла.
Не смотря на то, что в главном центре Лисп-разработки все работы над сборщиками мусора были для Лисп-машин, эти сборщики задержались существенно сильнее, чем Лисп-машины. Ранние Лисп-машины в МТИ не имели сборщика мусора много лет, а когда получили инкрементальный, пользователи предпочитали его отключать [Stee96]. Даже Хьюит и Либерман, изобретающие принципиальное решение проблемы, рассматривают отсутствие сборки как реальную альтернативу. И дело не только в Лисп-машинах. Имплементаторы Interlisp с копирующим сборщиком для обычного железа рекомендовали сборщиком мусора не пользоваться [Bate82]. Для нас такой итог разработки сборщиков мусора лисперами выглядит необычно.
Сегодня не так просто сказать, насколько ненормальным и неправильным является то, что для имплементации Алгол 68 никак не могут сделать сборщик мусора. Какой код писали на Алголе 68? Может, все в порядке, сборщик мусора и не нужен? Ну а как насчет Лиспа, которому никак не могут сделать сборщик мусора? Как это вообще работает?
Новый план лисперов идентичен их плану для первой демонстрации Лиспа, состоявшейся за пару десятков лет до того: работаем, пока память не кончится. Прошла пара десятилетий, а имплементация работающего сборщика все откладывается. Но что делать, когда память закончится? Ведь есть уже несколько неигрушечных программ. И не закончится ли память довольно быстро?
Не обязательно. Лисперы стараются писать программы на диалекте MacLISP для Лисп-машин так же, как старались писать на всех прочих диалектах MacLISP - используя по возможности стек. Большинство программ, таких как компиляторы и текстовые редакторы были написаны так, чтоб избегать аллокации, явно переиспользовать структуры данных на месте. MacLISP делали “языком семидесятых” и потому разработка на нем EMACS для MULTICS и Лисп-машин - это не такой, скажем, “смелый” проект, как, например, попытка Netscape написать браузер на Java. Хотя и кажется таким на первый взгляд.
После всех этих усилий по сокращению аллокаций, размера адресного пространства Лисп-машин хватало, а виртуальная память работала достаточно хорошо, чтоб Лисп-машиной можно было пользоваться дни. И когда эти в лучшем случае дни, а в не самом лучшем случае - один рабочий день проходят и место подходит к концу, нужно сохранить образ системы. Включить сериализацию кучи на диск и идти домой отдыхать. Чтоб на следующий день с новыми силами вернуться на работу и загрузить сохраненный образ, сделав таким образом сборку мусора а-ля Мински [Stee96].
Да, с программой на функциональном языке так не поработаешь. И не все программы на Лиспе написаны как Гринберговский EMACS. Есть очень важная для лисперов программа, которая не так сильно отличается от компилятора ФЯ. MACSYMA аллоцирует 36Кб в секунду [Fode81]. Достаточно, чтоб заполнить адресное пространство Лисп-машины за полчаса работы. Получается не очень продуктивный рабочий день.
Но адресное пространство-то можно еще увеличить, а где хранить столько страниц? Прогресс не стоит на месте, утешают себя лисперы, можно будет использовать дешевые средства хранения для совсем старых страниц. Например, VideoDisc. В этих страницах все равно почти гарантированный мусор, но если вдруг среди этих гигабайтов мусора затеряется уж очень долгоживущий и невостребованный объект, то когда он понадобится - система просто подождет, пока диск не будет найден и нужная страница считана. За ночь или даже за выходные, пока оператор Лисп-машины отдыхает, эти архивы страниц можно затягивать в гигантскую память специальной машины, выделенной для сжатия лисповых куч.
Все это звучит не очень привлекательно. Что если Лисп-машины просто не будут использовать? Об этом Лисп-машинисты, уже поставившие будущее Лиспа на то, что программисты будут платить за компьютер в десятки и сотни раз больше только для того, чтоб использовать Лисп, старались не думать. Возможно, будет даже ниша для такого бизнеса, фантазирует [Whit80] важный имплементатор Лиспов Уайт. Машина будет машиной и в другом смысле, перемещаться по городу от офиса одного из многочисленных пользователей Лисп-машин к другому, как передвижной пылесос начала XX века, подключаться к их сетям и сжимать кучи за кучами.
Эта леденящая душу антиутопия, в которой никакого ФП, разумеется, не могло бы быть, вычеркнута из истории Лиспа. В черновой версии статьи [Stee96] Стила и Гэбриела есть пометка авторов о том, что надо бы рассказать про неё. Но пометка так и осталась только пометкой. Вместо того, чтоб познакомить читателей с этим пиком лисповой мысли 70-х Стил и Гэбриел предпочитают объяснить еще несколько лисповых шуточных акронимов. Мы-то стараемся читателей не подвести.
Но на рубеже десятилетий этот ужас был всегда рядом, не давал себя забыть разработчикам и имплементаторам сборщиков мусора для Лиспа. И Хьюит с Либерманом и разработчики Interlisp ссылаются на статью Уайта некритически. Десятилетие безуспешных попыток освоить виртуальную память деморализовало их.
В результате такого потерянного десятилетия в разработке сборщиков мусора в 1980-ом имплементаторы ФЯ не могли позаимствовать у лисперов ничего готового из того, чего не было в 1970-ом. Практически полезный, подходящий для ФП, Лиспа и для обычных и даже для Лисп-машин сборщик с поколениями изобретут не лисперы, а имплементаторы другого языка, который в 70-е годы вовсе сборку мусора не использовал.
Времена, когда функционального программирования не было, подходят к концу. А вместе с ними и нулевая часть нашей истории. В которой мы попытались разобраться что было, когда функционального программирования не было. И почему того, что уже было еще не было достаточно для того, чтоб функциональное программирование было.
Могло ли функциональное программирование появится раньше? Почти наверняка не могло в 60-е годы. Но в 70-е годы история ФП могла сложится иначе, при более удачном, но крайне маловероятном стечении обстоятельств. Если бы герои этой истории больше интересовались работами друг друга и внимательнее слушали доклады своих коллег. Если бы авторы PAL использовали для испытания своих последующих изобретений PAL. Если б те, кто хотел делать ФЯ в 70-е, имели доступ к таким же дорогим компьютерам, к которым имели доступ те, кто хотел делать ФЯ в 60-е. Если б лисперы МТИ захотели делать ФЯ раньше. Если бы разработки Йорктаун Хайтс были более открыты.
Уоррен и алголисты могли бы сделать компиляторы функциональных языков, отличающихся от языков эдинбургской программы гораздо меньше, чем нам было бы удобно отличать.
Избежало бы это более раннее функциональное программирование ловушку PDP-10? Могло бы выиграть от ИИ-бума? Было бы оно уничтожено ИИ-зимой? Ну, это был не последний ИИ-бум и не последняя ИИ-зима, так что мы еще увидим как и то и другое влияет на ФП.
Конечно, не всегда обстоятельства складывались неудачно, не все возможности были упущены. Участники Эдинбургской программы могли бы и не оказаться в Эдинбурге и его окрестностях и продолжить обмениваться идеями со скоростью переизобретателей продолжений. Могли бы не иметь доступа и к тем машинам, к которым имели. Милнер мог бы увлечься резолюционизмом или ООП. Бурсталл мог бы не разговориться с незнакомцем в книжном магазине. Мики мог бы не забросить генетику. Поплстоун мог бы не спасти паттерн-матчинг. Функциональное программирование могло бы появиться и еще на десяток лет позже, дождавшись интернета и компьютеров необходимой мощности доступных даже хоббистам. Вырасти в 90-е на руинах языков уравнений вроде Axiom, и обломках Лиспов, выкинутых на мороз после провалившейся коммерциализации. Или не появиться вовсе.
60-е и 70-е годы произвели много важных идей о функциональном программировании, но были не лучшим временем для имплементаторов этих идей. Идеи этих двадцати лет имеют определяющее влияние. Но влияние имплементаций - серьезно ограничено. Двадцатилетняя история разработки какого-нибудь сборщика мусора в 1980 году мало что значит. Потому, что за двадцать лет из этих двадцати мало что было сделано. Но сложно в этом винить тех немногих, кто имел к доступ компьютеру с памятью, как у сегодняшнего микроконтроллера. Достаточный доступ хотя-бы для того, чтоб написать 1KLOC за год, запуская компилятор не чаще раза в день.
Новые, более доступные машины с большой памятью изменили это навсегда. Нужно констатировать, что микрокомпьютеры и интернет в 80-е и далее так увеличили производительность и связность имплементаторов, что наработки, которые копили до того десятилетиями были воспроизведены и улучшены за годы и даже месяцы. Жалкие построения 60-х и 70-х были сметены следующими, все большими волнами воспроизводителей и переизобретателей.
Да, имплементаторы ФЯ пока что не умеют как следует использовать новую большую память, но это уже не остановит имплементацию. В последнюю зиму семидесятых в Эдинбурге, на новой машине с большой памятью, уже идет работа над первым компилятором функционального языка.
Но не все будет хорошо и легко для имплементаторов ФЯ в 80-е. Их ждут и новые испытания. Посещавший SRI Фурукава Коити (Furukawa Koichi) уже нашел там ксерокопию ксерокопии ксерокопии распечатки FORTRAN-исходников Марсельского интерпретатора Пролога. Это событие не окажет влияние на то, быть функциональному программированию или не быть. Это уже решено. Функциональное программирование обречено состояться. Но это событие окажет решающее влияние на то, какие функциональные языки используются сегодня.
[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
[ALGOL71]: AB32.3.3 C, Modals. Algol Bulletin No. 32, May 1971 http://archive.computerhistory.org/resources/text/algol/algol_bulletin/A32/P33.HTM
[ALGOL72]: AB33.3.3 IFIP WG2.1 Subcommittee: Maintenance of and Improvements to ALGOL 68, Algol Bulletin No. 33, March 1972 http://archive.computerhistory.org/resources/text/algol/algol_bulletin/A33/P33.HTM
[ALGOL72a]: AB34.3.1 Report on the meeting of WG2.1 at Fontainebleau Algol Bulletin No. 34, July 1972 http://archive.computerhistory.org/resources/text/algol/algol_bulletin/A34/P31.HTM
[ALGOL72b]: AB34.3.2 Report on considered improvements (Fontainebleau 9), Algol Bulletin No. 34, July 1972 http://archive.computerhistory.org/resources/text/algol/algol_bulletin/A34/P32.HTM
[ALGOL73]: AB35.3.1 Further Report on Improvements to ALGOL 68, Algol Bulletin No. 35, March 1973 http://archive.computerhistory.org/resources/text/algol/algol_bulletin/A35/P31.HTM
[ALGOL78]: AB42.1.1 Modules and Separate Compilation, Algol Bulletin No. 42, May 1978 http://archive.computerhistory.org/resources/text/algol/algol_bulletin/A42/P11.HTM
[ALGOL80]: AB45.4.2 ALGOL 68 Implementations - ALGOL 68C - Release 1, Algol Bulletin No. 45, January 1980 http://archive.computerhistory.org/resources/text/algol/algol_bulletin/A45/P42.HTM
[ALGOL81]: AB47.3.3 Survey of viable ALGOL 68 Implementations, Algol Bulletin No. 47, August 1981 http://archive.computerhistory.org/resources/text/algol/algol_bulletin/A47/P33.HTM
[Arnb72]: Stefan Arnborg. 1972. Storage administration in a virtual memory Simula system. BIT 12, 2 (Jun 1972), 125–141. doi:10.1007/BF01932808
[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
[Baec72]: H. D. Baecker. 1972. On a missing mode in ALGOL 68. SIGPLAN Not. 7, 12 (December 1972), 20–30. doi:10.1145/987059.987062
[Baec72b]: H. D. Baecker. 1972. Garbage collection for virtual memory computer systems. Commun. ACM 15, 11 (Nov. 1972), 981–986. doi:10.1145/355606.361886
[Bake77]: Henry G. Baker. 1978. List processing in real time on a serial computer. Commun. ACM 21, 4 (April 1978), 280–294. doi:10.1145/359460.359470
[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
[Bate82]: Raymond L. Bates, David Dyer, and Johannes A. G. M. Koomen. 1982. Implementation of Interlisp on the VAX. In Proceedings of the 1982 ACM symposium on LISP and functional programming (LFP ‘82). Association for Computing Machinery, New York, NY, USA, 81–87. doi:10.1145/800068.802138
[Baue76]: F. L. Bauer. 1976. Programming as an evolutionary process. In Proceedings of the 2nd international conference on Software engineering (ICSE ‘76). IEEE Computer Society Press, Washington, DC, USA, 223–234. doi:10.5555/800253.807679
[Baue79]: Bauer, F.L. (1979). Program development by stepwise transformations — The project CIP. In: Bauer, F.L., et al. Program Construction. Lecture Notes in Computer Science, vol 69. Springer, Berlin, Heidelberg. doi:10.1007/BFb0014671
[Baue79b]: Bauer, F.L. (1979). From specification to implementation — The formal approach. In: Bauer, F.L., et al. Program Construction. Lecture Notes in Computer Science, vol 69. Springer, Berlin, Heidelberg. doi:10.1007/BFb0014670
[Baue79c]: Bauer, F.L. (1979). Detailization and lazy evaluation, infinite objects and pointer representation. In: Bauer, F.L., et al. Program Construction. Lecture Notes in Computer Science, vol 69. Springer, Berlin, Heidelberg. https://doi.org/10.1007/BFb0014675
[Baue81]: Bauer, F. L., Broy, M., Dosch, W., Gnatz, R., Krieg-Brückner, B., Laut, A., … Wössner, H. (1981). Programming in a wide spectrum language: a collection of examples. Science of Computer Programming, 1(1-2), 73–114. doi:10.1016/0167-6423(81)90006-x
[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
[Beki84]: Bekić, H. (1984). Towards a mathematical theory of processes. In: Jones, C.B. (eds) Programming Languages and Their Definition. Lecture Notes in Computer Science, vol 177. Springer, Berlin, Heidelberg. doi:10.1007/BFb0048944
[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
[Birr77]: Andrew Birrell. System Programming in a High Level Language. Ph.D. Thesis, University of Cambridge. December 1977.
[Bish75]: Bishop, Peter B. “Garbage collection in a very large address space.” (1975).
[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
[Bour72]: S.R.Bourne and M.J.T.Guy. AB33.3.8: Comments on suggested improvements to ALGOL 68, Algol Bulletin No. 33, March 1972 http://archive.computerhistory.org/resources/text/algol/algol_bulletin/A33/P38.HTM
[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
[Bond77]: Bond, Susan Gillian, Philip Mayne Woodward, and ROYAL RADAR ESTABLISHMENT MALVERN (ENGLAND). Introduction to the ‘RS’ Portable Algol 68 Compiler. Royal Radar Establishment, Procurement Executive, Ministry of Defence, 1977.
[Bond2001]: Susan Bond. An oral history conducted in 2001 by Janet Abbate, IEEE History Center, New Brunswick, NJ, USA. https://ethw.org/Oral-History:Susan_Bond
[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).
[Chen70]: C. J. Cheney. 1970. A nonrecursive list compacting algorithm. Commun. ACM 13, 11 (Nov 1970), 677–678. doi:10.1145/362790.362798
[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.
[Cohe81]: Jacques Cohen. 1981. Garbage Collection of Linked Data Structures. ACM Comput. Surv. 13, 3 (Sept. 1981), 341–367. doi:10.1145/356850.356854
[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
[Coll60]: George E. Collins. 1960. A method for overlapping and erasure of lists. Commun. ACM 3, 12 (Dec. 1960), 655–657. doi:10.1145/367487.367501
[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
[Conr74]: R. Conradi, P. Holager, MARY Textbook, RUNIT rapport, 1974
[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).
[Curr70]: Currie, Ian F., Susan G. Bond and J. D. Morison. “Algol 68-R.” ALGOL 68 Implementation (1970).
[Curr76]: I.F. Currie, AB39.4.1: Modular Programming in ALGOL 68, Algol Bulletin No. 39, February 1976 http://archive.computerhistory.org/resources/text/algol/algol_bulletin/A39/P41.HTM
[Dahl78]: Kristen Nygaard and Ole-Johan Dahl. 1978. The development of the SIMULA languages. History of programming languages. Association for Computing Machinery, New York, NY, USA, 439–480. doi:10.1145/800025.1198392
[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.
[Dijk70]: AB31.1.1.1 “Minority Report”, Algol Bulletin No. 31, March 1970 http://archive.computerhistory.org/resources/text/algol/algol_bulletin/A31/P111.HTM
[Dijk2001]: Dijkstra, Edsger Wybe. “Oral history interview with Edsger W. Dijkstra.” (2001).
[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).
[Feni69]: Robert R. Fenichel and Jerome C. Yochelson. 1969. A LISP garbage-collector for virtual-memory computer systems. Commun. ACM 12, 11 (Nov. 1969), 611–612. doi:10.1145/363269.363280
[Feni71]: Robert R. Fenichel. 1971. List tracing in systems allowing multiple cell-types. Commun. ACM 14, 8 (Aug. 1971), 522–526. doi:10.1145/362637.362646
[Finn83]: Gavin Finnie, AB49.2.1 RS ALGOL 68 Implementors Group (RIG), Algol Bulletin No. 49, May 1983 http://archive.computerhistory.org/resources/text/algol/algol_bulletin/A49/P21.HTM
[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.
[Gard77]: P. J. Gardner. 1977. A transportation of ALGOL68C. In Proceedings of the Strathclyde ALGOL 68 conference. Association for Computing Machinery, New York, NY, USA, 95–101. doi:10.1145/800238.807148
[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 Report.” (1970).
[Gord78]: Gordon, Michael J. C., Robin Milner, L. Morris, Malcolm C. Newey and Christopher P. Wadsworth. “A Metalanguage for interactive proof in LCF.” Proceedings of the 5th ACM SIGACT-SIGPLAN symposium on Principles of programming languages (1978): n. pag. DOI:10.1145/512760.512773
[Gord79]: Gordon, Michael J. C.. “Edinburgh LCF: A mechanised logic of computation.” (1979).
[Gord2000]: Gordon M. From LCF to HOL: a short history. In Proof, language, and interaction 2000 Jul 24 (pp. 169-186).
[Gord2000b]: Gordon, M. Christopher Strachey: Recollections of His Influence. Higher-Order and Symbolic Computation 13, 65–67 (2000). doi:10.1023/a:1010097524009
[Gord10]: Gordon, M. ICFP 2010 - Tribute to Robin Milner https://vimeo.com/15325077
[Gree69]: GREEN, Cordell. “APPLICATION OF THEOREM PROVING TO PROBLEM SOLVING.” In Proc. of IJCAI-69. 1969.
[Gree74]: Richard Greenblatt, THE LISP MACHINE, Working Paper 79 November 1974
[Gree75]: E. M. Greenwalt, Robert A. Amsler, Jonathon Slocum, and Mabry Tyson. LISP Reference Manual : CDC - 6000. CCUM 2, Computation Center, University of Texas at Austin, December 1975.
[Gree77]: Bawden A, Greenblatt R, Holloway J, Knight T. LISP Machine Progress Report. MASSACHUSETTS INST OF TECH CAMBRIDGE ARTIFICIAL INTELLIGENCE LAB; 1977 Aug 1.
[Gree96]: Greenberg BS. The Multics MACLISP Compiler. The Basic Hackery–a Tutorial. MIT Press. 1977;1988:1996. https://multicians.org/lcp.html
[Gree96b]: Greenberg BS. Multics Emacs: The History, Design and Implementation. Technical report, 1996 http://www.multicians.org/mepap.html
[Gunt93]: Gunter, Carl A.. “Semantics of programming languages - structures and techniques.” Foundations of computing (1993).
[Guti82]: Claudio Gutierrez. 1982. Prolog compared with LISP. In Proceedings of the 1982 ACM symposium on LISP and functional programming (LFP ‘82). Association for Computing Machinery, New York, NY, USA, 143–149. doi:10.1145/800068.802144
[Gutt76]: John V. Guttag, Ellis Horowitz, and David R. Musser. 1976. The design of data type specifications. In Proceedings of the 2nd international conference on Software engineering (ICSE ‘76). IEEE Computer Society Press, Washington, DC, USA, 414–420.
[Gutt78]: John V. Guttag, Ellis Horowitz, and David R. Musser. 1978. Abstract data types and software validation. Commun. ACM 21, 12 (Dec. 1978), 1048–1064. doi:10.1145/359657.359666
[Harr70]: M. C. Harrison. BALM-SETL: A simple implementation of SETL. 5 November 1970. https://www.softwarepreservation.org/projects/SETL/setl/newsletter/setl_001_1970-11-05.pdf
[Hart62]: T. Hart and M. Levin. The New Compiler. Memo 39, Artificial Intelligence Project, RLE and MIT Computation Center, no date (circa 1962?) http://www.bitsavers.org/pdf/mit/ai/aim/AIM-039.pdf
[Hart88]: Hartel, P. H., & Veen, A. H. (1988). Statistics on graph reduction of SASL programs. Software: Practice and Experience, 18(3), 239–253. doi:10.1002/spe.4380180305
[Hart13]: Hartley, D. (2013). CPL: Failed Venture or Noble Ancestor? IEEE Annals of the History of Computing, 35(3), 55–63. doi:10.1109/mahc.2012.37
[Hart2000]: Hartley, D. (2000). Higher-Order and Symbolic Computation, 13(1/2), 69–70. doi:10.1023/a:1010001708080
[Heer80]: Heering J. The Intel 8086, the Zilog Z8000, and the motorola MC68000 microprocessors. Euromicro Newsletter. 1980 May 1;6(3):135-43.
[Hend76]: Peter Henderson and James H. Morris. 1976. A lazy evaluator. In Proceedings of the 3rd ACM SIGACT-SIGPLAN symposium on Principles on programming languages (POPL ‘76). Association for Computing Machinery, New York, NY, USA, 95–103. doi:10.1145/800168.811543
[Hewi74]: Hewitt, C., Bishop, P., Steiger, R., Greif, I., Smith, B., Matson, T., & Hale, R. (1974). Behavioral semantics of nonrecursive control structures. Programming Symposium, 385–407. doi:10.1007/3-540-06859-7_147
[Hewi09]: Carl Hewitt. Middle History of Logic Programming. 2009 doi:10.48550/arXiv.0904.3036
[Hind07]: Hindley, J. R. (2007). M. H. Newman’s Typability Algorithm for Lambda-calculus. Journal of Logic and Computation, 18(2), 229–238. doi:10.1093/logcom/exm001
[Hoar64]: C. A. R. Hoare. AB18.3.7: Case expressions, pages 20-22, Algol Bulletin No. 18, October 1964
[Hoar65]: C. A. R. Hoare. AB21.3.6: Record Handling, Algol Bulletin No. 21, November 1965
[Hoar72]: Hoare, Charles Antony Richard. “Chapter II: Notes on data structuring.” In Structured programming, pp. 83-174. 1972.
[Hoar75]: Hoare, C.A.R. Recursive data structures. International Journal of Computer and Information Sciences 4, 105–132 (1975). doi:10.1007/BF00976239
[Hoar89]: Hoare, C.A.R. Recursive data structures. Essays in computing science. Prentice-Hall, Inc., USA. (1989) doi:10.5555/63445.C1104369
[Holm98]: Holmevik, Jan Rune. “Compiling Simula: A historical study of technological genesis.” (1998) https://staff.um.edu.mt/jskl1/simula.html
[Honeywell72]: Honeywell Series 6000 User Manual
[Howe07]: Jim Howe - ARTIFICIAL INTELLIGENCE AT EDINBURGH UNIVERSITY : A PERSPECTIVE http://www.inf.ed.ac.uk/about/AIhistory.html
[Hud07]: Paul Hudak, John Hughes, Simon Peyton Jones, and Philip Wadler. 2007. A history of Haskell: being lazy with class. In Proceedings of the third ACM SIGPLAN conference on History of programming languages (HOPL III). Association for Computing Machinery, New York, NY, USA, 12–1–12–55. DOI:10.1145/1238844.1238856
[Huda89]: Paul Hudak. 1989. Conception, evolution, and application of functional programming languages. ACM Comput. Surv. 21, 3 (Sep. 1989), 359–411. DOI:10.1145/72551.72554
[Hugh83]: Hughes, Robert John Muir. “The design and implementation of programming languages.” Ph. D. Thesis, Oxford University 130 (1983).
[Hulz83]: Van Hulzen, J. A., & Calmet, J. (1983). Computer Algebra Systems. Computer Algebra, 221–243. doi:10.1007/978-3-7091-7551-4_14
[Hunt77]: R.B. Hunter et al. AB41.4.6 Some ALGOL 68 compilers, Algol Bulletin No. 41, July 1977 http://archive.computerhistory.org/resources/text/algol/algol_bulletin/A41/P46.HTM
[IFIP21]: IFIP Working Group 2.1 on Algorithmic Languages and Calculi https://ifipwg21wiki.cs.kuleuven.be/IFIP21/WebHome
[Ilif61]: ILIFFE, J. K. (1961). “The use of the GENIE System in numerical calculation,” Annual Review in Automatic Programming, Vol. 2, Pergamon Press DOI:10.1016/S0066-4138(61)80002-5
[IRON77]: High Order Language Working Group. “Department of Defense Requirement for High-Order Computer Programming Languages-Revised IRONMAN.” (1977). 5D. RESTRICTIONS ON VALUES
[Jenk71]: Griesmer, J. H., & Jenks, R. D. (1971). SCRATCHPAD/1. Proceedings of the Second ACM Symposium on Symbolic and Algebraic Manipulation - SYMSAC ’71. doi:10.1145/800204.806266
[Jenk74]: Jenks, R. D. (1974). The SCRATCHPAD language. ACM SIGSAM Bulletin, 8(2), 20–30. doi:10.1145/1086830.1086834
[Jenk75b]: Griesmer, J. H., Jenks, R. D., & Yun, D. Y. Y. (1975). A SCRATCHPAD solution to problem #7. ACM SIGSAM Bulletin, 9(3), 13–17. doi:10.1145/1088309.1088314
[Jenk76]: Richard D. Jenks. 1976. A pattern compiler. In Proceedings of the third ACM symposium on Symbolic and algebraic computation (SYMSAC ‘76). Association for Computing Machinery, New York, NY, USA, 60–65. doi:10.1145/800205.806324
[Jenk79]: Jenks, R. D. (1979). SCRATCHPAD/360. ACM SIGSAM Bulletin, 13(1), 16–26. doi:10.1145/1088282.1088285
[Jenk84]: Jenks, R. Scratchpad II, an experimental computer algebra system, abbreviated primer and examples. IBM Thomas Watson Research Center, Yorktown Heights, NY (1984)
[Jone78]: Jones, C.B. (1978). The meta-language: A reference manual. In: Bjørner, D., Jones, C.B. (eds) The Vienna Development Method: The Meta-Language. Lecture Notes in Computer Science, vol 61. Springer, Berlin, Heidelberg. doi:10.1007/3-540-08766-4_10
[Jone78b]: Henhapl, W., Jones, C.B. (1978). A formal definition of ALGOL 60 as described in the 1975 modified report. In: Bjørner, D., Jones, C.B. (eds) The Vienna Development Method: The Meta-Language. Lecture Notes in Computer Science, vol 61. Springer, Berlin, Heidelberg. doi:10.1007/3-540-08766-4_12
[Jone99]: Jones, C.B. (1999). Scientific Decisions which Characterize VDM. In: Wing, J.M., Woodcock, J., Davies, J. (eds) FM’99 — Formal Methods. FM 1999. Lecture Notes in Computer Science, vol 1708. Springer, Berlin, Heidelberg. doi:10.1007/3-540-48119-2_2
[Kidd81]: Tracy Kidder, The Soul Of A New Machine. 1981
[Klee52]: S.C. Kleene. Introduction to Metamathematics, North-Holland Publishing Co. (1952)
[Knut64]: Donald Knuth, AB17.2.4: Man or boy?, Algol Bulletin No. 17, July 1964 http://archive.computerhistory.org/resources/text/algol/algol_bulletin/A17/P24.HTM
[Knut73]: Knuth, Donald Ervin. A Review of “Structured Programming”. Stanford, California: Computer Science Department, Stanford University, 1973.
[Korn22]: Körner P, Leuschel M, Barbosa J, Costa VS, Dahl V, Hermenegildo MV, Morales JF, Wielemaker J, Diaz D, Abreu S, Ciatto G. Fifty years of Prolog and beyond. Theory and Practice of Logic Programming. 2022 Nov;22(6):776-858.
[Kowa74]: Kowalski, Robert. Logic for problem solving. Department of Computational Logic, Edinburgh University, 1974.
[Kowa88]: Robert A. Kowalski. 1988. The early years of logic programming. Commun. ACM 31, 1 (Jan. 1988), 38–43. doi:10.1145/35043.35046
[KRC81]: EMAS KRC https://www.cs.kent.ac.uk/people/staff/dat/krc/emas-krc.tgz
[KRC2016]: UNIX KRC https://www.cs.kent.ac.uk/people/staff/dat/krc/krc-2016-03-31.tgz
[Land64]: Landin, Peter J. “The mechanical evaluation of expressions.” The computer journal 6, no. 4 (1964): 308-320. DOI: 10.1093/comjnl/6.4.308
[Land65a]: Landin, Peter J. “Correspondence between ALGOL 60 and Church’s Lambda-notation: part I.” Communications of the ACM 8, no. 2 (1965): 89-101. DOI: 10.1145/363744.363749
[Land65b]: Landin, Peter J. “A correspondence between ALGOL 60 and Church’s Lambda-notations: Part II.” Communications of the ACM 8, no. 3 (1965): 158-167. DOI: 10.1145/363791.363804
[Land66]: Landin PJ. The next 700 programming languages. Communications of the ACM. 1966 Mar 1;9(3):157-66. DOI: 10.1145/365230.365257
[Land66b]: Landin, Peter J. A λ-Calculus Approach in Advances in programming and non-numerical computation. 1966
[Land98]: Landin, Peter J. “A generalization of jumps and labels.” Higher-Order and Symbolic Computation 11, no. 2 (1998): 125-143. doi:10.1023/a:1010068630801
[Landin]: Archive of Peter Landin https://archives.bodleian.ox.ac.uk/repositories/2/resources/9658
[Lavi12]: Lavington, Simon. “The Atlas story.” In Atlas Symposium, vol. 6. 2012.
[LCF77]: Code for LCF Version 5, Oct 1977 https://github.com/theoremprover-museum/LCF77
[Leav71]: B. M. Leavenworth. 1971. Transition functions: A method for semantic extensions. In Proceedings of the international symposium on Extensible languages. Association for Computing Machinery, New York, NY, USA, 96–103. doi:10.1145/800006.807989
[Lennox]: The GTL Programming Language http://web.archive.org/web/20020601161934/http://www.lennox.com.au/products/gtl.html
[Levi75]: Levi, G., Sirovich, F. (1975). Proving program properties, symbolic evaluation and logical procedural semantics. In: Bečvář, J. (eds) Mathematical Foundations of Computer Science 1975 4th Symposium, Mariánské Lázně, September 1–5, 1975. MFCS 1975. Lecture Notes in Computer Science, vol 32. Springer, Berlin, Heidelberg. doi:10.1007/3-540-07389-2_211
[Levi82]: Bellia, Marco, Pierpaolo Degano, and Giorgio Levi. “The call by name semantics of a clause language with functions.” Logic Programming 1 (1982): J82.
[Li96]: Li, X. (1996). Program Sharing: A new implementation approach for Prolog. Programming Languages: Implementations, Logics, and Programs, 259–273. doi:10.1007/3-540-61756-6_90
[Lieb80]: Lieberman H, Hewitt C. A Real Time Garbage Collector that can Recover Temporary Storage Quickly. MASSACHUSETTS INST OF TECH CAMBRIDGE ARTIFICIAL INTELLIGENCE LAB; 1980 Apr.
[Ligh72]: James Lighthill, Artificial Intelligence: A General Survey (1972) http://www.chilton-computing.org.uk/inf/literature/reports/lighthill_report/p001.htm
[Lind74a]: C.H. Lindsey, AB37.4.2: Partial Parametrization, Algol Bulletin No. 37, July 1974 http://archive.computerhistory.org/resources/text/algol/algol_bulletin/A37/P42.HTM
[Lind74b]: C.H. Lindsey, AB37.4.3: Modals, Algol Bulletin No. 37, July 1974 http://archive.computerhistory.org/resources/text/algol/algol_bulletin/A37/P43.HTM
[Lind76]: C.H. Lindsey, AB39.3.1: Specification of Partial Parametrization Proposal, Algol Bulletin No. 39, February 1976 http://archive.computerhistory.org/resources/text/algol/algol_bulletin/A39/P31.HTM
[Lind76b]: C.H. Lindsey, AB39.4.2: Proposal for a Modules Facility in ALGOL 68, Algol Bulletin No. 39, February 1976 http://archive.computerhistory.org/resources/text/algol/algol_bulletin/A39/P42.HTM
[Lind78]: C. H. Lindsey and H. J. Boom, AB43.3.2: A Modules and Separate Compilation facility for ALGOL 68, Algol Bulletin No. 43, December 1978 http://archive.computerhistory.org/resources/text/algol/algol_bulletin/A43/P32.HTM
[Lind83]: C. H. Lindsey. AB49.1.1 Hans Bekic, Algol Bulletin No. 49, May 1983 http://archive.computerhistory.org/resources/text/algol/algol_bulletin/A49/P11.HTM
[Lind93]: C. H. Lindsey. 1993. A history of ALGOL 68. In The second ACM SIGPLAN conference on History of programming languages (HOPL-II). Association for Computing Machinery, New York, NY, USA, 97–132. doi:10.1145/154766.155365
[Lisk74]: Barbara Liskov and Stephen Zilles. 1974. Programming with abstract data types. In Proceedings of the ACM SIGPLAN symposium on Very high level languages. Association for Computing Machinery, New York, NY, USA, 50–59. doi:10.1145/800233.807045
[Lisk93]: Liskov, B. (1993). A history of CLU. ACM SIGPLAN Notices, 28(3), 133–147. doi:10.1145/155360.155367
[Lond78]: London, Thomas B., and John F. Reiser. “A UNIX™ Operating System for the DEC VAX-11/780 Computer.”
[MACLISP]: MACLISP changelog LISP-NEWS.DOC
[MacQueen]: David MacQueen at LinkedIn https://www.linkedin.com/in/david-macqueen-463788b
[MacQ14]: Luca Cardelli and the Early Evolution of ML, by David MacQueen. A paper presented at the Luca Cardelli Fest at Microsoft Research Cambridge on Sept. 8, 2014.
[MacQ15]: MacQueen, David B. The History of Standard ML: Ideas, Principles, Culture https://www.youtube.com/watch?v=NVEgyJCTee4
[MacQ20]: MacQueen, David B., Robert Harper and John H. Reppy. “The history of Standard ML.” Proceedings of the ACM on Programming Languages 4 (2020): 1 - 100.DOI:10.1145/3386336
[MacQ22]: D. MacQueen, A New Match Compiler for Standard ML of New Jersey https://icfp22.sigplan.org/details/mlfamilyworkshop-2022-papers/3/A-New-Match-Compiler-for-Standard-ML-of-New-Jersey
[Mann70]: Manna, Zohar, and John McCarthy. Properties of programs and partial function logic. Machine Intelligence 5 (Meltzer & Michie, Eds.). American Elsevier, New York 79 (1970): 27.
[Mann71]: Zohar Manna and Richard J. Waldinger. 1971. Toward automatic program synthesis. Commun. ACM 14, 3 (March 1971), 151–165. doi:10.1145/362566.362568
[Mart76]: Martelli A., Montanari U. Unification in linear time and space: a structured presentation. Internal note IEI-B76-16, 1976.
[Mart82]: Alberto Martelli and Ugo Montanari. 1982. An Efficient Unification Algorithm. ACM Trans. Program. Lang. Syst. 4, 2 (April 1982), 258–282. doi:10.1145/357162.357169
[Mart2008]: Martelli, A. (2008). The Seventies. In: Degano, P., De Nicola, R., Meseguer, J. (eds) Concurrency, Graphs and Models. Lecture Notes in Computer Science, vol 5065. Springer, Berlin, Heidelberg. doi:10.1007/978-3-540-68679-8_49
[Matt83]: Matthews, David Charles James. Programming language design with polymorphism. No. UCAM-CL-TR-49. University of Cambridge, Computer Laboratory, 1983.
[McBr69]: McBride, F. V., D. J. T. Morrison, and R. M. Pengelly. “A symbol manipulation system.” Machine Intelligence 5 (1969): 337-347.
[McCa58]: J. McCarthy: An Algebraic Language for the Manipulation of Symbolic Expressions. MIT AI Lab., AI Memo No. 1, Cambridge, Sept. 1958.
[McCa60]: John McCarthy. 1960. Recursive functions of symbolic expressions and their computation by machine, Part I. Commun. ACM 3, 4 (April 1960), 184–195. doi:10.1145/367177.367199
[McCa60b]: J. McCarthy, R. Brayton, D. Edwards, P. Fox, L. Hodes, D. Luckham, K. Maling, D. Park and S. Russell. LISP I Programmer’s Manual. Computation Center and Research Laboratory of Electronics, Massachusetts Institute of California, March 1, 1960. https://www.softwarepreservation.org/projects/LISP/book/LISP%20I%20Programmers%20Manual.pdf
[McCa61]: John McCarthy. 1961. A basis for a mathematical theory of computation, preliminary report. In Papers presented at the May 9-11, 1961, western joint IRE-AIEE-ACM computer conference (IRE-AIEE-ACM ‘61 (Western)). Association for Computing Machinery, New York, NY, USA, 225–238. doi:10.1145/1460690.1460715
[McCa62]: McCarthy, J., Abrahams, P. W., Edwards, D. J., Hart, T. P., & Levin, M. I. (1962). LISP 1.5 programmer’s manual. MIT press.
[McCa64]: J. McCarthy, AB18.3.12: Definition of new data types in ALGOL x, Algol Bulletin No. 18, October 1964
[McCa74]: McCarthy, J. (1974). Artificial intelligence: a paper symposium. Artificial Intelligence, 5(3), 317–322. doi:10.1016/0004-3702(74)90016-2
[McCa78]: McCarthy J. History of LISP. In History of programming languages 1978 Jun 1 (pp. 173-185).
[McDe84]: Drew McDermott et al., “The Dark Ages of AI: A Panel Discussion at AAAI-84,” AI Magazine, Fall 1984, pp. 122-34.
[McKus]: Marshall Kirk McKusick. A BERKELEY ODYSSEY: Ten years of BSD history.
[Miln72]: Robin Milner. 1972. Implementation and applications of Scott’s logic for computable functions. SIGPLAN Not. 7, 1 (January 1972), 1–6. doi:10.1145/942578.807067
[Miln75]: Milner, R., Morris, L., Newey, M.: A logic for computable functions with reflexive and polymorphic types. Proving and improving programs, Arc et Senans 1975, pp. 371-394. IRIA, Cahier, 1975
[Miln76]: Milner R. Program Semantics and mechanized proof. In Mathematical Centre Tracts. Vol. 82. Amsterdam: Mathematisch Centrum. 1976. p. 3-44
[Miln78]: Milner, R. (1978). A theory of type polymorphism in programming. Journal of Computer and System Sciences, 17(3), 348–375. doi:10.1016/0022-0000(78)90014-4
[Miln82]: Milner, Robin. “How ML evolved.” (1982).
[MilnBird84]: Milner, R., & Bird, R. S. (1984). The Use of Machines to Assist in Rigorous Proof [and Discussion]. Philosophical Transactions of the Royal Society A: Mathematical, Physical and Engineering Sciences, 312(1522), 411–422. doi:10.1098/rsta.1984.0067
[Miln90]: Milner, Robin, Mads Tofte, and Robert Harper. “The definition of Standard ML.” (1990).
[Miln93]: Frenkel, Karen A. and Robin Milner. “An interview with Robin Milner.” Commun. ACM 36 (1993): 90-97. DOI:10.1145/151233.151241
[Miln2003]: interview with Robin Milner, held in Cambridge on the 3. September 2003 http://users.sussex.ac.uk/~mfb21/interviews/milner/
[Moon74]: David A. Moon. MacLISP Reference Manual, Revision 0. Project MAC, Massachusetts Institute of Technology, April 8, 1974
[Moor73]: Moore, J Strother. Computational Logic: Structure sharing and proof of program properties. http://hdl.handle.net/1842/2245
[Moor75]: Moore, J Strother. Introducing iteration into the pure LISP theorem prover. IEEE Transactions on Software Engineering 3 (1975): 328-338.
[Moor82]: Ian W. Moor. 1982. An applicative compiler for a parallel machine. SIGPLAN Not. 17, 6 (June 1982), 284–293. DOI:10.1145/872726.807002
[Moor13]: Moore, J Strother and Claus-Peter Wirth. “Automation of Mathematical Induction as part of the History of Logic.” ArXiv abs/1309.6226 (2013)
[Moor18]: J Strother Moore, The PLTP Archive. https://www.cs.utexas.edu/users/moore/best-ideas/pltp/index.html
[Moor18b]: J Strother Moore, The PLTP Archive. Listing I https://www.cs.utexas.edu/users/moore/best-ideas/pltp/scanned-images/Listing-I-OCR.pdf
[Moor18c]: J Strother Moore, Structure Sharing https://www.cs.utexas.edu/users/moore/best-ideas/structure-sharing/index.html
[Morr68]: Morris, J.H.: Lambda calculus models of programming languages. M.I.T. MAC-TR-57, 1968
[Morr72]: James H. Morris. 1972. A bonus from van Wijngaarden’s device. Commun. ACM 15, 8 (Aug. 1972), 773. doi:10.1145/361532.361558
[Morr73a]: James H. Morris. 1973. Types are not sets. In Proceedings of the 1st annual ACM SIGACT-SIGPLAN symposium on Principles of programming languages (POPL ‘73). Association for Computing Machinery, New York, NY, USA, 120–124. doi:10.1145/512927.512938
[Morr73b]: James H. Morris. 1973. Protection in programming languages. Commun. ACM 16, 1 (Jan. 1973), 15–21. doi:10.1145/361932.361937
[Morr93]: Morris, L. The next 700 formal language descriptions. LISP and Symbolic Computation 6, 249–257 (1993). doi:10.1007/BF01019460
[Mose70]: Joel Moses. 1970. The function of FUNCTION in LISP or why the FUNARG problem should be called the environment problem. SIGSAM Bull., 15 (July 1970), 13–27. doi:10.1145/1093410.1093411
[Mose74]: Joel Moses. 1974. MACSYMA - the fifth year. SIGSAM Bull. 8, 3 (August 1974), 105–110. doi:10.1145/1086837.1086857
[Mose08]: Moses, J. (2012). Macsyma: A personal history. Journal of Symbolic Computation, 47(2), 123–130. doi:10.1016/j.jsc.2010.08.018
[Mull73]: Mullish, Henry, and Max Goldstein. A SETLB Primer: With Over 100 Illustrative Programettes. Courant Institute of Mathematical Sciences, New York University, 1973.
[MULTICS1]: https://www.multicians.org/benchmarks.html#INRIA
[Muss74]: Ralph L. London and David R. Musser. 1974. The application of a symbolic mathematical system to program verification. In Proceedings of the 1974 annual conference - Volume 1 (ACM ‘74). Association for Computing Machinery, New York, NY, USA, 265–273. doi:10.1145/800182.810412
[Muss80a]: Musser, D. R. (1980). Abstract Data Type Specification in the Affirm System. IEEE Transactions on Software Engineering, SE-6(1), 24–32. doi:10.1109/tse.1980.230459
[Muss80b]: David R. Musser. 1980. On proving inductive properties of abstract data types. In Proceedings of the 7th ACM SIGPLAN-SIGACT symposium on Principles of programming languages (POPL ‘80). Association for Computing Machinery, New York, NY, USA, 154–162. doi:10.1145/567446.567461
[Naur64]: P. Naur. Proposals for a new language, Algol Bulletin No. 18, October 1964 http://archive.computerhistory.org/resources/text/algol/algol_bulletin/A18/P39.HTM
[Naur66]: P. Naur. The form of specifications, Algol Bulletin No. 22, February 1966 http://archive.computerhistory.org/resources/text/algol/algol_bulletin/A22/P37.HTM
[Naur78]: Naur, Peter. “The European side of the last phase of the development of ALGOL 60.” In History of programming languages, pp. 92-139. 1978.
[Neum23]: Von JOHANN v. NEUMANN, Zur Einführung der transfiniten Zahlen. Acta litt. Acad. Sc. Szeged X. 1(1923) p.199-208
[Newe75]: Newey, Malcolm C.. “Formal semantics of lisp with applications to program correctness.” (1975).
[NUPRL2002]: NuPRL Manual https://nuprl.org/html/02cucs-NuprlManual-appendixB.pdf
[O’Do82]: Christoph M. Hoffmann and Michael J. O’Donnell. 1982. Programming with Equations. ACM Trans. Program. Lang. Syst. 4, 1 (Jan. 1982), 83–112. doi:10.1145/357153.357158
[O’Do84]: Christoph M. Hoffmann and Michael J. O’Donnell. 1984. Implementation of an interpreter for abstract equations. In Proceedings of the 11th ACM SIGACT-SIGPLAN symposium on Principles of programming languages (POPL ‘84). Association for Computing Machinery, New York, NY, USA, 111–121. doi:10.1145/800017.800522
[O’Do87]: O’Donnell, M.J. (1987). Term-rewriting implementation of equational logic programming. In: Lescanne, P. (eds) Rewriting Techniques and Applications. RTA 1987. Lecture Notes in Computer Science, vol 256. Springer, Berlin, Heidelberg. doi:10.1007/3-540-17220-3_1
[O’Ke83]: R. A. O’Keefe. 1983. Prolog compared with LISP? SIGPLAN Not. 18, 5 (May 1983), 46–56. doi:10.1145/948249.948255
[Padg88]: Padget, Julian. “Three Uncommon Lisps.” In First International Workshop on Lisp Evolution and Standardization. 1988.
[PAL360]: PAL https://www.softwarepreservation.org/projects/lang/PAL/
[Part84]: Partsch, H. (1984). The CIP Transformation System. In: Pepper, P. (eds) Program Transformation and Programming Environments. NATO ASI Series, vol 8. Springer, Berlin, Heidelberg. doi:10.1007/978-3-642-46490-4_27
[Pate76]: M. S. Paterson and M. N. Wegman. 1976. Linear unification. In Proceedings of the eighth annual ACM symposium on Theory of computing (STOC ‘76). Association for Computing Machinery, New York, NY, USA, 181–186. doi:10.1145/800113.803646
[Perl78]: Alan J. Perlis. 1978. The American side of the development of ALGOL. History of programming languages. Association for Computing Machinery, New York, NY, USA, 75–91. doi:10.1145/800025.1198352
[Pett78]: Pettorossi, A. (1978). Improving memory utilization in transforming recursive programs. In: Winkowski, J. (eds) Mathematical Foundations of Computer Science 1978. MFCS 1978. Lecture Notes in Computer Science, vol 64. Springer, Berlin, Heidelberg. doi:10.1007/3-540-08921-7_89
[Pigo95]: Diarmuid Pigott, HOPL: an interactive Roster of Programming Languages, HOPE https://hopl.info/showlanguage.prx?exp=810
[Plot2000]: Plotkin, Gordon D., Colin Stirling, and Mads Tofte. “A brief scientific biography of Robin Milner.” In Proof, Language, and Interaction, pp. 1-18. 2000. DOI:10.7551/mitpress/5641.003.0004
[Plot10]: Gordon Plotkin - Robin Milner: A Craftsman of Tools for the Mind https://www.youtube.com/watch?v=Jg5VCLb2cMo
[Popp2002]: Robin Popplestone. 2002. POP, A Broad-Spectrum Programming Language, 1967–2002. Form. Asp. Comput. 13, 3–5 (Jul 2002), 196–213. doi:10.1007/s001650200009
[Popplestone]: Popplestone, R. J. The Early Development of POP https://www-robotics.cs.umass.edu/Popplestone/pop_development.html
[Prat73]: Vaughan R. Pratt. 1973. Top down operator precedence. In Proceedings of the 1st annual ACM SIGACT-SIGPLAN symposium on Principles of programming languages (POPL ‘73). Association for Computing Machinery, New York, NY, USA, 41–51. doi:10.1145/512927.512931
[Prolog]: Prolog and Logic Programming Historical Sources Archive https://www.softwarepreservation.org/projects/prolog/index.html#Edinburgh
[Prolog75]: PROLOG to DEC 10 Machine Code Compiler, Version 13 Sep 1975 https://www.softwarepreservation.org/projects/prolog/edinburgh/src/Warren-Prolog_Compiler-1975_09_13.pdf
[Prolog81]: DEC10 Prolog version 3 https://saildart.org/[PRO,SYS]/
[Quar86]: JOHN S. QUARTERMAN, ABRAHAM SILBERSCHATZ, and JAMES L. PETERSON. 4.2BSD and 4.3BSD as Examples of the UNIX System
[RABBIT]: RABBIT Scheme compiler http://www.cs.cmu.edu/afs/cs/project/ai-repository/ai/lang/scheme/impl/rabbit/rabbit.lsp
[Rand64]: Randell, Brian. & Russell, L. J. (1964). ALGOL 60 implementation : the translation and use of ALGOL 60 programs on a computer. London ; New York : Published for the Automatic Programming Information Centre, Brighton College of Technology, England, by Academic Press
[Rees2010]: Jonathan Rees, The T Project http://mumble.net/~jar/tproject/
[Reev86]: Peter G. Harrison and Mike Reeve. 1986. The parallel graph reduction machine, Alice. In Proceedings of the Workshop on Graph Reduction. Springer-Verlag, Berlin, Heidelberg, 181–202.
[Reyn69]: Reynolds, John C.. “GEDANKEN: A SIMPLE TYPELESS LANGUAGE WHICH PERMITS FUNCTIONAL DATA STRUCTURES AND COROUTINES.” (1969).
[Reyn70]: Reynolds, John C.. “GEDANKEN—a simple typeless language based on the principle of completeness and the reference concept.” Communications of the ACM 13 (1970): 308 - 319.
[Reyn72]: John C. Reynolds. 1972. Definitional interpreters for higher-order programming languages. In Proceedings of the ACM annual conference - Volume 2 (ACM ‘72). Association for Computing Machinery, New York, NY, USA, 717–740. doi:10.1145/800194.805852
[Reyn74]: Reynolds, John C. “Towards a theory of type structure.” In Programming Symposium, pp. 408-425. Springer, Berlin, Heidelberg, 1974.
[Reyn93]: Reynolds, John C. “The discoveries of continuations.” Lisp and symbolic computation 6, no. 3 (1993): 233-247. doi:10.1007/bf01019459
[Reyn98]: Reynolds, John C. “Definitional interpreters revisited.” Higher-Order and Symbolic Computation 11, no. 4 (1998): 355-361. doi:10.1023/A:1010075320153
[Reyn2012]: John C. Reynolds https://www.softwarepreservation.org/projects/lang/GEDANKEN
[Rich13]: Richards, Martin. “How BCPL Evolved from CPL.” Comput. J. 56 (2013): 664-670.
[Rich2000]: Richards, M. Christopher Strachey and the Cambridge CPL Compiler (2000). Higher-Order and Symbolic Computation, 13(1/2), 85–88. doi:10.1023/a:1010014110806
[Rich69]: Richards, M. (1969, May). BCPL: A tool for compiler writing and system programming. In Proceedings of the May 14-16, 1969, spring joint computer conference (pp. 557-566). doi:10.1145/1476793.1476880
[Rich74]: RICHARDS, Martin; EVANS JR, Arthur; MABEE, Robert F. The BCPL reference manual. MASSACHUSETTS INST OF TECH CAMBRIDGE PROJECT MAC, 1974.
[Ritc93]: Ritchie, Dennis M. “The development of the C language.” ACM Sigplan Notices 28.3 (1993): 201-208.
[Robi76]: Robinson, Lawrence, and Oliver Roubine. Special: A specification and assertion language. Menlo Park, Ca.: Stanford Research Institute, 1976.
[Roch71]: Arnold Rochfeld. 1971. New LISP techniques for a paging environment. Commun. ACM 14, 12 (Dec. 1971), 791–795. doi:10.1145/362919.362937
[Rosetta1]: Man or boy test in Pascal https://rosettacode.org/wiki/Man_or_boy_test#Pascal
[Ross61]: ROSS, DOUGLAS T., and STEVEN A. COONS. INVESTIGATIONS IN COMPUTER-AIDED DESIGN. MASSACHUSETTS INST OF TECH CAMBRIDGE ELECTRONIC SYSTEMS LAB, 1961.
[Ruti67]: Rutishauser, Heinz. “Description of ALGOL 60, volume 1, edited by Bauer, FL.” (1967).
[Ryde82]: Rydeheard, David Eric. “Applications of category theory to programming and program specification.” (1982).
[Ryde2002]: RYDEHEARD, D. E., & SANNELLA, D. T. (2002). A Collection of Papers and Memoirs Celebrating the Contribution of Rod Burstall to Advances in Computer Science. Formal Aspects of Computing, 13(3-5), 187–193. doi:10.1007/s001650200006
[Salu94]: Salus PH. A quarter century of UNIX. ACM Press/Addison-Wesley Publishing Co.; 1994 Dec.
[Same65]: K. Samelson, AB20.3.3. Functionals and functional transformations, Algol Bulletin No. 20, July 1965 https://archive.computerhistory.org/resources/text/algol/algol_bulletin/A20/P33.HTM
[Sann82]: Sannella, Donald. “Semantics, implementation and pragmatics of Clear, a program specification language.” (1982).
[Sann94]: Sannella, Donald and Martin Wirsing. “Specification Languages” (1994).
[Sann14]: D. Sannella, CV https://homepages.inf.ed.ac.uk/dts/cv.html
[Scho67]: H. Schorr and W. M. Waite. 1967. An efficient machine-independent procedure for garbage collection in various list structures. Commun. ACM 10, 8 (Aug. 1967), 501–506. doi:10.1145/363534.363554
[Schu74]: S.A. Schuman, AB37.4.1: Toward Modular Programming in High-Level Languages, Algol Bulletin No. 37, July 1974 http://archive.computerhistory.org/resources/text/algol/algol_bulletin/A37/P41.HTM
[Schw71]: Jacob T. Schwartz. Abstract algorithms and a set theoretic language for their expression. Computer Science Department, Courant Institute of Mathematical Sciences, New York University. Preliminary draft, first part. 1970-1971, 16+289 pages. This copy scanned from NYU Library, courtesy of Alex Kennedy-Grant. http://www.softwarepreservation.net/projects/SETL/setl/doc/Schwartz-Abstract_Algorithms-1971.pdf
[Schw82]: Schwarz, J. (1982). Using Annotations to Make Recursion Equations Behave. IEEE Transactions on Software Engineering, SE-8(1), 21–33. doi:10.1109/tse.1982.234771
[Somm77]: Sommerville JF. An experiment in high-level microprogramming. University of St. Andrews (United Kingdom); 1977.
[SPJ82]: Simon L Peyton Jones. 1982. An investigation of the relative efficiencies of combinators and lambda expressions. In Proceedings of the 1982 ACM symposium on LISP and functional programming (LFP ‘82). Association for Computing Machinery, New York, NY, USA, 150–158. doi:10.1145/800068.802145
[SPJ85]: Jones, S. L. P. (1985). Yacc in sasl — an exercise in functional programming. Software: Practice and Experience, 15(8), 807–820. doi:10.1002/spe.4380150807
[SPJ87]: Peyton Jones, Simon L. The implementation of functional programming languages (prentice-hall international series in computer science). Prentice-Hall, Inc., 1987.
[Scot93]: Dana S. Scott, A type-theoretical alternative to ISWIM, CUCH, OWHY, Theoretical Computer Science, Volume 121, Issues 1–2, 1993, Pages 411-440, ISSN 0304-3975, doi:10.1016/0304-3975(93)90095-B
[Shivers]: Olin Shivers, History of T http://www.paulgraham.com/thist.html
[Slom89]: Sloman, Aaron. “The Evolution of Poplog and Pop-11 at Sussex University.” POP-11 Comes of Age: The Advancement of an AI Programming Language (1989): 30-54.
[Smit73]: Smith, David Canfield and Enea, Horace J. (1973) MLISP 2 http://i.stanford.edu/TR/CS-TR-73-356.html
[Stee75]: Sussman, Gerald J. and Guy L. Steele. “An Interpreter for Extended Lambda Calculus: SCHEME,.” (1975).
[Stee75b]: Guy L. Steele. 1975. Multiprocessing compactifying garbage collection. Commun. ACM 18, 9 (Sept. 1975), 495–508. doi:10.1145/361002.361005
[Stee76]: Steele Jr, Guy Lewis. “LAMBDA: The ultimate declarative.” (1976).
[Stee76b]: Steele, Guy L. and Gerald J. Sussman. “Lambda: The Ultimate Imperative.” (1976).
[Stee77]: Guy Lewis Steele. 1977. Debunking the “expensive procedure call” myth or, procedure call implementations considered harmful or, LAMBDA: The Ultimate GOTO. In Proceedings of the 1977 annual conference (ACM ‘77). Association for Computing Machinery, New York, NY, USA, 153–162. doi:10.1145/800179.810196
[Stee77m]: Guy Lewis Steele. 1977. Macaroni is better than spaghetti. In Proceedings of the 1977 symposium on Artificial intelligence and programming languages. Association for Computing Machinery, New York, NY, USA, 60–66. https://doi.org/10.1145/800228.806933
[Stee77b]: Steele Jr GL. Fast Arithmetic in MacLISP. MASSACHUSETTS INST OF TECH CAMBRIDGE ARTIFICIAL INTELLIGENCE LAB; 1977 Sep 1.
[Stee78]: Steele, Jr. Guy L. “Rabbit: A Compiler for Scheme.” (1978).
[Stee82]: Guy L. Steele. 1982. An overview of COMMON LISP. In Proceedings of the 1982 ACM symposium on LISP and functional programming (LFP ‘82). Association for Computing Machinery, New York, NY, USA, 98–107. doi:10.1145/800068.802140
[Stee96]: Guy L. Steele and Richard P. Gabriel. 1996. The evolution of Lisp. History of programming languages—II. Association for Computing Machinery, New York, NY, USA, 233–330. doi:10.1145/234286.1057818
[Stee98]: Sussman, Gerald Jay, and Guy L. Steele Jr. “The first report on Scheme revisited.” Higher-Order and Symbolic Computation 11, no. 4 (1998): 399-404.
[Stra61]: Strachey, C., & Wilkes, M. V. (1961). Some proposals for improving the efficiency of ALGOL 60. Communications of the ACM, 4(11), 488-491.
[Stra66]: BARRON D. W. and STRACHEY C. Programming in Advances in programming and non-numerical computation. 1966
[Stra66b]: CPL Elementary Programming Manual https://web.archive.org/web/20190813130006/http://www.ancientgeek.org.uk/CPL/CPL_Elementary_Programming_Manual.pdf
[Stra67]: Strachey, Christopher S.. “Fundamental Concepts in Programming Languages.” Higher-Order and Symbolic Computation 13 (2000): 11-49.
DOI:10.1023/A:1010000313106
[Strachey]: Catalogue of the papers and correspondence of Christopher Strachey https://archives.bodleian.ox.ac.uk/repositories/2/resources/2561
[Stro70]: H. R. Strong. 1970. Translating recursion equations into flow charts. In Proceedings of the second annual ACM symposium on Theory of computing (STOC ‘70). Association for Computing Machinery, New York, NY, USA, 184–197. doi:10.1145/800161.805164
[Stro93]: Bjarne Stroustrup. 1993. A history of C++: 1979–1991. In The second ACM SIGPLAN conference on History of programming languages (HOPL-II). Association for Computing Machinery, New York, NY, USA, 271–297. doi:10.1145/154766.155375
[Thom90]: Lins, Rafael Dueire, and Simon J. Thompson. “Implementing SASL using categorical multi-combinators.” Software - Practice and Experience 20, no. 11 (1990): 1137-1165.
[TITAN]: Cambridge Atlas http://www.chilton-computing.org.uk/acl/technology/atlas/p011.htm
[Teit74]: Teitelman, Warren. “The interlisp reference manual.” (1974). https://www.softwarepreservation.org/projects/LISP/interlisp/Interlisp-Oct_1974.pdf
[Teit78]: Warren Teitelman with contributions by J. W. Goodwin, A. K. Hartley, D. C. Lewis, J. J. Vittal, M. D. Yonke, D. G. Bobrow, R. M. Kaplan, L. M. Masinter, and B. A. Sheil. Interlisp Reference Manual. Bolt, Beranek & Newman and Xerox Corporation, October 1978. https://www.softwarepreservation.org/projects/LISP/interlisp/Interlisp-Oct_1978.pdf
[Teit2008]: Teitelman, W. (2008). History of Interlisp. Celebrating the 50th Anniversary of Lisp on - LISP50. doi:10.1145/1529966.1529971
[Tenn77]: Tennent, R. D. (1977). On a new approach to representation independent data classes. Acta Informatica, 8(4), 315–324. doi:10.1007/bf00271340
[Turn79]: Turner, D. A. (1979). A new implementation technique for applicative languages. Software: Practice and Experience, 9(1), 31–49. doi:10.1002/spe.4380090105
[Turn79b]: Turner, D. A. (1979). Another algorithm for bracket abstraction . The Journal of Symbolic Logic, 44(02), 267–270. doi:10.2307/2273733
[Turn81]: D. A. Turner. 1981. The semantic elegance of applicative languages. In Proceedings of the 1981 conference on Functional programming languages and computer architecture (FPCA ‘81). Association for Computing Machinery, New York, NY, USA, 85–92. doi:10.1145/800223.806766
[Turn82]: Turner, D.A. (1982). Recursion Equations as a Programming Language. In: Darlington, John, David Turner and Peter B. Henderson. “Functional Programming and its Applications: An Advanced Course.”
[Turn83]: Turner, D. A. “SASL language manual (revised version).” University of Kent (1983).
[Turn12]: Turner DA. Some history of functional programming languages. In International Symposium on Trends in Functional Programming 2012 Jun 12 (pp. 1-20). Springer, Berlin, Heidelberg.
[Turn16]: Kent Recursive Calculator https://www.cs.kent.ac.uk/people/staff/dat/krc/
[Turn19]: David Turner. 2019 Peter Landin Semantics Seminar https://www.youtube.com/watch?v=ezFZIPuSQU8
[Vase85]: Vasey, Philip Edgar. First-order logic applied to the description and derivation of programs. (1985).
[Vuil73]: Jean Vuillemin. 1973. Correct and optimal implementations of recursion in a simple programming language. In Proceedings of the fifth annual ACM symposium on Theory of computing (STOC ‘73). Association for Computing Machinery, New York, NY, USA, 224–239. doi:10.1145/800125.804054
[Vuil74]: B. Courcelle and J. Vuillemin. 1974. Semantics and axiomatics of a simple recursive language. In Proceedings of the sixth annual ACM symposium on Theory of computing (STOC ‘74). Association for Computing Machinery, New York, NY, USA, 13–26. doi:10.1145/800119.803880
[Wads71]: Wadsworth, P. L. “Semantics and paragmatics of the lambda calculus.” PhD thesis, Oxford Univ. (1971).
[Wads2000]: Wadsworth, C.P. Continuations Revisited. Higher-Order and Symbolic Computation 13, 131–133 (2000). doi:10.1023/a:1010074329461
[Walt75]: Waltz, David L. “Understanding line drawings of scenes with shadows.” The psychology of computer vision (1975): 19-91.
http://www1.cs.columbia.edu/~waltz/Papers/Understanding%20Line%20Drawing%20of%20Scenes%20with%20Shadows%20PH%20Winston%201975.pdf
[Wand77]: Wand, M. Algebraic theories and tree rewriting systems. Indiana University. Computer Science Department Technical Report 66 (1977).
[Wand80]: Wand, M. First-order identities as a defining language. Acta Informatica 14, 337–357 (1980). doi:10.1007/BF00286491
[Warr75]: David H. D. Warren. Example 1: Quicksort. Circa 1975. Example of generated code from PROLOG to DEC 10 Machine Code Compiler. https://www.softwarepreservation.org/projects/prolog/edinburgh/src/Warren-Prolog_Compiler_Example_1.pdf
[Warr77]: David H D Warren, Luis M. Pereira, and Fernando Pereira. 1977. Prolog - the language and its implementation compared with Lisp. In Proceedings of the 1977 symposium on Artificial intelligence and programming languages. Association for Computing Machinery, New York, NY, USA, 109–115. doi:10.1145/800228.806939
[Warr77b]: David H D Warren. 1977. Prolog - the language and its implementation compared with Lisp. Slides https://www.softwarepreservation.org/projects/prolog/edinburgh/doc/slides-ACM1977.pdf
[Warr78]: Warren, David HD. “Applied logic: its use and implementation as a programming tool.” (1978).
[Warr81]: Warren, David HD. Higher-order extensions to Prolog - are they needed? Department of Artificial Intelligence, University of Edinburgh, 1981.
[Weinreb]: Dan Weinreb on NIL http://www.paulgraham.com/weinreb.html
[Weiz68]: Weizenbaum, Joseph. “The FUNARG Problem Explained. unpublished memorandum.” (1968).
[Well76]: Welliver, Leon. “IDEA: a symbolic integration program.” PhD diss., University of St Andrews, 1976.
[Whit70]: White, John L.. “An Interim LISP User’s Guide.” (1970).
[Whit77]: White, Jon L. “Lisp: Program is Data: A historical perspective on MACLISP.” In Proceedings of the 1977 MACSYMA Users’ Conference, MIT Laboratory for Computer Science, Cambridge, Mass, pp. 181-189. 1977.
[Whit78]: Jon L White. 1978. LISP/370: a short technical description of the implementation. SIGSAM Bull. 12, 4 (November 1978), 23–27. doi:10.1145/1088276.1088280
[Whit79]: Jon L. White. NIL: A perspective. Proceedings of 1979 MACSYMA Users’ Conference, Washington, D.C., June 1979. https://www.softwarepreservation.org/projects/LISP/MIT/White-NIL_A_Perspective-1979.pdf
[Whit80]: White JL. Address/memory management for a gigantic Lisp environment or, GC considered harmful. InProceedings of the 1980 ACM Conference on LISP and Functional Programming 1980 Aug 25 (pp. 119-127).
[Wich76]: Wichmann, B.A. Ackermann’s function: A study in the efficiency of calling procedures. BIT 16, 103–110 (1976). doi:10.1007/BF01940783
[Wijn66]: van Wijngaarden, Adriaan. Recursive Definition of Syntax and Semantics : (proceedings IFIP Working Conference on Formal Language Description Languages, Vienna 1966, P 13-24). Stichting Mathematisch Centrum. Rekenafdeling. Stichting Mathematisch Centrum, 1966.
[Wijn69]: A. van Wijngaarden (Ed.), Mailloux, B. J., Peck, J. E. L., & Koster, C. H. A. (1969). Report on the Algorithmic Language ALGOL 68. Numerische Mathematik, 14(2), 79–218. doi:10.1007/bf02163002
[Wijn77]: A. van Wijngaarcien, B. J. Mailloux, J. E. L. Peck, C. H. A. Kostcr, M. Sintzoff, C. H. Lindsey, L. G. L. T. Meertens, and R. G. Fisker. 1977. Revised Report on the Algorithmic Language ALGOL 68. SIGPLAN Not. 12, 5 (May 1977), 1–70. doi:10.1145/954652.1781176
[Wilk92]: Wilkes, M. V. (1992). EDSAC 2. IEEE Annals of the History of Computing, 14(4), 49–56. doi:10.1109/85.194055
[Wils92]: Wilson, P.R. (1992). Uniprocessor garbage collection techniques. In: Bekkers, Y., Cohen, J. (eds) Memory Management. IWMM 1992. Lecture Notes in Computer Science, vol 637. Springer, Berlin, Heidelberg. doi:10.1007/BFb0017182
[Wirs95]: Wirsing, M. (1995). Algebraic specification languages: An overview. Lecture Notes in Computer Science, 81–115. doi:10.1007/bfb0014423
[Wirt66]: Niklaus Wirth and C. A. R. Hoare. 1966. A contribution to the development of ALGOL. Commun. ACM 9, 6 (June 1966), 413–432. doi:10.1145/365696.365702
[Wirt76]: Wirth, Niklaus. “MODULA: a language for modular multiprogramming.” Berichte des Instituts für Informatik 18 (1976). doi:10.3929/ethz-a-000199440
[Wood66]: Woodward, Philip M. List Programming in Advances in programming and non-numerical computation. 1966
[Wood72]: Woodward, Philip M.. “Practical experience with algol 68.” Software: Practice and Experience 2 (1972) doi:10.1002/spe.4380020103
[Woze71]: J. M. Wozencraft and A. Evans. Notes on Programming Linguistics. M.I.T. Department of Electrical Engineering, February 1971
[Wray86]: Wray, Stuart Charles. Implementation and programming techniques for functional languages. No. UCAM-CL-TR-92. University of Cambridge, Computer Laboratory, 1986. https://www.cl.cam.ac.uk/techreports/UCAM-CL-TR-92.pdf
[Wulf73]: Wulf, William Allan et al. “The design of an optimizing compiler.” (1973).
[Zill70]: Stephen N. Zilles. An Expansion of the Data Structuring Capabilities of PAL. Report MIT-LCS-TM-015, Laboratory for Computer Science, Massachusetts Institute of Technology, October 1, 1970.
[Zill74]: Zilles, Stephen N. “Algebraic specification of data types.” Project MAC Progress Report 11 (1974): 28-52.
[Zill75]: Barbara Liskov and Stephen Zilles. 1975. Specification techniques for data abstractions. In Proceedings of the international conference on Reliable software. Association for Computing Machinery, New York, NY, USA, 72–87. doi:10.1145/800027.808426
[Берд83]: Бердж В. Методы рекурсивного программирования/Пер. с англ. С. П. Забродина, В. Г. Иваненко, Ю. П. Кулябичева; Под ред. Н. И. Иващенко. —М.: Машиностроение, 1983.