Текст
                    В.ББЩЖ
МЕТОДЫ
РЕКУРСИВНОГО
ПРОГВШМИРОВАНИЯ


Recursive Programming Techniques WILLIAM H. BÜRGE IBM Corporation Thomas J. Watson Research Center ^ ADDISON-WESLEY PUBLISHING COMPANY Reading, Massachusetts • Menlo Park, California London • Amsterdam • Don Mills, Ontario • Sydney
в. БЕРДЖ МЕТОДЫ РЕКУРСИВНОГО ПРОГРАММИРОВАНИЯ Перевод с английского сазАБРОДИНА, В.Г. ИВАНЕНКО, Ю.П.КУАЯБИЧЕВА Под редакцией д-ра техн. наук Н.Н.ИВАЩЕНКО ж МОСКВА «МАШИНОСТРОЕНИЕ» 1983
ББК 32.973 Б48 УДК 681.3.0-03 Бердж В. Б48 Методы рекурсивного программирования/Пер. с англ. С. П. Забродина, В. Г. Иваненко, Ю. П. Ку- лябичева; Под ред. Н. И. Иващенко. —М.: Машиностроение, 1983. — 248 с. ил. В пер.: 1 р. 40 к. Дано систематическое изложение методов рекурсивного программирования, получающих в последнее время все большее распространение. Большое внимание уделено применениям рекурсий для решения часто встречающихся на практике задач. Многочисленные примеры программ наглядно иллюстрируют широкие возможности рекурсивных методов н делают книгу доступной широкому кругу читателей. Книга предназначена для инженерно-технических работников, использующих в своей практике средства вычислительной техники, а также для программистов всех уровней подготовки. 2405000000-613 038@1)-83 65-82 ББК 32.973 вФ7.3 ИВ № 2841 Вильям Бердж МЕТОДЫ РЕКУРСИВНОГО ПРОГРАММИРОВАНИЯ Редактор Т. В. Абимва Художественный редактор С. С. Водчиц Технический редактор Т. И. Андреева Корректоры: Л. Е. Хохлова, О. Е. Мишина Оформление художника Я. П. Степанова Сдано в набор 09.06.82. Подписано в печать 30.И.82. Формат бОхЭО'Лв- Бумага типографская ^f^ 1. Гарнитура литературная. Печать высокая. Усл. печ. л. 15,6. Уч.-изд. л. 15,16. Тираж 8000 экз. Заказ 171. Цена 1 р. 40 к. Ордена Трудового Красного Зиамеии издательство «Машиностроение», 107076, Москва, Б-76, Стромынский пер., д. 4 Ленинградская типография ^f^ 6 ордена Трудового Красного Знамени Ленинградского объединения «Техническая книга» им. Евгении Соколовой Союзполнграфпро- ма прн Государственном комитете СССР по делам издательств, полиграфии н книжной торговли. 193144, г. Ленинград, ул. Монсеенко, 10. Copyright © 1975 by Addison—Wesley Publishing Company, Inc. Philippines copyright 1975 by Addison—Wesley Publishing Company. Inc. © Перевод на русский язык, «Машиностроение»,.JS8.3 Г,
ОГЛАВЛЕНИЕ Предисловие 7 Введение 8 Глава 1. Основные понятия и обозначения 10 1.1. Введение 10 1Л;. Выражения jinna оператор/операнд 11 1.3. Переменные и лямбда-выражения 16 1.4. Структуры данных 18 1.5. Дополнительные формы выражений 21 1.6. Примеры 28 1.7. Упрощение выражений 32 1.8. Лямбда-преобразование 36 1.9. Комбинаторы 40 1.10. Рекурсивные функции 42 1.11. Преобразование к комбинаторам 45 Ссылки и библиография 47 Глава 2. Структуры программ 48 2.1. Введение 48 2.2. Программы обратной польской записи 49 2.3. Значения идентификаторов 52 2.4. Вычисление комбинаций 54 2.5. Компилятор комбинаций 57 2.6. Блоки и процедуры 59 2.7. Вычисление выражений 62 2.8. Компиляция выражений 66 2.9. Методы повышения эффективности программ 72 2.10. Метки и операторы перехода 81 2.11. Совмещение, присваивание и копирование 86 2.12. Другие методы вычисления выражений 94 Ссылки н библиография 97 Глава 3. Структуры данных 98 3.1. Введение 98 3.2. Основные понятия 100 3.3. Некоторые простые структуры 105 3.4. Списки 107 3.5. Списковые структуры 115 3.6. Деревья и леса. .' 117 3.7. Бинарные деревья 122 3.8. Комбинации , , 125
3.9. Веса деревьев 127 3.10. Последовательности, сопрограммы и потоки 128 3.11. Комбинаторные конфигурации ■ 140 Ссылки и библиография 147 Глава 4. Грамматический разбор 148 4.1. Введение 148 4.2. Конечные автоматы и регулярные выражения 149 4.3. Контекстно-свободные языки 155 4.4. Выражения, описывающие языки 158 4.5. Нисходящий грамматический разбор 161 4.6. Левоугольный восходящий грамматический разбор 175 4.7. Правоугольный восходящий грамматический разбор 182 Библиография 191 Глава 5. Сортировка 193 5.1. Введение 193 5.2. Векторы упорядочения 195 5.3. Деревья двоичного поиска 204 5.4. Двоичная вставка 208 5.5. Быстрая сортировка 210 5.6. Обмен по основанию системы счисления и память типа «trie», 212 5.7. Вставка дерева и его перестройка 214 5.8. Упорядочение с помощью дерева сортировки 221 5.9. Сортировка на лентах 225 Ссылки и библиография 242 Указатель программ 244 Предметный указатель 247
Посвящается Марианне ПРЕДИСЛОВИЕ Область системного программирования возникла главным образом как результат труда многих программистов и исследователей, чья творческая энергия привела к созданию практических обслуживающих системных программ. Это было обусловлено бурным развитием вычислительной техники. На ранних стадиях программирование развивалось как вид искусства, где каждый программист создавал собственные программы решения задач с кратким описанием, которыми кроме него самого могли пользоваться лишь его непосредственные коллеги. В 1968 году Ашер Оплер, работая в то время в фирме ИБМ, пришел к выводу, что знания в области программирования следует привести к форме, доступной для всех системных программистов. Исследуя состояние данного вопроса, он убедился, что имеется достаточно оснований для систематизации рассматриваемого материала. По его рекомендации фирма ИБМ решила субсидировать Серию Системного Программирования,^ что дало возможность собирать, систематизировать и освещать в печати принципы и методы, имеющие важное значение для всех пользователей ЭВМ. Серия задумана как постоянно продолжающийся ряд монографий, отражающих личные взгляды авторов на предмет изложения, которые не всегда совпадают с мнением фирмы ИБ7Л. Каждая из книг задумана как изложение отдельного курса, но может служить и справочным пособием. Серия состоит из трех уровней: обширный вводный материал в книгах, посвященных основам программирования; более сложный материал в книгах по программному обеспечению; наконец, специальные вопросы в книгах, посвященных науке программирования. Как правило, книги Серии удовлетворяют и начинающих и опытных программистов, а также специалистов по математическому обеспечению вычислительных машин. Таким образом, книги Серии отражают современное состояние в этой области и могут стать основой для изучения системного программирования. * Предлагаемая lUHMaHHra читателей книга входит в состав указанной се- рин. ~- Прим. пер. Редакторы серии
ВВЕДЕНИЕ Книга посвящена особому методу программирования, который использует систему обозначений лямбда-исчисления. Применения этого метода иллюстрируются многочисленными примерами. При изложении материала книги основное внимание уделяется тем выражениям — частям языка, которые описывают получаемые с помощью машин конечные результаты, а не программам, обеспечивающим их достижение. Во многих случаях подобный акцент на выражения, а не на технику программирования позволяет упрощать и совершенствовать задачи. В первой главе вводится используемый в книге язык программирования, представляющий собой такую версию обозначений лямбда-исчисления, которая облегчает практическое программирование. В ней содержится также краткое описание известных подходов к вычислениям с использованием лямбда-функций. Излагаемые подходы к формированию функций представляют интерес для исследования математических и вычислительных проблем, а также целесообразны с точки зрения тфостоты и удобства написания программ. Во второй главе излагается несколько способов создания систем программирования, переводящих выражения в программы. При обсуждении механизма вычислений и стыковки программ с блочной структурой в качестве машинной модели используется машина типа SECD. Используемый язык программирования может применяться как модель для точного описания других языков. Для получения наиболее полного соответствия с другими языками в модель вводятся дополнительные свойства и операторы перехода. В результате машинная модель содержит в обобщенной форме также и свойства языков рассматриваемых программ, которые могут быть с успехом использованы при решении практических задач программирования. Третья глава содержит систематический способ составления программ для создания и анализа древовидных структур из наборов деревьев. Результирующие программы соответствуют структурам данных. В этой главе также описан способ представления
структур данных с помощью функций поточной обработки, которые материализуют составляющие части структур лишь при их необходимости. Список древовидных функций может быть преобразован в функции поточной обработки. Этот метод затем применяется для создания программ, образующих потоки комбинаторной конфигурации. Четвертая глава связана с методами распознавания и грамматического разбора цепочек символов. Контекстно-свободный язык создан в виде блочного описания программы его грамматического разбора. Программы анализа формируются переопределением операций на символьных цепочках, при этом структура программы грамматического анализа соответствует структуре языка. В пятой главе приведен ряд примеров программ сортировки, выраженных с помощью рекурсивных функций. Они позволяют простейшим способом составлять программы сортировки, поскольку сортировку всякого массива часто можно выразить через такие же программы для его подмассива. Книга предназначена для специалистов, работающих в области применения вычислительной техники, и в качестве дополнительного пособия для студентов старших курсов. Она содержит значительную часть материала, соответствующего учебным планам комиссии в области вычислительной техники при Ассоциации вычислительных машин (АСМ) и необходимого при изучении структур данных, языков программирования и конструирования компиляторов. Книгу можно также рассматривать как введение в методы программирования, которое особенно необходимо на начальных этапах создания программ. Большинство программ приведено в виде доступных для понимания примеров, они могут служить исходным рубежом для специалистов, приступающих к сложным задачам программирования по всем изложенным в книге направлениям. В. Бердж
Глава 1 ОСНОВНЫЕ ПОНЯТИЯ и ОБОЗНАЧЕНИЯ 1.1. Введение В этой главе приведено описание языка программирования, который далее в книге будет использован для иллюстрации методов программирования. Принятые в книге обозначения соответствуют обозначениям в обычной математике, а также в ветви математической логики, называемой комбинаторной логикой. В данной главе объясняется смысл таких обозначений. Все вводимые лингвистические выражения основаны на использовании двух способов построения сложных выражений из более простых: 1) с помощью конструкций оператор/операнд, которые описывают применение функции к аргументу, и 2) с помощью выражений, описывающих функцию с использованием лямбда-обозначений Черча. При этом дополнительное обозначение, добавляемое к основному, не привносит новых структурных особенностей, поскольку каждая новая часть обозначений может быть записана путем добавления лишь некоторых констант. Эти константы, часто используемые программистами, соответствуют условным выражениям, спискам выражений и определениям, описывающим сами себя. Дополнения создают мощную и практичную систему программирования, которая скорее напоминает семейство языков программирования, чем отдельный язык, поскольку подобные дополнения связаны в основном с объединением функций для образования новых, а не с самими объединяемыми функциями. Добавляя к основной структуре подходящий ряд элементарных структур, можно получить язык программирования для отдельной области применения. Система программирования вначале описывается неформально, затем даются примеры ее использования. Далее вводимые особенности описываются более формально с помощью лямбда-обозначений Черча [1—6]. При использовании лямбда-преобразований функция вычисляется путем последовательных ее преобразований к выражению, имеюще.му более простую форму. Краткое списание метода вычисления сопровождается иллюстрацией того, как с его помощью можно представить отдельные рекурсивные функции. Особое внимание уделено составным частям выражения, поскольку онп обладают очень ценным для практического програм- 10
мированпя свсиством: значение выражения зависит только от значения его подвыражений и не зависит от других их свойств. Это дает возхюжноеть программисту создавать сложную программу, разделяя ее на более простые, независимые части. Иными словами, оказывается возможным написать программу, структура которой соответствует структуре решаемой задачи. 1.2. Выражения типа оператор/операнд Функции и их типы, функция представляет собой правило, с помощью которого одной величине, называемой аргументом, ставится в соответствие другая единственная величина, которую называют значением функции для этого аргумента. Иными слогами, это соответствие можно выразить так; функция / преобразует X в у, или же у представляет собой результат применения f к х. Этот результат можно записать в виде выражения / {х) или fx, представляющего собой составное выражение, которое состоит из оператора f и операнда х. Обычно для указания результата применения функции используют запись, состоящую из двух выражений, одно из которых или оба заключены в скобки, причем оператор располагается перед операндом. Такое выражение будем называть составленным по применению. Многие функции, которые будут введены в дальнейшем, можно применять только к объектам определенного класса, получая в результате объекты также определенного класса. Множество объектов, к которым можно применять функцию, называется областью определения, а множество получаемых в результате объектов называют областью значений. Тип функции будем представлять записью А -^В, которая означает, что если функция применяется к объекту из множества А, результат будет принадлежать множеству В. Например, функция возведения целого числа в квадрат дает также целое число, что можно записать следующим образом: square £ (integer -» integer) . Другие примеры: sin g (real -*■ real) In $ (positive -»■ real) negative g (positive -» negative). В общем случае ж записи вида £^{А ^В) А VI В представляют собой множества, g обозначает функцию, устанавливающую соответствие между членами множеств Л и ß, нервое из которых является областью ее определения. Важно раз- 11
личать идентификатор /, обозначающий функцию, и f (х) ■— выражение, которое является результатом применения / к х. Выражения /, х и / (х) связаны между собой следующим образом. Если А я В представляют собой два множества, а х ^ А и f £ (А -^ В), то, очевидно, / (х) € В. Функция двух аргументов применяется к своим аргументам в определенном порядке. Если функция применяется к упорядоченной паре аргументов, первый из которых является членом множества А, второй — членом множества В, а результат принадлежит множеству С, то тип такой функции можно обозначить А X В —>■ С. Два примера рассмотренных функций: ~f ^ (real X real -> real), > ^ (real X real -> значение истинности). Результат применения функции / к двум аргументам записывают в виде / (х, у). Упорядоченную пару аргументов можно трактовать как частный случай списка аргументов. В общем случае, если Al, Ла, ..., An представляют собой множества, выражение Al X А^ X ... X An, называемое декартовым произведением множеств, можно использовать для обозначения множеств всех наборов^из я элементов, в которых первый, второй, третий, ... элементы являются членами множеств Ai, А^, Аз, ■■■ соответственно. Функцию, для которой область определения и область значений совпадают, иногда называют преобразованием, а функцию, дающую в результате ее применения значение истинности, называют предикатом. Отсюда следует, что А-преобразование есть функция типа {А -^А), а А-предикат имеет тип (Л -^значение истинности). Существует много других способов обозначения структур выражений. Для нахождения значения выражения необходимо раскрыть структуру оператор/операнд. Например, в выражении Зх^ + у оператором является знак -f, а 2>х^ и у представляют его операнды. В подвыражении Зх^ оператором является умножение, а операндами оказываются члены 3 и х^, последний из которых образован оператором возведения в квадрат операнда х. Раскрыть рассмотренную структуру можно с помощью следующих правил. ■ Инфиксный оператор, в рассмотренном случае + , располагается между операндами. ■ В выражении вида Зл^ оператор умножения неявный. ■ Оператором всего выражения является сложение, а не умножение или возведение в степень. ■ Знак '^ является постфиксным оператором возведения в квадрат. Структуру оператор/операнд можно представить в более наглядном виде, записывая выражение с помощью префиксной записи: -[- (X C, square (х)), у) 12
в префиксной записи задача выделения оператора и операнда решается проще, поскольку такая структура нагляднее. Для разбора любой записи как выражения необходимо получить ответы на следующие вопросы, относящиеся к оператору, операнду и ко всему выражению. 1. Является выражение простым или составным? 2. Если оно составное, что является в нем оператором, а что операндом} Если на эти вопросы можно ответить, то представляется возможным получить значение выражения. Простейшими подвыражениями в записи Зх^ + У являются х, у, -{-, X и square (возведение в квадрат), которые представляют собой идентификаторы. Идентификатор может быть записан в виде букв и цифр либо в виде специального символа, например +• При использовании многосимвольных идентификаторов смежные идентификаторы должны быть разделены интервалом. Значение и обозначение. Значение выражения зависит только от значений его подвыражений. Значение идентификатора представляет собой то значение, которое было ему присвоено. Идентификатор с известным значением, например +, будем называть константой. Значение составного выражения находят как результат применения операторной части выражения (которое должно быть функцией) к значению операнда. Слова «оператор» и «операнд» будем использовать для обозначения выражений, а не их значений. Значение выражения может быть числом, логическим значением, структурой данных, функцией или любым [другим объектом, который можно представить в вычислительной машине. Функция имеет такой же смысл по отношению к оператору, как число к числовому выражению. Функция задает значение для каждого аргумента из своей области определения. Можно сказать, что выражение обозначает свое значение, а значение является обозначением выражения. Два выражения, имеющие одно и тоже значение, называют эквивалентными. Поскольку значение выражения зависит только от значений его подвыражений, замена всякого подвыражения ему эквивалентным приводит к эквивалентному выражению. Функция, определяемая функциями. Введем используемую в комбинаторной логике форму записи, в которой оператор может быть составным выражением. Хотя функции, дающие в результате их применения функцию, и встречаются в математике, обычно для каждой из них используются специальные обозначения. Например, в записи log„x часть log можно рассматривать как обозначение функции, которая вместе с положительным числом п образует функцию logn, тип которой есть (positive -^{positive -^ r$al)). Другими словами, применение функции log к положительному действительному числу (positive) приводит к образованию функции, 13
преобразующей положительные действительные числа в действительные числа (real). Примером функции рассмотренного вида является оператор дифференцирования, в котором как действие, так и результат представляют собой функции. Другим таким примером является функция twice (применение 2 раза). В результате ее воздействия на любой преобразователь / получается новая функция, которая вначале применяет / к аргументу, а затем — к результату. Если, например, задана функция возведения в квадрат, в результате действия отмеченного правила получим функцию возведения числа в четвертую степень. Функцию twice можно определить в виде twice f (х) = f (/ (х)). Если twice действует на функцию / типа {А -» А), а объект типа А представляет собой, например, х, то в результате получим объект типа А. Иными словами, twice можно представить как действие функции типа (А -» Л) и создание функции такого же вида. Тип функции twice представляет собой (Л ->Л) -*{А -^А). Еще одним подобным примером является функция, действующая на две функции для создания третьей. Если область значений функции / содержится в области определения функции g, то оказывается возможным сформировать произведение функций g а f, записываемое в виде g-f, такое, что (g-f) х — g (fx), где сначала / применяется к аргументу х, после чего g применяется к полученному результату. Если / ^ (Л ->ß), & g ^ {В -»С), то g-f ^ ^ (а ~*С). Следовательно, инфиксная точка представляет собой функцию типа {{А -^В)-{В ->С)) ->(Л -у С). Поскольку понятие произведения функций можно обобщить, то выражение, подобное f-g-h, определяется однозначно, а я- кратное применение преобразования / можно записать в виде /". Следовательно, /"•/'" = f"+m^ поэтому f^ = f, а f представляет собой функцию тождественности I такую, что 1х = х. Таким образом, операторной частью выражения может быть составное выражение, которое можно представить в виде (/ (л:)) (у), где / применяется вначале к л; и образует функцию, которая затем применяется к у. Для объединения подвыражений применяются скобки, однако в ряде случаев можно обойтись без них. Например, в записи / (х) скобки можно опустить, тогда fx будет означать то же самое. Можно записать без скобок и выражение (/ (х) ) (у), представив его в виде fxy. Таким образом, в случае последовательной записи подвыражений они разделяются неявным оператором применения, при этом подразумевается, что подвыражения заключены в скобки слева. Так, выражение abcde записывается со скобками следующим образом: {{{ab) с) d) е. Следовательно, функ- 14
цию, число аргументов которой больше одного, можно представить как функцию одного аргумента, поочередно применяемую к каждому из аргументов; например, fab представляет собой (fa) b, а fabc эквивалентно записи {{fa) b) с. Если р обозначает функцию сложения, то выражение а -\- b можно представить в виде раЬ. Хотя такой подход фактически является простым изменением записи, удобным для теоретического анализа или для упрощения механической обработки, из него можно извлечь определенную практическую выгоду при программировании. Записывая выражение раЬ вместо а -\- Ь, программист может выделить три его составные части: \) р — функцию сложения; 2) {ра) — функцию, которая прибавляет а к некоторому числу; 3) {раЬ) — сумму. Поскольку описываемый язык предназначен как для практических целей, так и для объяснения понятий программирования, в нем сохранены оба способа задания функций многих аргументов. Так, одну и ту же функцию можно записать двумя способами, например {fxy) и / {х, у). Эти записи будут обрабатываться, как две различные функции одного аргумента. В первом случае / применяется к х, а получающаяся в результате функция — к у. Во втором случае аргумент представляет собой упорядоченную пару {х, у). Другими словами, тип первой функции представляет собой 4:А -^(В^ -^ с) а тип второй функции {А X В) -> С. Выражение а + b — с + / (а, с) можно представить в форме оператор/ операнд двумя способами: 1) с помощью упорядоченных пар + (- (+ {а, Ь), с), f {а, с)); 2) определяя функции р, т и g в виде рху = X + у тху — X — у gxy = / {х, у), тогда рассматриваемое выражение принимает вид р {т {раЬ) с) {gac). В книге далее используется как инфиксная, так и префиксная формы операторов. Будем считать, что операция применения имеет приоритет перед любым инфиксным оператором, который, в свою очередь, имеет более высокий приоритет перед запятой. Следовательно, в выражении fx -\- gy скобки можно разместить следующим образом: {fx) -Ь {gy), а запись а + Ь, с + d представить как {а + Ь), (c + d). Подведем итоги. Структура упрощенного (с некоторой потерей наглядности) выражения становится совершенно ясной, если для всего выражения, а также для оператора и операнда можно ответить на следующие вопросы: 1. Является оно {он) простым или составным} 15
2. Если является простым, что представляет собой идентификатор? 3. Если является составным, что относится к оператору, а что к операнду? 1.3. Переменные и лямбда-выражения Переменные находят широкое применение как в математике, так и в программировании. В соотношениях п 2 Ai и \(х- + x)dx 1=1 переменными являются i в первом выражении их — во втором. Их можно заменить, например, на / и у соответственно: п 2 Aj и f (г/2 + г/) dy. Значения выражений при этом не изменятся. При программировании выбор переменной х в определении функции, например / {х) = \<дх^ + Ах + 3, несуществен для вычисления значения /, и в обеих частях этого выражения х можно заменить, например, на у. Приведенное выше определение функции включает три составные части: 1) имя функции, в данном случае /, которое будем называть определяемым выражением или определением; 2) переменную х; 3) правую часть, которая для каждого аргумента определяет значение функции. Последнюю часть называют иногда связанной формой функции. С помощью лямбда-обозначений Черча можно записать так называемое лямбда-выражение, которое обозначает функцию. В таком случае выражение Ix . \Qx- + 4л- + 3 обозначает определенную выше функцию /. Такое же определение получим, записывая f ^-= Хх . Юл-- + 4л + 3. Лямбда-выражение вида кх . М состоит из двух частей. Часть, расположенную между лямбдой и точкой, будем называть связанной переменной, а часть, следующую за точкой, назовем телом. Лямбда-выражение, получаемое объединением Ял . со связанной формой М, называют составленным изолированно. Важно различать лямбда-выражения и связанную форму. Так, значение выражения 10л^ + 4л + 3 является числом, которое можно получить для известного значения числа л, значение Ъг . 10л^ + 4л + 3 является функцией. Другими словами, значение 10л^ + 4л -}- 3 имеет тип real, аХх . 10л''^ + 4л + 3 — тип (real -> real). 16
Лямбда-выражение может быть не только в правой части определения функции. Выражение, обозначающее применение приведенной выше функции к числу 5, можно записать в виде / E) или {%х . 10х^ + 4х + 3) 5. Предполагается, что в первом выражении функция / где-то определена, второе же выражение содержит всю информацию, необходимую для получения результата. Лямбда-выражение также может быть расположено на месте операнда, и выражение (V . / E)) (кх . \0х^ + 4x + 3) означает то же, что и два приведенных выше выражения. Функцию двух аргументов можно определить как g (у) = кх . 10х^ + 4ху + Зг/^ где правая часть представляет собой лямбда-выражение. Если g применяется к числу, в результате получаем функцию. Например, если g применить к 1, в результате получим функцию /, рассмотренную выше. С помощью приведенного правила для перемещения переменных из одной части равенства в другую ту же функцию можно определить как g (у) (х) = 10х- + 4ху + у'^ или в виде g = ку . Хх . Юх" + 4ху + Зу" Последнее определение, имеющее в левой части единственный идентификатор, будем называть определением в стандартной форме. Скобки в левой части определения могут быть опущены. В самом деле, нет необходимости заключать единственный идентификатор в скобки, поэтому рассмотренную выше функцию можно или определ: ить gy в виде = кх . gyx = Юх^ \0х^ + 4ху + 3f- + 4ху + Зу\ Как составная часть большего выражения само выражение может занимать место: 1) оператора, 2) операнда, 3) тела другого лямбда-выражения. Лямбда-выражение представляет собой еще один основной метод составления нового выражения. В наиболее строгой форме рассматриваемые выражения можно охарактеризовать следующим образом: Ш Выражение является — либо простым и есть идентификатор — либо лямбда выражением н имеет связанную переменную, которая является идентификатором, и тело, являющееся выражением, — либо является составным и имеет опеоатол и ппрппнЛ, представляющие собой выражения. 17
Для распознавания конца тела лямбда-выражения служит следующее правило. Тело продолжается до тех пор, пока не встретятся закрывающая скобка, запятая или пока не закончится все выражение. Следовательно, заключать тело в скобки необходимо только тогда, когда оно является списком, можно использовать скобки и для улучшения наглядности. Все лямбда-выражение можно не заключать в скобки, если за ним следует закрывающая скобка или запятая. Так, (Кх . Ку . х + у) а означает то же, что и (кх . (ку . X -\- у)) а , а выражение Хх . х -\- у, х имеет тот же смысл, что и (кх . X -\- у) , X . Возможны и иные сокращения, когда лямбда-выражение представляет собой тело другого лямбда-выражения. В таком случае выражение кх . ку . х + у можно записать в виде кху . х + у, а выражение кх . ку . kz . х + у + z — в виде кх у z . х + у + z. 1.4. Структуры данных Ниже рассмотрены различные структуры данных и описан метод, который используется далее для определения новых структур данных. Метод устанавливает абстрактный синтаксис структур данных в противоположность конкретному синтаксису, который относится к набору строк символов, определяющих содержание написанного. Каждое описание структуры вводит функции, используемые как для построения наборов данных, так и для доступа к их отдельным частям. Этот метод служит для организации новых типов данных безотносительно к способу их представления в вычислительной машине. Вводимые при определении данных функции ограничены лишь некоторыми аксиомами, задающими связь между ними. Новые структуры будут введены предложениями следующего вида: ■ Пара имеет первый элемент {Jini) и второй элемент {second). Если X является парой, то (first х) обозначает первый элемент пары, а (second х) — второй ее элемент. Это определение явно задает названия функций разделения или разделителей частей структуры, но не дает имени функции для создания пар, которое также должно быть введено. Назовем такую функцию cpair. Таким образом, имеем три функции — first, second и cpair. Для автоматизации выполнения програм.м, содержащих обращения к таким функциям, последние могут быть реализованы в виде программ, которые должны обладать лишь следующими свойствами: first (cpair х у) = х second (cpair х у) = у . Отсюда следует, что если z является парой, то будет справедлива запись cpair (first z) (second z) = z . 18
Все функции, вводимые определениями структуры данных, должны удовлетворять аксиомам такого типа. Набор структур данных может быть создан из двух или большего числа наборов различных форматов. Определяемый список может быть двух видов: нулевым {null), не содержащим компонент, либо некулевым (nonnull), состоящим из двух частей — головы и хвоста, также являющегося списком. Устанавливая принадлежность элементов составных структур данных, рассмотренные определения можно сделать более точными: Л-ß-napy, например, можно рассматривать как пару, первой частью которой является объект типа А (или принадлежащий списку А), а второй частью — объект типа В, например ■ А-В-пара (A-B-pair) — имеет первый элемент, которым является А, — и второй элемент, которым является В. Для обозначения набора пар, у которых первым элементом является целое число, а вторым — список, целесообразно ввести обозначение пара целое-список. Аналогично определение ■ Л-список (А-1Ш) является — либо нулевым {null) — либо имеет голову {head), которой является А, и хвост {tall), которым является Л-список {А-1Ы). вводит список, все элементы которого принадлежат множеству А. Теперь целое-список {integer-list) можно записать как имя набора списков, элементами которых являются целые числа. Если структура данных содержит более одного формата, используют предикаты, которые позволяют различать форматы. Для списков существует один предикат, называемый null. Если этот предикат применяется к пустому списку, в результате будет получено значение true, а если не к непустому списку — значение false. Функции head и tail называют разделителями. Функции, формирующие структуры из своих компонент, называют составителями. Составитель для ненулевых списков будем называть префиксом {prefix), а пустой список обозначим как ( ). Над списками могут быть выполнены операции следующих типов: null ^ A-Iist -> значение истинности head ^ (nonnull A-Iist -> А) tail ^ (nonnull A-Iist ->• А) prefix 6 (А -»■ (A-list ->■ A-Iist) , которые взаимосвязаны следующим образом: null ( ) = true null (prefix X у) = false head (prefix x y) = x tail (prefix x y) = у prefix (head z) (tail z) = z , где X представляет собой A, у есть Л-список, а 2 — ненулевой А-список {nonnull A-lisi). 19
На последующих страницах книги списки встречаются так часто, что для них целесообразно использовать специальные обозначения. Голову (head) и хвост (tail) списка будем обозначать соответственно h п t; х : у применим для обозначения записи prefixxy. Кроме того, при k'^2 конструкция Xi, х^, Хз, ..., Xk используется как синтаксический вариант записи xi : (¾ : {Хз : (... х^) : { ) ... ))) . Обычно голова списка записывается слева. Список длиной k называется k-list, а список разделителей для первого, второго, третьего и т. д. элементов называют first, second, third и т. д. В общем случае п-н элемент списка записывается как h . ^"~'. Список единичной длины, например (х : ( )), представляется в виде их, где и — функция, которая преобразует объект в содержащий его список единичной длины. Часто используется также определяемая ниже структура, в которой элементы списка сами могут быть списками: ■ Структура Л-списка {A-l'nt structure) — это — либо неделимое {atomic) и есть А — либо не неделимое (not atomic) и есть список структур Л-списков {{A-lui itructure)-liit). Для списков и для списковых структур используют одинаковые операции. Предикат atomic — единственное дополнительное действие, вводимое последним определением. Приведем пример записи, используемой для списковой структуры: (а, Ь), {{с, d), и е), { ) , которая обозначает список длиной 3, первым элементом которого является (а, Ь), вторым — ((с, d), и е), а третий элемент, т. е. ( ), оказывается нулевым. Приведем примеры других структур данных, которые уже обсуждались: ■ Тип — либо является простым (simple) и есть идентификатор — либо имеет левую часть (left) и правую часть (right), каждая из которых является типом. ■ Применяемое выражение является — простым (simple) и есть идентификатор — или лямда-выражением и имеет связанную переменную, являющуюся идентификатором, и тело (body), являющееся применяемым выражением, — или составным и имеет оператор (operator) и операнд (operand), являющиеся применяемыми выражениями. 20
1.5. Дополнительные формы выражений Список, образуемый связанными переменными. Синтаксис лямбда-выражений можно расширить, обеспечивая структурам идентификаторных списков возможность занимать место своих связанных переменных. Например, выражения X {х, у, Z) . f {X, g (z, у)) К {х, у) . [ {х, g (z, у)) X {и, х) . f {X, g {z, у)) ^( ) ■ / (^, g B, у)) обозначают функции, области определения которых ограничены списками размером 3, 2, 1 и О соответственно. В пределах своих областей определения эти функции эквивалентны записям Xs.f (first s, g (third s, second s)) Xs.f (first s, g (z, second s)) >,s.f (first s, g (z, y)) Xs.f (X, g (z, y)) . С другой стороны, выражение X [w, [х, у), z) . if (w, у), g {x, z)) обозначает функцию с областью определения, являющейся списком из трех элементов, из которых второй представляет собой список из двух элементов. В пределах своей области определения указанная функция эквивалентна выражению ks. (f (first s, second (second s)), g (first (second s), third s)) . Определения функций в левой своей части могут содержать списковую структуру идентификаторов. Например, / (х, у, г) = х2 + г/2 + z" означает то же, что и [ = X (х, у, z) . х^ + у^ + zl Список выражений также представляет собой выражение, называемое listing. Это не относится к списку, являющемуся оператором. Значением списка выражений будет список такого же размера, в котором каждому выражению соответствует его значение. Можно определить функции, результатом применения которых будет список. Например, функция cxplus (х, y)(s, t) = X + S, у + t создает пару, или список из двух элементов. Функция cxplus относится к типу (real X real) -> ((real X real) -> (real x realj) . 21
функции, аргумент которых, подобно аргументу приведенной ;;ыше /, представляет собой список из трех элементов, не обязательно должны иметь в качестве операнда список выражений типа / C, 5, 2). Необходимо только, чтобы операнд был выражением, значение которого является списком из трех элементов. Так, D качестве аргумента для / могут быть взяты выражения first (C, 5, 2), 6) и C : E : B : ( )))." Синтаксис рассмотренных выше выражений можно описать следующим образом: Ш — Выражение есть — идентификатор (idenilfier) — или лямбда-выражение (lambda exprealon) и имеет связанную переменную (bv), которая есть идентификатор- списковая структура, и тело (body), которое является выражением — или выразкение-список (liiiing) — или составное выражение (compound) и имеет оператор (operator) и операнд (operand), являющиеся выражениями. Связанные переменные, заданные в виде списковой структуры, служат для двух целей; 1) они налагают ограничения на область применимости функции и 2) называют компоненты списковой структуры, а не полную структуру. Синтаксис связанных переменных может быть задан и другими способами. Можно описать функции, применение которых ограничено некоторыми структурами данных. Выражение 'к {х : у) . X : X : у описывает функцию, область определения которой ограничена ненулевыми списками. Голова списка имеет имя х, а его хвост — у. Функция приписывает копию головы списка исходному списку. В общем случае связанную переменную необходимо интерпретировать как структуру данных, содержащую отдельные идентификаторы, при этом должна быть обеспечена возможность формирования последовательности разделителей, определяющих каждый идентификатор. Другая подобная конструкция аналогична описанию в языках программирования: переменным присваивается тип, в результате чего функция может применяться только к объектам данного типа. Выражение % integer х.х^ + 4.^ + 3 , например, обозначает функцию, область определения которой ограничена целыми числами. Вспомогательные определения. Присвоение имени переменной и последующее использование ссылок на это имя — настолько удобный способ декомпозиции вычислений, что для него целесообразно ввести специальную синтаксическую конструкцию. Соответствующая конструкция в лямбда-обозначениях представляет собой составное выражение, оператор которого является лямбда- 22
выражением. Другим способом представления такой конструкции служит запись М where х = N или let х = N'M > вместо i!kx . М) N , поскольку указанные альтернативы легче читать и проще понимать. Подобные конструкции более употребительны в случае, когда вспомогательное определение используется более одного раза, например, (х + 3) (х — 2) where х = ау^ + by + с . Конструкция where соответствует «программированию сверху вниз», когда вначале предполагается, что объекты существуют, а затем их определяют.- Конструкция let соответствует «программированию снизу вверх», когда объекты определяют до своего употребления. Иногда в выражении let для разделения N и М используют точку с запятой, а для большей выразительности структуры применяют запись в ступенчатой форме. Таким образом, выражения let X = М; N let X = М N М where х = N являются различными способами определения выражения со вспомогательным определением. Двусмысленность при этом устраняется правилом единственного уступа, заключающимся в том, что целое подвыражение лежит в левом нижнем квадранте страницы, определяемой его первым символом. Рассматриваемье выражения имеют две части: 1) выражение М, называемое главным выражением; 2) вспомогательное определение, принимающее одну из форм: й-, ■ where х = N либо let х = N. К описаниям структур выражений необходимо добавить следующее: ■ — или является определяемым и имеет главную часть (main), представляющее собой выражение, и вспомогательную часть {auxiliary), которая является определением, где определение имеет определяемую часть (dejinee), являющуюся связанной переменной и определяющую часть (definiem), которая является выражением. Приведенное определение имеет стандартную форму, однако оно может быть представлено и в другом виде. Выражения можно определять с помощью функций, например, / W + / (/ G)) where / {х) - х- + 3 . 23
в общем случае выражение М where / (л-) -- N можно привести к форме оператор/операнд в два этапа: вначале к виду М where/ = Xx.N а затем, окончательно, {Xf . М) {Хх . N) К определению структуры описаний необходимо добавить следующее: ■ Определение является — либо стандартным и имеет определяемую часть {deflnee), являющуюся связанной переменной, и определяющую часть (dejiniem), которая является выражением, — либо является функциональным и имеет левую часть {leftilde), являющуюся ненулевым списком связанных переменных, и правую часть (rightiide), являющуюся выражением. Функциональное определение преобразуется в стандартное следующим образом. Вначале определение fxyz = Л^ преобразуется к виду / = Xxyz . N а затем записывается как f = Хх . Ху . Xz . N Подобный прием будет использован для лямбда-выражений, имеющих связанные переменные в виде списковой структуры, например, {X {X, у, Z) . М) {R, S, Т) которые могут быть записаны одним из трех способов: 1. М where х, у, z = R, S, Т 2. М where х = R and у = S and Z ii Т 3. let X :^ R and у ^ S and z = T ", ('' Последние два способа позволяют одновременно определять выражение более чем одним определением. Дополнительное определение можно записать так: ■ — или является одновременным (iimullaneoui) и представляет собой список- определение 24
примеры одновременных определений: ах2+ Ьх + с where (а, Ь, с, х) = D, 3, 6, 1) let f (X) = х2 -L 5 and V ;:= 77 f (У") - 24y В общем случае выражение, являющееся результатом одновременного определения, формируется следующим образом. Вначале каждое дополнительное.выражение преобразуют к стандартной форме, затем определяемые части объединяют в список для формирования одной определяемой части и, наконец, определяющие части объединяют в список, образуя новую определяюп^ую часть выражения. Условные выражения. Условное выражение имеет вид if Р then А else В ИЛИ может быть записано в ступенчатой форме: if Р then А либо if Р then А else В else В В эту запись входят три выражения: Р, А я В, где Р имеет значение истинности. Если оно равно true, все выражение принимает значение Л, а в противном случае — значение В. Поэтому, прежде чем делать выбор — определять А или В, необходимо вычислить значение выражения Р. Например, в условном выражении if X = О then 1 else 1/х ВЫЧИСЛЯТЬ 1/х необходимо только при непулевых значениях х. Условное выражение нельзя понимать как применение некоторой функции к списку (Р, А, В), поскольку функция должна применяться к значениям выражений Р, А и В. Его, однако, можно привести, к форме оператор/операнд с помощью функции if, которая оперирует с логическими значениями и дает в результате указатель first, если имеем true, и second, если false. Таким образом, условное выражение можно преобразовать к следующему виду: {{ifP){X{ ).А,Х{ ).В)){ ) Функцию X { ) ■ А можно применять только к нулевому списку для получения значения А. Это связано с тем, что тело лямбда- выражения вычисляется только в результате его применения. Выражение (if Р) создает переключатель, выбирающий функцию, которая затем применяется к нулевому списку. Отсюда следует, что вычисляется либо только выражение А, либо только В. Значение условного выражения, имеющего вид if Р then А будет пеопредслено, если условие не выполняется. 25
Для описания условных выражений в определение выражений следует дополнительно ввести следующее: ■ — или является условным и имеет условие, которое есгь выражение, содержащее либо две ветви, левую и правую, каждая из которых является выражением, либо одну летвь, являющуюся выражением. i5^ Подобную конструкцию имеют и выборочные (case) выражения, в которых вместо пары используется список выражений. Такая конструкция может быть записана в виде case Е of {А, В, С, D, ...) и^^означает, что вычисление Л, ß, С и т. д. должно быть отложено до нахождения Е для получения спискового указателя, first, second, third и т. д. Выборочное выражение может быть записано в виде Е(Х{ ).А,Х( ).С, ...)( ) Циклические определения. На первый взгляд, трудно представить, как можно писать программы с циклами, используя выражения. Программа с циклом, однако, соответствует циклическому, или самоссылающемуся, определению функции, которое определяет ее результат через результат применения этой же функции к более простому аргументу. В терминологии, принятой в вычислительной технике, это известно как рекурсивное определение, хотя в теории рекурсивных функций термин рекурсивный относится как к циклическим определениям, так и к определениям функций через более простые функции. Примером циклического определения является следующее: S (п, к) = if к = 1 then 1 else if к = n ihtn 1 else s (n — 1, к — 1) + к X s (n — 1, k) . Oho определяет числа Стирлинга второго рода. Например, значение s D, 2) вычисляется следующим образом: S D, 2) = S C, I) Ч- 2 X S C, 2) = 1 + 2 X (s B, 1) -j- 4- 2 X s B, 2)) = 1 + 2 X A Ч- 2 X 1) = 7. Другие примеры циклического определения: длина X = if null X then О else 1 + длина (t х) факториал х = if zero X then 1 else n X факториал (n — 1) 26
Циклическое определение приводится к стандартной форме, где определяемая часть является идентификатором, а определяющая часть представляет собой выражение. Это реализуется с помощью выражения, которое обозначает функцию, ссылающуюся на саму себя, используя функцию поиска фиксированной точки, которой присвоим имя Y. Отметим характерное свойство функции У, а именно: Yf = f (F/). Некоторые возможные реализации такого свойства приведены ниже. Ссылающееся само на себя определение функции / можно представить как стандартное определение вида / = Ef, где выражение Е не содержит /.^ Одним из решений этого уравнения является (VE). Запишем еще раз определение функции длина, введенной выше, в другом виде: длина = Ях. if null х then О else 1 + д.1ина (t х) Выделим идентификатор длина в правой части следующим образом: длина = (Xf . Ях. if null х then О else 1 + f (t x)) д.пина Наконец, запишем все выражение, используя Y: длина = Y (Я[ . Ях. if null х then О else 1 + t (t, х)) Правая часть этого определения, имеющего стандартную форму, представляет собой определение функции длина, которое теперь не содержит идентификатора длина. Определение циклической функции далее будем записывать с помощью элемента синтаксиса гее, помещаемого перед определяемым выражением. Для этого определение необходимо расширить следующим дополнением: ■ — или ивляется циклическим определением и имеет тело, являющееся определением. Эквивалентное определение находится приведением тела циклического определения к стандартной форме х — М, а затем формированием нового стандартного определения х — Vkx . М . Функция Y может быть также применена к списку преобразующих функций. Циклический список L, определяемый в виде L = A : 2 : 3 : L) , ставит в соответствие L список, первый элемент которого есть 1, второй элемент 2, третий 3, четвертый 1 и т. д. Следовательно, приведенная выше запись является определением бесконечного периодического списка i: = 1, 2, 3, 1, 2, 3, 1, 2, 3. 1, ..., 27
который естественно представить в виде петлеобразной структуры, приведенной на рис. 1.1. Список L можно обозначить с помощью выражения VKL . A : 2 : 3 : L). Рис. г. г. циклический список ЕсЛИ Y ПрИМеНЯеТСЯ К фуНКЦИИ, преобразующей один список функций к другому списку функций, результаты называют взаимно рекурсивными функциями. Если f, g и h представляют собой три взаимно рекурсивные функции, при всяком появлении идентификаторов f, g и h в определяющих выражениях этих функций происходит обращение к уже определенным функциям, а не к другим значениям, которые могут быть с ними связаны. Приведем пример циклического определения трех взаимно рекурсивных функций: гес f (х) = ...g ... h ... / ... g and giy) = ... h... f ... f ... h and h {z) = ... f ... g ... h ... g Заметим, что одновременное определение начинается с гес. Определения вначале преобразуются к стандартной форме, т. е. к виду гес (/, g, К) = {{%х ... g ... h ... f ... g), (ky ...h...f...f ...h), (кг ... f ... g ... h ... g)), a затем к виду (/, g, h) = Yk (/, g, h) . {{%x ... g ... h ... f ... g), {%y ...h...f...h), {%z...f ...g...h...g)). Список связанных переменных может быть сведен к единственному идентификатору, как уже было рассмотрено ранее. 1.6. Примеры В данном параграфе приведены примеры программ, составлен ных с помощью введенных выше обозначений. Эти примеры записаны в форме последовательных сообщений, которые предназначены для передачи в вычислительную систему. Было использовано два типа сообщений. Первым является определение, ему предшествует слово def. Система не отвечает на такое сообщение, а сохраняет определение для последующего применения, добавляя его к списку пар идентификатор-значение. Вторым типом сообщений является выражение, в ответ на поступление которого 28
система печатает его значение. Предполагается, что в системе уже определены арифметические операторы +, —, X; операторы отношения <, = и >; функциональный оператор •; списковые операторы prefix, null, hut; пустой список ( ) и предикат списковой структуры atomic. 2+2 V C, 2) 4 13, 5 Как аргумент, так и результат может быть списком. V (V C, 2)) 194, 144 def greater х у = у > х def X = 2 X -(- X def f X f 2 X + X f (f 2) 8 let g X = X + x; g 2 4 def plus X у =.. X + у def mult X у = X X у def minus x у = x — у Префиксная запись, plus 2 2 (plus 2) 2 (plus B))B) def g у = у X у g 2 def W f X = f X X def double = W plus Определяющая часть может обозначать функцию. dou1-le 2 4 def square = W mult square 2 4 def twice f = f-f Композиция функций. twice square 2 16 def thrice f = f-f-f 256 thrice square 2 def V (x, y) X" + y^, x^ — y^ (greater x) равно true, если применяется к числу, большему х def less х у = у < х def max х у if X < у then X else у greater 2 3 true max 4 6 def div rti n = if m ■< n then 0, rrl else let X, у = div (m — n) n X + 1, у div 7, 2 I def divides x у = fsecond (div x y) = 0) divides 8 2 true (divides x) равно true, если применяется к числу, на которое х делится напело 3, 1 divides 8 3 false h A, 2, 3, 4, 5) 39
t A, 2, 3, 4, 5) 2, 3, 4. 5 prefix 0 A, 2, 3, 4) 0, 1, 2, 3, 4 0: A, 2, 3, 4) 0, 1, 2, 3, 4 null ( ) true null (i, 2, 3) false 0 : A : B : C : D : ( ))))) 0, 1, 2, 3, 4 def u X = X : ( ) def rec sum x = if null X then 0 else (h x) + sum (t x) sum A, 2, 3, 4) 10 Сумма списка чисел. def rec append x у = if null X then у else (h X) ; append (t x) у append A,2, 3)D, 5, 6) 1, 2, 3, 4, 5, 6 prefix A, 2, 3)D, 5, 6) A, 2, 3), 4, 5, 6 def rec reverse x = if null X then ( ) else append (reverse (t x)) (u X) reverse A, 2, 3, 4) 4, 3, 2, 1 def rec map f x =я If null X then ( ) else (f (h X)) : mapf (t x) map square A, 2, 3, 4) 1, 4, 9, 16 map length (( ), u 1, A, 2), A, 2, 3)) 0, 1, 2, 3 def rec concat x = If null X then ( ) else append (h x) (concat (t x)) concat A, 2), C, 4), E, 6) 30 1, 2, 3, 4, 5, 6 def postfix X у = append у (u x) postfix 7 E, 6, 8) 5, 6, 8, 7 def rec compose f x =r if null f then X else compose (t f) (h, f x) compose (plus 3, mult 2) 5 16 def rec sumls x = if atomic x then X else sum (map sumls x) sumls A, B, 3), 4) 10 def rec mapls f x = if atomic x then f x else map (mapls f) x mapls square A, B, C, 4), 5)) A, D, (9, 16), 25)) def rec revls x == if atomic x then X else reverse (map revls x) Изменение порядка расположения списков в списковой структуре. revls A, B, C, 4), 5)) (E, D, 3), 2), 1) reverse A, B, C, 4), 5)) (B, C, 4), 5), 1) def rec zip x у a« If null X then ( ) else (h X, h y) : zip (t x) (t y) def rec sp x у If null X then 0 else (h x X h y) + sp (t x) (t y) sp A, 2, 3) D, 5, 6) 32 def unzip X = map 1st x, map 2nd x zip A, 2, 3)D, 5, 6)
(I, 4). B, 5), C, 6) unzip ((I, 6), B, 5), C, 4)> (I, 2, 3), F, 5, 4) def and x у = if X then у else false def or X у = if X then true else у def not X if X then false else true def rec exists p x = if null X then false else if p (h x) then true else exists p (t x) def equal x у = x = у exists (equal 5) B, 6, 1, 5, 7) true def rec all p x = if null X then true else if p (h x) then all p (t x) else false all (greater 5) G, 8, 6, 10) true def rec filter p x = if null X then C) else if p (h x) then (h X) : filter p (t x) else filter p (t x) filter odd A,4, 6, 5,8, 7,2) where odd x = second (div x 2) = 1 1,5, 7 def belongs x = exists (equal x) def incl X у = all (belongs x) у def equalset x у «= and (incl x y) (incl x y) def intersection = filter-belongs intersection A,2, 3, 4, 5) C, 4, 5, 6, 7) 3, 4, 5 def difference = filter-(not'belongs) difference (I, 3, 5, 7, 9) A, 2, 3,4) 2, 4 def union x у = append (difference у x) у def related x у z = if X = z then у else ( ) def brother = related joe (al, bob) brother joe al, bob brother fred О def phi f a b X = f (ax) (bx) def orreln = phi union def sister = related joe (a'nne, jean) orreln brother sister joe a], bob, anne, jean def rec sumset x = if null x then ( ) else union (h x) (sumset (t x)) def prodf f g x — sumset (map g (f x)) def grandfather = prodf pa (orreln pa ma) def map 2 f x у = let g z == map (f z) у concat (map g x) def crossproduct = map 2 pair crossproduct A, 2, 3) D, 5, 6) A,4),A.5).A,6), B. 4), B, 5), B, 6), C. 4), C, 5), C, 6) 31
1.7. Упрощение выражений В п. 1.5 были введены различные дополнительные формы записи выражений. Ниже подробно рассмотрено их преобразование к простой форме, когда выражения формируются исключительно объявлениями и абстрагированием. Основной синтаксис комбинаций и лямбда-выражений расширяется добавлением следующих конструкций. 1) инфиксных операторов; 2) условных выражений; 3) выражений, уточняемых определениями; 4) списковых структур в позициях связанных переменных; 5) списков выражений; 6) определений функций; 7) циклических определений; 8) одновременных определений. Расширенные выражения имеют следующую структуру, формируемую с помощью добавлений, описанных в п. 1.5: ■ гес (рекурсивное) выражение есть — простое и является ■- идентификатором или лямбда-выражением и имеет связанную переменную (bv) и тело (body), которое является выражением — или составное и является либо регулярным и имеет оператор (rator) и операнд (rand), являющиеся выражениями, или является инфиксным и имеет оператор, который является идентификатором и имеет левую (left) и правую (right) части, являющиеся выражениями, — или является условным и имеет условие (condition), являющееся выражением, содержащим — ~ либо две ветви: левую и правую, каждая из которых является выражением, либо одну ветвь, являющуюся выражением, — или является листингом, представляющим собой список выражений, — или является определяемым и имеет главную часть (main), которая является выражением, и вспомогательную часть (auxiliary), являющуюся определением, и определение есть — стандартное определение и имеет определяемую часть (dejinee), являющуюся связанной переменной и определяюш,ую часть (definiem,), являющуюся выражением, — или является определением функции и имеет левую часть (leftside), которая является ненулевым списком связанных переменных, и правую часть (righiside), являющуюся выражением, — или циклическое определение и имеет тело (body), которое является выражением; — или является одновременным и является списком определений — или является уточняемым определением и имеет главную часть (main), являющуюся определением и вспомогательную часть (auxiliary), являющуюся определением, где связанная переменная является списком идентификаторов. Определяемая ниже функция trexp преобразует выражения к следующей упрощенной форме: ■ Выражение является — простым (simple) и представляет собой или идентификатор (identifier) или лямбда-выражение (lambda expression) 32
— — н имеет связанную переменную {bv) и тело (body), являющееся выражением, -- пли составным (compound) п имеет оператор (raior) п операнд (rand), являющиеся выражениями. Функции для построения лямбда-выражений и составных выражений рассмотренного только что вида будем далее называть соответственно conslambda и combine. Нам также понадобятся следующие вспомогательные функции: 1. Функция conslist, служащая для преобразования списка выражений в выражение: def гее conslist х = if null X ttien '( )' else combine (combine 'prefic' (h x)) (conslist (t x)) conslist ('a', 'b', 'c') = 'prefix a (prefix b (prefix c)))' 2. Функция consblock, формирующая выражение (kx . M) N из двух выражений М и N и граничной переменной х: lief consblock (х, (у, z)) -= combine (conslambda у х) z 3. Функция const Y для получения Y (кх . М) из х и М: def cons Y (х, у) ^- combine 'Y' (conslambda x у) 4. Функция conscond для получения выражения {if р) {%{ ) . а, к{ ) . Ь) { ) из р, а и Ь: def conscond (р, х) = combine (combine (combine 'if p) (conslist (map delay x))) (conslist( )) where delay x = conslambda (conslist ( )) x 5. Функция translam преобразует А-хг/г . М , например, к виду Хх . ку . кг . N , где N представляет собой результат преобразования М. def гее translam (х, у) --- if nnll X then trexp у else conslambda (h x) (translam (t x, y)) ^ Бсрдж В, 33
Теперь функцию irexp, необходимую для преобразования выражений, можно определить следующим образом: def гес trexp х =;; if simple х Itien if identifier x Ihen X eise translam (bv x, body x) eise if reguK'i,- x ihen couibiir- (iro\;) (raioi- л)) (irexp (rand x)) else if iiiiixed •. (hen cciiibiiie (ralor x) (coiislisi (trexp (left, x), trexn (rigiit x)) eise if lislin« x tticn conslist (inap Irexp x) eise if condilional x then conscond (trexp (condition x)) (trexp (if twoarmed x) tiien riglitarm x eise " underfined")) else consbloci< (Irexji (main x)) wtiere rec trdcf x = if standard x =r^ tlien dcfinee x, trexp (definiens x) else if function definition x ihen !i (leftside x-), translam (t (leftside x), rigiitside x) eise if circular x ttien let b, r :- trdcf (body x) b, coirs Y (b, r) else if s'multaneous x tiien unzip (map trdcf y.) eise !el b, r —- trdcf (main x) b, consblosk (r, trdef (auxiliary x)) Функция trdef преобразует определение к стандартной форме. Однако результирующие выражения имеют в качестве связанных переменных списковые структуры идентификаторов. Для их преобразования к форме лямбда-выражения, имеющего в качестве связанной переменной единственный идентификатор, будем использовать метод, предложенный в примерах п. 1.5. При этом список связанных переменных заменяется одним новым идентификатором, а идентификаторы, встречающиеся в теле лямбда-выражений, будут заменены в результате применения указателя списковой структуры к этому новому идентификатору. Функция trbvs, определяемая ниже, использует список идентификаторных списковых структур для получения имен этих указателей и новых идентификаторов. Новые идентификаторы создаются функцией cid, которая применяется к целому числу и производит идентификатор, которого нет в исходном выражении. Значение {cid п) будем обозначать х,1. Рассматриваемая функция использует также функцию positionlist, определенную ниже, которая получает список целых чисел, представляющих собой позицию идентификатора в списке рассматриваемой структуры. Эта позиция затем преобразуется функцией sels к выражению, которое описывает указывающую функцию. 34
функция sels преобразует список 2, 4, 1, 3, например, к 3-й-1-Й-4-Й-2-Й. def гее trbvs Е х = if identifier х then let b, р = positionlisl E x I if b then (if null (t p) then Г else combine (scls (t p)) (cid (li p))) else x else if lambda expression x then let El == postfix (bv x) E conslambda (cid (length El)) (trbvs El (body x)) else combine (trbvs E (rator x)) (trbvs E (rand x)) def rec positionlist x у n ~= if null у then false, ( ) else lei b, m = positicnls x (h y) 1 if b then true, n : m else positionlist x (t y) (n + 1) and positionls x у n = if atomic у then X = y, ( ) else positionlist x у n Выражение A, (w, (x, y), z) . f X w ßs . s g z x) например, преобразуется с помощью функции {trbvs ( )) к виду kxi . f {first-second x^ {first x^ (kx^^ . x<i g {third Xi) {first Xi)) Если идентификатор найден в списке Е, который был текущим в процессе поиска, то этот идентификатор является связанным во всем выражении и будет заменен либо новым идентификатором, либо списком указателей, применяемым к новому идентификатору. Если идентификатор в списке Е не найден, то он является свободным во всем выражении. В приведенном выше выражении идентификаторы fug оказываются свободными, а все остальные — связанными. Результирующие выражения имеют три формата, которые можно определить следующим образом: @ Выражение еиь — идентификатор — или лямбда-выраоюение и имеет связанную переменную, являющуюся переменной, и тело, являющееся выражением — или комбинация и имеет оператор и операнд, являющиеся выражениями. Приведение к такой простой структуре достигается введением констант Y, if, prefix, { ) и списковых указателей first, second, third, ... Инфиксную точку, обозначающую объединение функций, заменим константой В, где В f g х = f {g х). 2* 35
1.8. Лямбда-преобразование Выражения, структура которых была описана выше, представляют собой правильно составленные формы вычисления лямбда- преобразования. При этом идентификатор, расположенный не на месте связанной переменной, может быть связанным или свободным, что определяется с помощью следующих правил: 1. Идентификатор х оказывается свободным в выражении х. 2. Все X, имеющиеся в \х . М, являются связанными. Если кроме X ъ hx . М есть идентификатор у, то последний будет свободным или связанным в зависимости от того, свободный он или связанный в М. 3. Идентификатор, встречающийся в частях F или А выражения {FA), будет связанным или свободным в обще.м выражении в зависимости от того, свободный он или связанный в F или в А. Свободные (связанные) переменные — это переменные, которые по крайней мере один раз появляются в выражении в свободном (связанном) виде. Для нахождения значения всех выражений их свободным переменным должно быть задано значение. Существуют правила эквивалентности выражений, которые оказываются полезными для практики программирования. Они определяют, когда одну часть программы можно заменить другой, не изменяя значение результата. Эти правила основываются на вычислениях лямбда-преобразования и зависят только от структуры выражения, поэтому их можно применять механически, не внпкая в смысл выражения. Значение выражения зависит только от значения подвыражений и не зависит от других их свойств. Иными словами, если эквивалентность соотношений обозначить символом = , то: если Л1 =- Л', то %х.М, = Xx.N если F -■- G, то (F А) = (G А) если А = В, то {F А) -~= (F В) . Правила замены, которые преобразуют одни выражения в другие с тем же значением, можно выразить с помощью оператора замены вида [N/x] М, действие которого заключается в подстановке в каждое выражение М выражения N вместо х, находящихся в свободных позициях. Приведем три основных правила преобразования выражений к эквивалентному виду. Первое из них (а) Кх . М = ку . [у1х ] М при условии, что у не встречается в М. Это правило отражает тот факт, что выбор конкретной связанной переменной не влияет на значение лямбда-выражения, т. е. если связанная переменная и все ее свободные по:?иции в теле из- 36
менятся, то выражение будет иметь то же самое значение. Это правило нулодается в некотором уточнении: новый идентификатор не должен быть свободным, в противном случае свободная переменная станет связанной. Определим функцию subst, которая применяется для подстановки в выражение z идентификатора х во все свободные позиции идентификатора у. def гее subst х у z =■ if identifier z then if у = z then X else z else if lambda expression z then if у = bv z then z else conslambda (bv z) (subst x у (body z)) else combine (subst x у (rator z)) (subst X у (raiid z)) Второе правило применяется к выражению, которое уточняется вспомогательным определением. Выражение М where х = N будет иметь то же самое значение, что и результат подстановки Л^ во все свободные позиции х и Л^. Это второе правило лямбда-преобразования; (ß) {Ix . М) N == [Nix] М при условии, что связанные переменные из М отличны от свободных переменных из Л^. Третье правило состоит в замене правой части соотношения ß его левой частью. Если с помощью этих правил из выражения А можно получить выражение В, то говорят, что А я В являются обратимыми, и можно записать А сот В. Замена левой части правила ß на его правую часть называется приведением, а противоположная замена — разложением. Если А преобразуется в В только с помощью правила а и последовательных приведений, то говорят, что А приводится к В. Приведение представляет собой процесс получения результата с помощью содержащихся в выражении функций. Выражение, к которому нельзя применить приведение, называют нормальной формой. Нормальная форма является самым простым из всех выражений, имеющих одинаковое значение. Составное выражение, оператором которого является лямбда-выражение, будем называть beta-redex. Следовательно, выражение в нормальной форме не содержит beta-redex. Последовательное применение приведения вместе с правилом а позволяет вычислять выражения и получать их нормальную форму. Возможен случай, когда выражение не имеет нормальной формы, например приведение не изменит выражение вида 37
(Кх . хх) (кх .XX). Приведем пример приведения выражения к нормальной форме: (kf g X . f (g x)) (kv и . V и и) (kr s t . r i s) (kg X . (kv и . V и u) (g x)) (kr s t . t t s) (kg X . ku . g X и u) (kr s t . r t s) kx . ku . (kr s t . r t s) X и и кх . ku . (ks t . X t s) и и kx . ku . (kt . X t u) и kx . }m . X и и Правило ß нельзя применять без некоторой оговорки, поскольку при подстановке в М выражения N вместо х свободная переменная из N может оказаться связанной. Избежать этого можно, например, с помощью ограничения, согласно которому свободные переменные из N обязательно должны отличаться от переменных, содержащихся в М. Другой способ заключается в изменении связанных переменных М таким образом, чтобы они отличались от свободных переменных N. Поскольку выражение может содержать более одного beta-redex, последовательность приведений может быть различной. В соответствии с одной из основных теорем, касающихся лямбда-исчисления, конечный результат преобразований не зависит от последовательности выполнения приведений. Теорема Черча—Россера [1 — 5] устанавливает, что если А приводится к В, то найдется последовательность преобразований, в которой нет операций разложения, предшествующих любому из этапов приведения. Отсюда следует, что если В — нормальная форма для Л, то Л можно привести к ß, и нормальная форма является единственной (в пределах применимости правила а). Поскольку нормальная форма является эквивалентным представлением всего преобразуемого выражения, ее можно использовать для вычисления выражений, имеющих нормальную форму. Можно выделить два различных способа вычисления лямбда- преобразований. Первый из них называется ^,-1-исчислением. Для его применения требуется, чтобы в теле правильно составленного выражения связанная переменная, по крайней мере, одпн раз появлялась в свободном виде. При втором способе, называемом ?1-К-исчислением, выполнение такого условия не требуется. При ^,-!-нсчнслен1[и каждое подвыражение выражения, имеющего нормальную форму, также имеет нормальную форму, и всякая последовательность приведений преобразует выражение к нормальной форме за конечное число шагов. Это несправедливо при .\-К,-ясчисле,чии, так как подвыражение, не и.меющее нормальной формы, 1,:ожет сократиться в процессе приведения. Если выражение при л-К-исчислении имеет нормальную форму, то последо- пательность приведений, при которой вначале всегда сокра- 38
щается самый дальний beia-redex, всегда будет приводить к нормальной форме, поскольку подобная последовательность обеспечивает выполнение всех таких сокращений. Исчисление лямбда-преобразований является формальной системой, в которой любые взаимообратимые выражения имеют одинаковое значение при любом представлении. Выражения могут быть заданы в различных видах, однако наиболее естественно полагать {\х . М) функцией, а (кх . М) N — результатом применения функции к ее аргументу. Удивительно, что, применяя такой подход к выражению, не имеющему свободных переменных, вместе с правилами а и ß мы можем точно определить интуитивное понятие эффективно вычисляемой функции положительных целых чисел. Приведем краткое описание чисто синтаксического подхода к определению эффективно вычисляемых функций. Можно выбрать определенные формулы для обозначения целых чисел: Zo^ Zi Z2 'i,- = %f = ^/ ^^ Xf = kf x.x X.f X x.f if X) x.f if if , X)). Целое число n отождествляется с функцией, применение которой к аргументу / дает в результате произведение п функций /. Таким образом, (ZJ) является функцией, в которой / применяется к своему аргументу 4 раза. Арифметические операции основаны на свойстве, согласно которому функциональное объединение /" и /'" дает в результате f^+f". Если такие функции, представляющие целые числа, сформированы, то арифметические операции выполняются над их показателями. Заметим, что выражения для целых чисел даны в нормальной форме. Говорят, что функция / неотрицательных целых чисел является Х-определимой, если существует зависимость F, в которой пит — целые числа, а Л^ и УЙ являются выражениями для целых пит, и если fm = т, то FM conv N, а если функция / не существует для положительного целого т, то FM не имеет нормальный формы. Функция упорядочения, например, может быть ?i-определена с помощью выражения 1х у Z . у {X у Z) поскольку кх г/ 2.г/ (х г/ 2) Z„ = (кх у z.y {х у z)) {kf x.f" х) = = {ку z.y {{kf x.f- .X) у z)) = = ку z.y Цкх.у" x)z) = = ^y 2. (у iУ'^ 2)) = = ку z.y^^' 2 = Z„,i 39
Сложение определяется следующим лямбда-выражением: hn п / X. т f {п f х) которое представляет собой произведение функций ш-кратного и л-кратного применений функции / к х. Произведение можно представи.ть таким образом: ktn.Kn.kf.m (л /) т. е. оно является «г-кратным применением «-кратного применения функции /. Возведение в степень определяется в виде Хт.Хп.п т поскольку, например, Z3Z2/ X = {Z,r / X = /« X. Лямбда-определяемые функции отождествляются с функциями положительных целых чисел, которые можно вычислить с помощью алгоритма (или программы) Черча. Далее цитируется монография Черча Ц—6]. Понятие метода эффективного вычисления значения функции или понятие функции, для которой Существует метод вычисления, но является чем-то необычным в математических вопросах, однако это понятие обычно интуитивное и принимается без точного определения. Известные теоремы, относящиеся к Л-опре- делимости или рекурсивности, в значительной степени основаны на предположении, что понятие эффективно вычисляемой функции положительных целых чисел может быть введено точным определением, т. е. отождествляют его с Л-определп- ыой функцией или, что то же самое, с частично рокурсивпой функцией. Поскольку во всех приведенных выше случаях формальное определение вводится интуитивно, или эмпирически, никакое полное доказательство невозможно; автор, однако, имеет некоторые сомнения относительно бесспорности введенных определений. 1.9. Комбинаторы По своей сущности к теории лямбда-определяемых функций близко примыкает теория комбинаторов. Во всякой теории одни основные понятия определяются через другие (с незначительными оговорками). В теории комбинаторов лямбда-операторы не применяются, а используются выражения, составленные из последовательностей элементарных функций, называемых комбинаторами, которые воплощают в себе некоторые общие модели. При таком представлении выражения не содержат переменных, а каждый комбинатор имеет свое собственное правило приведения. Имена комбинаторам дали Карри и Фийс в работе [1—71, там же рассмотрены и другие свойства комбинаторов. Простейишм комбинатором является функция идентичности I, которая оставляет свой аргумент неизменным: I X ^ X I — Хх.х 40
Комбинатор С изменяет порядок аргументов функции, к кото- poii он применяется, следовательно, С / X г/ =--/(/ л- С -~^ If X y.f у X Таким образом, если комбинацию С I с аргументом х применить к функции /, то получим (/ х): С I X [ = I f X ^ f X Комбинатор W, применяемый к функции / двух аргументов, производит функцию одного аргумента, значение которого принимают оба аргумента в исходной функции /: W / А' = / X X W == If x.f X X Комбинатор (W mall) означает возведение функции в квадрат, а {V^ plus) — удвоение функции. Комбинатор объединения двух функций обозначается символом В и определяется следующим образом: В / g X '- f ig Л-) В -. Ц g x.f (g X) Выражеп1:с В / g обозначают также с помощью записи f-g. Комбинатор К, который определяет постоянную функцию, в ?^-1-исчисленпи не предусмотрен: К а: у — л: К = Кх у.х Итак, (К А') является функцией, которая при любом аргументе дает в результате х. Приведем примеры еще трех, более сложных комбинаторов: S f g X = f x{g х) Ф f a b X -= f {a x) (b x) 'i" f g X у -= f(g x) (g y) Предикат I < x < 5, например, можно записать без переменной в виде "Ф and (greater I) (less 5)", a функцию для формирования суммы синусов д,зух чисел "Ф plus sin'\ С помощью комбинаторов К, В, I и S можно определить целые числа: Zo - К 1 2,ц] -"^ S о Z„ 41
Это справедливо, псскольку Zy / .г = К I / .V --= I X -М и S в Z„ / х - В f(lnf)x = f (Z„ / А') = = Z„+i / X Упорядоченную пару можно сформировать с помощью комбинатора Do, определяемого следующим образом; И.т X у Z = Z (К у) X Применяя пару к Ъ^ или Ъ^+ъ выделим первый и второй ее элементы. В результате этого получим и^ X у Zq = X ^1 X у Z„+i = у поскольку Da X t/ (К I) = К I (К t/) X = I А- = X и Ог л: у Z„,i = Z„,j (К t/) х' = К // (Z„ {К t/) х) -- у Соответствие между представлениями упорядоченных пар с помощью комбинатора и в виде списка оказывается следующим: (X, у) Da X t/ first [х, у) (D, X ^)Z„ second (X, t/) @-2 X t/) Zi 1.10. Рекурсивные функции К простым рекурсивным срунщиям относятся все обычно используемые числовые функции, такие как частное и остаток при делении, наибольший общий делитель, п-е простое число и т. д. Простые рекурсивные функции определяются с по.мощью комбинатора простой рекурсии R, имеющего следующие свойства: R а g- Zo = а ^ а g Z„,i = g Z„ (R a g Z„) Комбинатор R можно сформировать с помоитью вспомогательной функции /: f{s, i) - ь-1- 1, gib, i) 1:ЛИ /// - \b'-^ у) 1 I, ^ (/V/s/у) (ла;;/((///) 42
которая при использовании введенного выше комбинатора D., и упорядоченной пары принимает следующий вид: / у = D, (S В (у Zo)) (g [у Zo) (у Z0 . Теперь R определяется как второй член пары, полученной п- кратным применением / к (О, а): Ra g п^ (п [{D^Zo а)) Z, . Существует и другой способ определения R. В таком случае используется не последующая (successor), а предшествующая (predecessor) функция Р, которую можно определить следующим образом: Р п = let /г Л' = Ог (S В (х Zo)) (х Zo) л h (D.i ZoZo) Zi , где функция h (х, у) =^ х + I, х применяется п раз к (О, 0), давая (п, п — 1). Результатом будет Еторон .элемент этой пары. Заметим, что F L(i =^ Lq р 7 7 В таком случае Rag Z„+j определяется через R а g Z„. Далее вводится ф|уикция /г: h а g [ Zo = а h а g f Z„^i = g Zn(f Z„) , T. e. h a g f w = {(D, (K a) (g (P w)) w) (f (P w)) . Ha следующем шаге определяется функция Y, обладающая свойством Y / = /(Y /), через которую, наконец, и находится R: R а g = \{h а g). Функция Y, называемая парадоксальным комбинатором, определяется так: У / :.- (W (В /)) (W (В /)) = W S (В W В) / . Свойство \ f = f (у f) можно доказать так: Y / = W S (В W В) / == S (В W В) / / --= В W В / (В W В /) ---: =- W (В /) (В W В /) =-= В / (^. W В /) (В W В /) = .-./(В W В /(В W ?../))=.- /(Y /) . 43
простые рекурсивные функции определяются через другие простые рекурсивные функции следующим образом: 1. / (х) = successor (х) 2. / = О 3. / (хь Х2, ..., Xh) = Xi 4. / (хи х.„ ..., Xk) = g{h^ (Хъ л-о, ..., Xh), /Ь (.ti, Х2, ..., Xk), hp (Xi, А'2, ..., A'ft)) 5. / (Xi, X2, ..., Xk_i, 0) = g- (Xi, ,Го, ..., A'ft.i) / (.X'l, ..., X,i_i, n + 1) = /l (Xi, ..., Xh^i, f (Xi, ..., Xft_i, n)) , TAe hi, hi, ..., hp, h и g—простые рекурсивные функции. Их можно также записать с помощью лямбда-выражений: 1. successor = S В 2. О = Zo 3. / = К'-1 К*-' = AXi X. ...Xk. X, 4. / = Ф(^, р) g /ii /½ Лз •■•Лр, где 'T^Ct. Р) §■ ^h ^2 ••• К Л'1 -^^2 ■•• Xh =-- .? {7li Xi Ха ... Xft) (Лг Xi X-i ... Xft) {hp Xi X.J ... Xft) 5. / Xi x, ... .Vft_i =- R (g- Xi .to ... Xft_i) (/i Xi Xo ... Xftj) . Можно показать, что фактически все алгоритмические функции обычной математики являются простыми рекурсивными функциями. Конечно, можно сформировать алгоритмические функции, не являющиеся простыми рекурсивными. Это будут частично рекурсивные функции, определяемые последовательностью рекурсивных уравнений, аналогичных эффективно вычисляемым функциям. Показано, что их вычисление для целых чисел может принимать форму неограниченного поиска, удовлетворяющего некоторому условию. Согласно теореме Клини о нормальной форме tl-П ], все частичные рекурсивные функции можно определить через две простые рекурсивные функции hug следующим образом: / (xi, Хг, ..., Хп) ^= h (ji k.g (х^, х,2, ..., х„, k) =-- 0) Здесь jx k.g (xi, Х'2, ..., х„, k) =--0 5;вл51егс;5 наименьшим значением k, если, вообще, таковое существует, так что g (Xi, Xi, ... 44
..., Xn, k) =^ 0, и не определяется, если такая величина не существует. Предположим, что фуикиня и обладает таким свойством, что если имеется целое п ^ т, удовлетворяющее условию р п ~ = Zo, то \урт является нанлченьшим целым. Функция \i определяется через вспомогательную функцию /: let f X у Z =-0^ Z {X //(SB 2)) (у z) М = Y / Частично рекурсивную функцию / теперь можно определить так: / Л'1 Х., ... Х„ = /l (,U {g Xi X., ... X,i) 0 . 1.11. Преобразование к комбинаторам Комбинаторы можно исключить из выражения подстановкой на их место определяющих лямбда-выражений. Возможно и обратное преобразованпс, т. е. исключение лямбда-выражений за счет введения комбинаторов. Следовательно, хотя на практике неременные удобны, без них вполие можно обойтись. Это осуществимо различными способами. Са-чияй простой нз них — использование комбинаторов S и К. Ниже приведена такая программа устранения идентификатора х пз выражения Е. Все лямбда- операторы при этом исключаются введением комбинаторов I, К и S, причем от комбинатора I можно избавиться с помощью подстановки 'SKK'. Функции, формирующей комбинацию, дадим при этом имя combine. tief rec extract x E ^= if identifier E then "if E = X then 'Г else combine 'K' E else if lambdaexp E then extract x (extract (bv E) body E)) else let F = extract x (rator E) let A = extract x (rand E) combine 'S' (combine F A) Выражение Kf.Kx.f x x можно преобразовать следующим образом: 'kf.'kx.jx X If .S{K /)(S I I) S(S(K S)(S(K KI))(S(K S)(S(K 1)(K 1))) Отсюда видно, что объем выражения быстро увеличивается и что в отдельных случаях существуют определенные возможности «оптимизации» выражений. Приведенная ниже программа перед преобразованием выражения обнаруживает гюявление переменной в операторе или в операнде комбинации. Опишем частные случаи преобразования выражения S f g х ^-=- (/ л) (g х).
1. Если оператор не зависит от х, вводится комбинатор В; В / g X = (/) (g- А-) 2. Если операнд не зависит от .v, вводится комбинатор С: Cfgx=^{fx) ig) 3. Если ни оператор, ни операнд не зависят от х, то комбинатор не вводится: / g = (/) ig) 4. Если g == I, то S / I X = (/ Х) A А-) = / А- X == W / ЛГ в / I X == (/) (I X) = / X 5. Если / =■■- К, то S !( д- X - (К X) (g X) - I X С К g X -- (К X) (g) - 1 X Приведем -ьгкст всей iip-oipajM-vibi: def rec extract x E = if identifier E then if E = X then true, 'I' else false, E else if iambdaexp E then let bI, EI =. extract (bv E) (body E) let E2 = if Ы then EI else combine 'K' EI extract X E2 else let bI, EI = extract x (ralor E) let b2, E2 = extract x (rand E) if Ы then if EI = 'K' then true, 'Г else if b2 then if E2 = 'Г then true, combine 'W EI else true, combine 'S' (combine EI E2) else combine 'C (combine EI E2) else if b2 then if E2 = 'Г then true, El else true, combine 'B' (combine El E2) else false, combine El E2 Функция extract производит пару, первый '!леп которой представляет собой значение истинности. Если оно равно true, то х оказывается свободным в Е, а второй член предстагляет собой результат. Если же имеем false, то рсзультагом будет гсомбипа- ция 'К' El, где El —второй член ип!)ы. 46
ссылки и БИБЛИОГРАФИЯ Вычисления с помощью лямбда-преобразований были предложены Черчем 11—3] п описаны в его монографии A-61. Аналогичные 1гсследования на основе upHNiencHHH комбинаторов проведены Карри и опубликованы в двух томах [1 — 7, 1—8]. Введение в комбинаторную логику можно найти в работах [I —17], [1-9], [!-!, 1-21 и A-12]. Понятия общей рекурсивности, лямбда-определяемости и исчисляемости были введены независимо друг от друга и почти одновременно. Эквивалентность лямбда-определяемых и обычных рекурсивных функций доказали Черч [1—4] и Клнни [I—10], а эквивалентность исчисляемых и лямбда-определяемых функций — Тьюринг [1—!8]. Первым языком программирования, основанным на представлении рекурсивных функций, был ЛИСП [1—!4, 1 — 15, I—16]. Используемый далее язык тесно связан с ЛИСПом и языком ISWIM [I —13]. 1.1. Böhm С. and Gross W. «Introduction to the CUCH», in Automata Theory, E. R. Cainiello (ed.). New York and London: Academic Press, 1966, pp. 35—65. 1-2. Böhm С. «The CUCH as a forma! description language», in Formal Language Description Languages tor Computer Programming. T. B. Steel, Jr. (ed.), Amsterdam: North Holland, 1966, pp. 179—197. 1-3. Church A. «A set of postulates for the foundation of logic», Ann. of Math. B) Vol. 33, 1932, pp. 346—366; Second paper Ann. of Math. B), Vol. 34, 1933, pp. 839—864. 1-4. Church A. «An unsolvable problem of elementary number theory», Amer. J. of Math., Vol. 58, I936a, pp. 345—363. 1-5. Church A. and Rosser J. B. «Some properties of conversion». Frans. Amer. M,ifh. Soc, Vol. 39, 1936b, pp. 472—482. 1-6. Church A. «The calculi of lambda conversion», Ami. of Math. Studies. Vo). 6, Princeton, N. J.: Princeton University Press, 1941. 1-7. Curry H. B. and Peys R. Combinatory Logic Vol. I, Amsterdam: North Holland, 1958. 1-8. Curry H. В., Hindley J. R. and Seldin J. P. Combinatory Logic, Vol. II, Amsterdam: North Holland, 1972. 1-9. Feys R. and Fitch F. B. Dictionary of Symbols in Mathematical Logic, Amsterdam: North Holland, 1969. 1-10. Kleene S. С «^-definability and recursiveness», Duke Math. J., Vol. 2, 1936, pp. 340—353. I-ll. Kleene S. C. Introduction to Metamathematics, van Nostrand, Princeton, 1964. 1-12. Hindley J. R., Lercher B. and Seldin J. P. Introduction to Combinatory Logic, Cambridge, England: Cambridge University Press, 1972. 1-13. Landin P. J. «The next 700 programming languages», CACM, Vol. 9, N. 3, 1966, p. 157—164. I-14. McCarthy J. «Recursive functions of symbolic expressions and their computation by machine». Part I, CACM, Vol. 3, N. 4, I960, pp. 184—195. I-I5. McCarthy J., Abrahams P. W., Edwards D. J., Hart T. P. and Levin M. J. LISP 1.5 Programmers Manual, Cambridge, Mass.: M. I. T. Press, 1962. 1-16. McCarthy J. «A basis for a mathematical theory of computation», (in) Computer Programming and Formal Systems, P. Braffort and D. Hirshberg (eds.), .Amsterdam; North Holland, 1963, pp. 33—70. Ы7. Rosenblooin P. С The Elements of Mathernatie-'il Logi(\ New York: Dover, 1950. 1-18. Turing A. M. «Compntability and X-delinabihty», J. SvmboHc Lo^^ic, Vol. 2, 1937, pp. 153-163. 47
Глава 2 СТРУКТУРЫ ПРОГРАММ ...Король закладывает их за щеку, как <)без1.яиа. Сует в рот первыми, а проглатывает последними. Понадобится то, чего вы насосалась, — он взял выдавил вас, п снова вы сухи для новой службы. ((Гамлет», акт 4, сцена 2 (перевод Б. Пастернака) 2.1. Введение Во второй части первой главы был приведен ряд примеров про- граммированн.ч с применением специальной системы сбозкачений комбинаторной логики. Основной проблемой использованного раздела комбинаторной логики, называемого комбинаторной арифметикой, является решение вопроса о наличии или отсутствии вычислительных методов, а не об их эффективности или оптимальной структуре полученных на их основе программ. Использование системы специальных обозначений лямбда-исчисления, на первый взгляд, кажегся не слишком удачным подходом при построении моделей языков нрограммировання, однако введение дополнитель[!ых синтаксических конструкций, приведенных в гл. 1, позволяет распшрнть возможности при.мененпя sroii системы для нрограм.мирования. Аналогично, и метод вычисления выражений путем их приведения к нормальной форме также кажется не такой удачной вычислительной моделью, как простые операции машины Тьюринга. В настоящей главе представлены другие методы вычисления выражений, более близкие современной технике программирования. Считая неизменной основную структуру выражений, расширим рассмотренную ранее систему програм.мирования за счет: 1) такого представления значений выражений, когда соответствующие им программы могут быть построены из набора простейших элементов, что оказывается более удобным для пользователей, чем запись выражений в нормальной форме; 2) применения более эффективных методов вычисления выражений. Несмотря на указанные изменения системы ирограм^шрованнл, выражения сохраняют два важных свойства, которые имеют большое значение при практическом программировании; 1) значение выражения зависит только от значений составляющих его подвыражений и не зависит от других их свойств; 2) приводимые выражения имеют одно и то же значение. Использование выражений в качестве базовых моделей позволяет описывать с их помощью семантику языков программирования. Мы можем установить соответствия между выражениями н различного вида блоками, процедурами, сопрограммами и соб- ственньпш (own) неремеипымн. Если бы требовалось лишь опи- 48
сать семантику языка, мы могли бы просто более детально рассмотреть эти соответствия и заменить их известными семантическими конструкциями лямбда-исчисления. Однако мы можем пойти еще дальше и сформировать такую последовательность действий вычислителя, в которой будут учтены структурные особенности выражений. Этому и посвящена данная глава. Развитие взятой за основу вычислительной машины, которая реализует метод обратной польской записи, будет проведено по пути последовательного наращивания дополнительных возможностей, и завершится 5£'С1)-машиной ^. Вычислительные машины можно разделить на два основных типа. В машинах первого типа последовательность вычислений определяется структурой вычисляемого выражения. В машинах второго типа исходное выражение сначала преобразуется в последовательность инструкций, или программу, а значение выражения получается после того, как эта программа будет выполнена. Использование выражений в качестве модели языков программирования не позволяет описать два важных элемента их структуры. Это, во-первых, операции переходов или операторы типа go to. Введение обобщенной операции перехода в SECD-машине позволяет удобно описывать операторы перехода и метки. Обобщение операций перехода в современных языках программирования представляет дополнительные возможности, которые могут оказаться весьма полезными ка практике. Второй элемент языков, который не описывается простыми выражениями, — оператор присваивания. Хотя некоторые случаи локального присваивания н можно трактовать как вспомогательные описания, присваивания более общего вида так описать нельзя. Это связано с тем, что в результате их выполнения происходит запись информации в некоторую позицию состояния ма- ишны. В результате одновременно изменяются все элементы состояния, в которые входит данная позиция. Эффект применения оператора присваивания объясняется здесь путем описания компонентов состояния машины и задания правил совмещения позиций выделенных для них элементов памяти. Использованный в большинстве параграфов этой главы метод заключается в вычислении значения операнда перед применением к нему значения оператора. В последней части этой главы приведены другие методы вычислений, при которых часть расчетов может быть выполнена раньше других или задержана до определенного момента. 2.2. Программы обратной польской записи В вычислительных машинах, описанных в этой главе, применяется получивший дальнейшее развитие известный принцип, основанный иа преобразовании арифметического выражения в про- ^ Понятие SfCO-машины будс1 об'ьясцспо в п. 2.7. — Прим. пер- 49
грамму, реализующую обратную польскую запись. Эта программа выполняется с использованием магазинного списка, предназначенного для запоминания промежуточных результатов вычислений. В гл. 1 было показано, что арифметические выражения являются специальным типом выражений в терминах лямбда-исчисления. В настоящей главе будет показано, как компилятор и машина могут быть модифицированы для обработки выражений более обпхего типа. Начнем с подробного рассмотрения того, как работает компилятор обратной польской записи и как выполняется программа, получаемая после компиляции. В том случае, когда аргументы функции находят перед ее применением и порядок их вычисления задан, последовательность обработки выражения жестко определена. Если аргументы выбираются слева направо, то выражение, дерево которого приведено на рис. 2.1, преобразуется в следующую программу в обратной польской записи: ab + с — acf +. Значение выражения будет получено при интерпретации этой программы. Здесь символами а, & и с обозначены инструкции, предназначенные для получения значений а, b и с \\ записи этих значений в начало магазинниго списка. Символы -^-, — и / служат для обозначения операций плюс, минус и некоторой (функции / над аргу.^:ентами, котг;рые расположены в начале списка. Все промежуточные результаты и значения подвыражений также помещаются в магазинный список. На рис. 2.2 показано соответствие между структурой выражения, программой и магазинным списком. Линии, пересекающие дерево, отделяют последовательные состояния процесса вычислений. Эти линпи разделяют программу в точках, в которых происходит изменение содержимого счетчика инструкций, и пересекают дерево в точках, представляющих текущее состояние магазинного списка. Каждая точка пересечения линии с деревом соответствует отдельной ячейке этого списка. Число, содержащееся в ячейке списка, равно зна- /\ /\ Р',\с. 2.{. Структура дерева выралчс- нин 50 1>ис. 2.2. lloc.neAOBaTejibHocib ния выражения
Состояние 1 2 3 4 5 6 7 8 9 10 Программа а b + с — а с 1 + Магазинный список 1 1, 2 3 3, 3 0 0, 1 0, 1, 3 0, 10 10 чению выражения, которому соответствует дерево, располо- женнсе ниже точки пересечения. Каждая линия может пересекать ветви, в которых вычисления осуществляются или не осуществляются. Операция программы, расположенная между соседними линиями, изменяет состояние списка, в результате чего предыдущее состояние, соответствующее левой рис. г.З. последовательность выполнения линии, заменяется состоянием, "ро'"?»""" соответствующим правой линии. На рис. 2.3 приведена последовательность выполнения рассматриваемой программы, при значениях а, b н с, равных 1, 2 п 3, и функции / {х, у) =- х"- + г/^. Здесь же показано, как изменяется состояние магазпнною cniiCKa в процессе выполнения программы. По окончании работы программы результат может быть также занесен в список. Программа в этом случае должна состоять из инструкций следующих двух типсв: 1) засылка числа в магазинный список; 2) выполнение оператора над соответствующим числом операндов, значения которых расположены в голове списка, и замена их полученным результатом. При интерпретации программы в обратной польской записи необходимо придерживаться следующих правил. 1. Каждый оператор должен иметь только один результат. 2. Один идентификатор не должен обозначать одновременно оператор и операнд. 3. Каждый оператор должен иметь конечное число операндов. Проведем контур, содержащий несколько связанных узлов дерева (рис. 2.4, а). Часть дерева внутри контура определим как новую функцию или программу. Такой контур можно заменить новым операторным узлом d (рис. 2.4, б). В этом случае ветви, входящие в этот контур снизу, будут определять аргументы функции, а выходящая из контура ветвь представляет результат (рис. 2.4, в). Верхнее дерево соответствует выражению ((а + b)~c)+f (а, с), а нижняя пара деревьев соответствует выражениям d {а, с) -f f (а, с) и d (х, у) --- {х + Ь) ~ ■ у. Компилятор должен быть построен таким образом, чтобы вычисления по обеим программам приводили к одному результату. Это свойство позволяет изменять программу путем введения 51
a с a Рис. 2.4. Деревья двух эквивалентных выражений НОВЫХ инструкций. Инструкция, реализующая выполнение функции d, должна содержать обращение к подпрограмме. Инструкции, соответствующие ветвям, которые пересекают контур, необходимо заменить инструкциями, определяющими значения величин а я с. Сущность этих инструкций и формат подпрограмм будут рассмотрены в следующих параграфах. 2.3. Значения идентификаторов Предположим, что существует преобразование, заданное функцией Е, которое ставит в соответствие каждому идентификатору его значение. Эта функция используется для присваивания значений идентификаторам, входящим в выражение. Идентификаторы являются константами системы, их значения будем называть примитивами. Если значение идентификатора есть (Ех), то для до- 52
бавления к преобразованию Е новой пары ипдификатор — значение можно воспользоваться функцией расширения extend: def extend (х, у) Е z = if X = Z then у else Е z . После того как новая пара включена в состав функции преобразования, любое предыдущее определение х считается недействительным. Преобразование Е можно представить в виде таблицы ассоциаций или оперативной среды, которая может быть структурно реализована как список пар либо как два списка. При первом способе реализации оперативной среды значение идентификатора может быть получено с помощью функции поиска lookup: def rec lookup E x = if null E then false, ( ) else if X = 1st (h E) then true, 2nd (h E) else lookup (t E) x . Данная функция формирует пару, первый элемент которой есть true (истина), если требуемый идентификатор найден в списке, а второй представляет собой значение этого идентификатора. Если преобразование Е реализовано в виде двух списков, то порядок расположения значений идентификаторов в одном списке должен соответствовать последовательности идентификаторов в другом списке. В этом случае определение функции lookup для получения значения идентификатора должно быть следующим: def lookup Е X = let b, V = position Ast E) x if b then select v Bnd E) else false, ( ) where rec select n x = if null X then false, ( ) else if n = 1 then true, h x else select (n — 1) (t x) . При таком способе определения функции lookup значение идентификатора будет получено в два этапа. Сначала определяется место идентификатора в первом списке, а затем значение его выбирается с помощью функции select из соответствующей позиции второго списка. Два этих списка могут быть объединены в списковую структуру с помощью функции positionlist, рассмотренной в п. 1.8. 53
функцию select можно заменить функцией selecils, которая определяется следующим образом: def гее selcclls п х == let b, V —- select (h n) x if b then selectis (t n) v else false, () . Для упрощения будем считать, что каждый идентификатор, значение которого отыскивается с помощью этой функции, содержится в списке. Связь между идентификатором и его значением реализована с помощью функции размещения location. Операция получения значения идентификатора, включенного в преобразование Е, записывается в виде {location Е х). 2.4. Вычисление комбинаций Структура программы, полученной с помощью компилятора обратной польской записи, зависит только от структуры исходного выражения. При этом идентификаторы обозначают любой объект, который может быть представлен в машине, или любую функцию, которая реализована в ней. Рассмотрим более подробно, как содержание программы зависит от вида выражения. Подлежащие вычислению выражения, симметричные относительно структуры оператор/операнд, называются комбинациями и определяются следующим образом: ■ Комбинация есть — или идентификатор (Identifier) — или составная комбинация (compound), которая содержит — — оператор (operator), являющийся комбинацией, и операнд (operand), являющийся комбинацией. Для сокращения записи будем далее использовать вместо терминов operator и operand соответственно rator и rand. Машина, описанная ниже, предназначена для нахождения значения комбинации в заданной оперативной среде. Ее реализация может быть представлена в виде следующей функции: def гее value Е х = if identifier х then location Е х Е else (value Е (rator х )) (value Е (rand х)) . Как И ранее, состояние машины определяют два компонента: 1) стек, являющийся списком объектов и используемый как рабочее пространство; 2) управляющая строка, которая является списком комбинаций и представляет рабочую программу. Состояние, стек которого есть S, а управляющая строка С, будем обозначать (SC). Содержащиеся в управляющей строке 54
комбинации включают специальную комбинацию применения 'apply' нспсльзуемую как инструкция, которая инициирует применение функции к заданному аргументу. Интерпретатор комбинаций представлен ниже в внде фуккп,:и1 перехода transition, которая преобразует текущее состо.чние машины в последующее: def transition Е (S, С) = if null С then (S, С) else let X = h С if identifier X then (location E X E : S, t C) else if compound X then (S, rand X : (rator X : ('apply' : (t C)))) else if X = 'apply' then let f : у : SI = S (f У :SI, t C). Если в момент начала работы машины стек пуст и управляющая строка содержит комбинацию х, то, когда управляющая строка будч^т исчерпана, в результате применения функции перехода в стеке будет содержаться значение х, соответствующее оперативней среде Е. Другими словами, будет выполнено следующее преобразование состояния: (О, х: {)) ^ {value Ех : (), ()). Машина будет интерпретировать любую комбинацию, содержащую идентификаторы, определяемые средой Е, и любую составную комбинацию, в которой оператор является функцией, применимой к операнду. Если в голове управляющей строки находится идентификатор, машина загружает его значение в стек и устраняет первый элемент строки. В случае, когда в начале управляющей строки находится составная комбинация, строка преобразуется. Комбинация заменяется тройкой, состоящей из операнда, оператора и указателя при.менения 'apply'. Если же этот указатель находится в голове управляющей строки, то вычисляется функция, находящаяся в голове стека. Ее аргументом будет следующий элемент стека. После этого первые два элемента стека заменяются значением вычисленной функции, а управляющая строка заменяется ее оставшейся частью. Последовательность действий машины определяется только структурными свойствами выражений: набор объектов и идентификаторов, преобразование идентификатор — значение и функции являются для нее лишь параметрами. Используя их, можно pacujnpuTb преобразование идентификатор — значение в преобразование комбинация — значение, 55
можно предста- Некото- па них Выражение а + b — с -{- f (а, с), например, вить в виде следующей комбинации: р (т (р а Ь) с) if а с), где р X у -=■ X ~\- у; т X у '-=-- X — у; f X у =--= х- + у\ Последовательность состояний машины при вычислении этого выражения показана на рис. 2.5. Стек изображен слева, правый элемент стека является его головой. Последовательность управляющих строк приведена справа, пх головой является крайний левый элемент. Символ 'Л' используется вместо указателя применения 'apply'. В оперативной среде заданы следующие значения параметров: о = 1, Ь = 2, с = 3. Обозначение 'р 2' соответствует функции, добавляющей 2 к значению аргумента, рые достаточно очевидные шаги на рис. 2.5 опущены, мы остановимся подробнее в следующем параграфе. S С О О О О 1, 2 1, 2, I. Р 3 3 3, 3, 3, О О О о, о, о, о, о, о, о, о, 3 3, т т 3 1, ; /1 i Р 10 Рис. 2.5. р A а с) (т с (р b т с (р b а), р (f а i'), /; b а, т с, А, р (f а с), а, р Ь, А, т с, А, р (f а с), р Ь, Л, т с, А, р (f а с). Ь, р, А, А, т с, А, р (f а с). р, А, Л, т с. А, р (f а с), А, А, 111 с. А, р (f а с), А, т с. А, р A а с), т с, А, р (f а с). с, т, А, А, р (f а с), т, А, А, р if а с). А, А, р (f а с). А, р (f а с) р (f а с) f а с, р, А, с, f а, А, р, А f а, .4, р. А, а, f. Л, Л, р, А f, Л, Л, р, А А, Л, р, Л, Л, р, А, Р, Л. А, сть вычисления комбинации а)) А А А А А А А А А А А А А А А А А А А А А Л А А 56
Отличие машины, рассмотренной в этом параграфе, от машины, описанной в п. 2.2, состоит в следующем. ■ В число промежуточных результатов, загружаемых в стек, включены значения всех операторов. ■ Каждая функция имеет лишь один аргумент. Если у функции больше одного аргумента, она преобразуется в список. ■ Значение комбинации может быть функцией, которая вычисляется позже. 2.5. Компилятор комбинаций Рассмотрим процедуру вычисления комбинаций, которая выполняется в два этапа. Сначала комбинация компилируется в список инструкций или программу. Затем эта программа интерпретируется, в результате чего будет получено значение комбинации. Компилятор, который мы назовем revpol, обрабатывает комбинацию и исходные данные и формирует список инструкций вида ■ Инструкция есть — или инструкция загрузки (load) с операндом (operand), который является переключателем, — или инструкция применения ('apply'). Функцию, результатом применения которой является инструкция загрузки, будем обозначать consload. Определим компилятор revpol следующим образом: def гее revpol Е х = if identifier х tiien consload (location E x) etse concatenate (revpol E (rand x), revpol E (rator x), u 'apply') . Компилятор заменяет идентификаторы их значениями и преобразует составные комбинации в список из трех инструкций. При интерпретации этих трех инструкций сначала в стек загружается значение операнда, затем загружается значение оператора и пссле этого оператор применяется к данному операнду. Функция перехода к новому состоянию (функция transition), приведенная ниже, оказывается более простой по сравнению с описанной в п. 2.4. Это объясняется тем, что на этапе компиляции идентификаторы заменяются их значениями, а составные комбинации представляются в виде списка инструкций. def transition Е (S, С) = if null С then (S, С) else let X = hC if load X tiien (X : E : S, tC) else if X = ' apply' tiien let f : у : SI = S (fy : SI, tC) . Функция revpol преобразует выражение p (m (p a b) c) (f a c), соответствующее дереву на рис. 2.6, в программу следующего вида: 3, 1, f, А, А, 3, 2, 1, р. А, А, т, А, А, р, А, А. В этой программе инструкция применения 'apply' обозначена буквой А, а все остальные — являются инструкциями загрузки. Такая программа обратной польской записи удовлетворяет требованиям, сформулированным в п. 2.2. Она содержит 57
/\ д с /\ /77 Д / \ А b / \ р а лишь одни оператор А, который не используется в качестве операнда и всегда имеет два аргу- ■лента. Строка программы формируется путем соответствующего объединения результатов компиляции правой п левой ветвей дерева п оператора А. На рис. 2.7 приведена последовательность состояний машины при выполнении рассмотренной программы. Она стала короче, чем в случае интерпретации (см. рис. 2.5), так как преобразование составной комбинации в список из трех инструкций уже было проведено на этапе компиляции. Как видно из рассмотренного примера, компилятор для комбинаций не имеет преимуществ перед интерпретатором. Результирующая программа лишь несущественно отличается от предыдущей. Описание компилятора было проведено только для того, чтобы подготовить читателя к изложению более эффективного метода вычисления сложных выражений, содержащих лямбда- выражения. При обработке таких выражений тело лямбда-выражения может вычисляться неоднократно, тогда как его компиляция проводится только один раз. В следующем параграфе рассмотрены особенности языков программирования, связанные с вычислением выражений, содержащих лямбда-выражения. S С Нис. 2.6. Дерево выражения 3 3, 1 3, 1, f 3, f 1 10 10, 3 10, 3, 2 10, 3, 2, 1 10, 3, 2, 1, р 10, 3, 2, р 1 10, 3, 3 10, 3, 3, т 10, 3, m 3 10, О 10, О, р 10, р О 10 3 1 / А А с b а 'а А т А А Р А А Рис. 2.7. Стадии работы компилятора 58
2.6. Блоки и процедуры Две описанные в предыдущих параграфах машины могут быть использованы только для вычисления выражений, содержаищх константы. Возможности машин будут далее расширены для вычисления выражений, содержащих переменные. Эти улучшенные машины могут служить моделью выполнения блоков п процедур в языках программирования. Ниже рассмотрены проблемы, связанные с наличием блоков и процедур в языках программирования. Примеры взяты из языка АЛГОЛ-60. В языке АЛГОЛ-60 синтаксическая структура блока имеет следующий вид: begin D; D; D; .,.; S; S; S; S end . Здесь символом D обозначены описания, а символом S — операторы. Например, описание integer х служит для выделения области памяти для числа целого типа, ссылка на которое по имени может содержаться в операторах блока. Наличие списка описаний в блоке приводит к появлению списка областей памяти, выделенных программе. Блок, показанный на рис. 2.8, а, реализуется в виде программы, структура которой приведена на рис. 2.8, б. При блочной структуре области памяти для переменных X и у, называемых локальными переменными, используются только в процессе выполнения программы, соответствующей данному блоку. Блок может иметь вложенную структуру, так как входящие в него операторы также могут быть блоками. Программа такого типа приведена на рис. 2.9, а. Здесь переменная z локализована begin integer х, у; • • X « епа. X У X — begin integer x/j • X .,._ • • • • begin Integer z • X • z • end end t J EH ^ {2,1) « 1,1) J a) Ю a) Ю Piic. 2.8. Блок Л''ГОЛ-вО описанием в языке Рис. 2.9. Индексация при ссылках па переменные во вложенных блоках 59
bffgi.n I Л1 Блок1' begin Z JJ2 end end end begin 'f * end begin integer >v; ßßOKZ- E/iOKiJ Блок ^' end begin procedure f(x,y,z) begin X w end Рис. 2.10. Ссылки на переменные begin integer w Щг,7) end end a) Рис. 2.11. Опнсаиия процедур begin proeedure fix) begin g end procedure g(y); begin f end end Ю BO вложенном блоке. Переменная x является локальной пере- менноР! для внешнего блока, но она не будет локально?! во вложенном блоке. При вложенной структуре блоков удобно для ссылки на переменную использовать два индекса. Первый из них определяет глубину вложения и указывает на список описаний данного блока. Глубину 1 имеют локальные переменные внешнего блока; локальные переменные блока, вложенного во внешний блок, имеют глубину i и т. д. Второй индекс указывает на место в списке элементов области памяти, выделенной для данной переменной. Вид оттранслированной программы приведен на рис. 2.9, б. Так как преобразование переменных в их позиции в оперативной среде определяется только блочной структурой программы, оперативную среду часто называют статической цепью. На рис. 2.10 приведена программа, состоящая из четырех блоков 1—4. Справа стрелками показаны допустимые ссылки на переменные, расположенные в каждом из блоков. Описание процедуры обычно имеет вид procedure f (х, у, z); S, где S — оператор. Переменные х, г/ и г называют формальными параметрами процедуры. Значения формальных параметров передаются процедуре в момент обращения к ней в операторе или выражении, например / C, 2, 7). Необходимо отметить, что значения используемых в операторе 5 переменных зависят от того места, где процедура описана, а не от места, где содержится 60
обращение к пей. В схеме программы на рнс. 2.11, а процедура /■ описана в одном блоке, а обращение к ней находится в другом блоке. В момент обращения к процедуре / с параметрами C, 2, 7) изменяются как текущая оперативная среда, так и точка входа в программу. Текущая оперативная среда заменяется оперативной средой, соответствующей точке описания процедуры /. К ней добавляются значения аргументов C, 2, 7), необходимые для выполнения процедуры. Новой точкой входа в программу теперь будет вход процедуры. После того как процедура будет выполнена, управление передается следующему оператору основной программы и восстанавливается оперативная среда этой программы. Часть состояния, используемую для хранения информации о передачах управления при выполнении процедур, называют дампом (dump). В нем должны содержаться счетчик ячеек и оперативная среда. Кроме того, он должен содержать дамповую компоненту для получения результата расчетов в случае, если обращение к процедуре является составной частью вычисляемого выражения. Дамп также должен содержать и текущий стек. Список состояний, связанных между собой компонентами дампа, часто называют динамической цепью. Еще одна особенность языка АЛГОЛ-60 состоит в том, что аргумент процедуры также может быть процедурой. В этом случае процедуре необходимо передать информацию, содержащую указание на место расположения тела процедуры, которая является аргументом, а также оперативной среды, соответствующей точке программы, где расположено описание процедуры — аргумента. Если в одном блоке содержится описание нескольких процедур, то по правилам языка АЛГОЛ-60 область их действия должна включать и тела этих процедур. На рис. 2Л1, б показан блок, в котором описаны процедуры fug, содержащие обращения друг к другу. Такие описания называются взаимно рекурсивными. Появление / и g в теле процедур означает обращение к соответствующим процедурам, описанным во внешнем блоке, а не к процедурам с теми же именами, которые могли бы быть описаны во вложенных блоках. Границы массивов в языке АЛГОЛ-60 могут быть заданы в виде выражений. В этом случае значения граничных выражений должны вычисляться в оперативной среде непосредственно вложенного блока. Проблемы, связанные с установлением области действия переменных такого типа, могут быть решены путем перехода от текста программы к выражениям лямбда-исчисления, где области действия переменных четко определены. Соответствующие правила построения программ по исходному тексту на языках программирования могут быть выведены из правил построения программ для лямбда-выражений. Изложению этих правил посвящены следующие три параграфа главы. §1
2.7. Вычисление выражений Вопросы обработки ч[1слеР1ных выражений были рассмотрены в п. 2,4. и 2.5 данной главы. Здесь будут изложены способы вычисления выражений, результатом которых являются выражения. Для этих целей мож(.т быть использован интерпретатор комбинаций, дополненный двумя особыми операциями для: 1) загрузки значения лямбда-выражения в стек и 2) применения значения лямбда-выражения к заданному аргументу. Введение этих операций определяет метод представления функций в виде машинных программ. Структура рассматриваемых далее выражений задается следующим определением; Ш Выражение есть — или идентификатор (Ideniljler) — или лямбда-выражение, состоящее из связанной переменной (он), которая является идентификатором, и тела (body), которое является выражением, — или составное выражение (compound), состоящее из оператора (rator), который является выражением, и операнда (rand), который является выражением. В машинах для вычисления комбинаций, описанных в ни. 2.3 и 2.4 данной главы, для присвоения значений идентификаторам используется оперативная среда. Результат применения функции, полученной из лямбда-выражения, будет найден после вычисления тела этой функции. Поэтому оперативная среда должна содержать связанную переменную лямбда-выражения вместе с ее значением, т. е. аргумент функции. Рассмотрим выражение ах"- -f bx + с, where х = d или его эквивалентную форму (kx.ax"^ -^ bx -i- с) d. Для того чтобы вычислить выражение ах^ -{- Ьх -{- с, в оперативной среде должны содержаться значения переменных а, Ь, с и X. Значение лямбда-выражения Хх.ах^ + Ьх + с не зависит от X. Однако значение х используется только в момент при.мене- ния функции к конкретному аргументу, и этот аргумент будет равен значению х лишь в процессе применения этой функции. Значение лямбда-выражения типа Кх.ах^ -^ Ьх + с в интерпретаторе, описанном ниже, формируется из самого лямбда-выражения, дополненного оперативной средой, содержащей значения а, Ь, с, и операций '+' и возведение в квадрат. Такой набор информации называется ядром (closure). Структура ядра имеет следующий вид: Ш Ядро состоит из — управляющей части (Сс), которая является списком инструкций, — свя:!анной переменной (bv), которая является идентификатором, — н оперативной среды (Ее), которая представляет собой список пар идентификатор — значение. 62
Ядро cfHj3MHpyeT.cH из этих трех компонентов с помощью фупк- uüii consdosure. При обработке выражения {%х.а>г 1- Ьх + с) d в оперативной среде, содержащей а = \, ö = 2, с = 3 и d ^= 4, первым вычисляется операнд, значение которого D) загружается в стек. Затем вычисляется оператор. После этого формируется ядро ('ах" + Ьх -\- с', 'х\ (а = \, b = 2, с ^-^ 3, d ^ 4)), которое такл^е загружается в стек. Далее связанная переменная объединяется с ее аргументом, и эта пара присоединяется к оперативной среде ядра. Наконец, результат будет получен после вычисления тела ядра в расширенной оперативной среде. Отсюда следует, что value (а = 1, ö = 2, с = 3, d = 4)' (Кх, ах" + Ьх + с) d' = = value (а = 1, b = 2, с =-- 3, d = 4, х = А)' ах^ + bx + с'. Значение выражения теперь можно определить с помощью функции value: def rec value E x = if identifier x then location E x E else if lambda expression x then f where f у = value ((bv x, y) : E) (body x) else (value E (rator x) (value E (rand x)) . Значение лямбда-выражения является функцией, после применения которой к аргументу у будет получено значение тела лямбда-выражения в расширенной оперативной среде. Эта среда формируется из текущей оперативной среды Е и пары, состоящей из связанной переменной лямбда-выражения и аргумента у. Таким образом, использование лямбда-выражений приводит к изменению оперативной среды в процессе вычислений. Вот почему она должна быть включена в состав состояния машины. В процессе применения ядра часть расчетов, связанных с вычислением тела, может выполняться на другой вычислительной машине. Ей передаются тело ядра и расширенная оперативная среда. После того как вычисления на второй машине будут закончены, первая машина загружает полученный результат в стек и завершает вычисления. Если используется одна машина, то необходимо запомнить текущее состояние, соответствующее точке начала вычисления тела ядра. Это необходимо для того, чтобы завершение вычислений можно было провести с той же точки программы. Часть состояния машины, предназначенная для запоминания текущего состояния, называется дампом. Структура состояния машины для вычисления выражений расширена за счет введения двух дополнительных компоненте с. Теперь состояние включает стек и управляющую строку, icaK об этом говорилось ранее, а также оперативную среду и компо- 63
нент для запоминания состояния, называемый дампом. С учетом дополнительных компонентов структура состояния задается следующим правилом: ■ Состояние содержит — стек {Sc), который является списком объектов, — оперативную среду (£s), которая является списком пар идентификатор/объект, — управляющую строку (Ca), которая является списком выражений, —■ и далт (Di), который является состоянием. Состояние будем описывать списком из четырех параметров. Теперь переход к новому состоянию, выполняемый с помощью функции transition, будет включать два дополнительных этапа по сравнению с функцией перехода для комбинаций (см. п. 2.5). Это, во-первых, загрузка ядра в случае, если лямбда-выражение находится в начале управляющей строки. И, во-вторых, применение ядра к аргументу. Четыре компонента состояния будем записывать символами 5, Е, С и D для обозначения стека, оперативной среды, управляющей строки и дампа соответственно. Назовем описанную машину SECD-uamuHOH [2—10]. В этой машине определение функции перехода имеет следующий вид; def transition (S, Е, С, D) = if null С then (h S: SI, El, CI, DI) where SI, El, CI, Dl = D else let X = h С if identifier X then (location E X E : S. E, t C, D) else if lambda expression X then consclosure (body X, bv, X, E) : S, E, I C, D else if X = 'applv' then let f : у : 31 == S if closure f then let consclosure (CI, J, EI) — [ (), (J, y) : El, u CI, (SI, E, t C, D) else f у : SI, E, t C, D else let combine (F, A) = X S, E, A : (F : ('apply' : t C)), D . Применение ядра к аргументу выполняется следующим образом. Ядро и аргумент удаляются из стека, оператор 'apply' исключается из управляющей строки, после чего это состояние записывается в дамп. Новый стек представляет собой пустой список. Новая оперативная среда формируется из оперативной среды ядра, объединяемой с парой «связанная переменная ядра/аргумент». Новая управляющая строка содержит лишь один элемент — тело ядра. Когда тело ядра вычислено, управляющая строка исчерпывается. После этого выполняются следующие действия: 1) из начала стека исключается результат вычисления тела ядра; 2) восстанавливается состояние машины, которое было записано в дампе; 3) результат загружается в стек, соответствующий вос- 64
станойленному состоянию. Таким образом, применение ядра к аргументу, в общем, аналогично применению обычной функции, так как в обоих случаях функция и аргумент удаляются из стека и заменяются результатом. Отличие состоит в том, что в случае применения ядра осуществляются более сложные изменения состояния. Применение ядра к аргументу соответствует вызову процедуры или подпрограммы. Действия, выполняемые после того, как управляющая строка исчерпывается, соответствуют выходу из подпрограммы или процедуры. И это соответствие будет еще более полным, если модель языка содержит также операторы присваивания и перехода (goto). Следует отметить, что любое выражение может быть непосредственным компонентом другого выражения и входить в него, как: 1) оператор составного выражения; 2) операнд составного выражения; 3) тело лямбда-выражения. Если этот компонент является лямбда-выражением, то первая конструкция означает, что процедура может быть применена к аргументу; вторая — что процедура может быть аргументом, а третья — что процедура может быть результатом применения процедуры к ее аргументу. Содержимое ядра является той информацией, которую необходимо передать процедуре, содержащей процедуру в качестве одного из аргументов. С другой стороны, набор содержащейся в ядре информации соответствует той информации, которая будет получена, если процедура является результатом применения некоторой процедуры к ее аргументу. На рис. 2.12 приведена последовательность состояний машины при вычислении выражения {(kf.Kx.f (/ х)) square 3). Ядро заключено в квадратные скобки. S Е CD О О 3, sq О 3, sq, [%x.f(f X), f, {)] О О il^sq) Vif X), X, if=sq)] О 3, Uifx), X, A= sq)] (l=sq) 0 ix=3, f=sq) 0 (л: = 3, / = sq) 0 (д; = 3, / = sq) 3 (x = 3, f = sq) 3, sq (x= 3, f== sq) 9 (x= 3, f=- sq) 9, sq (x=: 3, } — sq) 27 (x= 3, f= sq) 27 0 Рис. 2.12. Пример работы SECD-машины 3 Бердж В. 65 (XfJx.f (/ X)) sq 3 3, m.Kx.f if X)) sq), A ({Mlx.nf>'))sq), A sq, 'kf.Kx.f if x). A, A Kf.Xx.l If x), A, A A, A kx.f (/ л:) 0 A fifx) f x,f, A X, f, A, f, A f, A, f, A A, f, A f, A A 0 0 D D D D D D C, (), A, D) C, (), A. D) D i (). 0, (), D) ( 0, (). (). D) I 0- 0. (). D) i 0, 0. 0, D) ( ()- 0. (). D) i h. 0, (). D) ( 0, 0, (). D) ( 0. (). (). O) D
2.8. Компиляция выражений Способ выбора из оперативной среды значения идентификатора, который используется в качестве связанной переменной лямбда- выражения, не связан со значением этого выражения (конечно, при условии, что исключено смешивание связанных переменных). Отсюда следует, что сами эти идентификаторы не играют роли при вычислении лямбда-выражения. В этом параграфе будет показано, как можно исключить их на стадии компиляции. В лямбда-выражениях вида Кх.М используются идентификаторы трех типов. Первый тип — свободные переменные в лямбда- выражении, значения которых выбираются из текущей оперативной среды в процессе вычисления данного лямбда-выражения. Второй тип —■ связанная переменная лямбда-выражения, свободная по отношению к телу этого выражения. Например, это может быть идентификатор х в М, который служит аргументом лямбда- выражения. Значение такого идентификатора является аргументом функции и используется только при непосредственном применении этой функции. Третий тип идентификаторов — связанная переменная в теле лямбда-выражения. Если лямбда-выражение является оператором, оно вычисляется и сразу же применяется к своему аргументу. Если лямбда-выражение является операндом, то оно вычисляется один раз, а его значение может быть неоднократно применено к различным аргументам. В этом случае оперативная среда, используемая для вычисления тела лямбда-выражения, представляет собой список, состоящий из двух частей; головы, в которой содержится аргумент, и хвоста, в котором помещены значения необходимых свободных переменных. Хвост оперативной среды фиксируется в момент вычисления выражения Хх.М. Голова среды изменяется, если функция применяется к различным аргументам, а тело лямбда-выражения вычисляется каждый раз при обращении к новому значению аргумента. Оперативная среда разрастается тем больше, чем большее число вложенных лямбда-выражений содержится в вычисляемом выражении. При этом каждый раз вычисляется положение переменной, а ее значение загружается в стек. Позиция переменной в оперативной среде должна оставаться неизменной. Она задается раз и навсегда во время просмотра исходного текста при подсчете числа вложенных лямбда- выражений. Этот подход позволяет разграничить такие понятия, как положение переменной и область ее определения. Число, задающее положение переменной, позволяет получить ее значение из текущей оперативной среды. При появлении одного и того же идентификатора на различных уровнях лямбда-выражения изменяется глубина переменной. Положение идентификатора (или в общем случае подвыражения) может быть указано с помощью цепочки функций выбора, которые составляются из функций оператор (rator), операнд ее
(rand) и тело (body). Глубина переменной определяется числом функций body в цепочке. Рассмотрим в качестве примера выражение Кх. {g Xi) {Ц. (kz.x^ у 2)(/ Xs)), в котором переменная х- занимает три различных положения: 1) переменная х-^ выбирается цепочкой rand-rator-body и, следовательно, имеет глубину 1; 2) переменная х^ выбирается цепочкой rator'^-body-rator X X body-rand-body и имеет глубину 3; 3) переменная Xg выбирается цепочкой rand'^-body-rand-body и имеет глубину 2. Все входящие в выражение связанные переменные без ущерба для последующих действий могут быть заменены значениями их глубины. В частности, приведенное выше выражение преобразуется в этом случае к виду I. {g 1) (I. {К.З 2 1) (/ 2)). Вместо списка идентификаторов, которые являются свободными в выражении, можно использовать список их глубин. Глубина идентификаторов связанных переменных вычисляется путем суммирования их собственных глубин и длины списка свободных переменных. Функция замены идентификаторов их глубиной (далее обозначаемая replace) определяется следующим образом: def гес replace Е х = if identifier х then position Е х else if lambda expression x then cons X (replace (bv x : E) (body x)) else combine (replace E (rator x)) (replace E (rand x)) . После обращения к этой функции с параметрами g я f — replace ('§•', '/') ^ получим преобразованное выражение К. A 3) {К. {К.5 4 3) B 4)). Значение выражения может быть получено в том случае, если сформирован список свободных идентификаторов. Если идентификатор занимает в списке Е ту же позицию, что и его значение в списке F, то для получения значения этого идентификатора может быть использована функция val: def гес val F х = if integer х then select x F else if lambda expression x then f where f у = val (y : F) (body x) else (val F (rator x)) (val F (rand x)). Таким образом, значение идентификатора определяется в два этапа. 3* 67
Объединим теперь два этапа компиляции, которые используются для замены переменных их глубиной и преобразования дерева обрабатываемого выражения в список инструкций. В описанном ниже компиляторе обратной польской записи идентификаторы, которые являются свободными в компилируемом выражении, преобразуются, как и ранее, в инструкции, содержащие информацию о размещении связанных с ними значений. Позиции связанных переменных преобразуются в инструкции для выбора значений этих переменных из текущей оперативной среды. Формируемая компилятором программа не содержит идентификаторов. Результирующая программа состоит из инструкций четырех типов, определенных ниже. ■ Инструкция есть — или инструкция загрузки {toad), имеющая операнд, который является позицией, — или инструкция применения (appl), — или инструкция загрузки позиции (Ipoi), имеющая позицию, которая является целым числом, — или инструкция загрузки ядра (Idcl), имеющая ■ управляющую часть, которая является списком инструкций. Инструкции загрузки позиции (Ipos) и загрузки ядра {Idcl) формируются из соответствующих компонентов с помощью функций conslpos и consldcl. При работе компилятора используются два списка переменных. Первый список Е является списком идентификатор-значение. Он используется для нахождения значений свободных переменных. Второй список представляет собой список идентификаторов, предназначенный для определения глубины связанных переменных. Определение компилятора обратной польской записи теперь имеет следующий вид: def гее revpol (Е, F) х = if identifier х then let b, у = position F x if b then u (conslpos y) else u (consload (location E x E)) else if lambda expression x then u (consldcl (revpol (E, bv x : F) (body x))) else concatenate (revpol (E, F) (rand x), revpol (E, F) (rator x), u 'appl') . Оперативная среда и управляющая строка состояния должны быть изменены. Переопределим структуру состояния следующим образом: S Состояние состоит из — стека, который является списком объектов, — оперативной среды, которая является списком объектов, — управляющей строки, которая является списком инструкций, — дампа, который является состоянием. SS
Оперативная среда формируется в виде списка элементов, в котором содержатся значения связанных переменных. Необходимо также переопределить понятие ядра, новая структура которого имеет вид: S Ядро состоит из — оперативной среды {Ее), которая является списком объектов, и — управляющей части (Сс), представляющей собой список инструкций. Определим новую функцию перехода от предыдущего состояния к последующему (функцию transition): def transition (S, E, С, D) = if null С then h S : SI, E!, CI, DI where SI, EI, CI, DI = D else let X = h С if load X then X E : S, E, t C, D else if loadposition X then select (position X) E : S, E, t C, D else if loadclosure X then consclosure (control X, E) : S, E, t C, D else if X = 'apply' then let f : у : SI = S if closure f then let consclosure (CI, EI) = f ( ), у : El, u CI, (SI, E, t C, D) else f у : SI, E, t C, D . При интерпретации инструкций выполняются следующие действия: 1. По инструкции загрузки (load) объект загружается в стек. 2. По инструкции загрузки позиции (Ipos) в стек загружается объект, расположенный в п-й позиции от верхушки текущей оперативной среды (п — глубина, связанная с этой инструкцией). 3. По инструкции загрузки ядра (Idcl) формируется ядро из управляющей строки, связанной с этой инструкцией, и текущей оперативной среды. После формирования ядро загружается в стек. 4. По инструкции применения (apply') в голове стека отыскивается обычная функция или ядро. Если там находится функция, то она применяется к своему аргументу (т. е. к следующему элементу стека). После этого функция и аргумент заменяются результатом. Если в голове стека помещено ядро, то текущее состояние, за исключением самого ядра, его аргумента и функции применения 'арр1у\ записывается в дамп. Новый стек в этом случае будет пустым, новая оперативная среда будет средой, входящей в состав ядра, а новой управляющей строкой — управляющая строка ядра. 5. После того как управляющая строка ядра оказывается исчерпанной, выполняются следующие действия. Сначала восстанавливается состояние, которое ранее было записано в дампе текущего состояния, после чего результат применения ядра загружается в стек, соответствующий восстановленному состоянию. 69
Запишем исходное состояние в следующем виде: (( ), o.revpol (Е, ( )) X, D) . Применив функцию перехода (transition) к этому состоянию, получим новое состояние (value Е X :(),(),(), D) При условии, что выражение имеет значение. Транслятор обратной польской записи в результате обработки выражения {{if.kx.f (/ х)) sq 3) формирует следующую программу: load 3 С1 : Idcl С2 С2 : Ipos 1 load sq Ipns 2 Idcl CI appl appl Ipos 2 appl appl Последовательность состояний машины при интерпретации этой программы приведена на рис. 2.13. Лямбда-выражение может быть поставлено в соответствие подпрограмме или процедуре, а ядро, рассматриваемое как значение лямбда-выражения, соответствует машинной программе, которая является результатом компиляции процедуры. Применение ядра соответствует обращению к процедуре. Действия, выполняемые при исчерпывании управляющей строки, соответствуют выходу из процедуры. Имитация механизма обращения к процедуре в SECD-матине организована таким образом, что общий эффект применения простой функции и ядра оказывается одним и тем же; в обоих случаях функция и аргумент заменяются результатом вычислений. Отсюда следует, что функция и ядро взаимозаменяемы в том смысле, что любая из этих конструкций может быть использована в контексте, в котором допускается применение другой. Выражение, содержащее лямбда-выражение в качестве оператора, полностью соответствует блоку в языках АЛГОЛ-60 и ПЛ/1. Связанные переменные лямбда-выражения соответствуют локальным переменным блока, а тело выражения — операторам блока. О load 3 D 3, iq 3, iq, [(CI, 0 IC2, sq] 3, [C2, sq] 0 3 3, sq 9 9, sq 27 27 ()] 0 0 sq sq 0 C, C, C, C, C, C, 0 sq) sq) sq) sq) sq) sq) toad sq Idcl Cl appl Idcl C2 (exil) appl Ipos 1 Ipos 2 appl Ipos 1 appl (exit) D D C, (), appl, D) C, (), appl, D) D ( 0, ()> 0. D) ((). 0. 0, D) ( ()■ 0, (). D) ( (). (). (). D) ( 0, 0> 0. D) ( (). 0. (). D) D Рис. 2.13 Последовательность выполнения откомпи.лироЕанной програм|иы 70
begin real X lei л: = 0.0 X X {}.x. ..X.X..) 0.0 end Рис. 2.14. Блоки и let-выражеиия Операнд лямбда-выражения соответствует начальным значениям локальных переменных. Последнее соответствие нарушается, если, например, переменная объявлена целой (integer х), а начальное значение ей не присваивается. Для восстановления соответствия необходимо задать начальное значение, которое присваивается данной переменной при входе в блок. Соответствие между блоками, let-выражениями и бета-редексами иллюстрируется рис. 2.14. Использование лямбда-выражения в качестве операнда соответствует описанию процедуры. Связанные переменные соответствуют формальным параметрам процедуры, а тело лямбда-выражения— телу процедуры (рис. 2.15). Некоторую сложность представляет трансляция процедур, описания которых помещены в одном блоке. В языках АЛГО,Л-60 и ПЛ/1 такие процедуры могут быть взаимно рекурсивными. В этом случае в область действия описания процедуры кроме тела самой процедуры следует включить и тело параллельно объявленной процедуры. В этом случае процедуре может быть поставлено в соответствие выражение, которое включает функцию Y (рис. 2Л6). begin procedure / (х, у) let / (л, у) =^..х..у... beoin у end / / end Рис. 2.t5. Описание процедуры и определение вспомсгательной функции 71
begin ptocedute / (л,); begin (Я Q.g). ..) (YX (/, g). (Ax.../.g.. A^../.^-)) / end procedufe g (j/) begin / g end end Рис. 2.16. Описания взаимно рекурсивных процедур 2.9. Методы повышения эффективности программ В этом параграфе рассмотрены некоторые особые структурные свойства выражений. Выявление этих свойств на этапе компиляции позволяет значительно повысить эффективность результирующей программы. Будет введено несколько новых инструкций, объединяющих две (или больше) ранее рассмотренных инструкций. Эти составные инструкции дают возможность исключить некоторые действия, которые было бы необходимо выполнить при использовании только простых инструкций. Лямбда-выражения в качестве операторов. Когда лямбда-выражение выступает в роли оператора, оно применяется к своему аргументу только один раз. При этом компилятор, описанный в предыдущем параграфе, сформирует программу из двух инструкций: Idcl (С) и appl. (В первой инструкции символом С обозначена подпрограмма). Если полученную программу объединить с управляющей строкой, то ее можно будет интерпретировать как результат компиляции тела лямбда-выражения. В этом случае нет необходимости запоминать управляющую часть состояния в дампе или загружать в стек ядро. Более того, поскольку текущая оперативная среда будет представлять собой оставшуюся часть (хвост) оперативной среды после выполнения инструкций Idcl и appl, ее не нужно восстанавливать из дампа и, следовательно, не требуется предварительно записывать в дамп. Введем две новые инструкции — вход (enter) и выход (exit). Они предназначены для обрамления программы, полученной в результате компиляции тела лямбда-выражения, являющегося 72
оператором. Использование новых инструкций дает возможность сформировать следующую программу для выражения вида (Хх.М) N: program (N) enter program (М) exit . Без использования инструкций enter и exit получается следующая программа: program (N) С : program (М) Idcl (С) appl В последнем случае program (N), например, обозначает результат компиляции выражения Л^. Процесс преобразования состояния при выполнении инструкций enter и exit имеет следующий вид: (X : S, Е, enter : С, D) =ф- (( ), х : Е, С, (S, —, —, D)) И (г : S2, X : Е, exit : С, (S, —, —, D)) =*■ (г : S, Е, С, D). Результатом компиляции выражения является предписывающая управляющая строка, которая загружает значение в стек. Такая управляющая строка может состоять из простых инструкций или из двух предписывающих управляющих строк и инструкции применения appl. В последнем случае первая строка служит для загрузки операнда, а вторая используется для загрузки оператора. Если в начале управляющей строки помещена инструкция enter, а в конце находится инструкция exit, то эта строка становится функциональной управляющей строкой. Она предназначена для преобразования аргумента, загруженного в стек. Функциональная управляющая строка может быть также сформирована из предписывающей управляющей строки для функции, которая дополнена инструкцией appl. Применение таких блочных конструкций является наиболее эффективным методом реализации функциональных управляющих строк. Если при построении компилятора учтена возможность использования таких управляющих строк, то в результате обработки лямбда-выражения вида (Xf.Xx.f (/ х)) sq 3 будет получена следующая программа: load 3 С2 : Ipos 1 load sq Ipos 2 enter appl Idcl C2 Ipos 2 exit appl appl 73
3 3, sq 0 fC2, sq] S, [C2, sq] 0 3 3, sq 9 9, ^9 27 27 0 0 sq sq 0 C, sg) C, sa) C, sg) C, sq) C, sg) C, .¾) 0 load 3 load sq etiler Idcl C2 exit appl Ipos ! tpos 2 appl Ipos 2 appl {exit) D D C, - C, - D (()> ( (()■ ( @> ( @. ( @. ( @. ( D -, -, D) -, -, D) ), 0, D) ). 0, D) ), 0. D) ), 0. D) ), 0, ß) ). (). О) Рис. 2.17. Последовательность обработки блока На рис. 2.17 приведена последовательность состояний машины при интерпретации этой программы. Переменные в качестве операторов. Объединим инструкции Ipos (р) и appl в одну инструкцию apos (/?). Результат применения инструкции apos (р) будет аналогичен результату последовательного при.менения двух исходных инструкций, но в этом случае нет необходимости загружать функцию в стек. Теперь последовательность выполнения программы С2 будет иметь вид С2 : Ipos ! apos 2 apos 2. Лямбда-выражения в качестве операндов. Лямбда-выражение, используемое в роли операнда, может быть поставлено в соответствие описанию процедуры в языке АЛГОЛ-60. В процессе компиляции каждое появление имени операнда в операторе лямбда- выражения будет приводить к формированию инструкции Ipos. Эта инструкция содержит информацию о позиции соответствующего ядра в оперативной среде, которая будет текущей оперативной средой в момент выполнения данной инструкции Ipos. Ядро, вызываемое с помощью инструкции Ipos, было сформировано при выполнении инструкции Idcl и объединено с оперативной средой того состояния, в котором оно формировалось. В момент вызова ядра инструкцией Ipos (k) его оперативная среда становится k-u хвостом (/*) текущей оперативной среды. Следовательно, формирование ядра можно задержать до момента обращения к нему. Учитывая это, заменим две инструкции Idcl (С) Ipos (к) одной, в которую включена управляющая строка С и связанная с ней позиция k. В процессе выполнения новой инструкции из С и f' текущей оперативной среды будет сформировано ядро. Введем две новые инструкции: 1) line (k, С) для загружаемого ядра, которая формирует ядро и осуществляет его загрузку в стек; 74
2) ainc (k, С) для применяемого ядра, которая применяет ядро, сформированное ранее, к его аргументу. Преобразование состояния при выполнении инструкций line и ainc осуществляется следующим образом: (S, Е, line (к, С1) ; С, D) =ф- ((consclosure (С1, t^ Е) : S, Е, С, D), (X : S, Е, ainc (к, С1) ; С, D) =Ф. (( ), х : i^ Е, С1, (S, Е, С, D)) . Представим функцию возведения в квадрат (функцию sq) в виде ядра: sq = Хх.х X X . Используя новые инструкции, вместо программы load 3 Idcl СЗ enter Idcl С2 exit appl C2.: : Ipos 1 apos 2 apos 2 C3 : Ipos 1 Ipos 1 multiply получим следующую программу: load 3 C2 : Ipos 1 enter ainc B, C3) Idcl C2 ainc B, C3) exit appl Условные выражения. После того как при компиляции будет выделено условное выражение, оно преобразуется в программу следующего вида: If а then b else с program (а) test (L) program (b) go to (M) L; program (c) M: ... В полученной программе инструкция test используется для определения значения логической переменной, находящейся в первой позиции стека, и последующего удаления ее из стека. После этого управление передается оператору с меткой L, если логическая переменная имеет значение false. В противном случае выполняется оставшаяся часть управляющей строки. Инструкция go to (М) передает управление оператору с меткой М. 75
Преобразование состояния для Двух новых инструкций выполняется следующим образом: (true : S, Е, test (L) : С, D) =^ {S, Е, С, D); (false : S, Е, test (L) : С, D) =^ (S, Е, L, D); (S, Е, go to (М) : С, D) =^ (S. Е, М, D) . Саморекурсия и применение оператора Y. Оператор Y был введен в гл. 1 для построения выражений, содержащих саморекурсивные функции. Ниже описаны методы представления таких функций. Одно из определений функции Y имеет следующий вид; Y = A/.(;v£./(gg))(x^./feg)), откуда следует, что = (Ag.f (g g)) Og.F (g g)) = F {{Ig.F {g g)) (}.g.F (g g))) = F (YF). Версия функции Y, которая может быть использована в SECD- машине, задается соотношением Y/ = /i/i, где hg = / (Xx.g g х), в котором применение функции g к самой себе задерживается до тех пор, пока не закончится применение ядра, соответствующего Kx.g g X. Такой подход позволяет избежать зацикливания, так как в этом случае в 5£СО-машине тело ядра вычисляется только в момент его применения. Изложенный выше способ использования оператора Y для функций типа (XF.Xx ... F (у) ...) приводит к формированию ядра, которое в процессе применения к аргументу х будет также применено к у в том месте, где расположена функция F. Рассмотрим более подробно, как это происходит, на следующем примере. Запишем последовательность состояний SECD-машяны при обработке выражения Y (XF.Xx ... F (у) ...) х. Сначала осуществляется компиляция указанного выражения, в результате которой получается программа: F0: Y1: load X !dd Fl Idcl Yl appl anpl Idcl Y2 Idcl Y2 appl Fl : Idcl F2 Y2 : Idcl Y3 Ipos 2 appl F2 : load у Ipos 2 appl Y3 : Ipos 1 Ipos 2 Ipos 2 appl appl 76
1. о, Е, (load X. idcl Fl. Idcl VI, appl. app ), D 2. X, B, (Idcl F\, Idcl Kl, appl. appl), D 3. (fFl, E], X). E. (Idcl У1, appl, appl), D 4. ([VI. £], [Fl, £], X). E. (appl. appl). D Л. (), £1 = ([Fl, £]: £), (Wc/ V2, (de/ У2, appi). (д:, £, appi, D) = Dl 6. |V2, £1]. £1, (Idcl Y2, appl). Dl 7. ([V2, £1]. [У2:£1]), £1, appl. Dl ä. (), £2 = ([V2. £1 ] : £1), (Idcl V3, Ipos 2, appl), ( (), £1, (), D\) = D2 9. tV3, £2], £2, (Ipos 2, ßppO, D2 10. ([Fl, £], [V3;£2]), £2, appl, D2 11- 0. ([V3. £2] : £). Idcl F2, ( (), £2, (), D2) = D3 12. [F2, ([УЗ ; £2] : £) ], [V3, £2] : £, (), D3 13. [F2, ([УЗ, £2]: £)], £2, (), D2 Ы. [i'"-'. ([V3, £2] : £)], £1, 0, Dl 15. ([F2. ([УЗ, £2]: £)], x), E, appl, D 1С. (), (,r : [V3, £2] : £, (.., (oad y, Ipos 2, appl,.), ( (), E, (). D) 17. ,/. (X : [V3, £2] : £), (Ipos 2, appl,..), ( (), E (). D) 18. ([УЗ, £2), y), (X : [V3, £2) : £), (appl,..), ( (). £, (), D) = D3 19. 0, у : £2. (Ipos 1, Ipos 2, ipos 2, app;, appl), ( (), (x : [V3, £2] ; £), ..., D3) = Di 20. I/, у : £2, (/pos 2, Ipos 2, appi, app/), D4 21. ([V2, £l], II). у ■■ £2, (Ipos 2, йррг, appl), D4 22. ([V2. £1], [V2, £1], y). у : £2, (appl, appl). Di 23. (), £2, (lad УЗ, /pos 2, appO, (i/, i/ : £2, appl, Di) 28. [F2, (I.V3, £2] : £)], £2, (), (y, у : £2, appl, Di) Рис. 2. IS. Представление функции Y в ЗЕСО-млшине Процесс выполнения этой программы в оперативной среде Е с дампом D описывается последовательностью состояний 1—14 (рис. 2.18). При этом формируется ядро, соответствующее Y {XF.Kx ...{F)y...): [F2, ([F3, £2] :£)], где £2 = IY2, El] : £1, £1 = [Fl, £] : £. Применению полученного ядра к аргументу х соответствует последовательность состояний 15—23. Поскольку копоненты S, £ и С состояний 8 и 23 идентичны, то перед применением F к у будет сформировано то же ядро, что и ранее. Следовательно, 5£СО-машина может быть использована для обработки функций, содержащих саморекурсию. Предположим, что в текст исходной программы включены описания используемых переменных и операции с этими переменными. Появление описаний в процессе обработки такой программы приводит к объединению идентификаторов переменных с их значениями в таблице. Однако описание идентификатора может быть расположено после операторов, в которых участвует указанная переменная. В этом случае используют один из двух следующих приемов. Первый заключается в том, что позиции, в которые должны быть помещены значения этой переменной, последовательно связываются друг с другом указателями до тех пор, пока данная переменная не будет определена. После этого связанные позиции будут заполнены. Второй прием состоит в том, что при обращении к значению переменной используется косвенная адресация к специально выделенному пустому разделу памяти, в который будет записано значение данной переменной после ее определения. В этом случае трудности возникают при наличии взаимно рекурсивных оиреде- 77
a) d В) б) a a. E - 1 E - 1 ' ' tE ^ С E 1 IE Рис. 2.19. Последовательность применения функции Y лений, которые можно рассматривать как результат применения оператора Y. Рассмотрим возможность использования второго подхода в такой ситуации. Ограничимся функциями преобразования, для которых использование аргумента состоит лишь во включении его целиком или частично в результат. Причем получаемый результат должен иметь ту же структуру, что и аргумент функции или его отдельные компоненты. Пусть функция / такова, что ее применение к аргументу х приводит к результату у, структурно идентичному х. Предположим далее, что w есть результат замены отдельных компонентов х, выделенных функцией /, соответствующими компонентами г/. Тогда в результате применения f к w получим W. Рассмотрим применение функции Xf.Xz.M к аргументу х, также являющемуся функцией. Результатом такой операции будет ядро, которое содержит аргумент х в голове своей оперативной среды. Следовательно, ЦХг.М является примером вышеописанной функции, предназначенной для включения аргумента в результат, причем и аргумент, и результат представляют собой ядра. Применение оператора Y к такой функции приведет к замене головы оперативной среды ядра самим ядром, в результате чего мы получим ядро, структура которого показана на рис. 2.19. Обобщим изложенное выше. Итак, сначала создают фиктивный аргумент d, структура которого совпадает со структурой аргумента и результата применения оператора Y. Затем, применяя функцию / к аргументу d, получают результат у и заменяют позиции фиктивного аргумента соответствующими позициями результата. Наконец, применяя Y к функции /, получают искомое значение d. Предложенный подход может быть реализован только в том случае, когда нам известна структура аргумента и его выбираемые позиции. Для применения Y к функции Xf.Xz.M необходимо использовать неявное представление ядра. Пусть ядро представлено указателем, задающим два адреса — управляющей строки и оперативной среды ядра. Тогда фиктивный аргумент d будет иметь вид, показанный на рис. 2.19, а. Применяя Xf.Xz.M к аргументу d, получим ядро, в голове оперативной среды которого будет помещен этот аргумент (рис. 2.19, б). Теперь применение Y приведет к за- 78
мене пары компонентов, определяющих фиктивный аргумент, соответствующими двумя компонентами ядра, полученного на предыдущем шаге. Результирующее ядро (структура которого показана на рис. 2.19, в) будет содержать само это ядро в голове оперативной среды. Изложенный прием можно использовать также для функций, предназначенных для преобразования списков функций, например, следующего вида: Я (/, g, К). ((Хх ... / ... g ...), i%h ... f ... h ...), (kz ... f ... g ...)). В данном примере фиктивный аргумент должен быть списком, содержащим три фиктивных ядра, как об этом говорилось ранее. Каждое из этих фиктивных ядер должно быть, как и ранее, заменено соответствующей парой структурных компонентов. Рассмотренный подход можно также применять для функции преобразования списков. В этом случае применение оператора Y приводит к формированию петлевой списковой структуры (содержащей циклы). Применение Y к лямбда-выражению приводит к тому же результату, что и применение лямбда-выражения к его аргументу: У(Ц.Л4) = (Ц.М)(¥(Ц.М)). Поэтому так же, как и при использовании лямбда-выражения в качестве оператора, можно провести оптимизацию программы, применяя вместо инструкций Ipos (k) и apos (k) инструкции line (k, С) и ainc (k, С) соответственно. Это приведет к формированию ядра, в управляющей строке которого имеются циклы: С: ... С: ... line (к, С) ainc (к, С) Объединение операторов при выходе из программы. Предположим, что тело обрабатываемого лямбда-выражения является комбинацией или условным выражением, содержащим комбинацию. На конечном этапе применения такого выражения к аргументу будет получена функция, которая затем применяется к некоторому аргументу. Следовательно, в конце результирующей программы будет стоять инструкция применения 'аррГ. За ней следует выход из программы. Последовательность инструкций в такой программе будет иметь вид Idcl f appl f: ... (exit) (exit) 79
Две инструкции exit в этой программе можно объединить в одну, если первая инструкция exit сформирована аналогично второй. Это может быть сделано лишь при условии, что в первой инструкции выхода содержится ссылка на дамп вызывающей программы, а не на свой собственный дамп, который в действительности не существует. Кроме того, инструкцию применения 'apply: ( )' можно заменить новой инструкцией 'applyexif, при выполнении которой происходит следующее изменение состояния: consclosure (С1, El) : у : S, Е, applyexit : ( ), D => S, у : El, С1, D . Поскольку единственная инструкция, с которой связано обращение к дампу, есть инструкция выхода, восстанавливающая состояние соответствующей точки вызывающей программы, указанные изменения не повлияют на конечный результат. Более того, если оба ядра имеют одинаковую оперативную среду, а вместо инструкции apply использована инструкция aitic A, CI), то при выходе из программы достаточно лишь изменить управляющую строку и голову оперативной среды, т. е. у : S, X : Е, ainc A, Cl) : ( ), D =*■ S, у : Е, С1, D . Указанные условия соблюдаются при весьма распространенных рекурсивных обращениях глубины 1, когда вызывающая и вызываемая функции тождественны. Еще больше можно сократить программу за счет замены рекурсивного повторного входа простой инструкцией go to. В этом случае аргументы х я у должны иметь одинаковую структуру, и если X не включен в г/ или в вызываемую функцию, то аргумент х может быть заменен аргументом у. Рассмотрим в качестве примера функцию suml, которая предназначена для суммирования чисел списка х с начальным значением а. Ее определение имеет вид def гее sum 1 (а, х) = if null X then а else sum 1 (a + h x, t x) . Эту функцию можно реализовать в виде следующей программы: sum 1 ; if null х then а else (а, х) : = (а + h х, t х) go to sum 1 . Выделение общих подвыражений. Метод выделения одинаковых подвыражений при программировании или компиляции можно рассматривать как расширение правила лямбда-свертки. Он заключается в отыскании одинаковых частей выражения, например, в выражении типа а2 ^ ^,2 __ log A/(а^ +У)),
и организации однократного их вычисления (в данном случае подвыражения с^ + Ъ"^). Использование такого расширенного правила лямбда-исчисления позволяет преобразовать исходное выражение к следующему виду: X — log A/л;), где х = с^ -\- Ь^. Наибольший эффект от применения этого метода достигается при оптимизации циклических программ. 2.10. Метки и операторы перехода ... Забудь заклятья. Пусть дьявсч, чьим слугой ты был поныне, Тебе шепнет, что вырезан до срока Ножом из чрева матери Макдуф. Макбет», акт 5, сцена 8 (перевод Ю. Корнеева) Оператор go to, введенный -в предыдущем параграфе, приводит к изменению только управляющей части состояния. В случае, если язык программирования допускает использование блочных структур и процедур, область действия операторов перехода расширяется. Например, в языке АЛГОЛ-60 возможен переход из внутренних блоков во внешние. При таких переходах необходимо изменять не только управляющую часть состояния, но и восстанавливать оперативную среду, соответствующую внешнему блоку. Выход из блока с помощью оператора go to будем называть неестественным выходом в противоположность естественному выходу через последний оператор end блока. В языке АЛГОЛ-60 допускается также использование метки в качестве формального параметра (аргумента) процедуры. При выполнении перехода к такой метке с помощью оператора go to L, содержащегсся в теле процедуры, происходит выход из процедуры к точке программы, помеченной этой меткой. Такой выход из процедуры также называют неестественным выходом. В последнем случае в отличие от естественного выхода текущее содержимое дампа не используется. Использование метки в качестве аргумента приводит к необходимости передачи процедуре определенного набора информации. Такая информация называется значением метки. Настоящий параграф посвящен рассмотрению характера этой информации. В п.п. 2.5. и 2.6 мы дали ответ на вопрос: «Что означает лямбда- выражение?» или более конкретно: «Какую информацию необходимо передать процедуре, если она использует процедуру в качестве аргумента?» Этот ответ гласит, что вся необходимая в этом случае информация содержится в ядре. И хотя формат ядра может изменяться, оно всегда состоит из программной части и соответствующей оперативной среды. В этом параграфе мы попытаемся 81
LI: L3: L2: begin SI S2 go to S3 S4 go to S5 S6 end a) 12 LI begin PP LX S2 go to L2 PP L2 S5 S6 PP L3 S3 S4 go to LI SI go to LI end 6) ответить на вопрос: «Какую информацию необходимо передавать процедуре, если она содержит метку в качестве аргумента?» Можно провести аналогию между метками и именами процедур без параметров. Метка подобна локальному описанию и иногда может быть представлена как дополнительное описание в блоке. Для более пол- Рис. 2.20. Описания точек входа в про- «ОГО СООТВеТСТВИЯ МеЖДу ОПИ- грамму санием и использованием процедур и меток расширим язык программирования за счет введения описания значения метки, аналогичного описанию процедуры. Новый тип описания будем обозначать рр (сокращенно от program point — точка входа в программу) в отличие от описания procedure. Пример блока, в котором операторы снабжены метками, и соответствующей ему программы, содержащей описания точек входа, приведены на рис. 2.20, а, б соответственно. Вход в процедуру осуществляется только через заголовок, т. е. с использованием вызова процедуры. В точку входа программы можно войти по вызову, с помощью оператора go to или непосредственно после предшествующего ей оператора. Это приводит к нарушению их строгого соответствия. Для того чтобы это соответствие было строгим, необходимо заменить каждый переход к точке входа после выполнения предшествующего оператора оператором go to. Если каждый описатель точки входа заменить описателем procedure и исключить все соответствующие операторы go to, то блок будет содержать только описания и обращения к процедурам без параметров. При этом действия, выполняемые после обращения к процедуре или точке входа в программу, оказываются идентичными вплоть до достижения точки выхода. Далее при естественном выходе из процедуры управление передается той точке программы, которая задается дампом, сформированным при вызове этой процедуры. Что же касается точки входа, то естественный выход из соответствующей ей процедуры без параметров приведет к естественному выходу из блока, в котором содержится ее описание. Процедуры без параметров, не производящие результата, являются особым случаем процедур с параметрами, которые должны производить результат. Естественно поэтому выяснить, существует ли представление точки входа в виде формирующей результат процедуры с параметрами. Введем новое понятие вспомогательного результата, который обладает указанным свойством. Такой результат, трактуемый как значение метки, назовем 82
программным ядром. Структура его задается следующим правилом: ■ Программное ядро состоит из — тела (body), которое является функцией, и — дампа (dump), который является состоянием. Программное ^ядроf во'МНОГОМ аналогично обычному ядру: оно является функцией, применяемой к аргументу для получения результата. Тело программного ядра может быть простой функцией, ядром или программным ядром. Программное ядро формируется из результата применения функции conspclosure к некоторой функции и состояния. Тип программного ядра определяется типом функции, которая является ее телом. Теперь необходимо включить в SfCD-машину две дополнительные функции для формирования и применения программного ядра. В момент применения такого ядра к аргументу текущее состояние (за иключением самого ядра и аргумента) заменяется состоянием, записанным в дампе программного ядра. После этого функция, являющаяся телом программного ядра, применяется к аргументу. В результате состояние изменится следующим образом: (L : X : S, Е, apply : С, D) =»■ (body L : х : SI, EI, apply : CI, Dl) where SI, EI, CI, DI = dump L . Если тело программного ядра также является ядром, то при последующем преобразовании в текущий дамп будет записано то состояние, которое содержалось в дамповой части программного ядра. Это необходимо для завершения вычисленпй после применения программного ядра указанной структуры к аргументу. В этом случае результат применения тела ядра будет возвращен в состояние, соответствующее программному ядру, а не в то состояние, которое соответствует точке обращения к нему. Программное ядро можно формировать в два этапа, выбирая функцию и состояние независимо. Введем в язык специальный идентификатор J для выделения текущего состояния дампа. Значением J является функция, называемая дополнением состояния {state appender), которая содержит состояние. В результате применения функции дополнения состояния к некоторой функции будет сформировано программное ядро. Телом этого ядра является заданная функция, а дампом — состояние, содержащееся в функции дополнения состояния. Таким образом, происходят два последовательных преобразования состояния: (S, Е, J : С, D) =^ (consstateappender D : S, Е, С, D) и (consstateappender Dl : f : S, E, apply : С, D) =?- (conspclosure (DI, f) : S, E, C, D) . Программное ядро весьма удобно использовать для указания точек возврата при возникновении ошибок. При этом его состояние задает соответствующую точку возврата, а функция формирует результат, содержащий информацию- об ошибке. 83
Целесообразно сравнить последовательность действий, выполняемых в процессе применения ядра и программного ядра к соответствующему аргументу. В первом случае текущее состояние записывается в дамп, а затем вычисляется тело ядра и результат загружается в стек восстанавливаемого состояния. Таким образом, сначала производится преобразование состояния conspclosure (С2, I, Е2) : х : S, Е. apply : С, D => г : ( ), (I, х) ; Е2, ( ), (S, В, С, D) , где г — результат применения ядра, а затем состояние принимает вид г-.8, Е, С, D. Если же применяется программное ядро, телом которого является также ядро, последовательность действий может быть сокращена. Дамп программного ядра становится текущим дампом, управляющая часть ядра становится текущей управляющей строкой, а аргумент объединяется с оперативной средой и становится текущей оперативной средой. Когда программное ядро с ядром в качестве функции применяется к аргументу, также являющемуся ядром, перед его применением, выполняются следующие преобразования: let L =conspcIosure ((SI, El, CI, Dl), consclosure (C2, I, E2)) L : X : S, E, apply : С, D => ( ), (I, X) : E2, C2, (Sl, El, Cl, Dl) . Далее выполняются те же действия, которые были описаны выше. При выходе результат г загружается в стек состояния, содержащегося в программном ядре, и оно становится текущим состоянием: /■:(), (/, X) : £2, ( ), (S1, £1, С\, Dl) => =^/-: SI, £1, Cl, Dl. Такая конструкция программного ядра является существенным дополнением языка выражений. Следует отметить, что она не может быть описана в терминах применения и вывода, а выражения, содержащие J, не подчиняются правилам лямбда-свертки. Рассмотрим два выражения, которые могли быть преобразованы друг в друга, если бы J была простой функцией: 1. f C,2) where f (х, у) = (g (х) + у where g (z) = (sq (L (z, 2)) where L = J (Я (x, y) . x^ — y^))) 2. f C, 2) where f (x, y) = ((g (x) + у where g (z) = sq (L (z, 2))) where L = J (k (x, y) . x^ — y^)) 84
в первом примере определение L относится к выражению sq (L B, 2)), во втором примере — к выражению g (х) -{- у where g (z) = sq (L B, 2)). Рассматриваемые выражения эквивалентны с точки зрения правил лямбда-свертки, однако они отличаются с точки зрения применения программного ядра. В первом случае применение L приводит к выходу из функции g, а во втором случае — к выходу из функции /, причем функция sq не применяется в обоих случаях. При вычислениях в первсм примере будет выполнена следующая последовательность шагов: / C, 2)=g{3) + 2 = sq (L C, 2)) + 2 = L C, 2) + 2 = 5 + 2 = 7. Во втором примере будем иметь / C, 2) = ^'C) + 2 = sq (L C, 2)) + 2 = L C, 2) = 5. Добавление к языку выражений функций перехода позволяет прекращать вычисления," если известно, что значение полного выражения равно значению одного из его подвыражений. Рассмотрим выражение (XL. ... (Lx) ... (Ly) ... ) (J/). Если в процессе вычислений L применяется к некоторому аргументу (в данном случае х и у), то значение всего выражения будет результатом применения функции / к тому же аргументу. Поэтому мы можем сократить приведенное выше выражение следующим образом: ...(J / x)...{i f у)... Такое сокращение возможно, если функция L появляется только на верхнем уровне лямбда-выражений. В этом случае выражение (J /) не изменяет глубины этой функции. Во введенной конструкции (J / х) программное ядро применяется к аргументу сразу же после того, как оно будет сформировано. Поэтому такая конструкция может быть поставлена в соответствие оператору return, который имеется в некоторых языках программирования и позволяет осуществить естественный выход из блока или процедуры в любом месте, отличном от конечного оператора end. В выражении (J / х) программное ядро формируется из функции / и текущего содержимого дампа. После этого оно сразу же применяется к своему аргументу. Полученное в результате этого значение (/ х) объединяется со стеком состояния в текущем дампе, 85
и это новое состояние становится текущим. Отсюда следует, что выход осуществляется из процедуры, которая содержит (J / х) на верхнем уровне. Такое представление переходов является обобщением функций передачи управления, которые применяются в большинстве современных языков программирования. Тот факт, что описанный нами язык выражений расширен за счет введения обобщенной функции переходов, показывает, что переходы не обязательно связаны с операторами. Кроме того, возможность использования программного ядра вместо метки позволяет заключить, что переходы не обязательно связаны с помеченными операторами. Программное ядро, в свою очередь, может иметь метку, как и обычный оператор. Хотя новые средства были введены для описания переходов в языках программирования, их также можно использовать для указания точек выхода при появлении ошибок в процессе выполнения программы, при сбоях, для программ перезапуска и возврата. Так, прерывания программы, возникающие в результате применения некоторого оператора, можно интерпретировать как выполнение теста ошибки, следующее за применением программного ядра. Как показали эксперименты, такой подход более удобен, чем обычное использование операторов перехода. Применение обобщенного оператора перехода предоставляет нам следующие дополнительные возможности: ■ В состав программного ядра включаются функция и состояние, в то время как в соответствующем средстве языка АЛГОЛ-60 используется только состояние. Применение программного ядра заключается в применении его функции к аргументу и засылке полученного результата в стек соответствующего состояния для продолжения вычислений. ■ Состояние и функция программного ядра задаются отдельно, следовательно, их можно выбирать независимо. ■ Используется новый тип значений, который можно представить структурой. Например, переключатель в языке АЛГОЛ-60 можно рассматривать как список программных ядер, и, следовательно, его можно заменить выражением, значение которого представляет собой список программных ядер. Программное ядро может быть не только аргументом, но и результатом применения процедуры. Это дает возможность выполнять переходы внутрь блока, в котором описана метка, что аналогично ситуации, когда программное ядро является результатом вычислений. Возможность такого входа в блок позволяет обращаться к процедурам, описанным во вложенных блоках. 2.11. Совмещение, присваивание и копирование В гл. 1 был предложен метод описания наборов данных, при изложении которого рассматривался вопрос о представлении элементов этих наборов в вычислительной машине. Для формирования и выборки отдельных частей сложных информационных 86
Рис. 2.21. Способ представления списка Голода Хвост структур были определены специальные функции и перечислены правила, которым они должны удовлетворять. Рассмотрим теперь более подробно вопросы представления структур данных. SfCD-машина и ее расширенный вариант, включающий обобщенные функции переходов, были описаны с помощью указанных выше функций формирования и выборки. Из ранее изложенного следует, что нельзя изменять произвольный компонент состояния. Кроме того, подразумевалось, что и основные функции характеризуются лишь результатом их применения к соответствующему аргументу н также не приводят к изменению состояния. Применение основных функций состоит в удалении их аргумента из стека и замене его результатом. При этом остальные компоненты состояния не изменяются. Информационную структуру представим в виде дерева адресов. Каждую сложную структуру при этом можно представить двумя способами. С одной стороны, ее можно задать в виде единого сегмента, состоящего из последовательных ячеек памяти, в которых содержатся отдельные компоненты структуры. С другой стороны, ее можно представить с помощью адреса такого сегмента. Таким образом, информационная структура представляется либо сегментом, либо адресом сегмента, которые иногда называют L- значением и /^-значением соответственно. Причем /^-значение структурированного объекта может включать L-значения его компонентов. Основное правило задания структур данных, которые вводятся с помощью описаний, заключается в следующем. Будем считать, что структура представляется L-значением, содержащим указатель соответствующего /^-значения. Это /^-значение, в свою очередь, содержит адреса областей памяти, выделенных с помощью определенной функции выборки. Эти области называются непосредственными компонентами структуры или компонентами верхнего уровня. Например, непустой список будет представлен адресом сегмента, состоящим из двух частей: адреса головы списка (^ead или, сокращенно. К) и адреса хвоста списка {tail или f), как это показано на рис. 2.21. Функции выборки для структур данных могут быть составлены с использованием указателей позиций компонентов конкретной структуры. Рассмотрим следующий список: 1, E, 6), G, 2). Значение 5 занимает позицию h-h-t, а значение 2 — позицию впгорая-h-t-t. Введем функцию модификации данных (update), которая оперирует с позицией, объектом и структурой. Она фор- 87
d. ab ад Рис. 2.22. Совмещенная н несовмещенная списковые структуры мирует новую структуру, в которой содержимое заданной позиции заменяется соответствующим объектом. Например, update {h-h4) 3 A, E, 6), G, 2)) = 1, C, 6), G, 2); update (пятая) 10 A, 2, 3, 4, 5) = 1, 2, 3, 4, 10. Косвенная адресация, применяемая для представления структур данных, дает возможность использовать общие области памяти для тождественных компонентов, занимающих различные позиции. В этом случае говорят, что в позициях происходит совмеи^ение компонентов. Ясно, что при совмещении позиций совмещаются и все их подпозиции. Структуру можно изобразить в виде ориентированного графа, каждая вершина которого есть /^-значение, а выходящие из нее ребра являются непосредственными компонентами. Каждое из этих ребер может быть обозначено именем соответствующей функции выборки. Набор всех путей из корня графа можно разделить на классы, связанные отношением эквивалентности. Два пути (или две позиции) считаются эквивалентными с точки зрения условий совмещения, если они ведут к одной и той же вершине графа. На рис. 2.22 приведены два варианта представления структуры списка ((а, Ь), Ь, с, d). В первом графе (рис. 2.22, а) совмещены позиции h-t и h-t-h для компонента Ь. Во втором графе (рис. 2.22, б) они разделены, и мы имеем две позиции для этого элемента. В зависимости от вида представления структуры данных модификация позиций h-t или h-t-h будет приводить к различному результату. Так, в результате применения функции update {h-t) е {{а, Ь), Ь, с, d) получим список (а, е), е, с, d для структуры данных на рис. 2.22, а, и список (а, Ь), е, с, d для структуры на рис. 2.22, б. В первом случае одновременно осуществляется модификация двух позиций — h-t и h-t-h, во втором — только одной позиции h-t-h. Соответствующие графы результата приведены на рис. 2.23, где указано расположение компонентов и их совмещение. Для того чтобы строго определить изменение состояния, содержащего операции модификации данных, необходимо определить изменения как совмещенных связей, так и компонентов каж-
дой позиции состояния. Ниже будет приведено правило изменения совмещений применительно к функциям, которые обрабатывают и формируют списковые структуры. Рассмотрим следующую функцию преобразования списков: / (а, Ь, (с, d),e) = (е, g, (d, е, а)). На основании данного определения рассматриваемой функции можно сформировать соответствующий ей список пар позиций. Первым элементом каждой пары является позиция идентификатора в связанной переменной, вторым элементом — позиция того же идентификатора в теле функции. Так, например, идентификатор а.расположен в первой позиции связанной переменной и в третьей-третьей позиции тела функции. Полный список пар для заданной функции приведен ниже. Перед каждой парой записан соответствующий идентификатор: первая, третья-третья (а) F) {с) (d) {е) вторая-третья, первая-третья четвертая, первая четвертая, вторая-третья Элементы списка в позициях, соответствующих идентификаторам b и с, после применения функции не сохраняются. Идентификаторы связанной переменной должны быть различными. Рассмотренный способ объединения в пары предыдущих и последующих позиций будет определять изменения в совмещенных позициях в соответствии со следующим правилом. Пусть (Pi, qi), ip2, <?2), ■••, {Рп , Чп) есть пары соответствующих позиций. Тогда, если позиции pi и pj совмещены в аргументе функции, то позиции qi и qj будут совмещены в результирующей списковой структуре. В общем случае, если позиции r-pi и s-pj совмещены в аргументе, то позиции r-qi и s-qj будут совмещены в результате. Такой подход позволяет минимизировать число копируемых сегментов данных при преобразовании списков: копируются лишь L-значения, а R-зиа- чения не копируются. а е Рис. 2.23, Графы совмещений а 89
; / 2 t г л л , , 3 ; с 2 1 А у 1 * / 2| J[ i^ £ Рис. 2.24. Граф аргумента Рассмотрим применение введенной ранее функции / (а, Ъ, (с, d),e) = (е, g, (d, е, а)) к списку {А, X), В, (С, (X, Г)), (Г, D, £), граф которого приведен на рис. 2.24 (жирными линиями выделены совмещенные позиции). В заданном списке совмещены позиции для идентификаторов X \i Y: (X) вторая-первая совмещены с первой второй третьей; (F) вторая-вторая-третья совмещена с первой'четвертой. В результате применения функции / получаем список следующего вида: {{Y, D, Е), J, {{X, Y), (У, D, Е), {А, X))). Результат был получен следующим образом. В списке, являющемся аргументом функции, совмещены вторая-первая и пер- [ У 2 J Рис. 2.25. Модель совмещения результата 90 / 2 X 1 2 А
вая-вторая-третья позиции. Из списка пар функции /, приведенного выше, следует, что первая позиция связанной переменной становится третьей-третьей позицией тела функции, а вторая-третья — первой-третьей. Следовательно, вторая- - третья-третья и первая-первая-третья позиции результата будут совмещены. Кроме того, поскольку любая позиция совмещается с другой, если в них стоит один и тот же идентификатор (в данном случае — первая и вторая-третья позиции для компонента е), то первая-первая и вторая-первая-третья позиции также окажутся совмещенными в результирующем списке. Граф этого списка приведен на рис. 2.25. Попытаемся теперь на основании изложенного выше описать механизм присваивания, представив состояние SfCD-машины как списковую структуру. Оператор присваивания применяется к позиции состояния (более строго, к позиции оперативной среды) и к своему объекту. При модификации содержимого этой позиции одновременно будут изменены элементы всех совмещенных с ней позиций. Обычно такие действия выполняются путем изменения сегментов памяти (L-значений) и модификации соответствующих /^-значений. Все совмещенные L-значения состояния модифицируются одновременно. Для выполнения операции присваивания, записываемой в виде а : = Ь, необходимо, чтобы выражение а имело значение, являющееся позицией состояния. Кроме того, значения выражений а и b должны иметь одинаковую структуру. В результате присваивания /^-значение а заменяется /^-значением Ь. Введем в SECD- машину дополнительное правило преобразования состояний для описания механизма присваивания. Новый оператор : = применяется к паре L-значений. Если оператор применения 'apply' находится в голове управляющей строки, а оператор присваивания : = расположен в голове стека, то будет выполнено следующее преобразование текущего состояния: (S, Е, С, D) =>- (update (первый-второй■ ■ Ss)) (второй (второй S)) (S, Е, С, D) Стек, оперативную среду, управляющую строку и дамп нового состояния соответственно обозначим Ss, Es, Cs, Ds. После указанного преобразования состояния выполняется инструкция загрузки значения в стек: X Ца, Ь) : S, В, С, D).b : S, В, С, D. Совмещенные позиции также будут изменены с помощью соответствующих функций, копирующих свои аргументы. Введем функцию отделения separate, которая оперирует с L-значениями и служит для получения результата, являющегося копией R- значения. Последовательное применение этой функции позволяет получить копии всех сегментов структуры. С точки зрения отношений эквивалентности функция separate исключает позицию 91
одного эквивалентного класса п формирует новый класс, содержащий один компонент. Рассмотрим особые случаи влияния условий совмещения на результат применения оператора присваивания в SECD-шашипе. 1. Если выражение состоит лишь из идентификатора (обозначим его Y), то выполняется преобразование исходного состояния (S, Е, Y : С, D) в состояние {location Е Y Е : S, Е, С, D). Отсюда следует, что значение идентификатора У является позицией оперативной среды или L-значением. В новом состоянии позиции h-Ss (голова стека, куда записано значение Y) будет совмещена с позицией {location Е Y)-E s оперативной среды. Следовательно, значение любого идентификатора как свободного, так и связанного может быть использовано в качестве левого аргумента оператора присваивания. Таким образом, передача аргументов в этом случае выполняется аналогично передаче параметров подпрограммам в языке ФОРТРАН. Такой способ передачи параметров называют «вызовом по ссылке» {«-call by referencei>). 2. Если в голове управляющей строки находится лямбда- выражение, то выполняется следующее преобразование состояния: (S, Е, conslambda (х, М) : С, D) => (consclosure (М, х, Е) : S, Е, С, D) . При этом текущая оперативная среда совмещается со средой ядра, так что позиции Es и Ec-h-Ss результирующего состояния будут совмещены. Оперативную среду, связанную переменную и управляющую строку ядра будем обозначать Ее, Во и Со соответственно. 3. Если в голове управляющей строки находится оператор применения 'apply', а в голове стека— ядро, то произойдет следующее преобразование состояния: (consclosure (С1, х, El) : у : S, Е, apply : С, D) => (( ), (х, у) : El, и С1, (S, Е, С, D)) . В новом состоянии хвост оперативной среды {t-Ec) будет совмещен с оперативной средой ядра и всеми ранее совмещенными с ней сегментами. Теперь, учитывая результаты п. 2.2, можно утверждать следующее. В процессе применения функции к ее аргументу оперативная среда ядра и, следовательно, текущая оперативная среда, соответствующая моменту его формирования, может быть изменена с помощью свободных переменных. Обычно такие изменения называют побочным эффектом применения функции или процедуры. Новая вторая-h-Es позиция будет совмещена со старой второй-Ss позицией и со всеми позициями, которые с ней были совмещены. Таким образом, применение функции может приводить также к побочному эффекту, обусловленному аргументом, т. е. у. 4. После выхода из ядра состояние (S, Е, {С, (S1, £1, С1, Dl)) преобразуется в состояние {h S : SI, El, CI, Dl). Голова /г-Ss 92
стека результирующего состояния совмещается с головой h-Ss стека предыдущего состояния, а оставшиеся позиции t-Ss, Es, Cs и Ds нового состояния совмещаются с позициями Ss-Ds, Es-Ds, Cs-Ds и Ds-Ds старого состояния. Тогда, если результат применения ядра (записанный в позиции h-S) также является ядром, сформированным в среде Е, то эта оперативная среда неявно сохраняется в качестве части ядра результата. В большинстве языков программирования такой эффект не допускается. При наличии подобного запрета оперативные среды и дампы могут быть помещены в память и переформированы по принципу магазинного стека (last-in-first-out). 5. При формировании программного ядра сначала извлекается текущее состояние машины, после чего оно присоединяется к функции. Поэтому дамп, содержащийся в дополнении состояния, совмещается с текущим дампом, соответствующим моменту формирования ядра. Теперь любое изменение в текущей оперативной среде будет приводить к неявному изменению дампа дополнения состояния, который также совмещается с формируемым в момент применения дампом программного ядра. При этом любое изменение в оперативной среде, входящей в состав дампа программного ядра, также приведет к неявному изменению данного ядра. 6. Следует четко определять изменения в совмещенных позициях, вызванные применением основных функций. 7. Значение каждого выражения представляет собой L-значе- ние. Следовательно, значение выражения можно использовать в левой части оператора присваивания. Хотя выражения вида допустимы, их можно использовать лишь тогда, когда значение выражения слева от оператора : = совмещено с какой-либо другой позицией состояния. Следует отметить, что после формирования программное ядро может быть изменено в результате выполнения присвоений в той части оперативной среды, которая совмещена со средой других ядер. Такая возможность подразумевается в языке АЛГОЛ-60, хотя яв-но об этом нигде не говорится. Можно предложить и другие стратегии, например считать, что после формирования ядро не должно подвергаться изменениям. Для реализации такого подхода достаточно получить копию оперативной среды ядра в MOiMCHT его формирования для того, чтобы уберечь ее от изменений, связанных с выполнением присваиваний. После того как это будет сделано, изменение оперативной среды может быть осуществлено только в результате применения операторов присваивания, содержащихся в управляющей строке самого ядра. Поэтому такое ядро не может иметь ссылок на оперативную среду любого другого ядра. Другой подход связан с получением копии ядра в момент его применения. В этом случае оперативная среда ядра может изме- 93
пяться в течение времени от момента его формирования до первого применения или даже между любыми двумя последующими применениями. Применение ядра, оперативная среда которого изменилась к моменту применения, не может повлиять на состояние оперативной среды другого ядра. Если оперативная среда копируется при формировании ядра и при каждом его применении, то она может быть изменена только с помощью его управляющей строки. В языке ФОРТРАН применяется стандартный метод передачи аргументов по ссылке. В языке АЛГОЛ-60 используются два других способа передачи аргументов — вызов по значению [call by value) и вызов по наименованию [call by name). При вызове по значению сначала должен быть вычислен аргумент, а затем копия его значения передается процедуре. В этом случае операции присваивания значения переменной могут вызвать изменение этой копии. При вызове по наименованию выражение, являющееся аргументом, вычисляется при каждом его появлении в теле процедуры. Аргумент, который вызывается по наименованию, может быть представлен в SfCD-машине в виде процедуры без параметров. Для этого каждый вызываемый по наименованию аргумент заменяется процедурой без параметров. Каждое появление соответствующей переменной в теле процедуры приводит к применению этой переменной к пустому списку аргументов. 2.12. Другие методы вычисления выражений Все машины, описанные до сих пор, реализуют метод применения функций к вычисляемым аргументам. В этом параграфе приведены другие методы обработки выражений, при которых часть вычислений может быть задержана или выполнена раньше других. Задержка вычислений. Прием, заключающийся в замене выра- л^ений с операндами лямбда-выражениями, был использован для задержки вычислений ветвей условных выражений и при описании механизма передачи аргументов по наименованию в языке АЛГОЛ-60. Вычисление значений подвыражений задерживается до тех пор, пока они действительно не станут необходимыми. Если же значения этих подвыражений оказываются ненужными, то они не вычисляются. Система использует основные функции двух типов. Для функций одного типа необходимо, чтобы их аргументы были вычислены перед применением (например, для функций типа сложения). Функции второго типа (например, функцию prefix) можно применять к аргументам, часть из которых еще не вычислена. В системе должно быть известно, допускает ли каждая из функций использование невычисленных аргументов. Кроме того, необходимо знать, какой из аргументов вычислен, а какой -— нет. Аргументы 94
любого ядра могут быть не вычислены слева. При этом для функции, содержащейся в ядре, могут быть начаты вычисления, если это необходимо. На первый взгляд метод задержки вычислений аргументов кажется малоэффективным, поскольку эти аргументы в конечном счете приходится вычислять каждый раз, когда необходимо получить их значения. В простых выражениях (не содержащих оператора J и присваиваний) значение аргумента не изменяется в процессе вычислений. В этих случаях можно так организовать процесс, чтобы выражение вычислялось только один раз, а при последующих обращениях к нему использовалось вычисленное ранее его значение. Если выражение необходимо представить его значением, то это выражение должно включать как само выражение, так и оперативную среду для значений его свободных идентификаторов. При встрече комбинации оператор/операнд последний должен быть объединен с текущей оперативной средой. Такое объединение приведет к созданию так называемого полного ядра с указанием на то, что оно еще не вычислено. После этого вычисляется оператор, и его значение применяется к полному ядру. Если функция использует вычисляемый аргумент, то осуществляется вычисление полного ядра в основном аналогично применению обычного ядра к его аргументу. При этом текущее состояние записывается в дамп, а новые компоненты состояния — оперативная среда Е и управляющая строка С — берутся из полного ядра. Однако отличие состоит в том, что после получения значения ядра все ссылки на это ядро следует заменить ссылками на его значение. Это означает, что все полные ядра и объекты в вычислительной системе должны быть неявно представлены L-значениями, каждое из i^-значений которых может содержать полное ядро и быть не- вычисленным, либо содержать произвольный объект и быть вычисленным. Тогда все объекты, содержащие ссылки на это R- значение, могут быть изменены с помощью обычного оператора присваивания. Метод задержки вычислений для выражений с операндами можно включить в ЗЕСО-машяпу для вычисления комбинаций, применения основных функций и реализации программ выхода. Если комбинация находится в голове управляющей строки, то полное ядро загружается в стек, сформированный из операнда и текущей оперативной среды. Если основная функция, допускающая использование только вычисленных аргументов, применяется к полному ядру, происходит инициализация вычисления этого ядра и затем повторное применение функции к полученному результату. Позиция, которая должна была измениться, будет записана в дамп.В связи с этим необходимо ввести новый тип дампа— присваивающий {assigning), который содержит состояние и позицию. Теперь при исчерпывании управляющей строки восстанавливается состояние, содержащееся в присваивающем дампе, а результат вычислений присваивается записанной в нем позиции. 95
при этом йсе позиции состояния, соотйетстЁующйе полному ядру, одновременно изменяются. Щ|-Изменение состояния SfCD-машины, осуществляющей задержку вычислений, описывается следующей функцией. transition (S, Е, С, D) = if null С then if assigning D then (y : =r) (SI, El, CI, DI) where y, (SI, El, CI, Dl) = D and r = h S else (h S : SI, El, CI, Dl) where SI, El, CI, Dl = D else let X = h С if identifier X then (location E X E : S, E, t C, D) else if lambda expression X then consclosure (body X, bv X, E) : S, E, t C, D else if X = 'apply' then let f : у : SI = S if closure f then let consclosure (CI, J, EI) = f ( ), (J, y) : EI, u CI, (SI, E, t C, D) else if sensitive f Д uneval у then ( ), El, CI, (y, (S, E, C, D)) where consuneval El, CI = v else f у : SI, E, t'C, D else let combine (F, A) = X (consuneval E (u A)) : S, E, F : 'apply' : t C, D . Если результат вычисления полного ядра еще не получен, оно снова заменяется этой же функцией и повторно вычисляется, пока не будет получено значение этого ядра. Хотя порядок обработки подвыражений в машине с задержкой вычислений несколько иной, полученный результат будет совпадать с результатом, полученным при обычном способе вычисления лямбда-1-выражений. Некоторые лямбда-К-выражения, значения которых не могут быть получены при обычном подходе вследствие того, что вычисление операнда не закончено, будут вычислены с помощью задерживающей машины, если значение операнда не требуется. Машина с задержкой вычислений может быть использована для проверки незаконченных программ. Выражения могут содержать невычисляемые подвыражения, и даже в этом случае их значения будут вычислены, если они не зависят от значений этих подвыражений. Рассмотренный здесь подход заключается в задержке вычислений, насколько это возможно. Допустимы и другие способы вычислений, занимающие промужуточное положение между естественным порядком обработки выражения и рассмотренной модификацией. Можно даже организовать обработку выражений так, чтобы вычисление подвыражений начиналось раньше, чем это необходимо. Преимущество такой стратегии состоит в том, что исключается необходимость проверки готовности аргументов применяемых внутренних функций. 96
Параллельные вычисления. Вычисления оператора и операнда можно начинать одновременно и выполнять их параллельно, используя для этого, например, две машины. Общее число операций при этом не изменится, однако в результате их одновременного выполнения сократится общее время счета. При параллельных вычислениях каждый объект либо уже вычислен, либо вычисляется, и поэтому невычисленных объектов быть не может. Действия в этом случае можно пояснить на примере оператора применения 'apply'. Указанный оператор задерживается до окончания вычисления операнда, ввиду чего можно использовать две стратегии. Можно дождаться, когда вычисление операнда будет закончено полностью, или начать применение оператора к еще невычислен- ному операнду. Согласно первому подходу формируется дерево задержанных действий. После этого выполняются действия, соответствующие конечным вершинам. Когда эти действия заканчиваются, входящие в них ветви отсекаются и выполняются действия, соответствующие вершинам, из которых начинались отсеченные ветви, и т. д. При втором, более рискованном подходе допускается фиктивное выполнение действий в неотсеченных ветвях. Поскольку окончания предшествующих вычислений может ожидать более чем одно действие, запрещено отсекать ветви, исходящие из уже вычисленных вершин, так как результаты предыдущих вычислений могут понадобиться где-нибудь в другом месте. ССЫЛКИ и БИБЛИОГРАФИЯ В этой главе используется понятие 5£СО-машины, которое впервые ввел Лэндин. Она представляет собой интерпретатор выражений, записанных в терминах лямбда-исчисления. SECD-uaumiia, дополненная функциями обработки операций перехода и присваивания, является одновременно моделью семейства языков программирования высокого уровня и средством объяснения семантических особенностей других языков. Краткое содержание этой главы приведено в статье [10]. Большое влияние на содержание главы оказали также работы по языку ЛИСП и реализации языка АЛГОЛ-60 [6]. Использование магазинных списков имеет длительную историю. Во многих вычислительных машинах применяется его аппаратная реализация. Были созданы другие языки программирования, в большей или меньшей степени использующие элементы лямбда-исчисления. Удивительно, насколько хорошо язык АЛГОЛ-60 описывается в терминах лямбда-исчисления [9], хотя его разработка велась с совершенно других позиций. Язык АЛГОЛ-60 явился наглядным примером совпадения результатов теоретических исследований процессов вычислений и результатов практической разработки системы для целей программирования. Возможность использования функций, формирующих функции в результате их применения, тогда ие была в достаточной степени исследована, чтобы ее можно было практически использовать. Применение таких функций может быть весьма эффективным, поскольку позволяет писать программы, предназначенные для объединения других программ. Примеры использования таких функций приведены в гл. 3. 2-1. Aümark R. И. and Lucking J. R. «Design of an arithmetic unit incorporating a nesting store», (in) Proc. IFIP Congress 62, Amsterdam: Nortti Holland, 1963, pp. 694—698. 4 Бордж В. • 97
2-2. Barton R. S. «A new approach to the functional design of a digital computer», Proc. WJCC, ACM, New York, 1961, pp. 393—396. 2-3. Bauer F. G. «The formula controlled logical computer 'Stanislaus'», Math. Сотр., Vol. 14, N. 69, 1960, pp. 64^67. 2-4. Bürge W. H. «The evaluation, classification and interpretation of expressions», Proc. 19th National ACM Conf., New York, 1964, pp. Al.4.1 —1— 1.4.22. 2-5. Bürge W. H. «Interpretation, stacks and evaluation», (in) Introduction to Systems Programming, (ed.) P. Wegner, London and New York: Academic Press, 1964, pp. 294—312. 2-6. Burks A. W., Warren D. W. and Wright J. B. «An analysis of a logical machine using parenthesis-free notation», M. T. A. C, Vol. 8, N. 46, 1954, p. 53. 2-7. Dijkstra E. W. «Recursive programming». Num. Math., Vol. 2, N. 5, 1960, pp. 312—318. 2-8. Hamblin С L. «Computer languages», Austr. J. Sei., 1957, pp. 135—139. 2-9. Hamblin С L. «Translation to and from Polish notation,» Computer J., Vol. 5, N. 3, 1962, pp. 210—213. 2-10. Landin P. J. «The mechanical evaluation of expressions», Computer J., Vol. 6, N. 4, 1964, pp. 308—320. 2-11. Landin P. J. «A correspondence between ALGOL 60 and Church's lamb- danotation», CACM, Part. 1: Vol. 8, N. 2, 1965, pp. 89—101. Part 2: Vol. 8, N. 3, 1965, pp. 158—165. 2-12. Landin P. J. «An abstract machine for designers of computing languages», Proc. IFIP Congress 65, Vol. 2, Washington: Spartan Books, London: Mac- Millend and Co., 1966, pp. 438—439. 2-13. Myanilin A. N. and Smirnov V. K. «Computer with stack memory», Proc. IFIP Congress 68, Amsterdam; North Holland, 1968. 2-14. Samelson K. and Bauer F. L. «Sequential formula translation», CACM, Vol. 3, 1960, pp. 76—83. 2-15. Randell B. and Russell L. J. ALGOL 60 Implementation. London and New York: Academic Press, 1964. Глава 3 СТРУКТУРЫ ДАННЫХ 3.1. Введение На ранних стадиях написания программы удобно иметь систематический метод, позволяющий определить, каким образом программа должна быть разделена на отдельные части и как эти части взаимосвязаны. Прежде чем написать любую часть программы в деталях, важно решить вопрос о назначении каждой подпрограммы и определить, будет ли использована она в собственном контексте. Протокол представления всей программы в целом на стадиях написания является важным документом, поскольку при необходимости внесения изменений он позволяет ясно оценить влияние каждого изменения. Подпрограмма обычно оперирует лишь с аргументами, которые принадлежат определенной совокупности аргументов, называемой областью действия подпрограммы, и образует другую совокупность, которая называется диапазоном значений подпрограммы или просто диапазоном. 98
Первый шаг при разработке большой программы состоит в определении области действия и диапазона всей программы в целом, а также каждой из ее подпрограмм. Метод для описания древовидных структур данных был приведен в первой главе. Настоя- ш,ая глава содержит систематический подход к написанию программ, которые соответствуют структуре данных. Мы попытаемся здесь разграничить логическую структуру информации и ее физическое представление. Это разграничение является суш,ествен- ным, так как необходимая структура данных зависит от решаемой проблемы, а выбор физического формата в большей степени зависит от состава операций и формы представления данных, принятых в используемой вычислительной машине и системе программирования. На практике границу между логической и физической структурой провести трудно, и заключения о логической структуре данных обычно оказываются недостаточно обоснованными. Физическое представление часто выбирают на слишком ранней стадии написания программы, и какие-либо изменения принятого представления становятся невозможными без переписывания больших частей программы. Однако это разграничение оказывается целесообразным, так как четкое представление о структуре набора информации до некоторой степени определяет форму программ, которые оперируют с этим набором. Иногда истинная структура данных может быть скрытой и принимать форму, соответствующую структуре арифметических действий, выполняемых с помош,ью команд программы. Оказывается предпочтительным, особенно на ранних стадиях, выбирать структуру явной, так чтобы программы, которые с ней оперируют, были простыми для понимания. Метод составления программ, отвечающих определенной структуре данных, состоит в том, что сначала пишут только остов программы, а затем ее тело, используя аргументы, характеризующие действия, которые должны выполняться внутри остова. Скелетная программа обусловливает все семейство программ, полученных этим способом. Причем любые свойства программы общего назначения присущи всем членам этого семейства. Все программы, выбранные в этой главе для иллюстрации рассматриваемого метода, применимы к корневым упорядоченным деревьям. Остов программы общего назначения включает метод сканирования структуры данных, а аргументы указывают действия, которые должны быть выполнены во время сканирования. Программы общего назначения могут быть образованы механически на основе описания набора сканируемых деревьев, иногда называемого абстрактным синтаксисом набора. Рассматриваемый набор деревьев имеет либо составные [compaund) компоненты, внутренняя структура которых исследуется с помощью программы общего назначения, либо атомные [atomic) компоненты, внутренняя структура которых не исследуется. 4* • 99
Набор структур данных может быть задан без учета природы его атомных компонентов. Можно, например, определить набор списков и записать функции, оперирующие со списками, не указывая характеристик списковых элементов, которые трактуются как атомные, поскольку речь идет о списковой функции общего назначения. Функция, учитывающая природу списковых элементов, будет задаваться как аргумент списковой функции. Элемент списка сам по себе может иметь структуру, например, быть некоторым деревом, содержащим атомные компоненты. Поэтому можно формировать функцию общего назначения для списка деревьев из соответствующих функций для списков и деревьев, используя одну из них как аргумент другой. Вводится метод представления структур данных с помощью потоков (streams), которые используются, подобно сопрограммам, для образования компонентов структуры только тогда, когда они необходимы. Часто бывает легче понять и запрограммировать некоторую последовательность шагов, чем рассматривать запутанный процесс, в котором различные понятия смешиваются в программе. С другой стороны, смешанная программа часто более эффективна. Можно использовать преимущества обоих подходов, если программу записывать как многошаговую, а при реализации ее частей отдельные шаги выполнять параллельно. Аналогично, часто легче рассматривать последовательность значений, которые принимает переменная в программе в процессе расчета, чем исследовать как используется, контролируется и изменяется значение этой переменной. Метод потоков, или сопрограмм, дает возможность сосредоточить в одном месте те куски программы, которые содержат определенную переменную и позволяет создавать наборы комбинаторных конфигураций. Эти наборы наиболее просто задаются в древовидной с|)орме, при этом программа получает элементы набора за один раз, поскольку образуется поток конфигураций. 3.2. Основные понятия В первой главе набор структур был определен как путем указания наименований его компонентов, так и путем задания типа каждого компонента. Если существуют различные форматы одного набора данных, то с каждым набором будут связаны предикат и имя. Определение выражения, имеющего инфиксные операторы, может быть записано в виде ■ Выражение является — либо атомным (atomic) и есть идентификатор, — либо составным (compound) и имеет оператор (operator), который является инфиксным оператором, а также правую (right) и квую (left) части, которые являются выражениями. 100
в этом определении имена предикатов и указателей выделены курсивом. Введем также конструирующие операторы. Например, для того чтобы сформировать составное выражение из двух выражений и инфиксного оператора, необходимо ввести соответствующий оператор. Структура английских предложений такова, что они в себе содержат систему обозначений. Информацию можно представить также с помощью специальной системы обозначений, которая, возможно, и труднее читается, но имеет некоторые преимущества, поскольку более точна и позволяет почти немедленно найти описания объектов, которые тесно связаны с набором структур. Абстрактный синтаксис. Можно отделить определение самого набора от наименований связанных с ним предикатов, указателей и построителей. Для этого вводятся две операции на наборах структур, а именно, декартово проигведение (обозначаемое ср) и объединение (обозначаемое du). Декартово произведение наборов А, В, С иО, записываемое в виде ср {А, В, С, D), является набором всех четверок данных, первый компонент которых типа А, второй — типа В, третий — типа С и четвертый — типа D. Такая форма записи является альтернативой обычной формы записи: AxBxCxD и характеризует набор структур, имеющих четыре компонента, типы которых заданы, но чьи указатели еще не определены. Наилучшим приближением к эквиваленту такой формы в английском языке является Ш имеет . . . , который есть и ... I который есть и . . . . который есть и , . . , который есть Префиксную запись ср используют потому, что она позволяет задавать одно- и нуль-компонентные структуры, записываемые ср{иА) и ср соответственно. Указатели компонентов могут быть получены и названы следующим образом. Предположим, что система программирования допускает определения наборов такие, как def complex = ср (real, real), И запоминает определение этого набора в форме, которая позволяет применять к нему функции. Используется также функция selectors, применение которой приводит к образованию списка указателей. Имена могут быть присвоены указателям с помощью определения: def real, imaginary = selectors complex . 101
кроме того, необходима функция, называемая construct, которая генерирует формирующие функции. Она применяется к списку объектов и образует объект, имеющий списковые объекты в виде его компонентов. Используя функцию construct и выражение construct complex E,3; 7,2), можно построить заданное комплексное число. С другой стороны, функцию, формирующую комплексные числа, определяют как def conscomplex = construct (complex) . Эту функцию можно применить непосредственно к паре действительных чисел, для того чтобы образовать комплексное число (т. е. conscomplex E,3; 7,2)). Указатели и формирующие функции взаимосвязаны. Если два компонента комплексного числа указаны и из них сформировано комплексное число, то результат должен быть равен оригиналу. Другими словами, X = construct complex (real х, imaginaru х) . Вообще, аксиомы, которые утверждают, что структурный объект может быть сформирован только одним способом, имеют следующий вид: ес.аи С^, Ca, Cg, ..., С^ являются типами данных и def S== ср (Ci, Q, Сз, ..., С„); def S;, Sa, S3 S„ = selectors S, то Sft (construct S (Xi, Xa, X3, ..., X„)) = Xä Й X = construct S (SxX, S2X, SgX, ..., S„X) при условии, что X типа S. Объединение двух наборов/4 и ß, записываемое как du (Л, ß), содержит все члены из Л к В, при этом предполагается, что каждый из членов отмечен именем набора, из которого он взят. Поэтому можно определить, взят ли член набора du {А, В) из А или из В. В английском языке эквивалентом du (А, В, С, D) является является или . или ., или . или . ,, и есть А .. и есть В .. и есть С .. и есть D Функция, названная predicates, будет использована для того, чтобы образовать предикаты для набора, который является рассматриваемым объединением. Функция du будет расширена таким образом, что может быть применена к списку наборов, а не к их паре. Учитывая это, определение выражения, имеющего инфиксные операторы, можно записать в виде def гее expression = = du (identifier, ср (expressioq, infixed operator, expresgion)), 1Ö3
a предикаты для выражения def alcmic, compaund -- predicates expression . Аргументы объединения du упорядочены, и результирующий список предикатов имеет такой же порядок. Первый предикат проверяет, соответствует ли компонент первой альтернативе, второй предикат проверяет вторую альтернативу и т. д. Удобно использовать и другую функцию, названную part, которая обратна оператору объединения parts {du (Ci, С,, ..., С,,)) = Ci, С^, ..., С,,. Теперь можно ввести наборы atomic и nonatomic выражений def atomic, nonatomic = parts expression И указатели для nonatomic выражений: def left, operator, right-selectors nonatomic . Аксиомы, которые утверждают, что предикаты эффективны, имеют вид ■ Если Т = du (Ci, Ca, ..., Cn) и P;, p.,, P3, ..., P„ = predicates T, TO ( true, если X g С; \ false, в других случаях. Функции. Существуют очень тесные аналогии между методами структуризации данных и методами структуризации функции, которая применима к этим данным. Если А является набором, то его члены могут выступать в качестве переменной х в выражении Ра Wi где Fj^ является функцией, которая применяется к А. Набор всех функций, которые применимы к элементам из ср (Л, В, С) и имеют указатели first, second и third, может быть выражен следующим образом: Лх . F (Рд (first X), Fb (second х), Fe (third x)) . Здесь функции F^, F^ и F^ применяются к компонентам, а результаты этих определений объединяются с помощью функции F. Набор функций, которые применимы к элементам из du (Л, В, С) и имеют предикаты is-А, is-B и is-C, может быть выражен как Ях. if is-А (х) then Fa (х) else if is-B (x) then Fb (x) else Fc (x) . Здесь, прежде всего, делаются проверки для альтернативных форматов, а затем применяются функции правильного типа. Если определение структуры ссылается само на себя, т. е. def гее А = ... А ... А ... , 103
то простейшая функция на элементах также ссылается сама на себя: def гее f^ ^ ...Fa-.-Fa--- ■ К каждому компоненту типа А применяется та же функция, что и ко всей структуре. Подсчитывающие генерирующие функции. Если п (Л) — число элементов Л, то « {ср (Л, В)) = п (А) х п (В), п {ср (Л)) = п (А) и п (ср ( )) = 1. Число элементов объединения du (А, В), т. е. п {du (Л, В)) равно п (Л) + п {В). Большую информацию об этих наборах можно получить следующим образом. Каждый рассматриваемый объект либо имеет компоненты, либо является атомным и не имеет внутренней структуры. Элементы каждого набора можно различать по числу атомных компонентов, которые они содержат. Это выполняется с помощью подсчитывающей функции enumerating generating function, в которой коэффициент при x'^ является числом элементов набора, имеющего п атомных компонентов. Предположим, что А {х) и В (х) — две генерирующие функции этого вида для двух наборов А и В соответственно: Л (х) = Ца^х«, В (X) = Jjb^x", где йп — число элементов, имеющих точно п атомных компонентов некоторого типа, а Ь„ — число элементов из В, имеющих п компонентов того же типа. Тогда числом элементов из ср (Л, В), имеющих точно п атомных компонентов, является коэффициент при л:" в соотношении Л {X) В (х) ='E!'E!aub„_hX". В этой сумме произведение akbn_h является числом элементов из ср {А, В), в котором первый сомножитель представляет k атомных компонентов, а второй — п — k. Число элементов объединения du (Л, В), имеющего п атомных компонентов, равно а„ + + Ьп- Поэтому генерирующую функцию для du (Л, В) находят путем суммирования генерирующих функций для А и В: А{х) + В {х) = Ц (а„ + Ьп) x'^. Такие же правила для конструирующих генерирующих функций применимы к наборам деревьев, имеющим более одного типа атомных компонентов и генерирующие функции многих переменных. Генерирующей функцией декартова произведения является произведение генерирующих функций входящих в него наборов, а генерирующей функцией объединения — сумма генерирующих функций для альтернативных наборов. Аналогии между наборами структур, функциями и генерирующими функциями сведены в таблицу и будут исследованы с помощью 104
ряда примеров в следующих параграфах. Выражения, представленные в последней строке таблицы, соответственно означают следующее: А = ...В ...В...— набор А задается в выражениях набора В; Т^ = ...^в-- ...STß... ^ функция Tj, задается в выражениях функции 9"в', А {В (х)) — выражение В {х) заменяется на X в А {х). Наборы ср {А, В) du (Л, В] Л= = ...В...В... Программа 9^в (second {х)))) if является А then Та{х) eise 5"ß{x) = ...з-в-ЗГв... Генерирующая функция А{х)-В(х) А{х)+В{х) А{В(х)) 3.3. Некоторые простые структуры 1. На первый взгляд кажется, что структура, не имеющая компонентов, не может быть достаточно удобной. Однако две или более из них можно взять в качестве аргументов объединения для того, чтобы образовать указатель. Описанием набора является ср ( ). Генерирующей функцией набора будет л:" или 1. Это означает, что он не имеет компонентов. Переключатель на два положения может быть определен следующим образом: def truthvalue = du (ср ( ), ср ( ) ) | def truth, falsehood = parts truthvalue def true = construct truth ( ) def false = construct falsehood { ) def istrue, isfalse = predicates truthvalue . Выражение, описывающее истинное значение, встречается наиболее часто в условном выражении. Если х является переменной для true, а у является переменной для false, то генерирующей функцией для истинных значений будет х + у. Если х + у заменяется на переменную в другой генерирующей функции, то коэффициент при х"" будет содержать число структур, содержащих п истин, а коэффициент при у'^ — число структур, содержащих п ложных утверждений. Наиболее часто используемыми^операци- ями над истинными значениями являются ИЛИ (or), И (and) и НЕТ (not). Их определения приведены в примерах гл. 1. 2. Аналогично, набор, имеющий пять альтернатив, может быть задан следующим образом: du (ср ( ),ср ( ), ср ( ),ср{ ), ср ( )). 105
Если альтернативы можно обозначить целыми числами от 1 до 5, то каждую из них можно использовать в case-выражении или cose-onepaTope вида case X of SI S2 S3 S4 S5. Функциями, применимыми к элементам набора этого вида, являются только: а) функции, которые конструируют одну из альтернатив, и б) предикаты этих функций. 3. Генерирующая функция для du (Л, В, С, D, Е) может быть записана в виде Л-1 + АГг + Хз + Х4 + Хв, где переменные х^, х^, Xg, х^, х^ используются для обозначения типов А, В, С, D, Е соответственно. 4. Аналогично генерирующей функцией для ср {А, В, С, D, Е) является XiXXiXXgXXiXx^. 5. Функция, называемая possible и используемая для описания поля записи, которое может существовать или не существовать, определяется следующим образом: def possible А = du (А, ср ( ) ) def exists, notexists = predicates (possible A). Набор (possible A) используется в контексте if exists X then f (x) else a, где только первое плечо может содержать ссылку на х. Соответствующая генерирующая функция имеет вид possible (х) = 1 -\- х. 6. Выражение ср (uA) обозначает набор структур, имеющих один компонент типа А. Единственной допустимой операцией для этого набора является конструирование одной структуры или указание его единственного компонента: def ref А = ср (и А), def addr (х) = construct (ref А) х; def content =--. ssleclors (ref A). Функция addr образует один уровень косвенной ссылки. Функция content формирует объект типа А из объекта типа ср {и А). 106
7. Набор целых чисел может быть определен следующим образом: def integer = du (ср ( ), ср (и integer)); def zero, nonzero = predicates integer; def zerointeger, nonzerointeger = parts integer; def successor x = construct nonzerointeger (u x); def 0= construct zerointeger ( ); def predecessor = first (selectors nonzerointeger). Если X — целое, at/ — целое, не равное нулю [nonzero), то zero О = true; zero (successor x) = false; nonzero 0 = false; nonzero (successor x) = true; successor (predecessor у) = у; peredecessor (successor x) = x. Семейство функций, наиболее естественно связанное с этим определением, встречалось уже в гл. 1, а именно: def гее R а g п = if zero n then a else g n (R a g (predecessor n). 8. Структура данных, имеющая структуру, аналогичную структуре системы целых чисел, является системой косвенных ссылок def гее indref А = du (А, ср (и (indref А))) . Семейство функций, которое соответствует косвенной ссылке, использует те же самые предикаты и указатели, что и для целых чисел: def гее R f g х ----- if zero X then f x else g (R f g (predecessor x)). 3.4. Списки Список является наиболее общей структурой данных, используемых в программировании. Определение списка элементов типа А имеет вид Н Л-список {A-lisi) является — либо нулевым (null), — либо имеет — — голову (head), которая есть А, и хвост (lall), представляющий собой А-список ИЛИ def гее list А = du (ср ( ), ср (А, list А)), 107
a действия над списками могут быть определены следующим образом: def null, nonnull = predicates (list A); def nullc, nonnulc = parts (list A); def h, t = selectors nonnullc; def prefix x у = construct nonnullc (x, y); def nullist = construct nullc ( ), где приняты сокращения: h — head; t — tail. Выражение x : у используют в качестве сокращения для prefix х, у { ) — в качестве обозначения нулевого списка, о, Ь, с, d, е — вместо а : ф : {с :{d : (е : ( ))))), а выражение их — для того, чтобы обозначить список, состоящий из одного элемента. Типы списковых функций вводятся в виде null A1 А = list ^- truth value; head (^ nonnull A-list^-A ; tail (^ nonnull A-list-^-A-li?t; prefix ¢: (A-* (A-list ^ A-list)); ( ) e A-list. Эти функции тесно взаимосвязаны: null ( ) = true; null (X ; у) = false; h (X : y) = x; t (X : y) = y; (h z) : (t z) = z; где X Представляет собой голову А, у — А-список, а z—ненулевой А-список. Многие функции, которые оперируют со списками, имеют одинаковую основную структуру. Например: def гее sum х = if null X then О else (h x) -f sum (t x) ; sum A, 2, 3,4)= 10; def rec product x = if null X then 1 else (h x) X product (t x) ; product A,2, 3,4) = 24 ; def rec append x у = if null X then у else (h X) : (append (t x) y) ; append A, 2) C, 4, 5) = 1, 2, 3, 4, 5 ; def rec concat x = if null X then ( ) else append (h x) (concat (t x)) ; concat (A, 2), C,4), ( )) == 1, 2, 3, 4; def rec map f x = if null X then ( ) else (f (h x)) : (map f (t x)) ; map square A, 2, 3, 4) = I, 4, 9, 16. 108
Можно заметить, что эти функции различаются только условным выражением, в том числе членом, который комбинирует результат применения функции к голове списка (обычно собственно голове) с результатом, полученным за счет применения той же самой функции к хвосту списка. Общие части этих функций могут быть представлены в виде выражения, в котором части, не являющиеся общими, сделаны переменными. Так, функция tisÜ определяется в виде def гес list 1 а g f х = if null X then a else g (f (h x)) (listl a g f (t x))). Если СПИСОК пустой, то результатом применения функции (listl а g f) к списку является а; с другой стороны, этот же результат получается в случае применения g к двум аргументам, представляющим собой: 1) результат применения / к голове списка и 2) результат применения той же функции к хвосту списка. Пять функций, указанных выше, могут быть теперь переопределены в терминах списка lisÜ. Очевидно, что рассматриваемый способ экономит запись и более удобен для создания правильной программы, так как комплексная программа (т. е. условное выражение и цикл) записывается раз и навсегда в функции listl. В следующих ниже определениях / — тождественная функция, К X у = X, а функция postfix добавляет новый элемент в конце списка: def postfix X у = append у (их) . Новые определения имеют вид def sum = listl О plus I; def product = listl 1 mult I; def append x у = listl у prefix I x; def concat = listl ( ) append I; def map f — listl ( ) prefix f, Некоторыми другими примерами являются def length = listl 0 plus (K 1); def sumsquares = listl 0 plus square; def reverse = listl ( ) postfix I; def identity = listl ( ) prefix I; Типы аргументов списка listl могут быть определены по способу их применения следующим образом. 1. Функция {listl а g f) применяется к списку. Предположим, что функцией является A-list и что она образует элемент из В, т. е. (listl а g f) g (A-list-»-ß), следовательно, аргументом а должно быть В. 109
2. Функция / применяется к Л и образует С, т. е. f е (А- q, значит g должна применяться к С, и результат применения {lisÜ а g f) к A-list, которым является В, должен быть В. Поэтому тип функции g имеет вид Я 6 (C^iB^B)). функция list] имеет свойство listl а g f (append х у) = listl (listl а g f у) g f х, При условии, что аргументы а, g и f имеют указанные выше типы для некоторых наборов А, В и С, и что х и у — оба являются А-списками (A-lists). Это следует из аксиом для списков и определений Usü и append. Рассматриваемое свойство подчеркивает тот факт, что во всех функциях, заданных через функцию listl, список сначала просматривается до конца, а затем действия g выполняются на обратном пути. Одни и те же результаты могут быть получены двумя способами: 1) путем конкатенации двух списков X R у я последующего применения {list 1 а g f) к результату; 2) путем применения {listl а g f) ко второму списку {у) и последующего использования результата как начального значения (аргумента а) для той же самой функции, когда она применяется к первому списку {х). Для доказательства указанного свойства функции listl необходимо рассмотреть два случая, когда х: 1) является нулевым списком и 2) не является нулевым списком. Доказательство проводится путем сведения каждой части к одному и тому же выражению. Пусть LHS= listl а g f (listl у prefix I x); RHS= listl (listl a g f y) g f X. Если X является нулевым списком, то LHS= listl а g f (listl у prefix I ( )) = listl a g f y; RHS = listl (listl a g f y) g f ( ) = listl a g f y; Оба шага выполнены на основе правила listl а g f ( )= а. Когда список х не является нулевым, он Имеет вид h х : t х и, следовательно, LHS = listl а g f (listl у prefix I (h х : t х)) = listl a g f (prefix (I (h x)) (listl у prefix I (t x))) = listl a g f (prefix (h x) (listl у prefix I (t x))) --= g (i (h X)) (listl a g f (listl у prefix I (t x))) = g(f (h X)) (listl (listl a g f y)g f (t X)) 110
н RHS = listl (listl a g f у) g f (h X : t x) = g (f (h x)) (listl (listl a g f y) g f (t x)) = LHS . Каждый шаг есть результат применения правила listl а g f (h X : t х) = g (f (h х)) (listl a g f (t x)), за исключением последнего преобразования левой части, для которого доказываемое свойство считается истинным для хвоста списка. Все другие свойства функций могут быть доказаны аналогичным образом. Все функции, заданные в терминах функции listl, просматривают список (т. е. накапливают результат) справа налево. Имеется также второе семейство функций, которые сканируют списки слева направо. Эта группа функций может быть определена через функцию, называемую list2, которая определяется следующим образом; def list2 а g f X = if null X then a else list2 (g (f (h x)) a) g f (t x). Аргументы a, g, f и X должны быть того же типа, что и для функции listl. Функция list2 имеет свойство, аналогичное свойству функции listl, а именно: i list2 а g t (append x у) = list2 (list2 a g f x) g f y. Существуют два способа получения одних и тех же результатов: 1) присоединение двух списков х и 'у и затем применение (listU а g f) к результату или 2) применение функции к л:, а затем использование этого результата в качестве исходного значения для той же функции, когда она применяется к у. Заметим, что списки X и у меняются ролями в свойствах функций listl и list2. В случае определения функций с использованием list2 результат накапливается при сканировании списка слева направо. Функция list2 реализуется с помощью^приведенной ниже итеративной программы и более эффективным может оказаться использование функции list2, а не listl: L: if null X tlien a else a : = g (f (h X)) a X : = t X go to L. Также можно доказать, что listl а g f X = list2 a g f (reverse x). Ill
Это означает, что функция {listl а g f) ъ случае применения к списку X дает такой же результат, что и {list2 а ц f) к списку, обратному X. Поскольку append X у= listl у prefix х= iist2 х prefix у reverse = listl ( ) postfix I = list2 ( ) prefix I, за счет подстановки определенных значений для а, g и / в соотношениях между Ust\ и list2 можно доказать, что reverse (reverse х) = х append (append х у) z = append х (append у z) reverse (append x у) = append (reverse y) (reverse x). Генерирующая функция для Л-списка может быть получена непосредственно из структурного описания списков list А = du (ср ( ), ср (А, list А)) list (х)= 1 + X list (х). Здесь один список (х" или 1) не имеет компонентов, а другой — ненулевой Л-список, имеет голову, которой является Л {х) и {х) хвост, представляющий собой Л-список (списка {х)). Это следует из решения последнего соотношения list (х) = 1/A — х) = 1+ Х+ Х2+ хЗ+ Х^+ ... Определение можно прочитать по-другому: список является пустым, либо является 1-списком, или 2-списком, или... Генерирующей функцией для списка длиной п является х'К Если компонентами списка служат либо х, либо у, {х -\- у), то число списков, содержащих т компонентов, будет равно биномиальному коэффициенту при х^ в соотношении {х -\- у)"-: п \ п\ т 1 ml (п — т)\ Этот коэффициент представляет собой также число возможных выборок по т. различных объектов из системы размерности п. Генерирующие функции для списков, длины которых ограничены некоторым способом, могут быть, как будет показано ниже, записаны непосредственно. Например, непустые списки имеют генерирующую функцию nlist (х) = х/{1 — х) = X + х^ + х^ + X* + ... . В качестве другого примера генерирующей функции для всех списков длиной не более чем / может служить функция A — x/+')l/(l ~х) = I + X + х^ + ... -\- X'. 112
Аналогично, если допустимы только О-, 3- и 5-списки, то генерирующей функцией будет 1 + л;^ + х'. Если различные объекты связаны с каждой позицией списка (предположим, что х, связан с позицией /) и либо занимают эту позицию либо нет, то результирующей генерирующей функцией будет A + Х-^Х) A + Х^Х) ... A + ХпХ). Коэффициент при х"^ содержит все комбинации произведений различных Xj, образованных из %, Хг, Хд, ..., x„: A + х-ух) A + x-iX) A + ЧА == = 1 + (Xi + ^2 + Хз) л: + {х ~Г XiX^XgX , которые называются элементарными симметрическими функциями. Генерирующей функцией для списка длиной п, /-й элемент которого имеет генерирующую функцию ^j{x), является произведение riAj (х). В частности, если каждая функция А^ представляет собой список элементов xj, то результирующей генерирующей функцией будет функция для списка списков, [1/A—Xix)] [1/A-^2^I [1/A ~ XsX)] ... [1/A — л;,,л;)], в которой коэффициентом при х'" являются все комбинации Xi, Xi, ..., Хт размера т с допустимыми повторениями. Если каждый Xj представляет собой набор, то результатом будет генерирующая функция для числа таких комбинаций: "<'--)'-2 С"^;;;*"' X"^. т т>0 Поскольку генерирующей функцией для непустых списков является х1{\ —х), то в случае, когда, по крайней мере, один из в каждой комбинации, генерирующая функция имеет вид -'О--.)-2(-:) Генерирующей функцией, соответствующей структуре du [Хх, Х2, ..., Хп), служит Xi + Х2 + ... + Хп, и тогда генерирующей функцией для ряда /г-списков, в которых Xi, х^, ..., л;„ могут встречаться в каждой позиции, является 1/[1 — (Xi + ^2 + ... + x„) х]. Можно создавать новые генерирующие функции путем замены генерирующей функции для переменной в другой генерирующей функции. Этот метод можно продемонстрировать следующим образом. Генерирующей функцией для непустого списка не- 113
211 иг Рис. 3.1. Композиции из числа 4 пустых списков является nlist (nlist (х)), в. которой nlist (х) = = ^/A — х). Поэтому генерирующая функция равна х/A — 2х) и существуют 2"-^' конфигураций с п атомными компонентами. Такие структуры обычно называются композициями. Восемь композиций из числа 4 приведены на рис. 3.1. Функцией, связанной с nlist и сканирующей непустой список, является def nlist g f X = if nuU (t x) then f (x) else g (f (h x)) (nlist g f (t x)). a функцией для сканирования композиций — nlist gj (nlist g^ /). Композиция из n + 1 может быть получена из композиции п либо добавлением новой головы к списку, либо добавлением новой головы к голове списка. Функция, которая оперирует с двулмя списками равной длины и сначала объединяет соответствующие элементы путем использования функции /, а затем объединяет результаты путем последовательных применений функции g, определяется как def гес zip а g f х у = if null X then а else g (f (h x) (h y) (zip a g f (t x) (t y)). Такая функция может быть использована, например, для формирования скалярного произведения (zip О plus mult) или образования списка пар из пары списков следующим образом: def zips -= zip ( ) prefix pair 114
при другом способе получения такого семейства функций требуется сначала организовать список пар, а затем использовать функции listl или list2. В результате получим zip а g f X у = listl а g fl (zips x у) where fl (x, y) = f x y. 3.5. Списковые структуры Структура Л-списка (A-list structure) является либо атомной и есть А, либо является {A-list structure) list: def rec liststructure A = du (A, list (liststructure A); def atomic, nonatomic == predicates (liststructure A) . Функции listl и list2 могут быть расширены для списковых структур следующим образом: def rec Isl а g f х = if atomic x then f X else listl a g (Isl a g f) x; def rec Is2 a g f x = if atomic x then f x else Iist2 a g (Is2 a g f ) x. В обоих случаях одна и та же функция, либо (Isl а g /), либо (/s2 а g f) применяется к компонентам списковой структуры так же, как к самой [списковой структуре. В этом случае / и (/si а g f) должны образовать один и тот же тип объекта. Следовательно, эти функции и некоторые из функций, заданные для списков (т. е. эти функции с g (z В -> (В ^>- В), образующие некоторое В), могут быть расширены с целью применить их к списковым структурам. Например, def sumis = Isl О plus I = Is2 О plus I; def productsls = Isl 1 mult I = Is2 I mult I; def concatis = Isl ( ) append u; def mapis = Isl ( ) prefix f; def Iengthls= Isl 0 plus (K 1); def sumsquaresls = Isl 0 plus square; def reversels = Isl ( ) postfix I; def identityls = Isl ( ) prefix I. Первые две функции образуют сумму и произведение атомных компонентов списковой структуры; функция concatis преобразует списковую структуру в единый список; функция (mapls /) применяет / ко всем атомным компонентам и образует списковую структуру, которая содержит результаты и имеет ту же форму, что и оригинал; функция lengthls подсчитывает число атомных компо- 115
нентов, а функция reverseis переставляет все списки в структуре в обратном порядке. Примерами применения этих функций являются let ls= A, B, C, 4)), 5); sumis Is = 15; productis Is = 120; concatls Is = 1, 2, 3, 4, 5; mapls Is= A, D, (9, 16)), 25); lengthls Is = 5 ; sumsquaresis Is = 55 ; reversels Is = E, (D, 3), 2), 1); identityls Is = A, B, C, 4)), 5) , Отношение между listl и list2 расширяется до Isl a g f X = Is2 a g f (reversels x) . Генерирующая функция для списковой структуры определяется соотношением is (х) = X + list (Is х) . Существует бесконечное число списковых структур, содержащих п атомных элементов, так как 0-списки и 1-списки могут быть добавлены к любой списковой структуре для ее расширения без добавления каких-либо атомных компонентов. Если 0-списки и 1-списки недопустимы, то функцией, генерирующей список, становится х^ /A — х). Генерирующей функцией для списковых структур без 0-списков или 1-списков является nls (л:) = л; + nls^ {х)/[1 — nls (х)], которая при решении дает 2nls^ (х) — {\ + х) nls (х) + X = 0; nls (л;) = 1/4 A + л; — К1 — 6л; + х^) = = л; + л;2 + Зл^ + Их* + 45^5 + ... . Генерирующая функция предназначена для подсчета способов расстановки скобок суммы п чисел, в которых каждая скобка должна включать, по крайней мере, два выражения. Например, а; а + Ь; а + b + с, {а + Ь) + с, а + (Ь + с); а + b + с + d, {а + b + с) + d, а + {b + с + d), (a + b) +c + d, a + {b + с) +d, a + b + {с + d), {a + b) + {c + d), ((« + b) +c) + d, {a + {b + c)) +d, a + db +c)+ d, a + {b + {c + d)). 116
Структура может быть перестроена путем поворота списковой структуры таким образом, что самый левый край становится верхним уровнем структуры. Генерирующая функция может быть перегруппирована: 2п1^ (х) — {\ + х) nls (х) + X = 0; [2nls (х) — I] nls (х) + X [I — nls (х) ] = 0; nls (х) = х/{1 — [nls (х)/1 — nls (х) ]}. Это соответствует следующей структуре: ■ A-nls имеет — корень (root), который есть А, — и тело (body), которое является {{А-п15)-п1Ш)-списком. 3.6. Деревья и леса Списковые структуры представляют собой упорядоченные корневые деревья, имеющие компоненты только в их остатках или концевых точках. Другой структурой, которая более употребительна, является дерево с компонентами в каждой узловой точке. Если внутреннюю и концевую точки трактовать одним и тем же способом, то соответствующая структура дерева определяется следующим образом: ■ Л-дерево имеет — корень (root), которым является А — и листинг (tiiiing), который является Л-лесом [A-joreii) Л-лес является списком Л-дерева (A-tree). Функция, используемая для того, чтобы построить дерево из Л и Л-леса, называется dree и определяется в виде: def гее tree А = ср (А, forest А) and forest А = list (tree А); def root, listing = selectors (tree A); def ctree == construct (tree A). Два семейства функций могут быть определены с помощью функций Ust\ и list2\ def rec treel a g f x = f (root x) (forestl a g f (listing x)) and forestl a g f x == listl a g (treel a g f) x; def rec tree2 a g f x = f (root x) (forest2 a g f (listing x)) and forest2 a g f x = list2 a g (tree2 a g f) . В этом представлении концевая точка является деревом, листинг которого является нулевым списком. Как и в случае списковых структур, одна и та л<е функция применяется как к компонентам дерева, так и ко всему дереву в целом, или как к компонентам леса, так и ко всему лесу в целом. 117
Выражение, приведенное ниже, будет использовано для того, чтобы описать дерево, представленное на рис. 3.2; запятая используется для списков, а инфиксная точка с запятой — для функции dree: А; (В; (Е; ( ); F; (G; ( ), Н; ( ))), С; ( ), D; (I; (К; ( )), J; ( ))). Рис. 3.2. Пример дерева СуЩеСТВуЮТ несКОЛЬКО СПОСОбОВ сканирования узловых точек дерева. Они могут быть представлены в виде функций для всех агомных компонентов листингов. Например, результатом применения функции preorder = (treel () append prefix) к дереву, изображенному на рис. 3.2, является ABEFGHCDIKLJ; результатом применения функции postorder — (treel ( ) append postfix) служит EGHFBCKLIJDA, a функция {tree2 ( ) append prefix) образует ADJILKCBFHGE, что является также результатом применения функции preorder к обратному дереву. Аналогично {tree2 ( ) append postfix) дает такой же результат, что и функция postorder, примененная к обратному дереву. Дерево и все его списки могут быть преобразованы к обратным структурам путем использования def reversetree = tree2 ( ) prefix ctree , хотя лес может быть реверсирован с помощью def reverseforest = forest2 ( ) prefix ctree . Легко показать, что reversetree (reversetree x) = x И reverseforest (reverseforest x) = x. Каждая функция для дерева, которое определено в терминах функций treel или tree2, может быть применена к лесу путем использования одних и тех же аргументов и функций forestl и forest2 соответственно и наоборот. Взаимосвязь между функциями listl и list2 может быть использована для того, чтобы получить следующие соотношения между функциями treel и tree2, а также между функциями forestl и forest2: treel а g f X = tree2 a g f (reversetree x) forestl a g f X = forest2 a g f (reverseforest x) 118
Генерирующими функциями для деревьев и лесов являются tree (х) = X forest (х); forest X = list (tree х), поэтому tree (х) = X list [tree (хI = х/[1 — tree (хI; tree^ (х) — tree (х) + х = 0; tree (х) =-1-A- V/" 1 — 4х) = X + х« + 2x3 + 5x1 _[. 14x5 _[, 42хв + 132х' + ... . Кроме ТОГО, поскольку tree (х) = X forest (х), ТО X forest» (х) — forest (х) + 1 = О . Леса с п узлами находятся в соответствии с деревьями с (и + 1) узлами, образованными путем добавления корня. Леса с р узлами находятся в соответствии с набором структурных перестановок ряда р открывающих и р закрывающих скобок. Структурной перестановкой ряда из riiXi, щх^, ..., п^Хт элементов является перестановка, в которой любой начальный сегмент содержит не более л:,_1 элементов из Х( для всех 1 < t <: яг. Поскольку число открывающих скобок равно числу закрывающих скобок и открывающая скобка всегда предшествует закрывающей, то все ряды обладают свойством структурных перестановок, которое состоит в том, что в любом начальном сегменте ряда имеется боль- ujee число открывающих скобок, чем закрывающих. Скобки можно интерпретировать как инструкции для добавления наименования к магазинному списку и удалению его из списка. Поэтому набор структурных перестановок соответствует всем возможным способам добавления и удаления наименований из магазинного списка. С другой стороны, ряд скобок можно интерпретировать как инструкции для построения массива из двух строк. Инструкция "(" добавляет свой разряд к первой строке, а инструкция ")" — ко второй строке. На каждом шаге первая строка никогда не бывает короче второй. Поэтому набор структурных перестановок с р открывающими и р закрывающими скобками находится в соответствии со всеми р столбцами двустрочных массивов, заполненных числами от 1 до 2р таким образом, что существует восходящий порядок как в строках, так и в столбцах. Эти соответствия для р = 3 показаны на рис. 3.3. Каждый лес определяет перестановку, образованную^примененнем набора ее CKo6ioK к списку 119
a / \ b С 1 a \ с / b 0„» Ш „, e ä e ä @H fZJ ZtS ct. 'OO^ gj ../ (@)) 2f «7^^ ()()() KS .„y Рис. 3.3. Разнообразные соответствия 1, 2, 3, 4, ... Открывающая скобка интерпретируется как инструкция для загрузки следующего числа в магазинный список, закрывающая — как инструкция, которая берет число с вершины магазинного списка и затем фиксирует его в окончательной перестановке. Аналогичная перестановка может быть также образована путем разметки леса числами 1, 2, 3, 4, ... в соответствии с функцией preorder. Затем к полученному дереву применяется функция postorder. Некоторые перестановки показаны на рис. 3.4. Другим способом представления структуры дерева без учета его компонентов является замена каждого корня дерева длиной его листинга и затем запись этих длин в заранее установленном порядке. Например, первое дерево на рис. 3.5 будет описано 120
(J) (г) О) (О (г) W (О О) B) (J) B) № Рис. 3.4. Структурные перестановки рядом 320200022000. Существует соответствие между изображенными на рис. 3.5 лесами, которое состоит в том, что: а) степень каждого корня в обоих случаях одинакова и б) один и тот же список корней образуется при сканировании первого дерева уровень за уровнем и в результате применения функции preorder ко второму дереву. Это может быть сделано путем добавления длины листинга к каждому корню и затем последовательным сканированием дерева. Результирующее кодовое слово должно описывать лес, так как корень достигается прежде, чем любое из его поддеревьев. Пример этого соответствия показан на рис. 3.5. 2 о о о 0 0 о о\ о о Рис. 3.5. Соответствие, которое сохраняет степени вершин 121
3.7. Бинарные деревья Два особых типа деревьев рассмотрены ниже. Мы покажем, что соответствия могут быть установлены между деревьями этих типов и ранее определенными деревьями. Первый тип деревьев называется бинарным деревом {binary tree) и может иметь О или два поддерева. ■ Л-бинарное дерево — либо пустое (empty), — либо имеет корень (root), которым является А, а так же левую (left) и правую (right) части, которые являются Л-би- нарными деревьями, def гее btree А = du (ср ( ), ер (А, btree А, btree А)) ; def empty, nonempty = predicates (btree A) ; def emptyc, nonemptyc = parts (btree A) ; def root, left, right = selectors nonemptyc ; def cbtree = construct nonemptyc ; def etree = construct emptyc ( ) . Функция для построения непустых деревьев называется cbtree. Именем пустого дерева является etree. Семейство функций, которое сканирует бинарные деревья, может быть образовано за счет использования функции def гее btree 1 а g f х = if empty X tiien a else g (f (root x)) (btreel a g f (left x)) (btreel a g f (right x)) . Типы аргументов могут быть выведены следующим образом: если {btreel а g f) ^ А = binary tree -^В, то а ^ В, f ^ А ^С ge С ^{В ^{В -^В)). Можно получить другую функцию на бинарных деревьях, которая изменяет роли левого и правого поддеревьев, названную btree2. def rec btree2 a g f x if empty X tiien a else g (f (root x)) (btree2 a g f (right x)) (btree2 a g f (left x)) . 122
в с D -^^ Е F I J \ \\ I I F I — I \ BCD. , /\ /\ "-' '-' в Н K—L G Н К L а I \ /\ н к J Рис. 3.6. Соответствие между лесом и бинарным деревом Функцией ДЛЯ реверсирования бинарного дерева является def reversebtree = (btree2 etree cbtree I) = (btreel etree (C ctree) I) where С f x у = f у x . Существует соответствие между бинарным деревом и лесом, которое можно пояснить с помощью так называемого развертывания структурного определения леса. ■ Л-лес (A-joreii) является списком Л-деревьев {A-iree)-liii, при этом он — либо нулевой, — либо имеет голову h, являющуюся Л-деревом (A-iree), и хвост t, который является списком Л-деревьев. Другими словами; ■ Л-лес — либо нулевой — либо имеет (rooi-h), которым является Л, и (lliilng-h), который есть Л-лес, и i, который также есть Л-лес. Это определение имеет ту же структуру, что и для бинарного дерева, а соответствие меяаду функциями на бинарных деревьях и лесах может быть выражено следующим образом. Бинарное дерево Лес empty null etree ( ) root root'h left listing-h right t btree X у z (ctree x y) : z Каждая функция, которая оперирует с бинарными деревьями или формирует их, может быть преобразована в функцию, которая оперирует с лесами или формирует их. Пример соответствия между лесами и бинарными деревьями показан на рис. 3.6. Существует преобразование лесов, которое идентично перестановке соответ- стгзующего бинарного дерева. Очевидно, что функция foresü 123
может быть «развита» таким же образом, как и определение структуры: forestl а g f X = listl а g (treel a g f) x = if null X then a else g (treel a g f (h x)) (listl a g (treel a g f (t x))) =|if null X then a else g (f (root (h x)) (forest 1 a g f (listing (h x))) (forest 1 a g f (t x)) . Другая сканирующая функция для лесов, называемая forests, может быть определена, если поменять ролями два леса в позициях listing-h и t в этом определении: def гее forests а g f х = И null X then а else g (f (root (h x)) (forests a g f (t x))) (forests a g f (listing (h x))). Функцией для поворота леса, т. е. получения леса, соответствующего перестановке исходного бинарного дерева, является def rotate = forests ( ) ctree prefix. Отсюда следует, что forest 1 а g f X = forests a g f (rotate x); rotate (rotate x) = x. Сканирование endorder, описанное Кнутом [3—11], является Последовательным (postorder) сканированием повернутого леса def endorder х »= postorder (rotate х) =* ä" forest 1 ( ) append postfix (rotate x) ■« =■= forests ( ) append postifix x . Если функция fQrest2 задается одним и тем же обращением, что и forestl, то получается функция forestA, обладающая свойствами forest2 а g f X == forest4 а g f (rotate x); forestl a g f X = forest4 a g f (rotate (reverse x)). Операция поворота осуществляет взаимный обмен двух лесов (рис. 3.7). Лес может быть преобразован к бинарному дереву с помощью функции forestl etree g pair, where g (x, y) z = cbtree x y, г and pair x у = x, у , 124
\ \ ß /f" л \/\/\ с F L i/\\4\ H /< L л 1 К Ч/У N. Бинарное дерево может А быть преобразовано к ле- / \ су с помощью функции вен btreel {) g I, / I I \ "^' . . . / 1 I \ g X у г — {ctree x у) : z. s p z j Генерирующая функ- / I / \ ЦИЯ для бинарных де- / I / \ ревьев находится из со- ^ отношения ^\ V\\ btree (х) = 1 + X btree« (х) \\ А J L и поэтому равняется forest (X). Рис. 3.7. Поворот леса 3.8. Комбинации Соответствие может быть также установлено между деревьями и структурой, которая называется комбинацией. Комбинациями являются деревья с компонентами только в их концевых точках, при этом каждое дерево может быть пустым либо иметь два поддерева ■ Л-комбинация (А-сотЫпаНоп) — либо является атомной {atomic) и есть А, — либо имеет левую (left) и правую (right) части в виде Л-комбинаций. def гее comb А = du (А, ср (comb А, comb А)); def atomic, nonatomic = predicates (comb A); def atomic, nonatomic = parts (comb A); def left, rigtit = selectors nonatomicc; def combine = construct nonatomicc; def rec combl g f x = If atomic then f X else g (combl g f (left x)) (combl g f (right x)), a comb2 определяется аналогично путем перестановки left и right. Внутренние узлы комбинации с п узлами образуют бинарное дерево с п — 1 узлами. Обе комбинации и бинарные деревья можно рассматривать как особые случаи структуры, в которой внутренние и концевые точки трактуются как два различных типа атомной компоненты. ■ Л-В-бииарное дерево (А-В-Ыпагу tree) — либо является атомным (atomic) и есть В, — либо имеет корень (root), которым является Л, и левую (leji) и правую (right) части, которые представляют собой Л-В-бинарные деревья. Бинарное дерево образуется путем приведения В к пустому дереву; комбинация образуется путем приведения А к пустому дереву. 125
2ж- ЗХ- 2х' 7+х+2х^+х'' x.-hlx^-t-Zx'' Рис. 3.8. деревьев Генерирующие функции для весов AM ß(x) Взаимосвязи между ' внутренпимн и внешними узлами можно установить путем использования ге- " ' нерирующих функций. Предположим, что А (х) = = 2] 07.x* и ß (.^)= YibkX^ являются генерирующими функциями для А-В-бя- нарного дерева, в котором ük — число внутренних узлов или корней на k-M уровне, а bk — число листьев или простых компонентов на k-u уровне. Генерирующие функции для дерева показаны на рис. 3.8. Функции Л (х) и В (х) взаимосвязаны следующим образом: Л (х) + В (х) = \ + 2хА (х). Число листьев есть ß A), а число внутренних узлов—Л A), тогда ß A) = 1 + Л A), В A/2) = 1. Если В (х) продифференцировать по х, что дает Вх (х), то ^д; A) будет представлять собой сумму длин траекторий к листьям. Аналогично Ах A) является суммой длин траекторий к внутренним узлам. Если отношение между Л (х) п В (х) продифференцировать, то получится соотношение А^ (х) + В^ (х) = 2Л (х) + 2хЛх (х), из которого следует, что В, A) = 2Л A) + Л,A). Это означает, что в Л-В-бинарном дереве сумма длин В1%! ранних траекторий Вх A) равна сумме длин внутренних rpatKiopHH Ах A) плюс удвоенное число д /1\ вел /\ внутренних узлов. Существует соответствие между деревьями и комбинациями. Можно записать функцию tree 5, которая сканирует дерево так, как если бы оно являлось соответствующей комбинацией. С учетом этого соответствия можно утверждать, что дерево с нулевым листингом соответствует атомной комбинации. В случае ненуле- 126 /\ Рис. 3.9. Соответствие комбинациями между деревьями
Boro листинга дерево в голове листинга соответствует левой части комбинации, а дерево, образованное из корня и хвоста листинга,— правой части комбинации: def treeS g f х ^ if null (listing x) then f (root x) else g (treeS g f (h (listing x)) treeS g f (ctree (root x) (t (listing x)))). Дерево может быть преобразовано в комбинацию путем использования (treeb combine I), а комбинация в дерево — за счет использования comb! g u, where g x у = ctree (root y) (x ; listing y), and u x = X : ( ). Пример этого соответствия дан на рис. 3.9, на котором оба дерева реверсированы. Соответствие может быть интерпретировано как перевод из функциональной системы обозначений, в которой аргументы записываются как А {В, С, D {Е, F (G, Я))), в систему, в которой функция применяется только к простому аргументу, т. е. ЦАВ) С) {(DE) {(FG) Н))). Генерирующей функцией для комбинаций является combination (х) = х + combination^ (х), поэтому combination (х) имеет то же рекуррентное соотношение, что и функция tree (х). 3.9. Веса деревьев Весом дерева по отношению к атомной компоненте некоторого типа является сумма длин путей из каждой атомной компоненты к корню. Генерирующие функции для весов деревьев могут быть также получены почти автоматически из определения структуры. Предположим, что Т (х, у) — генерирующая функция для набора деревьев, в которой коэффициент при х"у'^ представляет собой число деревьев с п узлами и весом w. Тогда, если этот набор деревьев передвинуть вниз на один уровень, то к весу дерева для каждой атомной компоненты в дереве или для каждого х в генерирующей функции должна быть добавлена единица. Поэтому, генерирующей функцией для набора деревьев, сдвинутого вниз на один уровень, является функция Т (ху, у), в которой X — переменная, соответствующая атомным компонентам, 1лу- 127
бины которых должны содержаться в весе. Следовательно, генерирующие функции для весов деревьев могут быть определены непосредственно из списания их структуры. Используя этот принцип, веса генерирующих функций для деревьев, бинарных деревьев и комбинаций можно представить в виде wtree (х, у) = х/1 — wtree (ху, у) wbtree (х, у) = 1 + х X wbtree^ (ху, у) wcombination (х, у) = х + wcombination^ (ху, у). Ожидаемые веса набора деревьев с п узлами могут быть получены однократным дифференцированием Т {х, у) по у и приравниванием у единице. Это дает эффект умножения числа деревьев с весом W, имеющим п узлов, на вес w. Если Т {х, £/)=!] tnwx"y'', то Т^ (х, у) = JjwtnwX'^y'^ и Ту{х, 1) = Hl СЕ win J X". Генерирующей функцией для числа деревьев с п атомными компонентами является Т (х, 1). Если Т (х, 1) = Ij^rtX", то предполагаемый вес деревьев с п узлами будет равен 1]йу^„и,/<„. Если вес комбинации обозначить через с, то рекуррентное соотношение для комбинаций можно продифференцировать по у, что дает 2с {ху, у) [Су {ху, у) + ХС^ {ху, У)\ = Су {Х, у), отсюда Су {х, 1) = 2хс {х, 1) Сх {х, 1)/A — 2с) {х, 1) == = X A — 1/1 —4х)/{1 — 4л;). Следовательно, сумма весов комбинаций с (п + 1) узлами равна 4« _ ( '*). Подобно этому сумма весов всех деревьев с п + 1 узлами или лесов с п узлами равна A/2) D"— ( „ ))' т- е. только половине суммы весов комбинаций. Сумма весов бинарных деревьев с п узлами равна 4" — (Зп + 1)/{п + I) (\ Два последних результата могут быть выведены один из другого при использовании только что описанных соответствий. 3.10. Последовательности, сопрограммы и потоки Если одна функция образует список в его естественном порядке, а другая — обрабатывает элементы списка в том же порядке, то в таких случаях часто нет необходимости получать весь список перед применением к нему второй функции. Две функции могут 128
быть скомбинированы таким образом, что на любой стадии вторая функция может выдавать требования для следующего элемента, который затем создается с помощью первой функции. Тем самым формирование следующего элемента списка откладывается до тех пор, пока он действительно будет необходим. Часто легче написать программы из двух частей, для которых список является промежуточным результатом вычислений. Однако более экономично по памяти использовать комбинацию двух функций, в которой только один элемент списка появляется как промежуточный результат. Этот параграф содержит проверку методов комбинации функций таким способом. Можно получить наилучший из обоих подходов за счет написания программы так, как если бы весь список появлялся в виде промежуточного результата, а непосредственное выполнение приводило бы к формированию списка по одному элементу. Функция, которая вызывается для получения следующего элемента списка, должна создавать его и вновь устанавливать себя таким образом, чтобы подготовиться к получению остатка или хвоста списка при очередном обращении. Соответствующей структурой данных является последовательность, определяемая следующим образом: ■ Л-последовательность (А-щиепсе) имеет — голову (hi), которая есть А, и — хвост (ii), который есть Л-последовательность. Потоки. Таким образом, последовательность представляет собой бесконечный список, и проблема выделения памяти для ее представления внутри машины становится весьма острой. Последовательность может быть представлена с помощью особого вида функции, называемой поточной функцией или потоком (stream). Поток применяется к пустому списку аргументов и образует пару, первый компонент которой является очередным элементом последовательности, а второй — потоком для оставшейся части последовательности. Таким образом, A-stream С (null list -> А X A-stream). Голова, хвост и префикс потока определяются следующим образом; def hs S = first (s ( )); def ts s = second (s ( )); def prefixs x s = Я ( ) . x, s. Голова (hs) потока — первый член пары, которая образуется в результате применения потока к нулевому списку, хвост (is) является ее вторым членом. Из этого следует, что s применяется каждый раз, когда применяется либо hs, либо ts. Часто более рационально учитывать, что поток можно применять только однажды, используя конструкции типа letx, y=s ();...х...у...х...х... . 5 Бердж в. 129
Поток может быть сконструирован из его головы х и хвоста s с помощью функции prefix х s <= X { ).х, s. В случае применения к нулевому списку эта функция образует пару (х, s). Аксиомами, которые связывают потоки и их компоненты, являются hs (Я ( ) . (х, у)) = х; ts (Я О . (X, у)) = у; prefixs (hs z)(ts z) = z. Функции обработки потоков. Рассмотрим ряд примеров пото- кообрабатывающих функций, являющихся аналогами спискооб- рабатывающих функций. Пример 1. Для преобразователя / и начального значения х поточная функция последовательности х, }х, }^х, fx может быть получена с помощью def гее generate fx ( ) = х, generate f (f х) . Первым членом последовательности потока (generate } х) является х, а ее оставшаяся часть представляется потоком generate I (f х). Если заданы ноль и функция iucceaor, последовательность неотрицательных целых чисел может быть представлена с помощью потока integer-(gener ate iucceaor 0) = = О, 1, 2, 3, ... Пример 2. Поточные представления последовательностей можно трактовать как списки. Потоки можно преобразовывать в другие потоки, например, путем использования функции maps, заданной как def гее maps f s ( ) = f х, maps f у where x, у = s ( ) . Функция maps преобразует последовательность xi, x^, x^, ... в последовательность fxi, fx2, fXi функция maps задерживает образование следующего члена а до тех пор, пока не потребуется следующий член (maps /s), затем она применяет функцию / к первому члену а для того, чтобы образовать первый член (mapi fs). Потоком для последовательности квадратов неотрицательных целых чисел, например, является (maps square integer) = О, 1, 4, 9, ... Пример 3. Функция iheflrst, которая находит первый член последовательности, обладающий свойством р, и получает этот член вместе с оставшимся потоком как результат, определяется ниже: def гее thefirst р s = let X, у = S () if р X then X, у else thefirst р у . Если предположить, что предикат потрасе проверяет, не является ли символ пробелом, то следующий отличный от пробела символ может быть получен из символьного потока применением к нему функции (thefirst nonspace) и выбором первого члена полученной пары. Например, первое целое, квадрат которого больше 1000, представляет собой первый член пары thefirst р integer where рх = х^ > 1000 . 130
пример 4. Функция titter воздействует на поток и предикат р и образует поток членов, обладающих свойством р. Эта функция определяется как def гее filter р s = let X, у = S ( ) if р X then X ( ). (х, filter р у) else filter р у. Поток отличных от пробела символов можно затем получить из символьного потока путем применения к нему выражения (Jttter nonspace). Другим примером является выражение (fitter prime integer), представляющее собой поток простых чисел. Пример 5. Два потока могут быть обработаны так, чтобы получить третий с помощью функции, которая аналогична функции zipl def rec zips f x у = X ( ). (f (hs x) (hs y)), (zips f (ts x) (ts y)). Поток пар образуется из двух потоков с помощью функции (zips pair). Пример 6. Потоки наиболее употребительны для функций, которые обрабатывают символьные потоки на входе. Функция white образует список из начального сегмента потока, поскольку все его члены обладают свойством р: def rec while ps = let X, у = s ( ) if p X then let u, V = while p у X : u, V else ( ), s. Родственной функцией является функция untit p^white (not-p). Если предикатом aame^/яе является not-(equat newtine), где newtine — символ, характеризующий строку в обратном направлении, то функция (wliite iametine) воздействует на символьный поток и образует пару, первый элемент которой представляет собой следующую строку входа, а второй — оставшуюся часть потока. Для того чтобы обеспечить возможность повторно применить ту же функцию, необходимо символ новой строки передвинуть путем использования функции remove (л, у) = х, ts у. Пример 7. Любая функция, образующая пару, второй член которой имеет тот же вид, что и аргумент, может быть трансформирована в потокопреобразу- ющую функцию применением к ней функции next, определяемой следующим образом: def rec next г s = let X, у = г s X ( ) . X, next г у. Функция next применяется к любой функции типа г ^А ->- В X А к образует функцию типа А -> B-stream. Из этого следует, что next g [(А -* В X А) -> (А -> B-stream). Функция fitter может быть переопределена в терминах функции next: def filter р s = next (thefirst p) s. Функция {next rernove-(while sameline)]} where rertiore (x, y) = x, ts, y, 5* 131
превращает символьный поток, содержащий символы новой строки, в поток строк, в котором строки представляют собой символьные списки между смежными символами новых строк. Пример 8. Инверсная операция преобразует поток списков в поток символов. Предположим, что concais является функцией для преобразования потока строк в поток символов, или более сбшее, для преобразования потока Л-списка в Л-поток, тогда def гее concats s = let X, у = s ( ) if null X then concats у else X ( ) . (h X, concats ?i ( ) . t x, y). Тогда операция, располагающая символы новой строки в обратном порядке и преобразующая поток строк в поток символов, определяется выражением {coneats-mapi {poüfix newline). Операция concais подобна процессу буффериза- ции по входу, в котором записи извне считываются поблочно, а программа использует каждый раз по одной записи. Поэтому функция concah преобразует подпрограмму блочного считывания в подпрограмму выборки следующей записи. Представление списков потоками. Для того чтобы представить список потоком, необходимо выбрать некоторый объект, не являющийся элементом списка, в качестве указателя конца списка в потоке. Назовем его end. Действительно, поскольку функцию потока можно применять всегда, то поток, соответствующий бесконечному списку идентификаторов end, может служить для указанной цели. Нулевой поток может быть определен с помощью функции nullists = (generate I end). Предикат для нулевого потока задается как nulls х = {{hs х) = end). С такими потоками можно обращаться так же, как и со списками. Соответствие между списковыми и поточными функциями имеет вид Списки Потоки null X null X = ((hs х) = end) h hs t ts ( ) nullists X : у ^ ( ) . X, у prefix prefixs u us X = prefixs X nullists Отсюда следует, что любая функция, оперирующая со списком или образующая их, может быть преобразована в функцию, которая оперирует с потоками или образует их. Поточные функции общего назначения могут быть определены по аналогии с функциями list 1 или list 2. def reo streaml a g f s = if nulls s then a else g (f (hs s)) (streaml a g f (ts s)) def rec stream2 a g f s = if nulls s then a else stream2 (g (f (hs s) a) g f (ts s)) 132
функция streaml образует весь список, прежде чем начинает оперировать с ним; функция stream 2 формирует список по одному элементу. Функции списков могут быть применены к потокам. Поточные версии функций тар, append и concat следуют из определений: def гее mapl f s = if nulls s then nuUist s else X ( ) . f (hs s), mapl f (ts s); def rec appendl x у = if nulls X then у else X ( ) . hs X, appendl (ts x) y; def rec concatl s = if nulls s then nullists else let (x, у = s() if null X then concatl у else X ( ) . h X, concatl (X. ( ) . t x, y). Заметим, что в этом представлении функция appendl более эффективна, чем функция append, которая должна сканировать первый аргумент для того, чтобы получить результат. Другим способом представления потока является представление его начального сегмента с помощью списка х и использование потока у для его остатка. Список подобен входному буферу. Поток также может быть сконструирован с помощью другой разновидности функции append: def rec appendls x у ( ) = if null X then у ( ) else h X, appendls (t x) y. Функция concatl оперирует с потоком списков и образует поток. Существуют восемь разновидностей функций конкатенации, полученных путем преобразования списка верхнего уровня, списка второго уровня или результирующего списка в потоки. Версия, в которой все три списка являются потоками, будет называться concatss и определяться как def rec concatss s = if nulls s then nullists else let x, у = s ( ) if nulls x then concatss у else X ( ) . hs X, concatss (X ( ) . ts x, y). Надо быть особенно осторожным, когда поток формируется, для того чтобы быть уверенным, что его элементы действительно не являются списками в открытой форме, другими словами, чтобы быть уверенным, что элементы потока не были сформированы слишком рано. Мы предполагаем, что в использованном методе вычисления значений функций сначала вычисляются операторная и опе- 133
рандовая части выражения, а затем значение оператора применяется к значению операнда. Мы также предполагаем, что тело лямбда-выражения, т. е. часть М выражения Кх.М, вычисляется только в момент применения функций. Если функцию appendl перевести в лямбда-приводимую форму append! х у = if null X then у else prefix (hs x) (appendl (ts x) y), TO внутреннее выражение appendl (ts x) у будет использоваться, если функция appendl применяется к х и у. Это приводит к созданию элементов потока х, которые с помощью функции prefix должны быть помещены в начало потока у. С другой стороны, в первой версии функции appendl выражение appendl {ts х) у будет вычислено только тогда, когда поток appendl ху применяется к нулевому списку. Два определения функции appendl являются лямбда-приводимыми и поэтому эквивалентны. Принятый метод вычисления, однако, приводит к тому, что эти функции могут проявлять себя по-разному. Для того чтобы образовать наиболее задержанную версию потока, следовало бы использовать конструкцию Я. ( ).х, у вместо prefixsxy, и выражения, содержащие функцию prefixs, были бы не нужны. Управление циклами. Потоки также пригодны для реализации последовательностей значений, которые принимает переменная в do, for или while цикле в языке программирования. Управление может быть выделено из цикла за счет использования потоков. Это означает, что одно и то же управление может быть использовано в двух различных циклах или что один и тот же цикл может быть использован с двумя различными управлениями. Список чисел от О до п, например, имеет поток whiles (less (п + 1)) integer, где def rec whiles p s = let X, у = s ( ) if p X then X ( ) . X whiles p у else nullists. Кроме того, существует сопутствующая функция untils р = = whiles (not-p). Потоком, соответствующим в АЛГОЛ-60 фразе а step Ь until с, является untils (Хх.х — с X sign (Ь) > 0) (generate (plus b) а). Потоком для 1 step 1 until n является def ti! n =5 whiles (less (n + 1)) (generate (plus 1) 1). 134
Выделение управления из цикла позволяет использовать нечисловые потоки для управления циклами. Двойной цикл, введенный повредством куска программы вида for i : = 1 step 1 until n do for i : = 1 step 1 until m do , МОЖНО рассматривать как цикл, управляемый с помощью потока пар. Функция тар, которая заменяет каждый элемент в списке его преобразованием, с помощью некоторой функции может быть расширена для того, чтобы ее можно было применять к двум спискам. Функция тар2, определяемая как def map2 f х у = let g г = map (f z) у concat (map g x) , образует список результатов применения / к каждой паре элементов, первый из которых берется из одного списка, а второй — из другого. Результатом применения {тар2 pair) к двум спискам A, 2, 3) и D, 5) является смешанное произведение A,4), A.5), B.4), B.5), C,4), C.5). Существует поточная версия этой функции, образованная заменой функции тар на mapl, а функции concat на соп- catss, т. е. def шар2 1 f х у = let g z = шар1 (f z) у concatss (mapl g x). Управление двойным циклом for, упомянутым выше, можно рассматривать как поток пар (гпар2 1 pair (til n) til m)). Прохождение деревьев. Часто структуру данных удобнее сканировать за счет использования потока, чем сначала получать листинг элементов структуры с последующим просмотром списка. Общий способ получения списка из структуры заключается прежде всего в замене всех атомных элементов с помощью 1-списков, затем в образовании списка списков из каждой неатомной компоненты и окончательном объединении этих списков. Функции для получения списков можно систематически изменять таким образом, чтобы каждый атомный элемент преобразовывался в 1-поток, а каждая неатомная компонента — в поток потоков, который объединяется с помощью функции concatss в один поток. Бинарное дерево может быть определено, как в п. 3.7. Функция для преобразования бинарного дерева в список имеет вид (btreel ( ) g и) where g х у z = concat (у, х, z). Это преобразование дает список узлов дерева. Для того чтобы обеспечить последовательное прохождение через узлы дерева, 135
1 поток для бинарного дерева не- у/^ ■ \,^^ обходимо создавать с помощью 2 1^ 1 функции (btreel ( ) g us) where g x у z = = concatss (y, X, z) J 1,Z 2,f 1,U Рис. 3.10. Верхние уроввв в*С10н*ч ного бинарного дерева К бинарному дереву. Другие сканирующие методы могут быть получены за счет перестановки д;, г/ и z в аргументе функции concatss. Сливающиеся потоки. Очевидно, любая древовидная структура данных может быть представлена с помощью функции, подобной потоку. Например, бесконечное бинарное дерево может быть представлено с помощью функции, которая в случае применения к нулевому списку образует корень и две функции, представляющие левое и правое поддеревья. Функция для генерации деревьев, в которых поддеревья зависят от корня, может быть определена как def гее genbtree f g х ( ) ■= х, genbtree f g (f x), genbtree f g (g x). Бинарное дерево, которое содержит композиции п (т. е. все списки положительных целых чисел, сумма которых равна п), на уровне п обозначается genbtree f g (и 1), где f X = {ih х) -{- \) : {t х), г. g X = \ : X. Эта функция генерирует бесконечное бинарное дерево, вершина которого показана на рис. 3.10. Пустое бинарное дерево определяется как emptys — (genbtree I I end), a предикат empty как empty f = {first (/ ( ))) = end. Функции roots, lefts и rights могут быть определены как first, second, third результата применения бинарного дерева к нулевому списку. Аналогичные этим функции на потоках могут быть сконструированы для деревьев. Например, функция prune def rec prune p s =— let X, y, J ■= s ( ) if p X then emptys else X ( ), X, prune p y, prune p z преобразует бесконечное дерево в конечное и аналогична функции untils для потоков. Бинарное дерево для композиций, расположенных ниже по отношению к уровню п, получается посредством функции prune {{greater n)-sum)). Эти методы обработки бесконечных деревьев могут быть использованы для решения следующей проблемы, упомянутой Дийкстра [3—5] и восходящей к Вайзенбауму. Дано целое число п, задача — написать программу для нахождения наименьшего числа, 136
которое может быть разложено в сумму двух чисел п-й степени, по крайней мере, двумя различными нетривиальными способами. Соответствующая структура данных представляет собой бесконечное дерево пар, которое начинается следующим образом: @,0)-@,1)-@,2)-@,3)-... A,1)-A,2)-A,3)-A,4)-... B,2)-B,3)-B,4)-B,5)-... Структура является деревом пар целых чисел (integer-pair)- tree, которое определяется в виде: ■ Л-дерево (A-tree) имеет — корень (root), который есть А, — левую часть (left), которая есть Л-дерево — и правую часть (right), которая есть Л-последовательность. Требуемое дерево имеет вид gen F G @,0) where F (х, у) = х -f- 1, у -f- 1 and G (х, у) = X, y-f- 1 and rec gen f g X ( ) = x, gen f g (f x), generate g (g x). Бесконечное дерево, сформированное за счет построения с помощью функции Н (х, у) = х" + у", обладает тем свойством, что его корень меньше, чем любой корень в его поддеревьях. Поэтому результирующее дерево может быть отсортировано выбором корня как первого элемента в рассматриваемой последовательности и затем объединением двух поддеревьев. Изображающей функцией является def гее mapt f х ( ) == let root, left, right = x ( ) f root, mapt f left, maps f right. Теперь дерево может быть выделено путем использования функции sort, которая образует поток сортированных чисел из дерева def rec sort х = let root, left, right = x ( ) X. ( ) . root, sort (merge left right) where rec merge x у =» let a, b, с = X ( ) let d, с = у ( ) if a < d then К ( ) . a, (merge b c), у else X, ( ) . d, X, e. 137
Далее мы находим первый повторно встречающийся член этого потока, используя def гее repeat s = let х, у = s ( ) let u, V = у ( ) if X = 11 then X else repeat у . В целом функция определяется следующим образом: def find n = let tree = gen FG @,0) where F (x, y) = x + 1, у + 1 and G (x, y) = X, у + 1 repeat (sort (mapt H tree))) where H (x, y) = х'Ч- у". Стирающие потоки. Поточные функции, рассмотренные выше, не содержат операторов присваивания. Это означает, что поток s еще существует и после того, как он был применен к нулевому списку. Часто бывает, что поток s после его применения уже больше не нужен. В таких случаях он может быть разрушен с помощью стирающей подпрограм,мы, которая содержит собственную память внутри себя, для того чтобы записывать хвост последовательности. Когда поток применяется к нулевому списку, собственная память восстанавливается с помощью оператора присваивания. Такая подпрограмма разрушает первоначальный поток. Возможность использования собственной переменной типа own предусмотрена в АЛГОЛ-60 для того, чтобы представить такую память, хотя возникают некоторые вопросы о том, каким образом она должна быть реализована. Целью было получить переменную, локальную в блоке, которая, в отличие от нормальной локальной переменной, имела значение, сохраняющее активацию блока. Версия собственной переменной, введенная здесь, придается процедуре, а не блоку, и, подобно потоку, зависит от специальной конструкции применяемого выражения. Эта конструкция может быть использована в любом контексте, что не связано с добавлением каких-либо специальных механических приборов для ее задания или реализации. Единственным обязательным правилом является то, что тело лямбда-выражения вычисляется только тогда, когда применяется функция. Выражение, которое вводит собственную переменную, имеет оператор, представляющий собой лямбда-выражение, которое, в свою очередь, имеет тело, являющееся лямбда-выражением. Другими словами, это выражение имеет вид (кх.Ку.М) N. Собственной переменной (own) в указанном выше выражении является х; когда вычисляется выражение, в качестве начального значения величины х берут Л'. Чтобы избежать присвоений этой переменной извне, начальное значение должно быть скопировано, поэтому выражение (Кх.Ку.М) (сору N) более предпочтительно, поскольку оно гарантирует, что един- 138
ственный способ присвоения — это использование присваивающего утверждения вида х := Е внутри М. Необходимо заметить, что указанное выше выражение отличается от конструкции ку.ЦКх.М) N), в которой определено тело лямбда-выражения, а не все выражение в целом. Эта конструкция соответствует процедуре, телом которой является блок, а х используется в общепринятом смысле как локальная [переменная. Несмотря на то, что конструкция Ху.Скх.М) N возможна в АЛГОЛ-60 или ПЛ/1, конструкция (Хх.Ху.М) N аналога не имеет. Группирующие потоки. Потоки, подобные сопрограммам, наиболее пригодны при задании группы процессов редактирования. Пример метода построения программ, основанных на использовании потоков, состоит в следующем. (Проблема взята у Дийкстра [3—6]). Вход состоит из последовательности слов, составленных из букв, разделенных любым числом промежутков, и законченных пробелами и точкой. Вход предполагается в виде символьного потока, называемого тс. Требуемая последовательность символов заменяет разделяющие промежутки одним пробелом, переставляет каждое альтернативное слово и оканчивается точкой. Функция (while letter) берет слово из головы входной последовательности, а функция (while space) берет пробелы до тех пор, пока не найдет символ. Функция absorbword определяется как def absorbword s = let w, si = (while letter s) let sps, s2 = (while space si) w, s2. Для того чтобы получить поток слов, эта функция должна быть использована повторно путем применения выражения (next absorbword) к потоку символов. Точка на конце (в действительности любой небуквенный символ) будет давать начало хвосту, составленному из последовательности пустых списков. Теперь каждое альтернативное слово должно быть переставлено. Последовательность (generate not false) может быть использована, для того чтобы записать или переставить слово. Два потока затем могут быть сведены к результирующему потоку, использующему функцию (zips g) where g х у = \l у then reverse x else x, которая в случае применения к потоку слов выражений (next absorbword тс) и (generate not false) образует поток слов с переставленными альтернативными словами. Затем должен быть образован символьный поток из потока слов. Должны быть также введены пробелы и точка. Пробелы могут быть введены с помощью постановки префиксного пробела в каждом слове, за исключением первого. Это осуществляется 139
йрймеНенйем функции (maps (prefix space)). Окончательно весь символьный поток получается за счет использования функции concatl и постфиксной точки. В целом программа теперь имеет вид let si = zips g (next absorbword rnc) (generate not false) where g x у = if у then reverse x else x let s2= concatl (untils (null-1) (maps (prefix space) si)) postfixs point (ts s2) where postfixs x у = append! у (us x) . Как BO всех системах программирования, здесь предусмотрен выбор стратегии, в которой одним из важнейших шагов, по- видимому, является шаг, связанный с переходом от бесконечных потоков к потокам, которые представляют списки. Если поток обозначается выражением и не имеет имени или имеет имя, но оно используется только один раз, то целесообразно применить понятие стирающего потока. Все потоки в указанном выше примере могут быть заменены стирающими потоками. Из этих примеров должно быть ясно, что любая функция, применимая к древовидным структурам данных, может быть приспособлена для того, чтобы воздействовать на поточные функции или образовывать их. 3.11. Комбинаторные конфигурации Набор может быть представлен списком не единственным способом, а список может быть представлен потоком. Часто удается написать программу, которая конструирует набор в более простой форме, и в этом случае систематически изменять конструирующие операции таким образом, что программа будет строить поток. Несколько примеров программ для генерации комбинаторных Конфигураций приведены в этом параграфе. Перестановки. Вводятся некоторые дифференциальные операторы, соответствующие программам, которые сводят конфигурацию к более простой форме. Эти операторы могут быть использованы для получения коэффициентов в случае, когда одна генерирующая функция расширяется на основе других функций. Дифференциальные операторы могут быть интерпретированы специфическими программами для шагов, выполняемых при дифференцировании. Путем введения такой интерпретации можно трактовать дифференциальный оператор как программу, которая образует набор конфигураций. Простейшим примером использования этого метода развития дифференциальных операторов является создание программы для генерации перестановок с помощью интерпретирующих операций, выполняемых в случае применения дифференциального оператора (d/dx) к д;^. 140
Рассмотрим результат применения didx к х', записанному как XXX. Дифференциальный оператор можно трактовать, как оператор замены х на единицу в трех различных случаях: {dIdx) х^ = Х.х.х + хЛ.х + х.хЛ = Зх^. Эти второстепенные операции приведения х к единицам могут быть записаны в трех диаграммах: 100 010 001 где 1 означает позицию х, которая уничтожается. Если теперь dIdx применяется к результату, то один х уничтожается во всех возможных случаях для всех трех элементов: Х.х.х + х.х.х + х.х.х 1.1.x + 1.x.1 + 1.1.x + X. 1.1 + 1.x.1 + х.1.1. Соответствующие диаграммы имеют вид 100 010 001 100 100 010 010 001 001 010 001 100 001 100 010 Если дифференциальный оператор применяется третий раз, то последний х стирается из каждого элемента. В результате получаются {dIdxY х^ = 6 диаграмм или перестановок, приведенных ниже: 100 100 010 001 001 010 010 010 001 100 001 100 001 100 010 001 010 100 Это простой пример метода, интерпретирующего математическое действие путем задания оператора или программы интерпретации. В рассматриваемом случае число 6, которое является значением {d/dxY х^, равно числу диаграмм, сконструированных данным способом. При интерпретации операторы создают набор конфигураций, а значение выражения дает число различных образованных конфигураций. Часто наиболее простой способ написания программ, который генерирует комбинаторные наборы, обеспечивается рассмотрением древовидной структуры. Метод, производящий перестановки, приведенные выше, является одним из примеров. Однако комбинаторные конфигурации, как правило, требуются всего лишь один раз, поэтому они могут быть выполнены другой программой. С помощью метода поточных функций, рассмотренного в предыдущем параграфе, программа для генерации за один раз одной конфигурации может быть получена очень просто. Комбинации. Простейший метод написания программы для образования всех комбинаций т элементов из п определяем гене- 141
рирующей функцией или рекуррентным соотношением. Таким образом, если A 4 х)" = A^+ X) A + х)«-' = (Р+ х)«-' + х{\ + Л')«-' п \ (п — 1\ (п — 1 т j \ т ) \т — 1 j' то соответствующая программа имеет вид def гее comb п m = if m < О V ^' > п then ( ) else append (О : comb m (m — 1)) A ; comb (m — 1) (n — 1)) . Эта программа может быть затем преобразована в программу, которая создает поток комбинаций следующим образом: def гее comb п m = if m < О V П1 > п then nul lists else append (prefixs 0 (comb m (n — 1)) (prefixs 1 (comb (m — 1) (n — 1)) . Программы генерации перестановок. Каждая программа, которая связана с выполнением дифференциального оператора, указанного выше, прежде всего выбирает первый, второй или третий элемент списка A, 2, 3) и перемещает его для того, чтобы сформировать последний элемент в перестановке. Следующий раз она выбирает первый или второй член из оставшегося списка, чтобы сформировать предпоследний элемент. Оставшийся элемент берется в качестве первого элемента. Дерево, приведенное ниже, отслеживает действия дифференциального оператора: ..1 ..2 ..3 •21 -31 -12 -32 -13 -23 321 231 312 132 213 123 Перестановка может быть задана единственным образом с помощью списка позиций, используе2\1ых для ее создания из списка A, 2, 3). Этот список позиций называется сигнатурой перестановки. Та же сигнатура ^ioжeт быть использована для построения инверсной перестановки. При этом сигнатура читается в обратном порядке и перестановки из п элеме;чтов получаются из перестановок п — 1 за счет введения в позицию, определяемую следующим сигнатурным элементом. Существуют п возможных позиций в выражении хххх ... X, которым придается значение единицы. Предположим, что число п располагается во всех позициях каждой перестановки из {1, 2, 3, 4, ..., /г — 1}. Позиции, в которые может быть введено число п, номеруются О, 1, 2, 3, 4, ..., п от конца 142
перестановки. Список позиций, в которых числа 1, 2, 3, 4... вводятся в указанном порядке, является сигнатурой и представляет собой метод задания перестановки. Рис. 3.11, на котором изображено дерево, демонстрирует, как перестановки из |1, 2, 3, 4} генерируются этим методом. Программа для создания перестановок от 1 до п с помощью рассматриваемого метода имеет вид def гее perms п — if п = О then ( ) else insert n (perms (n — 1)) where insert n x = concat (map (put n) x) where rec put n x = if null X then (n : ( )) : ( ) else (n : x) : map (prefix (h x)) (put n (t x)). Эта функция может быть преобразована с помощью метода, описанного в п. 3.10, в функцию, которая образует поток перестановок, а не список. Внутри каждой группы из четырех перестановок, полученных из одной перестановки последовательности {1,2, 3|, показанной на рис. 3.11, следующая может быть получена путем изменения соседних элементов. Если непосредственные поддеревья каждого корня, который содержит случайную перестановку, реверсировать, то можно перейти от одной группы к следующей за счет внутренней перестановки соседних элементов. Последовательно весь набор перестановок может быть сгенерирован из чисел 1, 2, 3 я путем чередования соседних элементов. Дерево для этого метода генерации показано на рис. 3.12. Данный метод генерации перестановок известен как «Алгоритм Джонсона—Троттера» [3—12, 3—15]. Одним из способов написания программы является, прежде всего, генерация сигнатурного дерева, первые четыре уровня которого показаны на рис. 3.13. Это дерево сканируют, используя префиксный способ, для получения последовательности 000012313210201231232101012300321. Затем нули удаляют, так как каждое число введено для того, чтобы представить чередование или переход от узла к его правому соседу. Результирующим рядом чисел будет 12313212123123211123321. Числа в этой последовательности определяют порядок чередования, которое должно быть осуществлено в списке 1234 для того, чтобы сгенерировать всю последовательность перестановок. Число / должно быть интерпретировано как инструкция для чередования элементов в позициях 4 — i и 5 — i. Результирующие перестановки даны на рис. 3.12. Они считываются от вершины к^низу, 143
ZI. ^ 13ZI* \^ /^3Z ^гзг ° 31Z'i^ "^3/2^—^ 374'Z \> 3WZ ^ ^/z ^ ггзи ,zf3~2 гг^з ^ Z3JU ' Z31~j Z3^1 "^ 3^27 MZ7 4^*^ Ч Рис. 3.11. Генерация перестановок с помощью вставок Рнс. 3.12. Генерация перестановок путем смежных перемещений Программа может быть записана как функция, которая формирует последовательность как поток. Сначала даются потоки А А А А А А 01Z3 3210 01Z3 3210 0123 32/ff Рис. 3.13. Размеченное дерево 144
для каждого уровня дерева, а именно: def up п = whiles (^ п) (generate (+1) 0) ; def down n = whiles (>.0) (generate (—1) n) ; def rec genalt n ( ) = up n, (Я ( ) . down n genalt n). Результатом применения функции genalt к n является поток потоков. Приведем несколько примеров: genalt 0= U О, U О, U О, ... genalt 1 = (О, 1), A,0), (О, 1), A,0),... genalt 2 = (О, 1, 2), B, 1, 0), (О, 1, 2), ... Эти уровни дерева затем располагают вместе за счет использования функции trees, определяемой ниже: def trees n = trees 1 n 0 where rec trees n x = if X = n then generate I nulHsts else next (first (x + 1)) (zips ctree (concats (genalt x)) (trees 1 n(x+ 1))) where rec first n s = if n= 0 then ( ), s else let y, z = first (n —1) (ts s) hs s : y, z. Функция trees n образует бесконечный поток деревьев. Первое из них может быть выбрано, тогда префиксной сканирующей функцией будет def rec prescan х = let root, listing = x ( ) Я ( ) . root, concatl (mapl precan listing). Нули могут быть удалены путем использования функции (filtert nonzero). Разбиения. Разбиение целого числа п представляет собой группу положительных целых чисел, сумма которых равна п. Целые числа в группе называются частями разбиения. Два различных разбиения одних и тех же частей представляют собой одно и то же разбиение, а части описываются в порядке уменьшения их значения. Разбиение числа 10 на части 5, 4 и 1 будет записываться как E 4 1). Массив узлов, называемый графом Феррера, часто используют для обозначения разбиения. Номера узлов в строках являются частями разбиения, и наибольшая часть располагается в наивысшей строке. Граф Феррера для разбиения E 4 1) приведен ниже 145
Разбиениями первых пяти чисел являются 1. A) 2. B), A1) 3. C), B1), A11) 4. D), C1), B2), B11), A11) 5. E), D1), C2), C11), B21), B111), A1111) Для повторяющихся частей будем использовать показатели степени, тогда последняя строка запишется как E), D 1), C 2), C Р), B^ 1), B 1«), (F). Разбиения могут быть перечислены путем выбора сначала числа единиц из выражения (A/1 — х)), затем числа двоек из (A/1 — х^)), затем числа троек и т. д. partitions (х) = A/A — х)) A/A — х^)) A/A — х^))... = 1 + X + 2х^ + Зх» + 5х* + Тх"^ +... . Ограничения, которые накладывают на число и размер частей, могут быть отражены в ограничениях на размер этого результата и на природу его элементов. Например, генерирующий функцией для разбиений, не содержащих частей больших, чем k, является A/A — X)) A/A — х^)) A/A — х^)) ... A/1 — X*)) . Генерирующей функцией для разбиений точно на i частей является коэффициент при z^ в выражении 1/A —ZX) A — гх») A — гх^)..., которое имеет расширение 1 + гх/A — х) + 2^x^/A — X) A — х^) + • • • ...+ г'х'7A — х) A — х^) ... A — x^), показывающее, что генерирующей функцией для разбиений точно на i частей служит X^ A/A — Х)) A/A — Х^)) A/A — X«)) ... A/A — Х')). Это будем записывать сокращенно в виде x4[i]\. Программа генерации разбиения числа п точно на i частей может быть получена следующим образом: поскольку хЧЦ]\ == A —X' + хО X' X [iV. = x.x'-V[t — 1]! + x'.x4[i]\, 146
то, если Р (п, i) является числом разбиений числа п на I частей, Р (п, i) = Я (п — 1, i—\)-\-P(n — i, i) Р (п, 0) =» 1, if n "= 0; ^ О, if n > 0; Р (п, i) = О, if п < i. Если разбиения представлены списками целых чисел в возрастающем порядке, то последующая функция формирует разбиения из разбиений таким образом, что они могут как иметь наименьшую часть, так и не иметь ее. Приведенная ниже программа конструирует список разбиений, однако она может быть использована также для получения потока. def гее partitions п i = if п< i then ( ) else if / = О then if n > 0 then ( ) else ():() else append A : (partitions (n — 1, i — 1)) (map (+1) (partitions (n — i, i))). ССЫЛКИ и БИБЛИОГРАФИЯ Хорошие обзоры по структурам данных и методам представления их можно найти в работах Berztiss [3—1], Ноаге [3—8], D'Imperio [3—10] и Knuth [3— 11 ]. Методы задания новых структур данных введены в программирование McCarthy. Некоторыми языками программирования, наиболее приспособленными для указанной цели, являются ALGOL-68 [3—16], PASCAL [3—19] и POP [3—2]. Потоки, подобные сопрограммам (Conway, 1969), и аналогичные поточные методы обсуждаются в работах [3—2, 3—5, 3—7 и 3—14]. 3-1. Berztiss А. Т. Data Structures, Theory and Practice, London and New York, Academic Press, 1971. 3-2. Burstall R. M., Collins J. S. and Popplestone R. J. Programming in POP-2, Edinburgh: Edinburgh University Press, 1971. 3-3. Bürge W. H. «Combinatory programming and combinatorial analysis», IBM J. Res. and Dev., Vol. 16, N. 5, 1972, pp. 450—461. 3-4. Conway M. E. «Desigh of a separable transition-diagram compiler», CACM, Vol. 6, 1963, pp. 396—408. 3-5. Dahl 0. J. and Nygaars K. «Simula-an ALGOL-based simulation language», CACM, Vol. 9, 1966, pp. 671—678. 3-6. Dijkstra E. W., Dahl 0. J. and Hoare С A. R. Structured Programming, London and New York; Academic Press, 1972. 3-7. Golomb S. W. and Baumert L. D. «Backtrack programming», JACM, Vol. 12, 1965, pp. 516-524. 3-8. Hoare С A. R. Record Handling in Programming Languages, (ed.) F. Genuys, London: Academic Press, 1968, pp. 291—347. 3-9. Holt A. W. «A mathematical and applied investigation of tree structures». Ph. D. Thesis. University of Pennsylvania, 1963. 3-10. D'Imperio M. E. «Data structures and their representation in storage», Ann. Rev. Automatic Programming, Vol. 5, 1969, pp. 1—75. 147
3-11. Knuth D. E. The Art of Computer Programming: Vol. 1: Fundamental Algorithms, Reading, Mass.: Addison — Wesley, 1968. 3-12. Johnson S. M. «An algorithm for generating permutations», Math. Сотр., Vol. 17, 1963, p. 28. 3-13. Naur P. «Programming by action clusters», BIT, Vol. 9, N. 3, 1969, pp. 250—258. 3-14. Stoy J. E. and Strachey С «0S6-An experimental operating system for a small computer», Computer J., Vol. 15, 1972, N. 2, pp. 117—124; N. 3, pp. 195—203. 3-15. Trotter H. F. Algorithm 115: Peim, CACM, Vol. 5, 1962, pp. 434—435. 3-16. Van Wijngaarden A. «Report on the algorithmic language, ALGOL 68», Num. Math., Vol. 14, 1969, pp. 79-218. 3-17. Wells M. B. Elements of Combinatorial Computing, Oxford; Pergam- mon Press, 1971. 3-18. Wirth N. Systematic Programming, Englewood Cliffs, N. J.: Prentice — Hall, 1973. 3-19. Wirth N. «The programming language PASCAL», Acta Informatica, Vol. I, N. 1, 1971, pp. 35—63. Глава 4 ГРАММАТИЧЕСКИЙ РАЗБОР 4.1. Введение] В этой главе рассмотрено несколько подходов к проблеме распознавания и анализа структуры списков символов. Символы трактуются как простые опознанные элементы, а символьный список, как обычно принято на практике, называют символьной цепочкой. Множество символьных цепочек составляет язык. Распознающая функция языка определяет, относится ли данная символьная цепочка к этому множеству. Функция грамматического разбора языка не только определяет, относится ли цепочка к языку, но также осуществляет преобразование этой цепочки в некоторый объект. Существуют два важных класса языков, называемых регулярными и контекстно-свободными языками. Множества символьных цепочек, составляющих эти языки, могут быть формально определены, а распознаватели и грамматические анализаторы получены из их описания. Если грамматический анализатор, который соответствует регулярному языку, может быть сделан детерминированным и соответствующая программа получена относительно простым способом, то грамматический анализатор, который соответствует грамматике контекстно-свободного языка, является недетерминированным, и возникает проблема конструирования эффективной программы грамматического разбора для этого языка. Материалы, представленные в этой главе, больше связаны с конструированием программ распознавания на основе известных методов, чем с самими методами. Аналогичные методы могут быть 148
применены к другим задачам, возникающим при решении проблемы распознавания, в которой недетерминированная программа должна быть преобразована в детерминированную. Наиболее общей программой грамматического анализа является программа, генерирующая структурное описание распознаваемой цепочки. Это структурное описание представляет собой^дерево, которое отражает фразовую структуру цепочки. Три основные стратегии грамматического разбора базируются на нисходящем и восходящем левоугольном и правоугольном способах конструирования этого дерева. 4.2. Конечные автоматы и регулярные выражения Простейшим видом языка_, является^ регулярный язык. Регулярный язык может распознаваться с помощью^ программы, основанной на просмотре направленного графа, называемого_^диа- граммой состояния. Вершины этого графа представляют собой состояния некоторого механизма, а каждое ребро помечено символом и обозначает преобразование состояния, которое должно быть выполнено в случае, когда символ на этом ребре находится в голове распознаваемой цепочки. Одно из состояний выбирается в качестве начального, а одно или более состояний — в качестве конечных состояний. Цепочка распознается, если она преобразует начальное состояние в одно из конечных состояний, и не распознается, если она преобразует начальное состояние в состояние, которое не является конечным. Иначе говоря, распознаваемый язык состоит из всех возможных путей перехода из начального состояния в конечное. Пример диаграммы состояний, с помощью которой распознаются числа в АЛГОЛ-60, приведен на рис. 4.1. На диаграмме L^ — начальная вершина, а конечные вершины обозначены двумя концентрическими окружностями. Заметим, что на рис. 4.1 сделано сокращение. Буква d обозначает множество цифр от О до 9. Вообще, если все символы множества преобразуют некоторое состояние в одно и то же состояние, то такое сокращение допустимо. С помощью рассматриваемой диаграммы могут быть определены все числа в АЛГОЛ-60. Ответ на вопрос, является ли 537 числом в АЛГОЛ-60?, например, мог бы быть найден путем перехода из вершины Li в L^. Поскольку L^ — не конечное состояние, то ответ отрицательный. I Граф может быть задан как множество правил. Каждая часть графа вида а приводит К правилу, которое описывается следующим образом: А ->аВ. Аналогично, если А — конечное состояние, то добавляется правило Л -> е, где е — нулевая цепочка. Каждая вершина Должна быть рассмотрена, если требуется описать язык, состоя- 149
Рис. 4.1. Диаграмма состояний для чисел АЛГОЛ-60 щии ИЗ всех путей от данной вершины до конечного состояния. Оба правила и диаграмма характеризуют эти пути. Правило А -^ аВ означает, что любое множество путей из Л в конечное состояние формируется прежде всего ^я счет выбора пути а, а затем — выбора множества путей из ß в конечное состояние. Продукция А -^ е означает, что А — уже конечное состояние, и, таким образом, пустая цепочка будет переводить его к самому себе, т. е. конечному состоянию. Правила, соответствующие Lj -xiLg; L-^ —> -Ti^ai •bi —> L^', ^1 —*■ loi's'i Lj —V L4; L2 —> L^', 1^2 —*■ loi'.e; L2 —> uLs', Lg —> dLg; ^3 -^ loi'si Lg —V L4; диаграмме состояний, имеют вид L^ —> dL^; Lf, -^ dLf,; ^& —*■ lo-'^ei ^6 -^ +^7; Lg —> —L^; Lg ->dLs; L^ —^ ^Lq\ Lg —> uLg; Lg ->e; L5 ->e; Lg ->e. 150
Однако здесь отсутствует информация о том, что начальным состоянием является L^. Программа для распознавания цепочек, заданных этими правилами, может быть легко написана. Например, распознающая функция, соответствующая правилам L, -^ +W, Lg -^ —Lj] Lg -^dLg может иметь следующий вид: Lß (s) =-= if null s then false else if h s = '+' then L, (t s) else if h s = '—' then L, (t s) else if digit (h s) then Li (t s) else false . Правила, имеющие общие левые части, группируются для того, чтобы образовать функцию. Пример, приведенный выше, не соответствует конечному состоянию. Фрагмент программы, которая соответствует конечному состоянию, должен начинаться так: if null s then true . Функции, образованные этим способом, являются взаимно рекурсивными. Однако, поскольку они находят применение только в операторной форме, их целесообразно переписать в виде программы, используя утверждения назначения go to в следующей программе. Предполагается, что nnpi^OÄMMa продолжается с оператора, помеченного меткой fail, если цепочка не распознается, и с метки, называемой succeed, в противном случае. Li : if null s then go to fail else X : = h s s : = t X go to if X = '+' then L, else if X = '—' then L, else if digit x then Lg else fail . Фрагмент программы, который имеет метку, соответствующую конечному состоянию, должен начинаться так: if null s then go to succeed . 151
Диаграмму состояния можно интерпретировать двумя путями: 1) как метод распознавания цепочки символов и 2) как метод для генерации всех цепочек языка. Во втором случае язык, соответствующий определенной вершине, состоит из всех путей от этой вершины до конечного состояния. Продукции могут быть также рассмотрены для того, чтобы описать язык или, что то же самое, множество цепочек символов посредством системы уравнений. Это приводит к другому способу описания языков и построения связанных с ними грамматических анализаторов. Правила можно рассматривать как множество уравнений, осуществляющих две операции над языками, называемые объединением и декартовой конкатенацией соответственно. Результатом объединения является множество, полученное объединением двух множеств А и В. Эта операция записывается как А\В и определяется следующим образом: А\В = {х\х ^ А или X ^ В\. Декартова конкатенация двух множеств цепочек А и В представляет собой множество, элементы которого получаются сцеплением элемента из Л с элементом из В: А'В = [append ху\х ^ А, у ^ В\. Иногда знак + используется вместо вертикальной черты | , а знак • часто пропускается. Третья операция, называемая итерацией или замыканием Клини, определяется следующим образом: Л* = е| Л l^l^^l/l^l..., где Л" = А'^-^-А. Регулярные языки описываются регулярными выражениями. Простыми регулярными выражениями являются: 1) символ для пустого множества 0, 2) символ е для множества, содержащего один элемент, который является нулевым списком, и 3) символы алфавита (каждое а в алфавите обозначает множество \а\). Сложными регулярными выражениями являются А\В, А-В и Л*, где Л и ß — регулярные выражения. Следующие тождества вытекают из определений. Оператор • в этом параграфе будет опускаться: Л|Л = Л; Л|5 = В\А; А\{В\С) = {А\В)\С] А {ВС) = {AB) С; АВ\\АС = А {В\С); АС\ВС = {А\В) С; 152
{A\B)* = {A*\B-*)* = (Л*В*)*; Ae = eA = A; A0 = 0A = 0; 0* =e; Л|0 = Л; Л* = е/ЛЛ*. Можно также показать, что, если Л = В А \ С, то Л = ß*C. Последний факт может быть использован для решения урав- нений, которые соответствуют правилам. Уравнениями для чисел в АЛГОЛ-60 являются Li = аЬз I + L2 I — Ьг 110-^61 --^41 L2 = .L^\ loLgiciLg', Lg = dLalwLel.Lile; L4 = dLg', L5 = ciL^ I loLg I e; Lg = +L71 — L71 dLg", L7 = uLg', Lg = dLgle. Из этого следует, что, используя приведенные выше тождества, получим Ls = d*; Le = (+ I —) tW* 1 dd* = (+ I — I e) dd*; Lb = d* iioLe\e); L4 = dd* (ioLe|e); L3 = a* (lo^e I .L4 I e); L2 = d.d'^ (lo-^e I --^4) I --^4 I loi'e; Li = (+|-|еIз; La = dd* (.dd* (loLe I e) I (loLe I e) | .dd* dole) | e)ioLe = = {dd*\dd*.dd*\.dd*) UU)\ioU, ^1 = (+ I -1 e) {{dd* I e).dd* | dd*) U+ \-\e) X X dd*|e)|(+| —|e) dd*. 153
Ls=dLMdd.'\-äd Рис. 4.2. Преобразование диаграммы состояний Последнее выражение можно переписать, выделяя общие выражения и используя вспомогательные определения: signed (number) where integer = dd* and signed x = (+ | — | e) x and fraction = . integer and exponent = i„signed (integer) and decimal = integer | fraction] integer fraction and number = decimal | exponent | decimal exponent. Может быть построен другой вид диаграммы, в которой ребра помечены регулярными выражениями, а не символами. Шаги решения можно интерпретировать как преобразования диаграммы состояния, в которой несколько путей между вершинами заменяются одним путем, помеченным регулярным выражением, представляющим собой исходное множество. Регулярное выражение для Lg может быть получено так, как это показано на рис. 4.2. Вводится новое конечное состояние, а диаграмма перестраивается таким образом, что все ребра будут направлены к нему. Обратный процесс, т. е. построение множества продукций по регулярному выражению, также возможен. Правила для регулярных языков могут быть представлены в одной из следующих форм: fcr« А -^ е; А -^ а; А -^ аВ. В последнем правиле первый символ правой части является буквой и правая часть имеет не более двух символов. Такая грамматика называется левосторонней. Правила, свободные от этих ограничений, будут рассмотрены в следующем параграфе. 154
4.3. Контекстно-свободные языки Регулярный язык можно рассматривать как совокупность всех путей в программном графе, контекстно-свободный язык также можно представить как совокупность всех путей в программном графе, который имеет процедуры. Как и в случае регулярных языков, множество цепочек контекстно-свободного языка может быть определено на основе имеющихся множеств путем использования операций объединения и декартовой конкатенации. Правила или уравнения больше не подчиняются левосторонней форме, которая характерна для регулярных языков, и правая часть уравнения может быть конечным объединением конечных декартовых конкатенации. Из этого следует, что определения могут быть самовнедряющимися. Это означает, что они допускают появление заданного символа, который не находится в конце конкатенации. Контекстно-свободные языки включают как регулярные языки, так и некоторые языки, которые имеют вложенные фразы и являются контекстно-свободными, но не регулярными. Символы подразделяют на терминальные и на нетерминальные. Нетерминальные символы — это заданные символы, они встречаются в левой части правил; элементы множества терминальных символов не задаются. Терминальные символы обозначим маленькими буквами, а нетерминальные — заглавными. Правила принимаются во внимание при генерации множества цепочек с помощью отношения =^, которое вводится между двумя цепочками символов. Так, х =^ у означает, что у может быть получен из X путем подстановки правой части правила вместо его левого символа, появляющегося в х. Язык, обозначаемый символом S, является множеством цепочек терминальных символов, связанных с символом 5 отношением, получаемым рефлексивным транзитивным замыканием отношения =^. Это новое отношение обозначается =>*. Транзитивное замыкание отношения может быть определено следующим образом. Результатом двух отношений г и s является отношение, которое существует между х и у, если и только если существует и такое, что х связано отношением г с и и и связано отношением s с у. Другими словами, если г и s рассматриваются как функции вида (А -> Л-список) или (А -> Л-поток), то их результат может быть определен следующим образом: def prodf г s X = sumset (map s (r x)) where sumset = listl ( ) union I. Отношение может быть представлено направленным графом, в котором вершины представляют собой объекты, а ребра — отношения между ними. Направленное ребро от х к у существует 155
S^AbC S^Cb C^abS C^C A^a A^aC Рис. 4.3. и right S—*^ С—*-a. \ с b Left J, A uyi ^A a \ \ с b Right графы отношений left тогда и только тогда, когда у и х связаны отношением. Отношение степени п может быть выражено как def гее power п г = if п= О then I else prodf г (power (n — 1) г) . Отношение (power п г) связывает вершину X в графе со всеми теми вершинами, которые могут быть достигнуты с помощью направленного пути из п ребер. Выражение {power п г) может быть также записано в виде г". Транзитивное замыкание отношения связывает вершину графа со всеми вершинами, к которым в графе существует направленный путь. Другими словами, 00 г* = \] г'^. п=1 Рефлексивное транзитивное замыкание включает идентичное отношение и определяется в виде оо г* = и г'^. Несколько удобных отношений могут быть получены из правил контекстно-свободного языка. Каждый нетерминальный символ X можно связать посредством отношения, называемого left, с крайними слева символами правых частей правил для X. Правила и граф отношения left для них приведены на рис. 4.3. Транзитивное замыкание отношения left, т. е. left*, представляет собой отношение между символом X и всеми символами, которые могли бы встретиться в голове цепочек, полученных из X. Для правил, представленных на рис. 4.3, отношение left* имеет вид left+ 'S' = {А, С, а, с}; left+ 'А' = {а}; ]eft+ 'С = {с}. Если выражение left* X включает X, то говорят, что X рекур" сивно слева. Отношение first ограничивает область действия от' ношения left*, допуская лишь терминальные символы, т. е. first X = {у е left+ X I у е terminal }. Аналогичным образом, отношение right представляет собой отношение между символом X и символами, встречающимися в крайних правых частях цепочек, полученных из X. Если выражение right*X включает X, то говорят, что X рекурсивно справа. Из рис. 4.3 следует, что S рекурсивно справа. Отношение last ограничивает область действия отношения right*, допуская лишь терминальные символы, т. е. last X = {у € right+ X I у € terminal }. 156
Существуют два хорошо известных способа генерации цепочки языка: один из них основан на замене крайнего слева нетерминального символа в цепочке, а другой — на замене крайнего справа символа. Соответствующие отношения будут обозначаться =>i и =^д. Рассмотрим, например, язык, заданный начальным символом S и правилами, представленными на рис. 4.4. Дерево на рис. 4.4 показывает, что цепочка aabcbbc относится к языку. Это дерево может быть получено: а) путем последовательного раскрытия стоящих слева в цепочке нетерминалов, б) путем последовательного раскрытия стоящих справа в цепочке нетерминалов или в) с помощью какого-либо другого метода: а) S =>LAbC =^i. aCbC =>l aabSbC =^ =^L aabCbbC =^l aabcbbC =^l aabcbbc; б) S =^д Abc =^д Abc =^j^ aCbc =^j^ aabSbc =^ =^j^ aabCbbc =^j^ aabcbbc; в) S^i^AbC =>L aCbC =^1^ aabSbC => =>j^ aabSbc =^j^ aabCbbc =^j^ aabcbbc. Дерево называется структурным описанием цепочки. Его концевые точки соответствуют терминальным символам, а промежуточные вершины — нетерминальным символам. Фрагмент дерева, соответствующий использованию правила X -> ABCD, изображается в виде X А в Ci D Фигура представляет собой дерево, корнем которого является X, а листьями А, В, С, D. Создание программ грамматического разбора для контекстно- свободных языков основано на выполнении операции инверсии; т. е., имея на выходе цепочку и множество правил, получить струк- / j ч^ турное описание цепочки. Кроме того, программы грамматического разбора имеют дополнительные возможности, которые обычно используются после того, как распознается правая часть правила. Левая часть заменяет правую, и эта замена называется редукцией. Одни и те же множества цепочек могут быть описаны различными правилами. Часто целесообразно йзме- 157 5 ^ Со C^abS С ^ с А^а d^aC ' Рис. 4.4. цепонки А Ö С / \ 1 а ОС /\\ а b S 1 \ С Ö / С Структурное описание
нить правила, для того чтобы получить множество правил меньшей мощности для описания того же множества цепочек. Например, часто бывает удобно удалить символы, которые могут образовать пустую цепочку. Вопрос о том, может ли нетерминальный символ образовать пустую цепочку, решается с помощью процесса разметки. Прежде всего пометим символы пустой цепочки. Затем найдем непомеченную левую часть правила, правая часть которого содержит только помеченные символы. Пометим этот символ левой части правила и все символы в правых частях остальных правил. Процесс будем повторять до тех пор, пока дальнейшая разметка будет невозможна. Нетерминальный символ может образовать пустую цепочку, если только он помечен. Такая же процедура может быть использована для того, чтобы определить, может ли нетерминальный символ образовать какие- нибудь терминальные цепочки. Для этого прежде всего пометим все терминальные символы, а затем проведем процесс разметки, как прежде. Неотмеченными символами являются те, которые не образуют терминальных цепочек. Такие нетерминальные символы бесполезны, и их можно удалить вместе с правилами, правыми частями которых они являются, что не повлияет на язык. 4.4. Выражения, описывающие языки Правила, которые описывают язык, можно трактовать как метод определения новых множеств на основе старых при использовании формальной записи, введенной в гл. 1. Будет показано, что описание множества цепочек может быть рассмотрено как описание распознавателя для элементов множества, которое затем после переработки может быть использовано для грамматических анализаторов языка. В правиле контекстно-свободного языка терминальный символ используется для описания множества с одним членом, который является единичным списком, имеющим этот символ в качестве единственного элемента. Функция, называемая Q-функцией, вводится для того, чтобы иметь возможность проводить различие между именами множеств и именами цепочек. Функция Q воздействует на отдельный элемент и образует множество элементов, каждый из которых равен первоначальному. Поэтому выражение Q'a' обозначает множество, члены которого равны единичному списку, единственным элементом которого является символ а. Выражения, описывающие множества цепочек символов, формируются с использованием операторов объединения (|) и конкатенации (•) множеств. Они оба воздействуют на два множества символьных цепочек и образуют другое их множество. ,, •- Пустое множество называется empty @). Множество, единственным элементом которого является нулевой список, называют nultistset (е), а множество единичных списков, элементами которых являются буквы используемого алфавита, называется 15а
any character. Оператор декартовой конкатенации следует теперь делать явным для того, чтобы сохранить возможность сопоставления имен при использовании префиксного оператора, например такого, как Q. Одним из следствий этого изменения записи является то, что при использовании круглых скобок исключается путаница, обусловленная тем, что их иногда применяют в качестве кавычек для группировки выражений, которые составляют множество. Другим следствием такой интерпретации является возможность определения новых функций от множеств, например, def гее list а b = b | b-а-list а Ь . Множество (list ab) — это множество ненулевых списков, состоящих из элементов Ь, разделенных элементами а. Звездная функция, которая встречается в регулярных выражениях, может быть определена следующим образом: def гее star х = х-star х | nullistset . Множество (star х) состоит из строк, образованных путем любого числа повторений фразы х. Следующие функции пригодны для задания повторяемых фраз: def perhaps х = х ] nullistset; def гее upto n х = if n= О then nullistset else x-upto (n — 1) x | nullistset ; def ree exactly n x = if n = 0 then nullistset else X-exactly (n — 1) x ; def atleast n x = exactly n x-star x ; def rec qualify x у = x | qualify x y-y . Функции perhaps, upto, exactly и atleast воздействуют на.целое число п и фразу х для того, чтобы получить множество фраз, содержащих т раз X, где т = О или 1;0<;т<л;т = лит^ ^ п соответственно. Операции конкатенации и объединения могут быть расширены и использованы для воздействия на список множеств следующим" образом: def гее cclist х = if null X then nullistset else h X-cclist (t x) или def cclist = list 1 nuUistset cc I where cc x у = x ■ y; def ree unionlist x = if null X then empty else h X unionlist (t x) или def unionlist = list 1 empty un I where un x у = x | y. 159
От абстрактного к конкретному синтаксису. Хотя абстрактный синтаксис характеризует структуру независимо от вида ее представления, часто можно найти один или более способов задания форматов, которые естественно связаны с каждой структурой. Описание представления (или его конкретный синтаксис) может быть также получено из структурного описания путем определения порядка расположения компонентов и добавления особых символов для устранения двусмысленности. Предположим, что для представления каждой атомной компоненты структуры выбрана некоторая цепочка символов. После этого множество цепочек, соответствующих множеству структур, может быть описано выражением, которое имеет ту же структуру, что и описание абстрактного синтаксиса. Язык, получаемый в результате операции объединения, является объединением языков. Язык, связанный с выражением du (А, В, С, D, Е), есть А I В I С 1 D I Е, где А, В, ... теперь являются языками для А, В, ... Аналогичным образом, языком для выражения ср (А. В, С, D, Е) является A.B.C-D.E. Списки. Определения Л-списка имеют следующий вид: def list А= du (ср ( ), ср (А, list А)). Язык для списков задается с помощью выражения list X = empty |'x-list х либо empty | (list х)-х . Этот язык состоит из фразы х, которая может повторяться произвольное число раз. В том случае, когда элементы списка следует разделять, может быть использована следующая функция: def list у X = empty | nlist у х where гее nlist у х = х | x-y-nlists у х . Поскольку в язык не должны входить цепочки, оканчивающиеся разделителем у, последняя функция имеет более сложный вид. Списковые структуры. Абстрактно синтаксис списковых структур задается так: def гее liststructure А = du (А, list (liststructure А)). Каждый язык списковых структур образуется путем заключения в скобки списковых структур, состоящих более чем из одного элемента: def Is Z у X = X I z.list (Is х).у или def Iss S Z у X = X I z.lists s (Iss s z у x).y , 160
если элементы списка должны быть разделены. Из этого следует, что цепочка (Iss comma open close x), где def open = Q' (' def close-= Q ')' ; def comma = Q ',' , является языком для списковых структур. Деревья и леса. Структура деревьев и лесов может быть задана так: def гее tree А = ср (А, forest А), and forest А = list (tree А) . Существует несколько языков для задания деревьев и лесов; простейшим, возможно, является следующий: def гее tree х= open.x-forest x-close and forest x = list (tree x) . Здесь деревья заключены в скобки, леса внесены в списки, а корнем является первый элемент в списке. Например, (/. X, ig, у, г)). Множество (tree empty) является языком структурных перестановок. Другой язык для деревьев задается с помощью функциональной записи, в которой используются операции, полученные за счет сопоставления корня и листьев, а также путем применения аргументов внутри скобок. Например: def гее tree х = х-forest х and forest x=open.lists comma (tree x)-close. Примером является / {X, g, (у, 2)). 4.5. Нисходящий грамматический разбор Автомат магазинного типа. Программа нисходящего грамматического анализа, соответствующая контекстно-свободному языку, является недетермированной и может быть реализована автоматом магазинного типа (МП-автомат). Состояние МП-автомата определяется двумя списками: 1) распознаваемой цепочкой терминальных символов и 2) содержанием магазина, который может иметь как терминальные,, так и нетерминальные символы. Переходы из состояния в состояние можно обозначить так: (р. <?) -^ (''. S). где риг — цепочки символов; q и s — цепочки нетерминальных символов. Головы цепочек риг ставятся справа, а головы цепочек q и S — слева. Каждое преобразование состояний является 6 Бердж в. ■ 161
условным. Если р стоит в начале содержимого магазина, а q является началом входной цепочки, то они должны быть удалены и заменены строками г и s соответственно. Правила преобразования состояний для нисходящего МП- автомата могут быть получены непосредственно из правил грамматики. Каждое правило языка вида А ^иги.и,... и„ соответствует следуюп1ему преобразованию для нисходящего МП-автомата: (А, О) ->(f/„LVi... U,U,Uu О), а каждый терминальный символ а соответствует следуюи1,ему переходу; (а, а) ^(( ), ( )). Другими словами, когда нетерминальный символ находится в верхушке магазина, он заменяется правой частью продукции. Входная цепочка остается неизменной. Если терминальный символ находится как в верхушке магазина, так и в голове цепочки, то оба символа удаляются. Если нисходящий МП-автомат начинает работу, находясь в состоянии (S, х), где х — распознаваемая цепочка, а S — начальный символ языка, и при этом могут быть найдены последовательности переходов, которые приводят к состоянию (( ). ( )), то цепочка должна распознаваться как элемент языка 5. Нисходящий МП-автомат, который соответствует правилам, рассмотренным выше, приведен на рис. 4.5, где переходы пронумерованы, что дает последовательность состояний, ведущих к распознаванию цепочки aabcbbc как элемента языка S (рис. 4.6). Поскольку нетерминальный символ в верхушке магазина раскрывается раньше символов, состоящих ниже его, распознавание происходит путем раскрытия нетерминалов и проведения проверок, соответствуют ли цепочки, полученные этим способом, входной цепочке. Последовательность переходов, которая ведет к конечному состоянию, не обязательно существует. В этом слу- Правила Переходы состояний 5 -^ АЬС S -^ СЬ С -*abS С -^с А -* а А-^аС E, 0 ) ^ (СМ, 0 ) E, 0 ) ^ (ОС, 0 ) (С, 0 ) ^ {ßba, 0 ) (С. 0 ) ^ (с, {) ) (Л.())-(а, О) (Л, 0 )^ (Ca, 0) (а, а) -> ( (), 0 ) (й, Ь) -> ((), () ) {с, с) -* ( (), 0 ) A) B) C) D) E) F) G) (8) (9) Рис. 4.5. Соответствие между правилами и переходами состояний 162
Магазинный список (голова справа) S СЬА СЬСа СЬС CbSba CbSb CbS CbbC Cbbc Cbb Cb С с О Рис. 4.6. Последовательность состояний, распознающих цепочку Входная цепочка (голова слева) aabcbbc aabcbbc aabcbbc abebbe abebbe bcbbc ebbe ebbe ebbe bbe be с с 0 Число переходо 1 6 7 3 7 8 2 4 9 8 8 4 9 чае цепочка не относится к языку. Если существует хотя бы одна последовательность, то цепочка может быть распознана и структурное описание получено. Если к конечному состоянию может привести более чем одна последовательность переходов, то в этом случае грамматика является неоднозначной. «Хотя общего теста для определения неоднозначности нет, часто можно проверить, является ли грамматика однозначной. Отношения грамматического разбора. Один и тот же МП-автомат может быть описан различными способами в зависимости от используемых отношений грамматического разбора. Предположим, что для каждого символа, терминального или нетерминального, существует отношение, связывающее цепочку с множеством цепочек. Отношение, которое соответствует данной фразе, пред- сгавляет собой отношение между двумя цепочками. Причем из второй цепочки должна быть удалена начальная часть, являю- ицшся вхождением этой фразы. Отношение можно рассматривать как функцию, преобразующую цепочку в множество цепочек. Если (функция не находит нужной фразы в голове цепочки, результатом является пустое множество. Отношение, которое соответствует каждому терминальному символу, контролирует, находится ли этот символ в голове цепочки. Если это так, то результатом является множество, содержащее один элемент — хвост цепочки, если нет, то результатом будет пустое множество. Если множества представлены поточными функциями, то функция Q, определенная ниже, воздействует на терминальный символ и образует соответствующее ей отношение def Q X S : if null s then nuHists else if X = h s then us (t s) else nullists . 163
Если цепочка начинается с символа х, то результатом является поток us {t, s), в противном случае — nullists, т. е. поток, который представляет собой пустое множество. Существуют два специальных отношения, которые соответствуют: 1) пустому множеству, значением которого всегда является пустое множество, и 2) множеству нулевых списков, значением которого является множество, содержащее один элемент — цепочку," соответствующую первоначальному аргументу. Их грамматические анализаторы определены ниже: def nullstring s = us s ; def empty s = nullists . Из приведенных отношений могут быть сконструированы новые отношения заменой каждого объединения (f\g) объединением двух отношений: def union f g s = appendl (f s) (g s) и заменой каждого оператора декартовой конкатенации (f-g) конкатенацией двух отношений; def followedby f g s = concatq (mapl f (g s)) where concatq = stream 1 nullists appendl I . Поэтому отношение {followedby f g) сначала находит множество цепочек, полученных применением / и s, затем для каждого элемента множества определяет множество, полученное применением g, и наконец формирует объединение этих множеств. Теперь контекстно-свободные правила языка без левой рекурсии можно интерпретировать как множество взаимно рекурсивных функций, задающих отношение грамматического разбора для языка. Цепочка относится к языку при условии, что нулевой список является элементом множества, образованного применением отношения грамматического разбора к цепочке. Другими словами, если L представляет собой отнош'ение для языка, то цепочка распознается при условии, что выражение {exists null {L s)) истинно. Выражение exists определено ниже: def гее exists р s = If nulls s then false else let X, у = 5 ( ) If p X then true else exists p у . Для того чтобы построить грамматический анализатор из этого распознавателя, необходимо преобразовать отношение так, чтобы оно связывало между собой множество пар. Первая из пар — это объект, образованный из фразы, стоящей в голове цепочки. Второй парой является хвост цепочки. Функция 164
fotlowedby преобразовывается, и в качестве аргумента используется теперь другая функция. Функция [ее h f g), определенная ниже, порождает множество пар путем применения функций / н g и функции h к результатам использования fug: def ее h f g s = eoncatq (mapl q (f s)) where q (u, v) = mapl r (g v) where r (y, z) = (h u y, z). Оператор union не изменяется. Отношение грамматического разбора, которое порождает множество структурных описаний цепочки для языка, заданного правилами S -^АЬС; S -^СЬ; С -> abS; С —^с; А -^а; А -^аС может быть получено следующим образом. Сначала правила переписываются в виде S = (A-Q'b'-C)\(C-Q'b'); С = Q'a'-Q'b'-S\Q'c'; А = Q'a'\Q'a' -С и затем определения основных отношений преобразуются из (string -^ string-stream) к strings (Axstring) — stream следующим образом: def nullstring s = us ( ( ), s) ; def empty s = nulHsts . Когда символ распознан, его структурному описанию соответствует дерево, корнем которого является символ, а листьями— нулевой список, Определение грамматического анализатора, связанного с функцией Q, дано ниже: def Q X S = if null s then nullists else if X = h s then us (etree x ( ), t s) else nul lists . Операцию cc можно расширить для того, чтобы обеспечить возможность преобразования списка отношений в список объектов, получаемых из отдельных компонентов фраз и оставшейся цепочки: def eclist = list 1 nullstring (cc prefix) I. 165
функция, называемая 'edit', вводится для изменения первого члена каждой пары, образованной из анализатора применением к нему функции /: def edit f г s = mapl g (r s) where g (x, y) = (f x, y). Поэтому функцией для добавления корня к листьям с целью получения дерева является def struct л: = (edit (ctree х)), а анализатором для образования структурных описаний строки — def гее S = struct 'S' (union (cclist (A, Q 'b', C)) (cclist (C, Q 'b'))) and C= struct 'C (union (cclist (Q 'a', Q'b', S)) (Q'c')) and A = struct 'A' (union (Q'a') (cclist (Q'a'C))). Если S применяется к цепочке, то образуется поток пар. Первый элемент каждой пары является структурным описанием фразы S, а второй — оставшейся цепочкой. Нисходящий грамматический разбор с ограниченным возвратом. Грамматические анализаторы, описанные выше, осуществляют анализ цепочки путем воздействия функции (fitters (nult-second)) на поток, образованный применением отношения грамматического разбора L к цепочке. Можно получить все возможные результаты анализа, преобразуя поток в список. Здесь будет использован тот же метод, позволяющий интерпретировать правила как описания грамматических анализаторов, но на этот раз будет проведен, в лучшем случае, только один анализ. Хотя рассматриваемый способ интерпретации множеств правил как описаний грамматических анализаторов допускает возврат, не все возможные пути проверяются, и, следовательно, нет гарантии, что анализатор, образованный этим способом, является распознавателем языка. Это вытекает из того, что стратегия, соответствующая объединению X ~*а \ ab, должна отбирать первый возможный вариант анализа и игнорирует любой другой возможный вариант. Если X появляется в продукции Y -*Хс, то анализатор, соответствующий Y и применяемый к аЬс, будет трактовать а как образ X и не будет в состоянии найти с и, следовательно, сообщит о невозможности найти Y, тогда как в действительности Y =>- *аЬс, и это может быть обнаружено только после проверки второй альтернативы для X. Несмотря на эту труд- 166
Рис. 4.7. Правила в виде дерева ность, НИСХОДЯЩИЙ способ С частичным возвратом наиболее широко используется, так как программы согласуются с фразами и могут быть реализованы в виде последовательности семантических расширений. Это осуществляется путем преобразования программы, соответствующей данной фразе. Существует важный класс языков, для которых не требуется возврат во время грамматического разбора, основанного на использовании данного метода. Для этих языков нисходящий метод с частичным возвратом наиболее удобен для написания анализатора. Используя этот метод, можно совмещать грамматический разбор контекстно-свободных языков с грамматическим разбором неконтекстно-свободных фраз. В действительности, поскольку при написании программы руководствуются только синтаксисом языка, то на любой стадии можно выйти за пределы ограничений, обусловленных контекстно-свободными правилами, если это оказывается целесообразным, что может быть достигнуто введением анализаторов, которые зависят от внешней информации, не содержащейся в правилах, а также новых функций высокого уровня, которые воздействуют на грамматические анализаторы и генерируют их. Разбор, управляемый таблицами. Правила можно расположить в виде таблицы или дерева, которое имеет форму двухуровневого списка для каждого нетерминального символа. Первый уровень списка представляет собой объединение, второй — декартову конкатенацию. Правила языка изображены на рис. 4.7. Преимущество этого метода состоит в том, что программа может быть написана раз и навсегда. Для того чтобы изменить язык, который обрабатывается, требуется только изменить таблицу. Предположим, что существует функция, называемая trän, которая воздействует на символ и образует связанный с ним список символьных списков {symbol-listyiist. Обычно, когда функция trän применяется к терминальному символу, она образует нулевой список. Функция parse, определяемая ниже, воздействует на символ и цепочку и дает упорядоченную тройку: либо false, 167
( ) и исходную цепочку, либо true, структурное описание цепочки и нулевой список: def parse g s = if null s then false, ( ), s else if null (tran g) then if g = h s then true, ctree (h s) ( ), t s else false, ( ), s else let bl, cl, si = union (tran g) () s bl, ctree g cl, si. Функция parse определена с помощью функции union, которая использует список символьных списков {symbol-list)-list в качестве аргумента и проверяет, описывают ли члены списка цепочку. Эта функция находит первый элемент списка и возвращает лес, образованный из его компонентов: def гее union х с s = if null X then false, ( ), s else let Ы, cl, si = coat (h x) с s if Ы then Ы, cl, si else union (t x) с x and ccat x с s = if null X then true, с s else let Ы, cl, si = parse (h x) s if bl then ccat (t x) (postfix cl c) si else false, ( ), s. Функция ccat содержит структурное описание в своем аргументе с. Тот же алгоритм может быть получен путем формирования программы из правил языка. Этот способ будет рассмотрен ниже. Функции грамматического разбора. Другой подход основан на построении программ грамматического разбора, которые имеют ту же структуру, что и правила языка. Существует соответствие между синтаксическими фразами и их грамматическими анализаторами. Каждая фраза может быть связана с анализатором, определяющим, начинается ли цепочка с этой фразы, и формирующим упорядоченную тройку. Первым элементом триплета является флаг истинности, показывающий, распознан ли начальный отрезок цепочки, вторым — объект, полученный из цепочки, если она распознана, и третьим — цепочка, которая либо является остатком исходной цепочки, после того как распознанный отрезок удален, либо, если первый элемент — false представляет собой исход- 168
ную цепочку. Основные элементарные анализаторы могут быть определены следующим образом: def anycharacter s = if null s then false, ( ), s else true, h s, t s; def empty s = false, ( ), s; def nullistset s = true, ( ), s. Анализатор, соответствующий выражению anycharacter, не применяется, если цепочка нулевая. Анализаторы empty и nullist соответствуют множествам с теми же самыми именами. Функция, называемая ('s, формирует грамматической анализатор, выполняющий функции предиката такие, как digit, letter, vowel, def is р s = let Ы, cl, si = anycharacter s if bl then if p cl then bl, cl, si else false, ( ), s else false, ( ), s; def eqch x = is (eq x). Функция eqch, образующая анализатор, который проверяет, равен ли первый символ цепочки ее аргументу, определена выше на основе функции is и функции eq, которая проверяет, равны ли два символа. Соответствующие объединению и конкатенации операции, воздействующие на анализаторы, определены ниже как префиксные функции, называемые ип и ее: def ип А В S = let bl, cl, si = А s if bl then bl, cl, si else b s; def cc f A В s = let bl, cl, si = A s if bl then let b2, c2, s2= В si if b2 then b2, f cl, c2, s2 else false, ( ), s else false, ( ), s. Функция ип воздействует на два анализатора А и В для образования анализатора, который сначала проверяет, распознается ли цепочка анализатором А. Если это так, то результат применения функции (ип Л В) будет тот же, что и результат применения анализатора А к цепочке, в противном случае функция применяет к цепочке анализатор В. Функция (ее f А В) проверяет, распознается ли начальный отрезок цепочки анализатором А, а начальный отрезок оставшейся части — анализатором В. Если 169
обе проверки успешны, то результатом является триплет, состоящий из 1) true, 2) результата применения функции / к частям результата (второй части триплета) ^4 и ß и 3) оставшейся цепочки. Действие, направленное на раепознавание фразы, заключается в применении функции Д представляющей собой аргумент функции. Можно получить различные объекты на основе использования одного и того же языка путем последовательного изменения грамматичес^ких анализаторов для терминальных символов и для всех операций функции. Для того чтобы воздействовать на список анализаторов, эти две операции можно расширить следующим образом: def гее unlist х = if null х then empty else un (h x) (unlist (t x)) или def unlist = list I empty un I; def rec cclist x = if null x then nuUistset else cc prefix (h x) (cclist (t x)) ИЛИ def cclist = listl nullistset (cc prefix) I. Грамматический анализатор (unlist x) проверяет, применимы ли анализаторы из списка х к цепочке, и использует первый из них. Анализатор {cclist х), последовательно применяя распознаватели, образует список результатов анализа. Для того чтобы воздействовать на часть результата работы знализатора, вводится функция, называемая edit. Она определяется следующим образом: def edit f р s = let bl, cl, si = р s if bl then bl, f cl, si else false, ( ), s. Эта функция применяет свой аргумент / к результату грамматического разбора, осуществленного р. Анализатором, используемым для формирования структурного описания, соответствующего правилам А -^И1«2 •• А -^ViVa .. «Еляется edit (tree'A' (un (cclist (щ, U2, . И«; • Vm ..-, u, .., Un)) (cclist (Vi, V2, ..., V,.„))). Генератор анализаторов, соответствующий функции Q, воздействует на цепочку х и образует анализатор, который проверяет, является ли цепочка х начальным отрезком. Аналогичная операция, в которой ее заменяется union, называется seek х п контролирует, начинается ли анализируемая цепочка с одного из си.«вслов цепочки х. dd S';ck ;; - unlist (map eqch x) . 170
Леворекурсивное правило приводит к программе, которая зацикливается. Однако всегда можно переписать правила таким образом, что левые рекурсии будут устранены. Другой путь, связанный с этой проблемой, а также с устранением внешних уровней структурного описания за счет левой рекурсии, основан на использовании функции qualify, определенной ниже. Функция {qualify х у) распознает х, за которым следуют столько у, сколько можно встретить при сканировании слева направо. Она накапливает результаты грамматического разбора путем последовательного присоединения слева функционального аргумента /: def qualify f х у s — let Ы, cl, si = X s if bl then qua! if у 1 f cl si else false ( ), s where rec qualifyl f с у s = let bl, cl, si = у s if bl then qualifyl f (f с cl) у si else true, c, s. Если digit является предикатом для цифр и спит преобразует цифру в целое, то функция integer, определенная ниже, берет самую длинную последовательность цифр, которую можно найти в голове цепочки, и преобразует ее к целому: def integer = qualify асе d d where ace x y= 10 X x-\- у and d = edit cnum (is digit). Определения star, perhaps, upto, exactly и atleast, которые были введены в п. 4.4. для того, чтобы описать языки, переносятся на анализаторы. Если вертикальная черта интерпретируется как ип и • как (ее pfx), то результатом будет анализатор, который генерирует распознанную цепочку в случае успешного применения. Грамматические анализаторы, соответствующие множествам, образованным путем отображения другого множества, трудно получить в каком-либо формальном виде. Однако существует пример, в котором множество цепочек имеет начальный отрезок, за которым следует фраза, зависящая некоторым образом от этого найденного отрезка. Удобная в таких ситуациях функция называется иптар и определяется ниже: def unmap g р s = let bl, cl, si = p s if bl then g cl si else false, ( ), s. 171
функция unmap воздействует на анализатор р и функцию g, генерирующую анализатор. Сначала применяется анализатор р, а затем функция g воздействует на результат р для создания другого анализатора, который затем применяется к остатку цепочки. Например, грамматический анализатор (иптар Q (Q х)) проверяет, начинается ли цепочка с дважды повторяющейся цепочки X. Функция иптар соответствует стратегии грамматического разбора, при которой информация накапливается в начале цепочки с целью предсказать синтаксис оставшейся цепочки. Второй пример показывает роль этой функции при грамматическом разборе фраз, заключенных в скобки. Предположим, что имеется множество левых скобок, называемых opens, и функция, называемая mate, которая ставит каждой левой скобке соответствующую ей правую скобку. Анализатор, заключающий фразы в спаренные скобки, может быть определен следующим образом: def bracket х = unmap f (seek (opens)) where f у = cc 1st x (eqch (mate y)). В качестве третьего примера использования функции иптар рассмотрим анализатор для множества фраз р, сгруппированных с помощью разделителей . или , , или ; , или :, в котором каждый отдельный разделитель может использоваться так, что конструкции а, Ь, с, а; Ь; с, а . b . с и а : b : с разрешаются, а комбинации такие, как а; Ь, с , недопустимы. Анализатор для этих фраз может быть определен на основе функции иптар следующим образом: def separated h s р = qualify h p (unmap (?. у . separated h (u y) p) seek s). Анализатор (separated h s p) проводит грамматический разбор строки, состоящей из фраз, разделенных символами, которые относятся к множеству s. После того как найден первый разделитель у, остаток цепочки анализируется на основе множества s, включающего один разделитель у. Ниже рассмотрены правила, для которых соответствующий анализатор работает без возвратов. Грамматический разбор без возвратов. Анализаторы могут быть классифицированы по максимальному числу символов в голове цепочки, которое проверяют для решения вопроса о применимости анализатора. Распознаватели empty и nulUstset не анализируют символов, а распознаватели, соответствующие терминальным символам, проверяют каждый символ. Возврат в цепочку происходит в случае, когда выбран ложный выход из анализатора. Грамматический анализатор, работающий без возвратов, должен быть построен так, чтобы он мог успешно распознавать все цепочки языка и отбрасывать все, что не является цепочкой. Единственным анализатором, который терпит неудачу при распознавании цепочки языка, является ана- 172
лизатор с возвратом по первому символу. Очевидно, для такого анализатора любой выход с возвратом по п-му символу с п > 1 должен вызывать немедленное отбрасывание всей цепочки. Если множество анализаторов включает nullistset, то этот распознаватель должен применяться последним, поскольку он никогда не терпит неудачу, и любой анализатор, расположенный после него, никогда не будет использован. В большинстве случаев применяют анализаторы, которые никогда не заканчивают разбор неудачей. Они называются nonfalse анализаторами. Например, анализатор nullistset S = true, ( ), s всегда возвращает true, а анализатор qualifyl f с у распознает любое число вхождений фразы у в голову цепочки (включая*ноль). В общем случае nonfalse анализатором является любой анализатор, составленный из объединения, которое включает nonfalse анализатор. Не существует анализатора, который может быть всегда использован после применения nonfalse анализатора. Таким образом, если правила контекстно-свободного языка интерпретировать как анализаторы, то вопрос, будут ли результирующие программы точно распознавать все цепочки языка, является неразрешимым. Можно путем проверки правил определить, будет ли анализатор, соответствующий грамматике, требовать возвратов. Предположим, что все правила представлены в стандартной форме X ->Y^\Y,\...\Ym\Z,Z,...Zn, где т, п > О, У и Z являются либо терминальными, либо нетерминальными символами. Если п = О, то ZiZ^ ■■■ Zn необходимо рассматривать как множество нулевых списков. Согласно [4—31 ], существуют четыре условия, которые являются необходимыми и достаточными для обоснования рассмотренного выше метода разбора без возвратов, если не существует бесполезных нетерминальных символов. 1. Грамматика не имеет леворекурсивных нетерминальных символов. 2. Множества first (Yi), first (У^), ... ,first (Ym), first {Z^Z^. ... ... Zm) попарно не пересекаются. 3. Если ZjZa ... Z„ =^ е, то first {Yj) не содержит символов так же, как и множество символов, которые могут следовать за фразой X в языке. ^- Ух, Y^, ..., Y-m не являются неложными. Нетерминальный символ X будет неложным тогда и только тогда, если: 1) Y] является неложным для некоторого / A < / < < т), или 2) п == О, или 3) п > О и Zj является неложным. Нисходящий граиматический разбор с использованием меток. Описанные выше анализаторы представляют собой функции, воз- 173
вращающие как часть своего результата флаг успеха или неудачи, который затем должен быть проверен с помощью вызывающей функции. Можно более эффективно написать программы, имеющие две ветви: одну для успеха, а вторую для неудачи. В этом разделе те же самые анализаторы переопределяются таким образом, что каждый будет иметь дополнительный аргумент, характеризующий действие, которое должно быть выполнено в том случае, когда не находится соответствующая ему фраза в голове цепочки. Этот метод допускает большую гибкость в структуре программы за счет уничтожения в полученной подпрограмме вызываемого и возвращаемого форматов. Каждая функция грамматического разбора снабжается дополнительным аргументом, который может быть базисной функцией, замыканием или программным замыканием и применяется к цепочке в случае, когда фраза, соответствующая анализатору, не распознана. В результате действия анализатора получается пара, первым элементом которой является объект, образованный, из цепочки, а вторым — оставшаяся часть цепочки. Простейшие анализаторы определяются следующим образом: def empty Е s = Е s; def nuliistset E s = ( ), s; def anycharacter E s = if null s then E s else h s, t s; def is E p s = let Ы, si = anycharacter E s if p Ы then bl, si else E s. Функция, соответствующая объединению анализаторов, приобретает следующую простую форму: def un f g Е S = f (g Е) s. Другими словами, функция объединения представляет собой функциональную композицию функций / и g. Сначала к цепочке применяется функция /, и если это приводит к неудаче, то применяется функция (gE). Функция, соответствующая оператору декартовой конкатенации, является более сложной и использует оператор перехода J, описанный в гл. 2. Он предназначен для того случая, когда применение обоих аргументов / и g не имеет успеха, и обеспечивает выход из анализатора {ее h f g Е) даже тогда, когда Е не является программным замыканием. def со h f g Е S = let El = JE let cl, sl = f El s let c2, s2= g E2 sl where E2 (sl)=- El s h cl c2, s2. 174
Этот метод позволяет в случае неудачи работы анализатора перенести действие на некоторый уровень, более высокий, чем точка, в которой был вызван анализатор. Другим удобным методом комбинирования анализаторов является dcf difference f р Е s = let Ы , si, == f Е s if p bl then E s else bl, si, который становится неприемлемым, если анализатор /, вообще, неприменим или если результат грамматического разбора удовлетворяет предикату р. Таким образом, striiignotcontainingsemicolon---star (difference anycharacter (cq'; '). Результатом модификации других анализаторов является def qualify h f g Е s ■'-'- qualifyl h g (f E s) where qualifyl h g (b. s) -= let El = J (Xsl . b, s) let bl, si == g El s qualifyl h g (h b bl, si); def uumap f g E s = let E = J E let bl, si = p E s f Ы E si; def edit g f E s = let bl, si = f E s gbl, si. 4.6. Левоугольный восходящий грамматический разбор Метод левоугольного восходящего грамматического разбора называется так потому, что конструирование структурного описания дерева начинается из левого угла снизу-вверх. Программа грамматического разбора всегда находится в состоянии, когда найдена некоторая фраза X, которая является начальным отрезком цепочки, и ведется поиск целевой фразы G. Такой стратегии соответствует недетерминированный МП-автомат. МП-автомат для левоугольного распознавания. Существуют- три типа правил перехода для автомата. Правила первого типа имеют вид (G, и,) -^{GXiU,U,_,... и„ О). 175
Их строят для каждого нетерминального символа G и правила I — специальный символ, который не является ни терминальным, ни нетерминальным. Правила перехода второго типа имеют вид {В, В) ^(( ), ( )). Их строят для каждого терминального или нетерминального символа В языка. Третий тип правил {Al, ( ))^(( ). А) устанавливается для каждого нетерминального символа А. Стратегия этого МП-автомата может быть описана неформально следующим образом. Пусть достигнуто состояние (... G, Vi, ...), это означает, что автомат ищет G в голове цепочки и нашел Ui. Переход в состояние {GXlVnVn^x... и„ ( )) переключает его на поиск [/а, а затем подряд следуют U^ и f/,j. Символ X запоминается в магазине, чтобы указать, что если голова цепочки согласуется с U^Uo. ... Un, то эта голова есть X. Преобразование (GX/, ( )) ^(G, X), проведенное на этом шаге, регистрирует факт обнаружения X, в то время как основной целью остается G. Из этого следует, что число указанных правил перехода можетбыть уменьшено за счет ввода требования, в соответствии с которым остаются только правила перехода, удовлетворяющие свойству X £ lcft*G. Это допустимо, так как, если X не принадлежит left*G, то состояние (G, X) не может привести к состоянию (( ), ( )). МП-автомат начинает свою работу из состояния {S, х), где х — распознаваемая цепочка, а S — символ языка. Если последовательность переходов приводит к состоянию (( ), ( )), то цепочка распознана. Переходы, соответствующие правилам S ->СЬ; С ^abS; С ->с; Л ->а; А -^аС, приведены на рис. 4.8. Шаги, предпринятые при распознавании цепочки 'ааЬс bbc', показаны на рис. 4.9. 176
(a) (S, A (S, С (S, a (S, a (S, a (S, с (A, a (A, a (C, a (C, с (b) {X,X -> (sslcb, 0) - isslb, 0) - {SAC, 0 ) -. {scsb, 0) - (SCI, 0) -*(AA, I, 0) - (ЛЛ/С, 0) - (cc So, 0) - (cc/, 0) -. ( 0, 0 )forX = ) - ( 0, Y) for Y ■ S, A, с a, = S, Л, С Ö, с, Рис. 4.8. Пример пере.150дов левоугольного МП-автомата Левоугольный восходящий грамматический разбор с ограниченным возвратом. При левоугольном восходящем грамматическом разборе удобно модифицировать правую часть правил и хранить их как лес символов. В этом лесу правила с общим символом головы рассматриваются как дерево, корнем которого является этот символ, а листья составлены из деревьев, образованных таким же образом из хвостов правил, имеющих общую голову. В каждой концевой точке этого дерева запоминается символ, характеризуемый правилом, лежащим на пути, ведущим от корня к концевой точке. Правила S ^СЬ\ АЬС; С -^с I abS; А -^а\ аС приведены к виду, показанному на рис. 4.10. Переход • (^^(?' О) ■ (CC/Sb, о) @^0) (SC/, о) (О, О) - (О, 5) > ( О' О) - (О, С) • @. О) - ((. А) >■ (ss/cb, о) (().,()) (CCI, о) - ( О, С) ■ ( 0. О) - (О, S) ■ ( О, О) Рис. 4.9. Шаги при распознавании цепочки с помощью левоугольного восходящего МП-автомата !77 МП-автомат S SAIC SA/CCiSb SAICCIS SAlCClSS/b SA/CC/SSI SAiCCiS SAICCI SAIC SAl S SSiCb ssic ssicci ssic SS/ s 0 Цепочка aabcbbc abebbe bcbbc ebbe bbe be Sbe be Cbe be Abe be e 0 с 0 s 0 (S, а) - (С, а) - (b, b) -. (S, с) -^ (b, b) -. (S/, 0) (S, S) - (C/, 0) (C, C) - {A, /()) (S, A) - (b, b) -. (C e) -^ (Cl, 0 ) (C, C) - (S/, 0) E, S) -
^1-- й Рис. 4.10. Лес грамматического ""/7 1 5 Й Правил 1 анализа b с л с Предположим, что су- [Л й Ö ществует функция, иазы- I Jl ваемая trän, которая преоб- J' Ш разует каждый символ к ле- [s] су. После того как фраза найдена, число возможных для левоугольного „ " ' проверяемых продолжении может быть уменьшено за счет использования таблицы для отношения succr. Если (succr р q) есть true, то р £ left*q. Таблица для правил, упомянутых выше, имеет вид S С А S С V А V а V V V b с V Следующая программа, называемая parse, проверяет, относится ли цепочка к языку g и, если относится, образует структурное описание цепочки в виде символов def parse g s = if null s then false, ( ), s else recognize (h s) ( ) g (t s). Основная работа по грамматическому разбору выполняется функцией, называемой recognize. Функция parse не обрабатывает правила, содержащие множество нулевых списков. Функция {recognize х с g) воздействует на символ х, который уже распознан, на лес символов с, представляющий собой структурное описание цепочки, распознанной к данному моменту, и на g — конечную цель. Результатом работы функции является либо false, нулевой список н исходная цепочка, либо true, структурное описание дерева с корнем g и оставшаяся часть цепочки def recognize х с g s = if succr X g then let bl, cl, si -=■ diagram (trail x) (u (ctree x c)) g s if bl then bl, cl, si else if X = g then true, ctree g c, s else false, ( ), s else if X = g then true, ctree g с s else false, ( ), s. 178
Основная нагрузка в функции recognize возложена на функцию diagram. В функции recognize предусмотрен тот факт, что продолжение возможно даже, если, х = g. Следовательно, эта функция находит самую длинную фразу g в голове цепочки при условии, что существует леворекурсивное правило. Функция diagram воздействует на х, лес символов которого описывает возможные продолжения, на с — лес символов, представляющий структурное описание, найденное до сих пор; на g — целевой символ; и па S — цепочку, которая должна быть распознана: def гее diagram х с g s = if null X then false, ( ), s else let bl, cl, si = diag (h x) с g s if bl then bl, cl, si else diagram (t x) с g s. Функция diagram, осуществляющая проверку объединения возможных продолжений, определяется на основе функции diag, которая воздействует на дерево символов и проверяет конкатенации продолжений: def diag х с g s = if null (listing x) then recognize (root x) с g s else -let bl, cl, si = parse (root x) s if bl then diagram (listing x) (postfix cl c) g si else false, ( ), s. Если символ является терминальной вершиной, то это означает, что фраза, описанная с помощью данного символа, найдена и следующий шаг должен завершить распознавание цели g. Другими словами, делается попытка распознать корневую вершину, за которой следует один из символов листвы. Грамматический анализатор (recognize х с g) является распознавателем, который используется после того, как найдена фраза X. С его помощью делается попытка привести х к форме фразы g-. Правила можно преобразовать так, что при использовании нисходящей стратегии и этих преобразованных правил по существу будут иметь место те же шаги, что и в случае использования левоугольной восходящей стратегии на исходных правилах. Для этого вводятся новые фразы типа lx:g], представляющие собой множество цепочек, которое создает фразу g при условии, что она связана с концом фразы х. Нисходящий грамматический анализатор фразы 1х : g] представляет собой функцию (recognize х с g). 179
Правила могут быть преобразованы следующим образом. Сначала запишем их в матричной форме: / 000\ E, С, Л) = (S, С, Л Ь 00 + @, abS + с, а + аС), \ ЬС 00/ т. е. X = xG +/, где х — вектор нетерминальных символов. Умножение обозначает декартову конкатенацию, а сложение —■ объединение. Правые крайние символы из / являются терминальными. Уравнения могут быть решены относительно х: X = /G*, где G* = I + GG*, здесь I — матрица с диагональньши элементами, равными е (множеству нулевых списков), и недиагональными элементами, равными 0 (пустому мнол<еству). Решение является новым множеством правил. Элементы G* принимаются за исходные нетерминальные символы, что позволяет образовать новые нетерминальные символы вида [А : В]. Новые правила, соответствующие зависимости х = /G*, формируются из старых правил, правая часть которых начинается с терминального символа. Таким образом, соотношение {S, С, А) = @, abS + с, а + аС) G* дает выражения S = (abS + с) [С : S] + {а + аС) [А : S]; С = (abS + с) 1С : С] + {а + аС) [А: С]; А = {abS + с) [С : А] + {а + аС) [ А : А]. Они могут быть упрощены, так как символ [X : Y] является пустым, если X не принадлежит left*Y. Из этого следует, что [А : С] и [С : А] пустые, а правила имеют вид S -^abS [С: S]; S ->с 1С : S]; S -^а [А : S]; S ->аС [Л : S]; С ->abS 1С : С]; С ->с 1С : С]; А -^а 1А : Л]; А ->аС [А : Al. Из другого уравнения G* = I + GG* могут быть определены новые нетерминальные символы fe 0 0\ /0 0 0\ G*- 0 е 0 U b 0 0 G*. \00 е J \bC0 0/ 180
Рис. 4.11. Дерево нисходящего грамматического разбора с использованием преобразованных правил Первый элемент приводит к правилам [S : S]-^e; [С: С]->е; [Л : Л] ->е. Второй элемент образует новое правило вида [С : Л] -> и^и^ ... и^ [В : А] для каждого исходного правила в котором с — первый нетерминальный символ, А — также нетерминальный символ такой, что С ^ left*А. Таким образом, из правил 5 -> ЛоС выводятся правила 1С : S]-^b [S : S]; la: S]-^bC [S : S]. Исходные нетерминалы характеризуются правилами, имеющими терминальный символ в качестве первого символа в правой части. Правила для новых символов также имеют исходный символ в качестве первого символа в правой части. Отсюда следует, что правила могут быть представлены таким образом, что каждое правило с непустой правой частью будет иметь в качестве первого символа терминальный символ. Это достигается путем подстановки во все правила новых символов таким образом, чтобы первый элемент правых частей правил был нетерминальным. Новым набором правил является 5 ->aö5 1С : Sh 5 ->с [С : 5]; 5->а [Л : 5]; S -^аС [Л : 5]; С -> abS; С-^с; А -^ а; А -^аС; [С : S ] -> Ö; [Л : 5] ->ÖC. Нисходящая стратегия, основанная на этих правилах, образует структурное описание, показанное на рис. 4.11, для случая, когда она применена к цепочке aabcbbc. 181
4.7. Правоугольный восходящий грамматический разбор При иравоугольном восходящем грамматическом разборе дерево разбора конструируется, начиная с правого угла, при скаии- рованнн входной строки слева-направо. Магазин используется для того, чтобы зафиксирохвать процесс разбора анализируемой цепочки. Основными шагами процедуры разбора являются; 1) переместить символ из входной строки в верхушку магазина и 2) найти правую часть правила в голове магазина и заменить ее левой частью этого же правила. Поэтому стратегия по суш,еству основана на обратном просмотре уже проанализированного начального отрезка цепочки и на попытке построить дерево разбора. МП-автомат. Для правоугольной стратегии грамматического разбора суш,ествует недетерминированный МП-автомат. Он имеет два типа правил перехода. Для каждого правила грамматики А -> и^и^ ... и„ существует переход вида {U,U,... f/„, ( ))->(Л, ( )). В результате выполнения этого перехода правая часть правила, находящаяся в голове МП-автомата, заменяется его левой частью. Такая процедура называется шагом свертки или операцией свертки. Переходы второго типа имеют вид (( ), А) -> (а, ( )). Они формируются для каждого терминального символа а и называются сдвигом. В результате этой процедуры символ перемещается из головы цепочки в голову МП-автоката. Если МП- автомат, осуществляющий восходящий грамматический разбор, начинает работу из состояния ( ), л:, где х — цепочка, которая должна быть расиознапа, и если может быть найдена какая-либо последовательность, приводящая к состоянию S, ( ), то считается, что цепочка относится к языку 5. Правила грамматики и соответствующие им правила перехода даны на рис. 4.12. Последовательность состояний автомата, с помощью которой распознается цепочка aabcbhc, приведена на рис. 4.13. 5 -* АЬС S ^ СЬ С ^ abS С ^ с А ^ а А -^ аС {АЬС, 0 ) - (S, 0 ) (СЬ, 0 ) -^ (S, 0 ) {abS, 0 ) -^ (С, 0 ) (^ 0 ) - (С, 0 ) (а())-(Л, 0) (а. СО )^ {А, 0) ( 0, а) -* {а, 0 ) ( 0. Ь) -* (Ь, 0 ) ( (), с) -^ (с, 0 ) A) B) C) D) E) F) G) (8) (9) Рас. 4.12. Переходы для правоугольного МП-автомата 182
Магазинный список (голова справа) о а аа ааЬ ааЬс аоЬС aabCb aabS аС А Ab Abc AbC 1 S Рис. 4.13. Шаги при распознавании в правоугольном МП-автомате Входная цепочка голова слева) йаЬсЬЬс abebbe bebbe ebbe bbc bbe be be be be с Номер перехода 7 5 8 9 4 8 2 3 6 8 9 4 Поскольку правая часть правила при проведении операции свертки должна находиться в вершине МП-автомата, то порядок, в котором используются правила свертки, является обратным но отношению к порядку использования правил при правом выводе цепочки. Один из способов работы, выполняемой с пoмoп^ью МП-автомата, основан на организации множества магазинных списков, которые предположительно являются структурными описаниями начального сегмента цепочки как леса символов, в котором участвуют как головы, так и хвосты цепочек. Программа поочередно прибавляет новый символ к лесу и затем сворачивает его путем замены какой-либо ветви, соответствующей правой части правила, символом, стоящим в его левой части. Удобно хранить правила в лесу в обратном порядке так, чтобы совмещались хвостовые отрезки правых частей правил. Правила С- Л АЬС ! СЬ; -> abS I с; -^ а\ аС на рис. 4.14, левой частью хранятся так, как показано где концевые точки являются правил. дит в порядке, рис. 4.15. Пути от корня к листу сквозь лес представляют собой все возможные способы анализа цепочки. Поскольку S находится на Анализ цепочки aabcbbc происхо- которып иллюстрирует Рис. 4.14. Лес правил для правоугольного грамматического разбора 183
a A /\ a A Сдвиг a Cffepmna A-*-a Cäßus a A Свертка /\ a A Сдвиг b Сдвиг с Сдвиг ä Свертка S-^AC C-^abS A-^aC Сдвиг b Свертка ^-^Cb а А I X / свертка C^C S-^Cb S^AbC а А Рис. 4.13. Шаги при правоугольном грамматическом разборе цепочки 'aabcbbc' 184
верхнем уровне и является концевой точкой, то цепочка относится к языку 5. Следующая функция генерирует множество деревьев, непосредственно зависящих от леса у, который соответствует правилам х, представляющим собой лес символов (см. рис. 4.14): def гее trees х у = if null X then ( ) else concat {try (h x) y) (trees (t x)y) where rec try x у ~ if null у then ( ) else if null (listing x) then u (ctree (root x) y) else if root x = root (h y) then trees (listing x) (listing (h y)) else try (t y). Функция работает циклически до тех пор, пока существует возможность производить свертку с помощью функции reduce. Свертка определяется так: def rec reduce х у = let р = trees х у if null р then ( ) else union p (reduce x p). Затем лес преобразуется путем поочередного сдвига и свертки с помощью следующей функции: parse X у S = if null s then у else let z = u (ctree (h s) y) parse X (union x (reduce x z)) (t s). Инфиксные операторные выражения. Хотя структура,включающая инфиксные операторы выражений вида а + Ö = (с + d) — е// {х), может быть реализована с помощью правил грамматики, проще ввести обозначение «старшинства» инфиксных операторов. Наиболее часто используемые правила могут быть выражены аббревиатурой BODMAS, образованной из начальных букв слов, которые обозначают следующие операции: заключение в скобки, взятие функции от, деление, умножение, сложение и вычитание. Это означает, что сначала выражения в скобках интерпретируются как отдельные блоки, после этого следует операция применения функции к аргументу (зависимость / (х) может быть прочитана как / от л:), и далее, деление, умножение, сложение и вы- 185
( a+i ^c+ä) r + d) + bxc+d) abcx+,^—_„ +!i) ( ab G y- af} a OCX i-d.—.t—^ ) + abc ^ < , f ß) abcxfd+^ ^ ; чихание в указанном порядке. Обычно операциям сложения и вычитания присваивается один и тот же приоритет. Для того чтобы точно задать порядок вычислений, правила должны «заучивать» выражения вида а -\- b — с -]' d, операторы которых имеют равный приоритет. В АЛГОЛ-60 операторы имеют следующие приоритеты: -^ X ( ( Рис. 4.1К. Сдвигающие выражения д-пя реверсирования польской записи 3 = 4 ZD 5 V 6 Л 7 —1 8 <, 9 +, 10 X, И t <, — , /, ¥=, -^. ' 12 application neg Правила в АЛГОЛ-60 разрушают связи путем использования скобок так, что выражение а -\- b — с -\- d преобразовывается в выражение {(а + Ь) — с) + d. . \, ' Форма выражения, включающего инфиксные операторы, может быть преобразована в форму обратной польской записи с помощью магазинного списка. Идентификаторы передаются из входной цепочки в выходную. Когда оператор находится в голове цепочки, его приоритет сравнивается с приоритетом оператора в голове магазинного списка. Если оператор в голове цепочки обладает большим приоритетом, то он добавляется к магазинному списку, если — меньшим, то оператор из головы магазинного списка передается в выходную цепочку, а голова входной цепочки проверяется по отношению к новой голове магазинного списка. Шаги в преобразовании выражения (а-\-Ьхс — d) для получения обратной польской записи показаны на рис. 4.16. Исходный магазинный список должен содержать оператор, который обладает меньшим приоритетом, чем любой другой в цепочке. На рис. 4.16 это сделано с помощью открывающей скобки (. Цепочка должна также заканчиваться оператором с малым при- 186
оритетом ( )), что позволяет просмотреть все операторы в магазинном списке, расположенные ниже по отношению к открывающей скобке (. После сравнения обе соответствующие круглые скобки удаляются. Несомненно, должно быть предусмотрено правило, согласно которому всякий раз, когда магазинный список появляется в программе, существовала бы возможность представить эту программу в форме обычной функциональной записи, в которой не используется магазинный список. Пусть программа грамматического разбора выражений, содержащих инфиксные операторы, имеет два аргумента: 1) ops — множество инфиксных операторов, допустимых в цепочках, ч 2) р — тип фразы, которая содержит инфиксные операторы. f Выражение, содержащее инфиксные операторы, может быть сгруппировано следующим образом. В него входит фраза р, за которой следует список группы'. Группа, в свою очередь, состоит из инфиксного оператора о, за которым идет выражение, содержащее инфиксные операторы, приоритет которых выше, чем о. Инфиксная операторная цепочка аХЬ -{- cXd -{- е X f должна быть сгруппирована так, как показано на рис. 4.17. На этом рисунке каждый список групп принимает участие с самого левого ребра дерева. Функция, выполняющая анализ рассмотренным образом, называется infop. Ее аргументами являются р — анализатор для фразы, которая должна быть выделена, ops — множество разре- Рис. 4.17. Группирование инфиксцо-операторной цепочки 187
шенных инфиксных операторов, и / — функция для накопления результатов грамматического разбора. Функция также зависит от отношения, называемого stronger и существующего между индексными операторами. Функция infop определяется так: def гее infop / ops = qualify f p (unmap ((infop f p)-stronger) (seek ops)) . Грамматический анализатор сначала ищет фразу, используя анализатор р, и если она найдена, то затем ищет список групп. Если анализатор р неприменим, то анализатор (infop f р ops) также неприменим. Анализатор (seek ops) проверяет, начинается ли группа с инфиксного оператора о, относящегося к ops. Если этот анализатор находит его, то анализатор (infop f p{stronger о)) применяется к оставшейся части цепочки. Последний анализатор обрабатывает выражения, содержащие инфиксные операторы с приоритетом выше, чем о. Функция / накапливает результаты грамматического разбора групп. Оказывается, что лучше ввести другой функциональный аргумент g, который применяется к инфиксному оператору и остатку группы. Анализатор тогда может быть переопределен следующим образом: def гее infop f g р ops = qualify f p (unmap group (seel< ops)) where group x = edit (g x) (infop f g p (stronger x)). Предположим, что p представляет собой анализатор для идентификатора, значением которого является он сам, т. е. identifier=qualify prefix letter (un letter digit), тогда: 1) цепочка может быть заключена в скобки путем использования fxy = concat ее X, у, ')') а g = prefix; 2) двоичное дерево может быть сконструировано с использо* ванием функции f (х, у) ~ х, у и g х (у, z) ~ сЫгее у (cbtree х etree etree) г; 3) цепочка в обратной польской записи может быть сконструирована при использовании g ~ postfix п f = concat; 4) выражение может быть вычислено с помощью функций: gxy = X, у и fx (у, г) = apply (value Ey) {value Ex, г); б) инфиксные операторные цепочки со скобками могут быть проанализированы анализатором р, оперирующим с выражениями, заключенными в скобки, т. е. infop f g (bracket (infop f g p ops)) where bracket x = cc 2nd (Q'(') (cclst x (Q')') where 1st x у = x and 2nd x у = y. Грамматики простого предшествования. Для создания эффективной стратегии грамматического разбора из общей правоуголь- 188
пой восходящей стратегии требуется метод для уменьшения числа магазинных списков, предпочтительно до одного. Это можно сделать за счет правильного выбора операции свертки или сдвига для проверки верхнего символа из магазинного списка и верхнего символа входной цепочки. Предположим, что в лучшем случае существует одно из трех отношений <• ,= или > между двумя символами. Отношения <• и=^ ведут к операции сдвига, а отношение •>—к операции свертки. Если выбрана операция свертки, то необходимо определить, сколько символов должно быть свернуто и чем их надо заменить. Кандидатами для свертки являются символы, стоящие в магазинном списке до символов, между которыми существует отношение <•. Отношения простого предшествования могут быть получены из правил: 1) Л <-В, если существует правило видаD->... АС ... и С^^^В... 2) А =, если существует правило D -> ... AB ... 3) А- >В, если существует правило D -> ... EF ... и £ =^ ^^ ... А, F^-'B ... . Грамматический анализатор определяет, когда операция свертки должна быть выполнена путем сравнения символа в голове магазинного списка (А) и символа в голове цепочки (В). Если они не связаны посредством какого-либо из отношений <•, = или • >, то цепочка не относится к языку. Если последовательность символов связана следующим образом: А <■ В = С = D-> Е, то при условии, 4ToD-> Е, вызывается программа свертки, которая пытается найти правило с правой частью BCD. Если оно найдено, т. е. f -> BCD, то цепочка BCD заменяется F, и снова начинается сопоставление путем сравнения А п F. Анализатором грамматики простого предшествования может быть функция, имеющая аргумент ошибочного действия Е, к которой обращаются всякий раз, когда либо не существует отношения между двумя символами, либо функция, осуществляющая свертку, не способна найти правило, правая часть которого равна текущему кандидату. Второй аргумент у является структурным описанием сегмента (т. е. ß = C = D в ...А <-В — С ~D ...) def rec ргес Е у s = if null s then reduce E у else if h у < h s tlien let bl, si = prec E (u (h s)) (t s) prec E у si (bl : si) else if h у = hs tlien prec E (h s : y) (t s) else if h у > h s tlien prec E (u (reduce E y)) s else E s. 189
A A В С a b В С = = a = •> ■> Ь <■ < Рис. 4.18. Приоритетные отношения Правила В^ВаС В ->С С -> ЬС С-^Ь приводят к отношениям, выраженным в табличной форме (рис. 4.18). Шаги ;при грамматическом разборе цепочки babb, соответствующие этим прзилам, показаны на рис. 4.19. Функция ргес Е {и {h s)) (t s) применяется к цепочкам. Другими словами, анализ начинается с- операции сдвига. В результате анализа устанавливают, относится ли цепочка к В. Результирующим структурным описанием цепочки является ff \ / Грамматика называется грамматикой простого предшествования, если и только если не более чем одно из отношений- >, = или <-справедливо между любыми двумя символами, и если грамматика не имеет общих правых частей в своих правилах. 190
b '> abb С •> abb В = abb Ba <• bb l~" Ba = с вас О В Рис. 4.19. Шаги при анализе простого приоритетного языка БИБЛИОГРАФИЯ 4-1. Aho А. V. and llllman J. D. The Theory ot Parsing, Translation and Compiling, Englewood Cliffs, N. J.: Prentice — Hall, 1973. 4-2. Bar Hillel Y. Language and Information, Reading, Mass.: Addison — Wesley, 1964. 4-3. Bobrow D. Proc. AFIPS FJCC, 4-4. Brooker R. G. «Syntactic analysis of English by computer — a survey», Vol. 24 A963), pp. 365—387. A. and Morris D. «The Compiler-compiler», Ann. Rev. in Automatic Programming, Vol. 3, Oxford: Pergamon Press, 1963, pp. 229—275. 4-5. Brzowski J. A. «A survey of regular expressions and their applications», IRE Trans. EC-II, June, 1962, pp. 324-335. 4-6. Brzowski J. A. and McCluskey E. J. «Signal flow graph techniques for sequential circuit state diagrams», IEEE Trans. EC-12, N. 2, Apr. 1963, pp. 67—76. 4-7. Cheatham T. E. The Theory of Construction of Compilers, Computer Associates, Inc., 1967. 4-8. Cheatham T. E. and Sattley K. «Syntax directed compiling,» Proc. AFIPS SJCC, Vol. 25, Spartan Books, 1964, pp. 31—57. 4-9. Chomsky N. «Three models for the description of language», IRE Trans. I. T.-2, N. 3, 1956, pp. 113-124. 4-10. Chomsky N. Syntactic Structures, The Hague, Holland: Mouton Co., 1959. 4-11. Chomsky N. and Schultzenberger M. P. «The algebraic theory of с jd- text-free languages», (in) Computer Programming and Formal Systems, P. B; affort and D. Hirschberg (eds.), Amsterdem: North — Holland, 1963, pp. 118—lol. 191
4-12. Cohen D. J. and Gotlieb C. C. «A list structure form of grammars for syntactic analysis», Comp. Surveys, Vol. 2, N. I, 1970, pp. 65—82. 4-13. Colmerauer A. «Total precedence relations», JACM, Vol. 17, N. 1, 1970, pp. 14—30. 4-14. Earley J. «An efficient context-free parsing algorithm», Ph. D. Thesis, Carnegie — Mellon U., 1968. CACM, Vol. 13, N. 2, 1970, pp. 94—102. 4-15. Feldman J. A. «A formal semantics for computer languages and its application in a compiler-compiler», CACM, Vol. 9, N. 1, 1966, pp. 3—9. 4-16. Feldman J. A. and Cries D. «Ttanslator writing .systems», CACM, Vol. II, N. 2, 1968, pp. 77-113. 4-17. Floyd R. W. «A descriptive language for symbol manipulation», JACM, Vol. 8, N. 4, 1961, pp. 579—584. 4-18. Floyd R. W. «The syntax of programming languages—a survey», IEEE Trans. EC-13, Vol. 4, 1964. pp. 346—353. 4-19. Floyd R. W. «Syntactic analysis and operator precedence», JACM, Vol. 10, N. 3, 1963, pp. 316—333. 4-20. Ginsburg S. The Mathematical Theory of Context-Free Languages, New York: McGraw — Hill, 1966. 4-21. Glennie A. On the Syntax Machine and the Construction of a Universal Compiler, Tech. Rep. N. 2, Computation Center, Carnegie — Mellon U., 1960. 4-22. Gray J. N. and Harrison M. A. «Single pass precedence analysis», IEEE Conference Record of Tenth Annual Symposium on Switching and Automata Theory, 1969, pp. 106—117. 4-23. Greibach S. «A new formal form theorem for contextfree grammars,» JACM, Vol. 12, N. I, 1965, pp. 42—52. 4-24. Griffiths T. V. and Petrick S. R. «On the relative efficiencies of context-free grammar recognizers», CACM, Vol. 8, N. 5, 1965, pp. 289—300. 4-25. Harrison M. A. Introduction to Switching and Automata Theory, New York: McGraw — Hill, 1965. 4-26. Hopcrott J. E. and Dllman J. D. Formal Languages and Their Relation to Automata, Reading, Mass.: Addison — Wesley, 1969. 4-27. Ingerman P. Z. A Syntax Oriented Translator, New York: Academic Press, 1966. 4-28. Irons E. T. «A syntax directed compiler for ALGOL 60», CACM, Vol. 4, N. 1, 1961, pp. 51—55. 4-29. Kleen S. С «Representation of events in nerve nets», (in) Automata Studies, С E. Shannon and J. McCarthy (eds.), Princeton, N. J.: Princeton University Press, 1956, pp. 3—40. 4-30. Knuth D. E. eOn the translation of languages from left to right». Information and Control, Vol. 8, N. 6, 1965, 607—639. 4-31. Knuth D. E. «Top-down syntax analysis», Acta Informatica. Vol. i, N. 2, 1971, pp. 79—iiO. 4-32. Knuth D. E. «Semantics and context-free languages». Math. Systems Theory, Vol. 2, N. 2, 1968, pp. 127—146. 4-33. Kuno S. and Oettinger A. G. «Multiple path syntactic analyzer», IFIP Congress 1962, Poppleweli (ed.), Amsterdam: North — Holland, 1962, pp. 306—311. 4-34. McKeeman W. M., Hanning J. H. and Wortman D. B. A Compiler Generator, Englewood Cliffs, N. J.: Prentice — Hall, 1970. 4-35. McNaughton R. and Yamada H. «Regular expressions and state graphs for automata», IRE Trans. EC-9, Vol. 1, i960, pp. 39—47. 4-36. Parikh R. J. «On context-free languages», JACM, Vol. 13, N. 4, 1966, pp. 570—581. 4-37. Paul! M. С and Linger S. H. «Structural equivalence and LL-k grammars», IEEE Conf. of Record of Ninth Annual Symposium on Switching and Automata Theory, 1968, pp. 176-186. 4-38. Post E. L. «Recursive unsolvability of a problem of Thue.», J. Symb. Logic, Vol. 12, 1947, pp. i—11. 4-39. Rabin M. 0. and Scott D. «Finite automata and their decision problems», IBM. J. of Research and Development, Vol. 3, 1959, pp. 114—125. 192
4-40. Rosenkranlz D. J. «Matrix equations and normal forms for context-free grammars», JACM, Vol. 14, N. 3, 1967, pp. 501—507. 4-41. Rosenkrantz D. J. and Lewis P. M. II, «Deterministic left-corner parsing», IEEE Conf. Record of 11th Annual Symposium on Switching and Automata Theory, 1970, pp. 139—152. 4-42. Salomaa A. Theory of Automata, New York: Pergamon Press, 1969. 4-43. Samelson K. and Bauer F. L. «Sequential formula translation», CACM, Vol. 3. N. 2, 1960, pp. 76—83. 4-44. Wirth N. and Weber H. «EULER-a generalization of ALGOL and its formal definition», CACM, Vol. 9, 1966, pp. 13—23, 89—99. 4-4S. Wood D. «Bibliography 23: formal language theory and automata theory», Computing Reviews, Vol. 11, N. 9, 1970, pp. 417—430. 4-46. Younger D. H. «Recognition and parsing of context free languages in time n^». Information and Control, Vol. 10, N. 2, 1967, pp. 189—208. Глава 5 СОРТИРОВКА 5«If Введение Многие методы сортировки наиболее'просто описываются рекурсивными функциями, поскольку сортировку всего массива часто можно осуществить, применяя ту же стратегию, что и для его подмассивов. Существуют интересные соответствия между различными методами сортировки. Наибольший интерес представляют самые простые методы как составные части более сложных. Методы сортировки — это скорее самостоятельный предмет, чем отдельные программы. Один и тот же алгоритм часто можно описать несколькими способами. Первая из рассматриваемых ниже программ связана со структурой данных типа дерева. Путем выбора особых способов задания функций слияния и разделения эту программу затем можно применить к векторам или одномерным массивам. Программы сортировки основаны на операциях сравнения и простой пересылки или обмена. Часть элемента, которая определяет место его расположения, называется признаком. Соотношение, задающее порядок расположения, должно быть применено ко всем упорядочиваемым элементам, и само это соотношение либо обратное ему должно быть справедливо для любой пары элементов. Самые простые методы сортировки либо постепенно'" создают все более упорядоченную совокупность элементов поочередным добавлением признаков, либо перемещают наименьший признак совокупности, не изменяя ее состава. Вставка. Описываемая ниже функция insert добавляет новый признак в упорядочиваемый по возрастанию список, вставляя его перед первым признаком, который больше k: def rec insert к x = if null X then к : ( ) else if к < h X then к : x else h X : insert к (t x) V27 Бердж E. . 193
Список можно упорядочить, используя многократно вставку элементов с помощью функции (list2 ( ) insert I) или (listl { ) insert I). Выборка. Второй способ сортировки заключается в определении наименьшего элемента и его перемещения. Наименьший признак ненулевого списка можно выбрать с помощью описываемой ниже функции select, результатом применения которой является пара, первый член которой — наименьший признак, а второй—оставшаяся часть списка; def гее select х = if null (t х) then h X else let у z = select (t x) if h X < у then h X, у : z else y, h X : z Упорядоченный список можно получить последовательной выборкой наименьшего признака следующим образом: def гее sort х = if null X then ( ) else let у, z = select x у : sort z Слияние. Третий простой метод сортировки заключается в слиянии двух упорядочиваемых списков или массивов. Из двух списков отбирается и пересылается в результирующий список элемент с наименьшим признаком, после чего с остатками списков проделывается та же процедура: def гее merge х у = if null X then у else if null у then X "^Ise if h X < h у then h X : merge (t x) у else h у : merge x (t y) Непустой список элементов можно упорядочить методом слияния. Каждый элемент трактуется как список, состоящий из одного элемента, и осуществляется их слияние до тех пор, пока не останется один упорядоченный список def гее mmerge х = let гее mergepairs у = if null у then ( ) else ff null (t y) then у else (merge (h у) (h (t y)) : mergepairs (t (t y)) if nul! (t x) then h X else mmerge (mergepairs x) 194
Число элементов, передаваемых из одного массива в другой, можно представить как значения внешних узлов дерева, отражающего структуру слияния. В качестве примера приведем три различных способа слияния пяти списков: v, w, х, у, г, содержащих по одному элементу: merge (merge v w) (merge (merge x y) z) merge v (merge w (merge x (merge у z))) merge v (merge (merge w x) (merge у z)) Веса сформированных таким образом деревьев равны соответственно 12, 14 и 13. Самым эффективным представляется использование полностью выравненного дерева [5—5]. 5.2» Векторы упорядочения Четвертый метод сортировки, называемый обменом, применяется к вектору признаков. Основной операцией здесь является сравнение элементов, занимающих две позиции. За сравнением может следовать перестановка или обмхн. Каждая перестановка приводит вектор к более упорядоченному виду, перемещая меньшие признаки в одну сторону, а большие — в другую. К векторам применимы как операции вставки, так и выборки, причем их можно свести к операциям обмена. В этой главе векторные методы упорядочения будут применяться к вектору А, п элементов которого имеют индексы от 1 до п. Список позиций pi, р^, рз, ..., Рт этого вектора будем называть последовательностью. Результатом упорядочения последовательности должна быть такая перегруппировка, в результате которой элементы А Ipi), А Ip^], .». ..., А [рщ] будут расположены в неубывающем порядке. Для обозначения обмена используется инфиксный оператор : = : . С помощью функции insert (вставить) можно образовать функцию insertc, которая воздействует на ненулевую последовательность (pi, /?2, Рз, ■■■, ргг)- Она помещает А [pi] в упорядоченный список А [р^], А [/?д], ..., А \рт^ при использовании операции обмена def гее instrtc А х = if null (t х) then exit else if A [h x]< A [h (t x)] ' then exit else A [h x] : = : A [h(t x)] insertc A (t x) Последовательность x можно упорядочить поочередной вставкой элементов, осуществляемой следующим образом: def гее sort А х if null X then exit else sort A (t x) insertc A (h X : t x) 195
5 5 5 5 5 3 3 3 3 1 1 1 1 1 2 6 6 1 2 3 4 2 2 3 4 2 4 4 4 5 2 4 6 6 6 6 Рис. 5.1. Прямая вставка Последовательность можно представить как поток. Если поток имеет вид A step 1 until п), то программа сортировки соответствует методу прямой вставки. Пример выполнения сортировки вектора, осуществляемой таким методом, дан на рис. 5.1. for i : = п — 1 step — 1 until n do for] : = i step 1 unMl n —1 do if A [j ] < A [j - 1 ] then exit else A [j ] : = : A [j + 1 ] Упорядочение векторов можно осуществлять и с помощью многократного повторения процедуры выборки наименьшего признака. В таком случае сначала определяется положение наименьшего признака, с которым затем обменивается местом первый признак: def гее find х = if null (t х) tlien h X else let у = find (t x) if A [h x]<A [y] tfien h X else у Полное упорядочение достигается повторением подобной операции: def гее sort х if null X tfien X else A [ h x] : = : A (find x] h X : sort (t x) В ЭТОЙ программе допускается обмен элемента с самим собой (Л 1х] : = : А [х]). Последовательность можно представить потоком. В этом случае соответствующая последовательность A step 1 until ft) приводит к простейшей ]программе, реализующей линейную выборку. Пример последовательного упорядочения массива методом линейной выборки дан на рис. 5.2. for i : = 1 step 1 until n — 1 do к : = i for j : = i -|- 1 step 1 until n do if A (k]> A [j] tlien к : = j A-fi] : = : A [k] 196
5 3 3 2 2 2 2 1 5 5 3 3 3 6 6 6 6 4 4 4 4 4 4 6 5 2 2 3 5 5 6 Рис. 5.2. Линейная выборка Обнаруженный здесь наименьший признак запоминается в позиции k, и после завершения поиска наименьший и первый признаки меняются местами. Такой же алгоритм применяется и к оставшейся части списка, который представляется как пара (i, п), где 1 ■<: i < п + 1. Список является нулевым, если i » = п + 1, в противном случае голова списка равна i, а его остаток есть (i + 1, п). Слияние. Вектор можно упорядочить путем слияния. В таком случае для сохранения остатков списков необходимо использовать, второй вектор Р. Если А [i] является головой списка, то Р [i] содержит позицию следуюш;его элемента в упорядочиваемой строке. Пустой список представляется нулем. Ниже приведены обозначения функций, определенных на множестве списков» null X X = О h X А [х] t X Р [х] prefix X у Р [х]: = у; X О О В итоге функция merge (слияние) принимает следующий вид» (Vf гее merge х у = if х= О then у else if у = О then X else if А [х] < А [у] | then Р [х] : = merge Р [х] у; х еЬе Р [у] : = merge х Р [у]; у При представлении каждого элемента в виде списка из одного элемента начальным состоянием будет Р {1\ = 0,\ <. i <. п. Для полного упорядочения весь интервал A, п) разделяется на два примерно равных сегмента, каждый из которых упорядочивается, после чего осуществляется их слияние. Здесь и далее в этой главе для обозначения целой части результата деления будем использовать, как и в АЛГОЛ-60, оператор ч- . sort 1 п where гс с sort х у = if х=0 then у else let с = (х+ y)-^2 merge (sort х с) (sort (с + I) у) 7 .Бердж в. 197
Максимальное число сравнений определяется рекуррентным уравнением S (п)> S Г "/2 1 + S L п/2 J + п — 1, S A) = О, решение которого имеет вид S (п) = ПА — 2* + 1, где ^ = [~ loga/i"!' Потоки слияния. Метод слияния можно видоизменить таким образом, чтобы он обрабатывал и образовывал потоки, а не списки! def гее merge х у = if null X then у else if^nulls у then X else if hs X < hs у then prefix (hs x) (merge (ts x) y) else prefix (hs y) (merge x (ts y)) Следовательно, получить поток для упорядоченного списка можно с помощью слияния списка потоков. Поскольку к голове потока необходимо обращаться более 1 раза, для представления потоков эффективнее использовать буфер единичного объема. Каждый поток при этом представляется парой, первым элементом которой является голова последовательности, а вторым — остаток старого потока. При этом используются следующие обозначения; nulls S (hs) = end nullists (end, generate I end) hs h ts (x, s) s ( ) prefix X s X, X ( ) . s Функция merge с учетом этих обозначений принимает вид def гее merge х у = let U, f = X let V, g = у if u = end then у else if V = end then X else if u < V then u, Ä, ( ) . merge (f ( )) у else V, Я ( ) . merge x (g ( )) Функция mmerge порождает упорядоченную пару потоков, первый из которых представляет собой наименьший элемент, а второй —поток, принимающий форму дерева или турнира, называемого утраченным деревом. Для выполнения функции mmerge требуется п — 1 операция сравнения. Для получения элементов остающегося потока необходимо произвести по крайней мере logs п сравнений. 198
- minx у max xy Рис. 5.}/Эл*менты схемы срав- Схемы упорядочения« Стратегии вставки и выборки можно также представить как варианты фиксированного набора операций сравнения с возможным последующим обменом, осуществляемым блоком сравнения независимо «»нйя' от результатов предыдущих операций. Процесс сравнения можно представить в виде схемы или контурной диаграммы, элементы которой будем изображать одним из двух способов, приведенных на рис. 5.3. Последовательные изменения списка удобно изображать |на странице, где меньшие признаки всегда движутся влево. Схемы методов прямой вставки и линейной выборки показаны на рис. 5.4 и 5.5. Схема слияния. Сортировку на основе слияния можно также описать с помощью схемы сравнения. Операция сравнения двух элементов осуществляет их слияние и упорядочение. Четыре элемента упорядочиваются путем создания двух строк, каждая из которых состоит из двух элементов, и последующего слияния этих строк, как показано на схеме, приведенной на рис. 5.6. В общем случае схема сортировки для 2* элементов состоит из двух схем сортировки для 2*-' элементов и схемы слияния двух строк длиной 2*-' каждая. Схема слияния для 2* элементов состоит из двух схем слияния для 2*~' элементов и 2*~' — 1 операций сравнения. Вся схема упорядочения для 2* элементов приведена на рис. 5.7. Вначале объединяются нечетные, а затем четные элементы, в результате чего получаем новые массивы tv^Oxd^i »..и Ьфф^1 3 7___2 6 3 2 1___J J___2 6 7 2 J__/ 7 2 J ff__7 J 3 В 7 5 3 8 7 2 3 2 3 г 3 5 e Phc. S,4. Схема прямой «стаин 7« 'вса S.S. Схема линейной выборки 199
Рис. S.e. Четно-иечетная схема сортировки слиянием для четырех элементов показанные на рис. 5.7. Теперь % является наименьшим, а ö„ — наибольшим элементом, где п = 2*^'. Наконец, для сравнения пар (йг, bi), {аз, bi), @4, Ьз), .... (а„, b„,i) используется 2*~' — 1 операции сравнения. Сразу не очевидно, что такой метод правильно упорядочивает числовой массив. Простейшим доказательством этого является теорема Бурисиуса [5—29]. Предположим, что алгоритм или устройство упорядочения состоит из операции сравнения двух чисел. В случае, когда первое число больше второго, предусматривается одна операция, а когда первое число меньше, — другая. Если два числа равны между собой, то результат непредсказуем: может выполняться либо первая, либо вторая операция. Теорема Бурисиуса утверждает, что если алгоритм упорядочивает любой массив, в котором в качестве признаков содержатся только два разных числа (например, 10,1}), то с его помощью можно упорядочить любой числовой массив. Эту теорему можно использовать для доказательства правильности работы метода слияния. Предполагаем, что в первом массиве содержится S нулей, а во втором — t нулей. Тогда в массиве 01, Ö2, ... будет ps/2~| + \'~tl2' ,_s/2_| + \_t/2_\ нулей. Следовательно, количества нулей в а и b либо совпадают (sn t — четные), либо различаются на единицу (одно из чисел s и ^ четное, [другое — нечетное) или на две единицы (s и t — нечетные). Если эти количества равны или различаются на единицу, массив 01^102^2^3^3 ••• оказывается упорядоченным. Если же разность между ними равна двум, то нули и единицы должны расположиться следующим образом: ababababab а... О О О О О 0.0 1 О 1 1... и в результате заключительных сравнений этот массив станет полностью упорядоченным. 200 а в массиве Ь^, Ь^, Схема copmufiOÖKU 2^-7 Схема сортировки Схема слияния (./te-jemHi/e элементы) ,fl-2 2x2 Схема слияния (vemM/e мементш Ьг, Рис, 5.7. Формиромиив схемы сортировка слияяаем
Если М (п) представляет собой число сравнений, необходимых для реализации схемы слияния, а S (п) — число сравнений в схеме упорядочения, то справедливы следующие соотношения: • 5 B*) = 25 B*->) + М B*) М B*) = 2М B*-«) + 2*-> — 1 S{2) = М B) = 1 М B*) = (k~ 1J*-2+ 1, откуда следует, что 5 B*) = (Ä:^ — й + 4) 2*-2 — 1. Стратегию, используемую в схеме слияния, можно применить и для сортировки последовательностей. При этом вначале упорядочиваются подпоследовательности A, 3, 5, 7 .... 2q-\); B, 4, 6 2q), затем — две последовательности A, 2, 5, 6, 9, 10, ...); C, 4, 7, 8, 11, 12, ...) и, наконец, получаем (q — 1) подпоследовательностей 1; B, 3); D, 5); F, 7); ...; Bq-2, 2q - I); 2 Для упорядочения всех последовательностей используется один и тот же метод. При этом предполагается, что расстановка элементов, полученная в результате упорядочения первых последовательностей, не нарушается при упорядочении и вторых двух последовательностей. Это предположение представляет собой частный случай явления, которое рассмотрели Гейл и Кап [5—16], назвавшие сдвигом ряд последовательностей, в которые входят все упорядочиваемые элементы. Помимо решения ряда других вопросов, они выявили условия, при которых сортировка любого сдвига сохраняет расстановку элементов, полученную в результате другой сортировки. к«; 1. Объединение двух сдвигов не может содержать ориенти- рованных циклов, поэтому невозможен переход от позиции к са- мой себе при движении по последовательностям, сформированным из двух сдвигов. 2. Смежные позиции в последовательности одного сдвига не сохраняются в той же самой последовательности другого. 3. Два сдвига должны удовлетворять свойству коммутатив- ности. Это означает, что если возможен переход от одной позиции к другой при движении по последовательности первого сдвига, за которой следует последовательность второго сдвига, то можно получить тот же результат, выполняя сдвиги в другом порядке, т. е. вначале второй, а затем первый. *' I Непосредственные следствия этой теоремы проиллюстрированы ниже на примере двумерного массива. Два сдвига, состоя- 201
7 S e S в 7 f Z 9 ^ t S S 3 1 Z г Z 3 3^7 2*7 3 e * 3*6 see зев По строкам По столВцам Рис, 5.8. Упорядочение столбцов сохраняет упорядочение строк щие ИЗ последовательностей столбцов и строк, удовлетворяют всем трем условиям. Следовательно, если двумерный массив упорядочивается вначале по строкам, а затем по столбцам, то в результате он остается с упорядоченными строками. Пример приведен на рис. 5.8. Метод Шелла. Одномерный вектор можно представить в виде двумерного массива из р строк, каждый (t, /)-й элемент которого соответствует \i Л- р У- (/' — 1)]-му элементу исходного вектора. Говорят, что вектор упорядочен с шагом р, если упорядочена каждая из р строк соответствующего ему массива. Суть метода Шелла заключается в том, что вектор несколько раз упорядочивается с шагом р, причем значения р убывают. Полное упорядочение достигается при р ^= \. Используя функцию вставки insertc, программу упорядочения с шагом р применяют до тех пор, пока whiles «п) (generate (-f-p) i) for 1 < i < р . Из теоремы Гейла и Капа следует, что вектор, упорядоченный с шагом р, остается упорядоченным и после его упорядочения с шагом q. Программу упорядочения с шагом р можно составить в следующем виде: for / = 1 step 1 until п — р do for i : = / step — p until 1 if Afi-f p]>A [i] tlren A [i+ p] : = : A [i] else exit Ha рис. 5.9 приведен пример сортировки вектора методом Шелла со следующей последовательностью шагов упорядочения: 7, 5 и 1. Существует разновидность метода Шелла, основанная на том'факте, что для завершения сортировки сектор, последовательно упорядоченный с шагом 3 и с шагом 2, не пужлястся в последующем упорядочении с шагом I, выполняемо:.! в го 'iHOM объеме. При этом достаточно сравнивать только смсжхглз пр!!знаки. Более того, если признаки Ali] и А [t + I ] поменялась местами, нет необходимости сравнивать Л [t + 11 и А \i + 2], поскольку вектор был ранее упорядочен с шагом, равны:,! 2. В рассматриваемой разновидности в качестве последовательности шагов р используются значения 2^3'. При этом программу сортировки можно составить так, чтобы при упорядочении как с шагом 2, так с шагом 3 использовались те же приемы, что и при 202
15 13 1 7 Сортировка с шагом 7 15 13 1 7 5 14 9 4 2 8 11 3 12 10 5 14 9 4 6 2 8 11 3 12 16 g Преобразуется к виду 4 6 15 2 13 1 8 7 11 3 5 12 14 9 10 т. е. 2 1 7 3 12 9 б 13 8 11 Б 14 10 15 Сортировка с шагом 3 4 7 9 2 3 6 1 12 13 8 11 5 14 10 15 Преобразуется к виду 7 8 9 14 3 6 10 И 5 12 13 15 т. е. 4 2 1 Сортировка с ша- 1 2 3 гом 1 7 3 5 4 5 6 12 9 9 10 10 13 14 И 15 И 12 13 14 15 Рис. 5.9. Сортировка методом Шелла упорядочении с шагом 1. Программа, реализующая подобную идею, будет, однако, дважды осуществлять упорядочение с шагом 6, по одному разу как составная часть упорядочений с шагом 3 и с шагом 2 соответственно. Последовательное упорядочение с шагом 3 и с шагом 2 можно осуществить с помощью упорядочения с шагом 2 и программы перестановки, например, следующим образом: sort 1 п where гес sort а р п = sort3 а р sort2 а р interchange а р and sorts а р if а + Зр > п then exit else sort a (Зр) sort (a + p) Cp) sort (a + 2p) Cp) and sort2 a p if a -f 2p > n then exit ' else sortm a Bp) sortm (a + p) Bp) and sortm a p = sort 2 a p interchange a p and interchange a p = intch 0 where reo intch (q) = ifa-bp+q<n then exit else ifAIa+q]<A [a+p+ql then A la -f- q] : = : A [a + P + q] intch (q 4- 2p) else intch (q -+■ p) 203
5.3. Деревья двоичного поиска Упорядоченные признаки можно хранить и с помощью дерева двоичного поиска. Дерево двоичного поиска формируется таким образом, что его левая и правая ветви также являются деревьями, причем все признаки левой ветви меньше корня дерева, а признаки правой ветви больше корня. Вводимый в массив новый признак включается в левую ветвь дерева, если он меньше его корня, или в правую, если больше. Форма дерева зависит от порядка размеш,ення признаков. После вставки в дерево признак больше не перемеш,ается. Функция вставки признака k в дерево х имеет следующий вид: def гее insertb к х = if empty X then cbtree к etree etree else if к < root x then cbtree (root x) (insertb к (left x)) (right x) else cbtree (root x) (left x) (insertb к (right x)) Дерево можно выпрямить в упорядоченный список с помощью функции flatn: def rec flatn t = if empty t then ( ) else concat (flatn (left t), u (root t), flatn (right t)) Список признаков можно ввести за один раз с помощью функции {list 2 etree insertb I). Последовательность построения дерева двоичного поиска для массива 729618345 приведена на рис. 5.10. Аналогично можно получить и лес деревьев, изменяя функции формирования и выборки согласно следующей схеке: Двоичное etree root х left х right X cbtree X ; дерево у Z Лес О root (h х) listing (h х) t X (etree X у) : z Функция, предназначенная для ввода в лес новых элементог!, имеет вид def rec insert! к х = if null X then (etree x ( )) : ( ) else if к < root (h x) then etree (root x) (inscrtf к (listing (h x))) : t x else etree (root x) (listing (h x)) : inscrtf к (t x) 204
•7777 7 / / \ /\ / \ 2 2 9 2 9 2 9 \ /\ 6 16 7777 /\ /\ /\ /\ 2 9 2 9 2 9 2 9 /\/ /\/ /\/ /\/ / 6S 7 6 8 7 6 8 7 6 8 /// J J ^ \ Рис. 5.10. Этапы формирования дерева двоичного поиска На рис. 5.11 приведена последовательность действий при построении леса, соответствующего массиву 72961834 5. Для того чтобы вставить признак, его необходимо последовательно сравнить со всеми признаками, встречающимися при движении по ветвям дерева в направлении к его вершине. Следовательно, при определении среднего числа сравнений, необходимых для вставки нового признака в дерево двоичного поиска, имеющего п признаков, необходимо учесть веса деревьев, полученных из 1 1 Х\ 7 7 7 Э 1 1 2 2 /^ 7. 3 /\ 2 6 У\ 7 Э 2 6 /\ х\ у\ /^ 7 3 7 S 7 3 7 3 /\ I /\ I /\ I /\ I 2 S 8 г ваг в в z ев I II I /\ I /1\ 7 Г 3 1 3 't 1 1 ^ 5 Рис. 5.11. Этапы в формировании леса 205
Перестановка 1 Z3 1 JZ Л Z 213 2S1 3 21 Рис. 5.12. г Дере: Двоичное дерево '^2 ^^3 2^ <2 2 1^\ 2 1^^3 ^J= е вья двоичного DSpaai/Hitiia. gii/ннция 1+Х+Х^ 1+х+х^- 1+х+х^ 1+2Х 1+2Х l+x-1-x^ (e+Sx+^x^J поиска Образующая всех перестановок элементов множества {1, 2, 3, ..., п\. Рассмотрим обобщенное двоичное дерево следующего вида: Л-В-двоичное дерево либо — является атомарным и есть В ^- либо имеет корень А ■ и левую и правую части, являющиеся А-В- двоичными деревьями. Если An (х) — образующая функция, коэффициент которой при А"* является средним числом внутренних узлов на k-M уровне дерева двоичного поиска с п внутренними узлами, а Вп(х) — среднее число внешних узлов дерева на уровне k, то Л„ A) = п — числу внутренних узлов, и справедливо соотношение' Л„ (х) + Вп(х) = \ + 2хАп (х). На рис. 5.12 приведены шесть деревьев двоичного поиска с тремя внутренними узлами и двумя образующими функциями. При добавлении нового элемента дерево расширяется путем превращения узла типа В в узел типа А. Вероятности расширения двоичного дерева равны для каждого конечного узла, поэтому можно записать Лп^1 (х) = /1„ (х) + A/(п + 1)) в, {X), откуда следует, что Ап,1 (х) = A/(п + 1)) +1{{п + 2х)/{п + 1)) An (X). Среднее значение внутренних узлов дерева двоичного поиска равно S„ = An A). Дифференцируя An+i (х) по х и полагая X = I, получаем Sn^i = B/(п + 1)) An A) + {{п + 2)/(л + 1)) S„. Поскольку Л„ A) = 1, число внутренних узлов Sn+i'(n + 2) = SJin + 1) = 2rt/(n + I) (n + 2) = = 2n (\/(n + I) ~1 (n + 2)). Такил! образом, если Нп= t Vk, *=i 206
то SnJin + 2) = 2 (Я,,1 -1)- 2n/(n + 2) и S„ = 2(n+ 1) Я„ —4n. Значения внешних узлов мол<11о получить из ß„, задавая ß^ (X) = 2Л„ (х) + Bх - 1) An (х) и В'п{1) = 2п + А'пЩ. Следовательно, среднее значение внешних узлов дерева двоичного поиска с п узлами определяется соотношеппем S„ + 2п = 2 (п + 1) Я„ — 2л. Вводимая ниже функция tsearch служит для поиска требуемого признака в дереве двоичного поиска. При успешном поиске программа обращается к тому дереву, корень которого является искомым признаком, в противном случае происходит обращение к пустому дереву. def гее tsearch к х = if empty X then X else if к = root x then X else if к > root x then tsearch к (left x) else tsearch к (right x). Среднее число сравнений при безуспешном поиске равно среднему числу сравнений, ь!еобходимых для вставки {п -\- 1)-го элемента в дерево двоичного поиска, содержащее п элементов. Математическое ожидание значения всех (п + 1) внешних узлов равно 2 (п -\- \) Н„ — 2л, следовательно, среднее число сравнений при безуспешном поиске будет равно B (л + 1) Я„ — 2л)/(л+ + 1) = 2Я„^.1 — 2. При успешном поиске среднее число сравнений будет на единицу (больше, чем длина внутреннего пути дерева, и равно, следовательно, 1 -f- B (п'+ 1) Я«"— 4л)/л = B (п + + 1)/п) Я„ - 3. Для удаления из дерева элемента k вначале необходимо найти ветвь, корень которой k, и затем изъять этот корень. Процесс замены исходного дерева новым деревом с удаленным корнем проще всего выраз!;-ть в терминах лесов. После удаления корня головы списка образуются два леса. В результате применения операции конкатенации к этим лесам получается лес, обход которого в обратном порядке и дает упорядоченный список. Такой же результат можно получить и с помошью замены крайнего правого пустого дерева, содержащегося в левом дереве, правым деревом: def гее replace v х = if empty X then у else ctree (root x) (left x) (replace у (right x)) 207
5.4. Двоичная вставка Сегмент (от, п) упорядоченного вектора А можно трактовать как деревр двоичного поиска, корнем которого является элемент, расположенный посредине сегмента на позиции с = (от + п + -f 1)^-2, а левая и правая ветви дерева являются соответственно сегментами (от, с — 1) и (с + 1, п). В программе двоичной вставки вначале определяется положение, которое должен занять новый признак, после чего для освобождения места элементы перемещаются вправо от этого положения. Позиции сегмента (от, п) после ввода в него нового признака будут иметь номера т, от + 1, от + 2, ..., п, п + 1, и их можно трактовать как объединение. Объединение будем представлять в виде пары (от,, п). В атомарном объединении т == п. Левая часть неатомарного объединения является сегментом (т, с — 1), а ее правая часть — сегментом (с, п), где с = (т -\- п -\- 1)ч-2. Программа posn, служащая для определения положения нового признака k в упорядоченном сегменте (от, п), имеет следующий вид: def гее posn к (т, п) = if m = п then if к < А (m) then m else m + I else let с = (m + n Ч- 1)^-2 if k> A [c] then posn к (m, с — I) else posn к (с, n) Применяя ряд операций двоичной вставки, вектор можно полностью упорядочить следующим путем: for i : = 2 step I until n do let p = posn .4 [i] (I, i — I) if p^i then t : = A.[i] for r : =1 step — I until p ->r 1 do A [r] : = A [r— I] A [p]:= t Среднее число сравнений, необходимых при использовании метода двоичной вставки, меньше, чем среднее число сравнений для метода прямой вставки. Для вставки нового признака в упорядоченный вектор размера / требуется, по крайней мере, I 1о§2 (/ + 1) I сравнений, и их среднее число оказывается равным k -{- I — 2*/(/ 4- 1), где k = r~log.2 (/ + 1)~1. Это обусловлено тем, что дерево, лежащее в основе стратегии двоичной вставки, является выравненным с (/ + 1) концевыми точками, представляющими позиции, которые может занять новый признак. Число сравнений, необходимых для нахождения такой познцип, пред- 208
ставляет собой длину пути до нее. Среднее число сравнений для упорядочения п признаков определяется выражением- п Ц (й + 1 - 24j), где k = flog, /1, /=1 а максимальное число п Erlogt л = nk -2^ + 1, где k = |_log2 (л + 1)_|. /=1 Двоичный поиск. Представляется возможным определить, принадлежит ли данный признак упорядоченному вектору признаков, и если да, то найти его положение можно с помощью метода, аналогичного методу двоичной вставки, где рассматриваемый признак сравнивается со средним признаком. Если эти признаки равны, поиск завершен, в противном случае один из сегментов, расположенный слева или справа от признака, анализируется точно так же. Программа нахождения признака не дает результата, если сегмент, в котором осуществляется поиск, оказывается пустым. Основная структура является двоичным деревом, представленным сегментом (т, п). Дерево оказывается пустым, если т '^ п. Если же оно не пустое, его корень занимает позицию с = (т + п)-^2; здесь, как и ранее, оператор-^используется для определения целой части результата деления. Левое и правое двоичные деревья представляют сегменты (т, с ^ 1) и (с -f 1, п) соответственно. Программа двоичного поиска имеет следующий вид: def гее bsearch к (га, п) = if га > п then fail else let c= (m-H п)-ь2 if k= A [c] then с else bsearch к (if к < A [c ] then (m, с — 1) else (c+ 1, n)) Среднее число сравнений, необходимых для нахождения содержащегося в векторе признака, оказывается большим, чем средняя длина пути до внутренних узлов дерева. Общая длина пути при этом равна величине II Lbg2 /J = (n -М) (Ä: -М) - 2* - 2n, где k = rioga (л + 1I. 209
5.5. Быстрая сортировка Рассмотрим еще один алгоритм формирования деревьев двоичного поиска, который не только генерирует то же самое двоичное дерево, но и осуществляет те же сравнения, что и алгоритм двоичной вставки, только в иной последовательности. Здесь за корень принимается первый элемент неупорядоченного масспва, а остальные элементы разделяются на два списка, причем все признаки одного из них меньше признака корня, а все признаки другого — больше. Такому же разделению затем подвергаются обе части при формировании левой и правой ветвей дерева. Функция, реализующая такой алгоритм, имеет следующий вид: def гее qs х = if null X then etree else let d = h X let y, z = partition d (t x) cbtree d (qs y) (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 y, h X : z В TO же время выпрямить дерево можно с помощью функции qsl, используемой вместо qs: def rec qsl х = if null X then ( ) else let d == h X let y, z = partition d (t x) concat (qsl y, u d, qsl z) Рассмотренный метод можно применить к сортировке вектора Л [1 ], Л [2], ..., Л [п\, используя при этом операции обмена. Соответствующая программа имеет имя quicksort. Признаки Л [2], Л [3], ..., Л [п] разделяются на две группы. Признаки одной из групп меньше А [1 ], а признаки второй — больше, чем Л [11. В результате разделения получаем вектор, в котором все признаки, меньшие Л 111, находятся в его левой части, а большие Л [1 ] — в правой. Это осуществляется следующим образом. Просматривая исходный вектор слева направо, находим первый признак, превышающий Л [1]; затем, просматривая вектор справа налево, обнаруживаем первый признак, меньший Л [1], после чего меняем эти найденные признаки местами. Далее такой процесс поочередного просмотра слева и справа с последующим обменом повторяется. При этом в самом начале признак Л [1] удаляют из вектора. На это свободное место позднее помещают признак 210
меньший, чем А [1 ], найденный при первом просмотре вектора справа, место которого заполняют признаком большим, чем А [1 ], найденным при просмотре слева, освобождая, в свою очередь, место для другого признака, и т. д. Перебор признаков осуш,ествляется до тех пор, пока оба указателя не встретятся. В этот момент они указывают на место разделения признаков, куда затем и помеш,ается признак А [1 ]. После этого как левый, так и правый сегменты упорядочиваются аналогично. Текст программы quicksort, упорядочивающей сегмент (т, п) вектора А: def гее quicksort (m, п) = if m > п then exit eise let i = partition (m, n) A [i]:= A [m] quicksort (m, I — 1) quicksort (i + 1, n) where fee partition (m, n) let d= A [m] let E= Jl down (m, n) where fee down (i, j) = let к = findb (i, i) Ali] := Alk] up(i+ l.k) and up (i, j) = let к = findf (i, j) A Ij] := Alk] down (k, i — 1) where rec findb (i, j) = if i= j then E i else if A Ij ] < d then i else findb (i, j — 1) and rec findf (i, j) = if i = jj then E 1 else if A [i]>d then i else findf (i-f l,j) Функция findb определяет положение первого элемента, меньшего d, при просмотре справа налево, начиная с /; findf определяет первый элемент, больший d, при просмотре слева направо, начиная с позиции t. При встрече указателей завершается работа функции partition. В результате получаем номер той позиции, где встретились указатели. После этого попеременно вызываются взаимно рекурсивные функции down и up. Среднее число сравнений при упорядочении п элементов с помощью функции quicksort оказывается точно таким же, как и при формировании деревьев двоичного поиска с п узлами: 2 (л — 1) Я« — 4л. Пример выполнения действий в процессе разделения приведен на рис. 5.13. i 1ридавая каждому корню значение, на единицу большее числа узлов в его левой ветви, можно определить наименьший k-ti элемент. Это значение называют размером дерева. Ниже приведен алгоритм определения й-го элемента: def rec th к X = If empty X then fail else if к = size я then root X else if к — size x then th (k — size x) (right x) else tb к (left x) 211
5 D 5 6 6 3 3 3 a 5 6 r. Ü 3 3 2 ■; 2 2 2 4 4 4 4 4 Рис. 5.13. Этапы расчленения Метод быстрой сортировки можно применить для нахождения наименьшего /г-го элемента неупорядоченного вектора. Вначале с помощью функции partition определяется положение i, которое должен занять первый элемент. Если i = k, то А Ik] является наименьшим k-м элементом; если k > i, то наьменьший элемент находится в правом сегменте (i + 1, п); в противном случае наименьший /г-й элемент находится в левом сегменте A, i — I): def rec th к (га, n) = if m > n then fail else let i = partition (m, n) A [i]:= A [m] if k= 1 tiien A [i] else if к > i then th(k —i) (i + 1, n) else th к (m, i—1) 5.6. Обмен no основанию системы счисления и память типа «trie» Обмен по основанию системы счисления напоминает быструю сортировку, поскольку он осуществляет просмотр списка признаков с обеих сторон по направлению к середине. Однако каждый этап разделения определяется здесь результатом проверки значения старшего разряда. При просмотре признаков с левой стороны определяется первый ноль в старшем разряде, а при просмотре справа — первая единица, после чего эти признаки обмениваются местами. Подобный процесс затем повторяется для второй значащей цифры и т. д. Пример осуществления обменной сортировки по основанию дан на рис. 5.14. Метод напоминает также и древовидную сортировку. При таком подходе признаки хранятся в виде множества путей от корня до конечной вершины 12 13 6 15 10 5 14 8 3 4 1 1100 1101 оно 1111 1010 0101 lilO 1000 ООП 0100 0001 0001 0100 оно ООН 0101 1010 1110 1000 mi 1101 1100 0001 ООН оно 0100 0101 1010 1000 1110 1111 1101 1100 0001 ООН 0101 0100 оно 1000 1010 1100 1101 1111 1110 0001 ООН 0100 0101 оно 1000 1010 1100 1101 1110 1111 1 3 4 5 6 8 10 12 13 14 15 Рис. 5.14. Пример обменной сортнровкн по основанию 212
Рис. 5.13. Дереаовидиая память и обмен по основанию /4- • IS дерева, так что общие начальные сегменты необходимо запоминать только 1 раз. Множество байтов при такой организации хранения преобразуется в дерево, приведенное на рис. 5.15. Этот способ хранения последовательности признаков называют памятью типа «trie». В общем случае соответствующее дерево имеет структуру леса. Элементы, хранимые в таком лесу, могут иметь переменную длину. Поскольку слово может быть префиксом другого, в дереве необходимо хранить признак его конца. Методы быстрой сортировки и обмена по основанию системы счисления приводятся во взаимное соответствие предположением, что в деревьях обмена по основанию системы счисления принятие решения в зависимости от значения О или 1 фактически представляет собой сравнение с числом 2" для соответствующего п. При этом О соответствует результату «меньше», а 1 — результату «больше» или «равно». Это и есть значения внутренних узлов. Число сравнений или проверок двоичных значений равно сумме длин траекторий до конечных узлов дерева. Функция для добавления нового списка в лес def гее addtree к х = if null к then X else if null X then (ctree (h k) (addtree (t k) x)) else if (h k) = root (h x) then (ctree (root (h x)) (addtree (t k) (listing x) else h X : addtree к (t x) О t X) 213
функцию поиска дерева для списка элементов назовем sear- chtree: def rec searchtree к x = if null к then true else if null x then false else if (h k) = root (h x) then searchtree (t k) (listing (h x)) else Searchtree к (t x) 5.7, Вставка дерева и его перестройка В этом параграфе рассмотрен алгоритм, близкрш к алгоритму сортировки с помощью дерева двоичного поиска. Признаки образуют двоичное дерево, причем они упорядочены в направлении его роста. В случае объединенного леса корень меньше, чем любой элемент исходящей из корня ветви, а сами элементы располагаются в порядке возрастания. Лес формируется путем последовательного добавления элементов. Каждый новый элемент сравнивается с корнями деревьев высшего уровня в существующем лесу, и формируется новое дерево, в которое этот элемент входит в качестве его корня, а все деревья высшего уровня, значения корней которых превышают значение нового элемента, — в качестве его непосредственных ветвей. Полученное таким образом дерево- затем добавляется к уже существующим деревьям, в результате чего образуется новый лес. Следовательно, значение корня дерева будет всегда меньше значений корней его ветвей и всегда больше, чем значение корня соседнего левого дерева. Таким образом, значения корней деревьев в каждом лесу всегда располагаются в порядке возрастания. Функцию вставки дерева х в лес у назовем insert 1. На рис. 5.16 приведены этапы формирования леса из совокупности чисел I I Л ^ ^ " ' 2 7 2 7 8 17^3 А I А !/1\ /\ /\ 1/1\ 5 7 » 3 1 & 3 Л / 7 /\ 2 ^ 1\ /1 5 7 в \ 3 1 i в 1 \ С Рис. 5-16. Форхирсвекие леса с использоБакием ^^нкики insert 1 214
5J2 7.8 9 4 1 6 3. Заметим, что элементы вставляются в лес по одному и справа налево: def гее insertl х у = if null у then X : ( ) else if root x > root (h y) ttien X : у else insertl (ctree (root x) (h у : listing x)) (t y) Следует в первую очередь отметить, что заданное таким образом соответствие между совокупностями чисел и лесами (т. е. возрастание как в направлении роста, так и слева направо на каждом уровне) является однозначным. Это подтверждается тем, что породившую лес совокупность всегда можно восстановить путем упорядоченного перебора деревьев, а в кажом дереве — перебором его ветвей. Описанный метод считывания называют упорядоченным обходом. Операции формирования леса не нарушают исходной последовательности, получаемой в результате считывания. Для проведения анализа удобно рассмотреть двоичные деревья, лежащие в основе получаемых лесов. На рис. 5.17 изображена копия дерева двоичного поиска, приведенного на рис. 5.10. Следует отметить, что оба двоичных дерева на рис. 5.17 имеют одну и ту же форму. Дерево а — это двоичное дерево, из которого образован лес, полученный неоднократной вставкой элементов из совокупности 52789416 3. Дерево b служит деревом двоичного поиска для совокупности 72961834 5, являющейся перестановкой совокупности 52789416 3. Тот факт, что оба дерева имеют одинаковую форму, не случаен. Двоичное дерево, лежащее в основе леса, получаемого неоднократным применением функции insert 1 к совокупности р, как правило, имеет такую же форму, что и дерево двоичного поиска для р. Более того, если РгргРг ••■ Рп является совокупностью, порождающей одно из таких деревьев, в нем Pi занимает ту же позицию, что и I в дереве, полученном после перестановки. Это становится очевидным при рассмотрении другого способа формирования двоичного дере- * ва для леса с помощью функции insert 1. Такое дерево можно получить, выбирая в каче- а) стве корня наименьший элемент CDROKVnHnPTn и ой-кргтиняя чя. *''"=• ^-^^- Соответствие между лесами совокупности И ООЪеДИНЯЯ за- „ деревьями двоичного поиска 215 /\ /\ 2 J 2 3 /\1 /\1 i I в /ее / S)
тем аналогичным образом предшествующие ему элементы в виде левой ветви, а последующие элементы — в виде правой ветви. Если pk — 1 является наименьшим элементом совокупности PiPiPs ••• Рп, ОН займет корень полного дерева. Этот первый элемент служит корнем дерева двоичного поиска его перестановки, т. е. k. Таким образом, приведенное выше утверждение справедливо для корней полных деревьев. Разбиение на pip2ps ■■■ Ph-i и Pk+iph-^2 ■■■ Рп разделяет элементы на две группы, что соответствует разделению перестановки на две совокупности элементов, меньших и больших k, которые образуют соответственно левую и правую ветви дерева двоичного поиска для исходной совокупности. Это справедливо для каждого разбиения, т. е. если pj — наименьший ее элемент, то / оказывается первым элементом в соответствующем разбиении. Из такого соответствия следует, что если не принимать во внимание метки, то набор двоичных деревьев, соответствующих лесам, формируемых алгоритмом insert 1, является набором деревьев двоичного поиска для перестановки {1, 2, 3, ..., п\. Из описания алгоритма insert 1 видно, что если совокупности р соответствует некоторое дерево, то обратной совокупности р соответствует двоичное дерево, образованное перестановкой всех его правых и левых ветвей. ■ Определим число сравнений, используемых функцией insert 1 при создании лесов из перестановок |1, 2, 3, ..., п\. Можно показать, что число сравнений определяется только формой полученного леса и не зависит от того, как он размечен. Поэтому как формирование леса, так и последовательность разделения элементов зависит только от результата их сравнения и, следовательно, только от их относительных значений. Таким образом, вполне можно рассматривать перестановку первых п натуральных чисел как информацию, которую необходимо упорядочить. При использовании алгоритма insert 1 число, прежде чем оно становится корнем, сравнивается с корнями исходящих ветвей. Если число находится на верхнем уровне, оно сравнивается также с входящими элементами. После сравнения числа, расположенные в перестановке за проверяемым числом, образуют левую границу соседнего справа дерева. В соответствующем двоичном дереве числа, которые сравниваются с корнем, являются правой границей левой ветви и на левой границе правой ветви. Из анализа конфигурации полученного дерева можно определить число сравнений, требуемых для его создания. Оно складывается из числа сравнений, необходимых для формирования левой и правой ветвей, плюс число сравнений с корнем. Следовательно, при этом необходимо определить число узлов вдоль правого или левого краев дерева. Формируемые функцией insert 1 леса обладают следующим свойством. В корнях деревьев верхних уровней стоят такие числа, за которыми в перестановке нет меньших чисел. Любое меньшее 216
последующее число должно быть размещено выше. В совокупности 527894163 числа 1 и 3, удовлетворяющие этому свойству, занимают корни деревьев высшего уровня леса и находятся на правом крае соответствующего двоичного дерева. Эти числа будем называть локальными минимумами в направлении справа налево, или просто минимумами. Можно показать, что распределение минимумов в совокупности такое же, как и распределение в нем периодов. Для формирования циклического представления совокупности 527894163 заключим в скобки каждый сегмент, заканчивающийся минимумом: E278941) F 3). Обратное преобразование циклического представления осуществляется циклическим вращением каждого интервала таким образом, чтобы в нем наименьший элемент оказался последним, после чего интервалы упорядочиваются в возрастающем порядке по своим последним элементам и, наконец, удаляются скобки. Вероятность того, что в совокупности из первых п чисел имеется k минимумов, подобна вероятности существования k интервалов и равна коэффициенту при л:* в выражении х {х — 1) X X (л: — 2) ... {х -\- п — 1)/(«!). Следовательно, среднее число минимумов Я„ = 1 + 1/2 + 1/3 + .... + 1/л. Такое же значение принимает и среднее число узлов на левой (или на правой) границе двоичного дерева, получаемого любым методом. Если С„ является средним числом сравнений, необходимых для формирования леса с п узлами, то л Ci = О, с, = 1 и (п + 1) С„,1 = 2 I] (Cft + Нп), поскольку появление всякого интервала (к, п — k), k=\, 2, 3, ..., п в двоичном дереве равновероятно. Таким образом, при использовании функции insert 1 значение Сп = 2 {п — Я„) является средним числом сравнений, необходимых для создания леса, содержащего п узлов. Функция insert 1 формирует лес, переставленный по отношению к тому лесу, который требуется разделять на втором этапе. Можно либо переставить верхний уровень леса, либо представить его в виде списка, в котором возможен доступ к обоим концам. При условии, что такая перестановка осуществима, наименьший признак будет находиться в корне первого дерева. Его можно удалить, в результате чего образуются два леса, которые далее должны быть подвергнуты слиянию для формирования одного дерева, упорядоченного таким же способом. Существует несколько методов слияния лесов. Первый аналогичен методам, используемым в древовидной сортировке. В первом методе сравниваются корни двух заголовков лесов и удаляется меньший из них. При этом образуются два леса, которые затем должны быть подвергнуты слиянию. Подобный процесс повторяется с новыми лесами до тех пор, пока один из 8 Бердж в. ■ 217
них не окажется пустым. Ниже приведен соответствующий алгоритм слияния двух лесов: def гее merge I х у = if null X then у else if null у then X else if root (h x) < root (h y) then (ctree (root h x)) (merge 1 (listing (h x)) (t x))) : у else (ctree (root (h y)) x) : merge 1 (listing (h y)) (t y) Второй метод слияния аналогичен последовательной сортировке, осуществляемой путем слияния, с тем различием, что объектами слияния в данном случае являются деревья: def гее merge 2 х у if null X then у else if null у then X else if root (h x) < root (h y) then h X : merge 2 (t x) у else h у : merge 2 x (t y) Еще один метод получения полностью упорядоченного списка заключается в формировании из каждого леса упорядоченного списка и в последующем их слиянии. Числа, которые сравниваются перед их прохождением через корень, одинаковы как в этом методе, так и в алгоритме merge 1. Алгоритм merge 1 фактически является поточным представлением данного алгоритма упорядочения списка. Общее количество сравнений, используемых в merge 1, равно сумме сравнений, необходимых для упорядочения двух лесов и для слияния полученных при этом упорядоченных списков. Среднее число сравнений, необходимых для слияния списка длиной k со списком "ДЛИНОЙ п — к, можно определить рассматривая все возможные способы формирования одного списка из двух. Элементы списка длиной k обозначим через а, а списка длиной (п — к) — через Ь. Общее число размещений из а и b (п \ в общем списке равно I - )• При слиянии двух списков на каждый элемент требуется по одному сравнению, которые проводятся до тех пор, пока не будет исчерпан один из списков. Таким образом, количество сравнений определяется числом размещений элементов из а я Ь. Если по окончании сравнений остается t элементов из последовательности а, число размещений будет равно 218
n-t\ , I. Следовательно, при слиянии двух цепочек длиной k и п — k среднее число сравнений оказывается равным значению п п\\\Л \ I п—А (п — t п "-i"U Zj U-( +w--i-. = n — (n — k)l{k + 1) — kl{n — Й + 1). Если Mn является средним числом сравнений, необходимых для упорядочения леса с п узлами при использовании алгоритма merge 1, то п {п + 1) М„^1 = I] (Mft + M„_ft + n - (n - k)l{k +1)- n - Й (n — Й + 1)) = 2 S M, + (n + 1) (n + 2 — 2Hn). Следовательно, M„ = 2 {n + 2) Hn — 6п. Если сюда добавить среднее число сравнений, необходимых для создания дерева, то получим значение С„ + М„ = 2 (п + 2) Я„ — 6/г + 2 (п — Я„) = = 2 (п + 1) Я„ - 4п, которое оказывается равным средней сумме длин путей по всем деревьям двоичного поиска. В то же время оно равно числу сравнений, необходимых для создания результирующего дерева двоичного поиска. Леса двоичных деревьев, получаемые на промежуточном этапе, обладают рядом интересных свойств. Средняя сумма длин путей лесов составляет {п + 1) (Я„+г — 1) или 2j Hk, а ожидаемая глубина числа г в образованных лесах равна Я^.. Ожидаемая глубина числа г в связанных двоичных деревьях равна 2Я, — 2. Для анализа сумм длин путей, или веса, каждого из лесов предположим, что 7„ (у) является образующей функцией, в которой коэффициент при г/* равен вероятности того, что лес размера п имеет вес k. Как отмечалось выше, размерность леса высшего уровня соответствует циклической структуре перестановки. Так, если показатель цикличности для перестановок из {1, 2, 3| /13 = A/6) (S? + 3SiS2 + 25з), то это значит, что им соответствуют шесть возможных лесов, один из которых содержит деревья размера A, 1, 1), три — деревья размера B, 1), и два—-по одному дереву размера C). Каждое дерево размера k имеет корень и лес, сформированный тем же способом из перестановок (k — 1) других элементов интервала. 8* §19
Вес й-дерева, которое сдвинуто вниз на один уровень, находится добавлением единицы к каждому из путей или k ко всему весу. Образующая функция для набора й-деревьев, сдвинутых на один уровень вниз, находится умножением образующей функции дерева на г/*. Отсюда следует, что с помощью циклического индекса hn можно записать рекуррентное соотношение для образующей функции Тп (у)- Так, например То = 1; Ti = у, Т, = A/2) {{уТ,Г + У'Т,); Гз = A/6) ЦуТоГ + 3{уТ,) (уЧ,) + 2 (уП^)). В общем случае рекуррентное соотношение для Тп (у) находят подстановкой в показатель цикличности /г„ выражения y''Tk_i вместо Sft. Сами показатели цикличности определяют из соотношений 1 + hiX + fi^x^ + hgX^ -(-•••= exp (Sj + + s^xV2 + SsxV3 + •••)■ Следовательно, если Т (x, г/) = To + Тгх + Т^х^ + Т^х^ + • • •, Т {X, у) = ехр {Т,ху + Тг (ху)У2 + + T,(xyf/3 + •••). Продифференцируем обе части этого выражения по х: Т' (х, у)=уТ (ху, у) Т (х, у) и приравниваем коэффициенты при х", в результате чего получим п (n+l) Тп^г (y)=yl! y^Tk (у) Tn_k (у). Если теперь продифференцировать это выражение по у я положить у равным 1, получим соотношение для ожидаемых весов деревьев в следующем виде: п (п + 1) Гп+1 = (и + 1) (п + 2)/2 -f 2 I] Яй. Отсюда можно найти, что W,= {п + 1) (Я„,1 -1)= t Ни- Исследуем далее глубины чисел в лесах, которые соответствуют деревьям двоичного поиска. Лес, получаемый из совокупности р с исключенной единицей, получается удалением единицы и ее ветвей из леса, полученного из р. Отсюда следует, что средняя 220
глубина числа г является средней глубиной в {п — 1)-лесу для г > \, т. е. средней глубиной единицы в {п — г + 1)-лесу. Следовательно, средняя глубина числа г в п-лесу, который соответствует дереву двоичного поиска, будет равна Нп-г-и- Пусть средняя глубина г в дереве двоичного поиска с п вершинами равна Вп,г- Тогда г не может быть ни в левом fe-дереве при fe < г, ни в правом {п — ^)-дереве при k > г — 1 и оказывается в корне, т. е. имеет нулевую глубину при k = г—■ 1. Отсюда вытекает справедливость выражения г—2 п пВп+1, г = " + Ij B„_k. r-l-k + S ß*, г. ft=0 k=r которое преобразуется к следующему неожиданному результату: ■Оп+1, г+г ~ ^п, г — ^п, г+1 — ßn_l, г — ^л+г, л+1 — ^г, г- Однако глубина г в г-дереве соответствует глубине единицы в г-дереве. Она на единицу меньше числа вершин на левой границе дерева, поскольку корень находится на нулевом уровне, т. е. равна Н^— 1. Таким образом, Вп.1,г.г - Вп,г = (^г.1 - 1) - {fir -1) = 1/(г + 1), и, следовательно, 5.8. Упорядочение с помощью дерева сортировки Двоичное дерево можно записать в сегменте вектора (г, п), если условиться, что его корнем является А [i], а ветви определены как Bг, п) и Bг + 1, п). Двоичное дерево (t, п) будет пустым, если i > п. В противном случае его корнем является А [г], его левая ветвь есть Bг, п), а правая — B/ + 1, «). Структуру, в которой каждый узел больше любого элемента в своей ветви, называют деревом сортировки. Оно представляет собой промежуточный результат упорядочения вектора в памяти, где он хранится. Существуют два способа формирования дерева сортировки и два метода его упорядочения. К дереву сортировки A, п) можно добавить новый элемент, записывая признак Л [п + 1J и размещая его на пути от листа к корню. В итоге получается дерево сортировки A, п + 1). Формирование дерева сортировки (рис. 5.18) можно начать с дерева сортировки размера 1, содержащего элемент А [\], добавляя к нему по одному элементу из остатка. Функция вставки нового элемента на позицию р состоит из последовательности вставок, выполняемых с помощью цепочки р, р-^2, (/74-2L-2, ..., 1. Другими словами, цепочка от р к корню представляется потоком: def upchain р = whiles (>1) (generate (-ь2) р) 221
3 2 S В таком же порядке вставляются элементы, расположенные на позициях 2, 3, ..., п, при этом используется функция Inheap, определяемая следующим образом; def inheap А i = insertc А (upchain i) for j : = 2 step 1 until n do inheap A j Другой способ формирования дерева сортировки заключается в его образовании из двух деревьев сортировки и нового признака, который вставляется в ветвь дерева, идущую от его корня. Вставка в цепь, формируемую путем выборки наибольших признаков на каждом разветвлении, осуществляется с помощью функции insertc с обратным упорядочением. Функция получения из двоичного дерева цепи, образуемой выборкой наибольшей вершины на каждом ветвлении, может быть описана следующим образом: S / \ * 2 J / Рис. 5.18. Формирование дерева сортировки вставкой через листья def гес maxpath х = if empty X then { ) else root x [if empty (left x) then maxpath (right x) else if empty (right x) then maxpath (left x) else if root (left x) > root (right x) then maxpath (left x) else maxpath (right x)] Если, однако, левая ветвь дерева сортировки пустая, то 21 > п и правая ветвь должна также быть пустой. Оба дерева не оказываются одновременно пустыми или непустыми лишь в случае, когда соответственно 2t = п и 2i + 1 > п. Применяя функцию maxpath к дереву сортировки, получим поток позиций,- 223
def rec maxpath (p, n) = if p > П then nullists else л ( ) , p, let i = 2p if j > П then nullists else if j = n then maxpath (j, n) else if A[j]> A [j + 1] then maxpath (j, n) else maxpath (j + I, n) Дерево сортировки можно сформировать с помощью функции si[t def sift i п = insertc A (maxpath (i, n)) Применяемой к узлам, не являющимся конечными. Дерево сортировки строится с основания путем объединения двух деревьев сортировки и признака (рис. 5.19): for i : = n-f-2 step (—1) until 1 do sift i n Дерево сортировки можно полностью упорядочить с помощью функции sift с использованием сокращающихся деревьев (рис. 5.20) следующим образом: for i : = п step —1 until 2 do А [I] ; = : А [i] sift I (i — I) В результате обмена признак корня помещается в концевое положение, а ранее находившийся там признак вставляется затем в дерево через его корень. Каждая итерация уменьшает размер оставшегося дерева сортировки на единицу. /^^-^^\ з Другой метод упорядочения ^^ N. МРугии меи)д унорндиченин f -^ ^ у, -^ дерева сортировки заключается z 5 г <> i ДД^ 2 в последовательном перемеще- ^^ ^, НИИ наименьшего признака из корня в ветви до тех пор, пока у-—ч. У\ не освободится концевая вер- ^^ ^ /' % шина. Затем признак, занима- /^ \ ющии позицию, в которую еле- ^^ f / дует поместить наименьший ^—^ X 5 признак, с помощью функции ( \ » ^^ \- inheap помещается в концевую J J 2 f ; J—[^ вершину. Наконец, наимень- ^^ "^7 ший признак помещается в свое окончательное положение, / \ уменьшая при этом на единицу ,5" * 2 з ; /*\ ^ размер дерева сортировки, ко- ,/ ^^ торое подвергается дальней- iiiPVTAf \гпАпапт1РНн1Г> Г)прпят1Н1п ''"'^- ^■'*- !Формирование дерева сорти- Шему упорядочению, операцию ровки при помощи вставки через корень 223
5 ¥ Z 3 / if 2 3\ ^^1 if 1 Z 3\ if 3 Z l\ 7 0-21^ 3 1 Z\¥ 1 \ff \' к 5 5 S ./4 1 / 2 Рис. 5.2Ö. Упорядочение дерева! сортировки при помощивставки через корень 5 if г 3 г ^ч ^ 3^ ^/ if i^ 3 2 и 1 J S. ^ i^ 3 2 f \ S 7 2 1 nl /'^ 3 7 2 D\sr у/ Z ^-^, А J- / 2 I « .5- / 2 2 1 DU ^ /^ г 1 \з ¥ ff г 2 J ¥ ff' У Ш 2 7\ 3 if ff Г 2 3 <f ff /' / Рис. 5.21. Упорядочение дерева сортировки при помощи вставки через листья сдвига можно просто представить как сдвиг элементов цепи (maxpath A, п)) на одну позицию следующим образом: def гее shift s = let X, у = s ( ) if null у then X else A [x] : = A [hs y] shift у Результатом будет положение концевой вершины. Программа преобразования дерева (рис. 5.21) в упорядоченный вектор: for i : = п step —I until 2 do г : = A [I] let к = shift i if к =f i then A [k] : = A [i] inheap A к A [i]:=r 224
5.9. Сортировка на лентах Слияние. Программы слияния массивов, размещенных на магнитных лентах, можно получить с помощью операций слияния, объединяющих k цепочек в одну, длина которой равна сумме длин исходных цепочек. В этой операции k исходных цепочек и результирующая цепочка должны находиться на различных лентах. Вся процедура слияния начинается с исходного множества данных на одной ленте. Данные объединяются в цепочки такой длины, насколько позволяет внутренняя сортировка, и распределяются на доступные ленты. Любой вид слияния, используемый далее для объединения цепочек, в конечном счете завершается получением одной цепочки, содержащей все данные. Проблемы сортировки данных, размещенных на лентах, заключаются в создании такого метода, который позволяет на каждом шаге сократить максимально возможное число цепочек и не приводит к излишним перемещениям лент. Запись цепочек на ленту всегда должна осуществляться при ее перемещении в прямом направлении, а считывание может быть выполнено как в прямом, так и в обратном направлениях. Однако, как правило, в одной программе слияния считывание осуществляется либо в прямом, либо в обратном направлении, а одновременно редко. Процесс слияния можно изобразить в виде дерева, каждый узел которого представляет собой основную операцию слияния k цепочек в одну. Ребра узлов необходимо пометить ленточными символами, причем метки должны быть различными. Для задания порядка выполнения слияния узлы дерева помечают различными целыми, называемыми характеристиками узла. Исходные распределения цепочек обычно не включаются в дерево. Все концевые вершины должны быть помечены нулями, которые представляют состояние сразу после распределения цепочек. Поскольку цепочки не могут быть объединены до своего создания, характеристика всякого узла всегда будет больше, чем характеристика любого узла в его ветвях. Другой способ осуществления слияния заключается в использовании таблицы, элементы которой определяют либо количество и, возможно, длину цепочек на каждой ленте, либо действия, изменяющие состояние лент. Первая запись в такой таблице описывает исходное состояние лент. Каждому этапу слияния соответствует только одна запись таблицы. Ее элементы равны —1 для каждого считывания цепочки с ленты на данном этапе, +1 для каждой записи и О, если лента не участвует в данном этапе слияния. Запись такого вида не позволяет осуществлять считывание с пустой ленты, поэтому общая сумма элементов таблицы для любой ленты не должна быть отрицательной. Последняя запись в таблице должна отражать ее конечное состояние. При завершении сортировки получается 1 для той ленты, где находится результирующая цепочка, и Q для всех остальных лент. 225
Балансное слияние. Самым простым методом ленточного слияния является так называемое балансное слияние. При его осуществлении все ленты, предназначенные для слияния, делятся на два несвязных множества. Если имеем 2k лент, их делят на два равных множества, каждое размером k. В случае, когда имеем нечетное число лент {2k + 1), их делят на два множества размерами fe и fe + 1- Цепочки элементов вначале равномерно распределяются между членами одного множества, после чего их объединяют и распределяют между лентами другого множества. Перемещение всех цепочек из одного множества в другое называют прогонкой. Общее время, затрачиваемое на осуществление такого метода сортировки, определяется числом прогонок, необходимых для упорядочения всех данных. Общее число всех передач данных равно произведению числа прогонок на число цепочек. Пример построения таблицы и дерева балансной сортировки девяти цепочек при использовании четырех лент приведено на рис. 5.22. Число прогонок, необходимых для упорядочения N элементов, когда в распоряжении имеется 2k лент, равно plog;,. N^\ — числу уровней дерева. В рассмотренном примере имеем девять строк и четыре прогонки, поэтому необходимо 36 основных перемещений данных, плюс 9 дополнительных для их начального распределения. Показатель эффективности слияния равен S^ (S)is^ ^д,г S — число цепочек, а Т (S) — число перемещений данных. Очевидно, Т {S) = S logp S, где р — показатель эффективности слияния. Число перемещений данных представляет собой фактор, с помощью которого при полной прогонке всех цепочек уменьшается их количество. При выполнении балансного слияния эффективный порядок равен р, если имеем 2р лент, и У р (р +1) — если имеем Bр + 1) лент. Деревья и стратегии слияния. Рассмотрим процедуру слияния при считывании как в прямом, так и в обратном направлениях перемотки ленты. При считывании в прямом направлении лента используется как буфер или очередь. После перемотки ленты считывание с нее осуществляется в том же порядке, что и запись. При считывании в обратном направлении лента используется как магазинный список. Первая считываемая цепочка при этом оказывается последней цепочкой, записанной на ленту. Кроме того, считывание в обратном направлении имеет дополнительное осложнение, заключающееся в том, что цепочка, записанная в возрастающем порядке, считывается в убывающем порядке, и наоборот. Для описания слияния при считывании как в прямом, так и в обратном направлениях можно использовать такую же таблицу, что и рассмотренная. Правила создания деревьев слияния при чтении в прямом и обратном направлениях, однако, различные. Предположим, что одно ребро дерева помечено парой номеров этапов на своих концевых вершинах в убывающем порядке. 226
1 2 3 4 5 6 7 8 9 10 It 5 -1 -1 -/ -1 -1 + 1 0 -/ -1 ~7 + 1 « ~1 -1 -1 -/ 0 0 +7 -1 -/ 0 0 0 +7 0. + 1 0 +1 ~7 -7 +7 +7 0 -1 0 0 + 7 0 + 1 0 -7 ~7 0 0 + 7 -7 0 с. 9 77 7 .d 10 8 3 ^ .b Рис. 5.22. Балансное слияние Ребро (г, s) обозначает цепочку, которая записана на ленту на этапе г и считывается с ленты на этапе s. При чтении в прямом направлении первая записанная цепочка и считана будет первой. Следовательно, если для двух цепочек (г, s) и {i, и), расположенных на одной ленте, выполняется условие г < ^ то будет справедливо неравенство s < и. При считывании в обратном направлении первая записанная цепочка становится последней, поэтому, если г < t < S, то и < S. Отсюда следует, что если числа г, s, tau расположены в возрастающем порядке, то линии, соединяющие г с S и ^ с ц, не пересекаются. Правила создания деревьев слияния можно сформировать следующим образом. Дерево представляет собой схему слияния по fe-пути, если и только если: I) имеется, по крайней мере, {к + 1) различных ленточных меток на ребрах узла; 227
2) номера этапов на любом пути из корня в лист расположены в убывающем порядке; За) для дерева, считываемого в прямом направлении, для ребер (г, s) и (/, и) с одинаковыми ленточными метками неравенства г < t означает также, что s < и; 36) для дерева, считываемого в обратном направлении, для ребер (г, S) и (/, и) с одинаковыми ленточными метками справедливо неравенство г < t <_ s < и. При осуществлении слияния считыванием в обратном направлении возрастающие цепочки становятся убывающими, и наоборот. Все ребра одинакового уровня дерева, считываемого в обратном направлении, представляют собой возрастающие либо убывающие цепочки, чередующиеся от уровня к уровню. Критерием эффективности программ ленточного слияния служит число, показывающее, сколько раз исходные цепочки проходят через вычислительную машину до полного упорядочения. Оно равно сумме расстояний от узлов к листьям на дереве слияния. Существует несколько методов небалансного [слияния, которые соответствуют систематическим методам ввода сцепляемых деревьев в слои или уровни. В первом из них дерево растет на всех концевых вершинах с одинаковой ленточной меткой на одном этапе, где цепочки с нескольких лент неоднократно объединяются, пока одна из лент не станет пустой. Цикл состоит из нескольких таких фаз наращивания дерева (или слияния лент), после чего подобная стратегия повторяется. Порядок, связанный с каждым этапом слияния, представляет собой наибольшее дерево, которое может вырасти за п циклов. Распределение цепочек на лентах после каждого цикла называют промежуточным распределением для данного порядка. Можно создать две образующие функции, которые описывают распределение концевых вершин на каждом уровне для этих промежуточных деревьев. Первая перечисляет концевые вершины или цепочки в промежуточном распределении, а вторая — веса этих промежуточных деревьев. Оказывается возможным аппроксимировать эти ряды и получить приближенную оценку возможностей каждого метода сортировки. Самый простой небалансный метод слияния носит название многофазного слияния. Многофазное слияние. Стратегия многофазного слияния заключается в осуществлении слияния по k путям с использованием (k + 1) ленты. Цепочки данных вначале распределяются на k лентах, при этом {k + 1)-я лента остается пустой. Затем на k лентах осуществляется слияние. На этом этапе k лент содержат цепочки, а одна из них пустая. Такое слияние по fe-путям повторяется до тех пор, пока k лент не станут содержать по одной цепочке каждая. На последнем этапе эти k цепочек сливаются для формирования результирующей упорядоченной цепочки. Многофазное слияние состоит из циклов, каждый из которых содержит по одному этапу. 228
Номер этапа 0,1 +7 ч ц 1,3 -1 ^f 1,31 V -7 н ч 2,5 -1 # 0,5 11,1 -7 % % ^; Ь9 ~1 0,9 13,1 ~7 % i| % 17 0,17 05щее WMuvecmffo HenoveK Л 17 а 5 S f Число записей 7 ♦ Z 1 1 Длина записи 3 5 Э 17 31 IS b X с / ä^ 11 73 с/^/а\ a/dWK а/ A^N. 4^ 6 ff 7 Ю 11 /y^l b/cAc <Г*\ al ^ 2 3 S 8 b lcY\ b 1с^\Ьу^\сЩ\^ Рис. 5.23. Многофазная сортировка Пример таблицы и дерева, отражающих процесс многофазного слияния 31 цепочки при наличии четырех лент, приведен на рис. 5.23. В каждой паре (г, /) этой таблицы i является номером, а / — длиной каждой цепочки. Общее количество перемещений цепочек равно 31 (для начального распределения) плюс произведение числа записей на каждом этапе на их длину (эти данные приведены в таблице). Общее число перемещений цепочек равно 138. В нижеследующей программе все ленты представляются как список лент, а каждая отдельная лента —■ как список цепочек. Список лент располагается в порядке убывания числа цепочек. На каждом этапе с использованием функции mrnerge осуществляется объединение цепочек до тех пор, пока первая лента не станет пустой: 229
def phase x = phase I ( ) x where rec phase I у x = if null (h x) then y, t X else phase I (postfix (mmerge (map h x)) y) map t x) Функция phase производит пару, первая составляющая которой представляет результат слияния, а вторая — список оставшихся непустых лент. Полная многофазная сортировка повторяет выполнение функции phase до тех пор, пока не останется единственная цепочка. Между этапами результат добавляется спереди к остающимся лентам; def rec polyphase х = let у, z = phase x if all null z then у else polyphase (postfix у z) Для примера, рассмотренного на рис. 5.23, приведем число строк, размещаемых на лентах между выполнением этапов: bcdabcda 7 И 13 4 6 7 2 3 4 1 1 2 1 1 1 О О 1 В общем случае последовательность чисел, для которой существуют совершенные распределения, представляет собой обобщенные числа Фибоначчи, описываемые следующим рекуррентным соотношением: F {п + k) == F {п + k~l) + F {п + k — 2) + ••■+ F (п), в котором каждый член является суммой предшествующих k членов. Вид первых нескольких промежуточных распределений для k = 2 показан на рис. 5.24. Чтобы нагляднее изобразить рост дерева, на этом рисунке номера этапов приведены в обратном порядке. Деревья, образуемые при считывании как в прямом, так и в обратном направлениях, имеют одинаковую форму. Единственное различие заключается в нумерации этапов. Номера уровней концевых вершин каждого дерева можно представить с помощью образующей функции двух переменных fg (х, у), в которой коэффициент при х'^у'' является значением концевой вершины на 230
a] ь/ \c ^/ V V \ V V 2 ^ V V '/ V 2 3 2 f 2 J ' 2 f J 7 5 7tMfV 7?;ffV 7 'j 7k Vr ^y у+гу"- 3y^+2y^ 1 \ y^+Sy42y-^ b/ \a V V Рис. 5.24. Многофазные деревья r-M уровне n-й фазы дерева многофазного упорядочения по двум путям. Приведем несколько первых членов этой функции: Р, {X, (/) = 1 + 2ух + (г/ + 2у^) х" + {Ъф + 2(/«) х^ + + (г/2 + bf + 2(/*) X* + D(/3 + 7г/* + ,2(/5) х^ + • • • 231
Fn, n-e дерево, включает дерево Fn^i в качестве левого поддерева и дерево Fn.2 в качестве правого поддерева, а Fi я F2 являются первыми двумя деревьями на рис. 5.24. Образующая функция удовлетворяет условиям Pi (х, у) = ух Pi (X, у) + ух^Р^ (х, (/) + 1 + ху; Р, (х,у) = A + ху)/A -у(х + х')). Обе образующие функции могут быть получены из Р^ (х, у). Если у представляет собой множество, равное 1, то коэффициент при X" является числом концевых вершин дерева. Если Р^ (х, у) продифференцировать по у, то их число на каждом уровне умножается на номер уровня. Образующая функция для весов деревьев равна Pi (х, 1), следовательно, р^ (х, 1) = 1 + 2х + Зх^ + 5х^ + 8х* + + 13л-5 + ... = A + х)/A —X — х^У, РУ, {X, 1) = 1 + 2х + 5л-' + 12х^ + 2Ъx'^ + 50х» -} --= = Bх + х')/A —X — ху. Образующую функцию Р^ (х, у) или в общем виде Р^, (х, у) для многофазного слияния по k путям можно разложить на составные части следующим образом. Предполагается, что непустые ленты пронумерованы в соответствии с числом содержащихся на них цепочек, которые имеют совершенные распределения. Ленте с наибольшим числом цепочек придается номер 1, ленте с последующим по величине числом цепочек — номер 2 и т. д. Таким образом формируется список лент, оказывающийся аргументом с обратной нумерацией. Связь между уровнем ленты и ее номерам при этом от одного этапа к другому будет меняться. Предположим, что для каждой ленты с номером i существует образующая функция Ti(x, у), каждый коэффициент которой при х"у'' является числом концевых вершин у г-й ленты на г-и уровне дерева для п-го этапа многофазного слияния по k путям. В таком случае для k = 2 получим, что Тг = 1+ух+ (у + у') х' + B(/^ + у') х' + C(/^ + 4(/« + + у')х'+ ■■■ ; Т, =ух + yV + (у' + г/«) х' + B(/2 +у^)х'+ ■■■. Рассмотрим далее, как формируется многофазное дерево. На каждом этапе оно растет на концевых вершинах, соответствующих ленте с наибольшим числом концевых вершин. В конце этапа лента с наибольшим числом цепочек будет принимать цепочки из двух лент, которые до начала выполнения этапа содержали наибольшее и второе по значению число цепочек. Вообще, на ленту Г,- осуществляется запись как из предшествующей ленты Ti, так и из Ti^i. При этом T^^i остается в дереве без 232
изменений, а новые вершины, которые передаются из Т-^, сдвигаются на один уровень вниз. При k = 2 образующие функции определяются следующим образом: Ti = хуТ^ + хТ^+ 1; Тг = хуТг- Решая эти уравнения относительно Ti и Т^, находим Ti - 1/A -у{х + х-")) Т, = ху/{\ -у{х + х')), т. е. получаем тот же результат, что и ранее: Р^ (х, у) = Т^ -\- + Т^. Образующие функции для многофазного слияния по k путям определяются как Тг = хуТ^ + хТг+ 1; Тг = xyTi + хТз, Гз = xyTi + хТ^, Tk-i = xyTi + xTk\ Tk = хуТ^. Решение этих уравнений дает Pk{x, г/) = Г1+ П+---+ П = = A +г/((А- 1);с+ (A-2);cä+••• • • • + х*->))/A - г/ (^ + ^' +• • • + X*)). Если это выражение продифференцировать по г/ и положить у равным 1, то в результате получим образующую функцию для весов предшествующих деревьев: Wh {х) = (Ах -f (А — 1) хз + (Ä — 2) х^ +. • • • • • + Х*)/A — X — Х^ — X*)-. Если pk (х, 1) = и Pk,nX" и Wn (х) = и w.;,n х'\ то для упорядочения pk, п Цепочек необходимо осуществить (р/,^ „ + + W;;, п) их перемещений. Выражение Pf, (х, 1) можно приближенно аппроксимировать соотношением aj{l — а^,), а выражение W/i (х) — соотношением bj(l — аре) + с^ A — а^х)^, где а — величина, обратная наименьшему корню уравнения 1 — —X — х^ — ... — л;* = 0. Зная значения а^,, ^, ö^, и с,,, можно определить количество перемещений цепочек, равное U/ß In S + V/,, где S — число упорядочиваемых цепочек. Значения f/^ и V^ для некоторых k приведены на рис. 5.25. Показатель эффективности слияния, 233
It 2 3 4 5 6 7 8 9 10 15 "к 1.6180 1.8393 1.9276 1.9659 1.9836 1.9920 1.9960 1.9980 1.9990 1.9997 "к 1.5037 1.0148 0.8630 0.7958 0.7618 0.7436 0.7337 0.7281 0.7251 0.7215 Vk 0.9920 0.9645 0.9206 0.8635 0.7965 0.7229 0.6459 0.5682 0.4920 0.1655 Показатель 1.9445 2.6788 3.1860 3.5136 3.7160 3.8372 3.9081 3.9487 3.9717 3.9987 20 1.9999 0.7214 —0.0715 3.9999 Рис. 5.25. Эффективность многофазной сортировки равный ехр A/f//,), асимптотически стремится к 4, что эквивалентно балансному слиянию на восьми лентах. Если пр_и этом основным критерием является число перемещений цепочек, то многофазное слияние предпочтительнее балансного только при А < 8. Каскадное слияние. Стратегия метода каскадного слияния заключается в распределении цепочек по всем лентам, причем одна из них, {k + 1)-я, предназначается для слияния. Цепочки с k лент неоднократно объединяются, пока одна из них полностью не освободится. Затем осуществляется слияние по {k — 1) пути из {k — 1) непустых лент до тех пор, пока одна из них не станет пустой. Процесс повторяется, обеспечивая слияние по {k — 2) путям, (k — 3) путям и т. д., а заканчивается слиянием по двум путям, за которым следует слияние по одному пути (или копирование). Освободившаяся лента в оставшейся части цикла участия больше не принимает. В течение полного цикла слияния по k, (k — 1), (k — 2), ..., 1 пути все цепочки перезаписываются из одной последовательности k лент в другую. Этот цикл затем повторяется на каждой новой последовательности k лент до тех пор, пока каждая из них содержит еще по одной цепочке, объединяемых далее в одну упорядоченную цепочку. Пример таблицы и дерева каскадного слияния 31 строки при использовании четырех лент приведен на рис. 5.26. Все шаги в пределах каждого отдельного этапа задаются одинаковыми числами. Уровни дерева соответствуют циклам, в каждом из которых все цепочки перезаписываются из одной последовательности k лент в другую, содержащую до этого наибольшее число цепочек. Общее число перемещений цепочек равно числу деревьев для начального распределения плюс единица, умноженному на общее число цепочек. В рассмотренном на рис. 5.26 примере это число оказывается равным 5 X 31 = 155. Как и в случае многофазного слияния, входом каскадного слияния является список лент, 234
i1) B) (J) a /4- -s -5 -3 // -6 -5 + 3 С s -e +5 0 d 0 + 6 0 0 M (.'TJ F) G) (,Ю (S) A0) 0 + 3 n 0 3 -/ -/ -/ ß 7 / J -3 +2 0 z -/ -/ +1 1 _^ 0 5 -3 -2 + 7 7 -7 + 1 0 1 — '/ 0 6 -3 -2 -7 0 + 1 0 Ü 1 -/ 0 fW (^/ V v^Vy V 3 2 7 2 l\ 7 al b и 5 l\\ I Ö 3 2 12 a a I ь\аШ\г\а1аЬШ \e Рис. 5.26, Таблица и дерево для каскадного слияния расположенных в возрастающем порядке по числу содержащихся в них цепочек: def гее cascadecycle х = let у, Z = phase х if null z then ( ) else у ; (cascadecycle z) Каждый каскадный цикл оставляет ленты в убывающем порядке по числу их цепочек, следовательно, между циклами список должен изменять свой порядок на противоположный: def гее cascade х = let Z = cascadecycle х if all null (t z) then h z else cascade (reverse z) 235
ft- 77 6 О G) -6 ~6 ~6 -hff IZ) -5 -5 -hS 0 (J) c^) (Sr) F') G) 3 -3 +z ?. -7 -7 n +7 n +3 0 3 -7 -7 7 -7 5 -3 -2 n ■1-7 0 7 -7 в -3 -2 7 -7 ■f-7 7 -1 a 3 e 5 (y^/ Y b/ \a bids - / \ / \ / 2 7 3 ^3 /1 /l\ /l\ /\ /n a b abc acd с d аса / 1 / 1 \/ 1 \ / \/ l\ 2 r Z 1 Z 1 H W ^^?K cd ab с ^ \ \ \\\ 2 1 a babe ababc ababc ab ah с /I/ l\ //// I /I/ \ \ \\\\\ Рис. 5.27. Таблица и дерево для модифицированного каскадного слияния Последний этап каскадного цикла, состоящий только из операции копирования, можно исключить из всего процесса упорядочения. Получаемая в результате стратегия известна под названием модифицированное каскадное слияние. Модифицированный каскадный цикл можно записать следующим образом: def гее modcascadecycle х = let у, Z = phase х if null (t z) then у : z else у : (cascadecycle z) Если цепочки, приведенные на рис. 5.26, объединяются с помощью модифицированного каскадного метода, то этапы C), F) и (9) таблицы на рис. 5.26 будут пропущены, в результате получаем таблицу и дерево, приведенные на рис. 5.27. Образу- 236
ющая функция для каскадного слияния будет Tj + Tj + • • • + Т\, где Ti связаны следующими соотношениями: Т^^ху{Т^+Т^+ ■■• + П) + 1; Г, = хг/ (Ti + Г, + . • ■ + r,.i); Г, == ;с(/ (Г1 + Г, + ... + П,1_,); П-1 == ху (Т, + Т,); Tk = хуТ,. В модифицированном каскадном методе изменяется только Т^: Т, = ху(Т, + Т,+ ... + n_i) + Г, + 1. Уравнения каскадного слияния можно решить с помощью правила Крамера, задавая Ti = hk_i {xy)lhk (ху), где /i* = det (б,; — aij) и 0, если i -\- j у k-\- 1; 1, если t + / < А + 1. Эти уравнения связаны рекуррентным соотношением hk (х) = (-^\)"-^xhk_i (—X) + hk_2 (X), /lo = 1, hl = I — X. Полагая теперь х равным 2 cos у, решение можно найти в виде [5-32] h^ == (_1)" ("+1)/2 sin {{k + 1/2) у (--l)*)/sin {у/2). Корни Лд. = о задаются соотношением (k + 1/2)(/ = тп и равны (—1)* 2 cos Btnnl{2k + 1)) для m = 1, 2, 3, ..., k. Наименьший положительный корень равен 2 cos {knl{2k + 1)) или приближенно л1{2к + 1). Обратной величиной этого значения является показатель эффективности каскадного слияния по k путям. 237
k 2 3 4 5 6 7 8 9 10 IS 20 Рис. п оказатель эффективности «А 1.6180 2.2470 2.8794 3.5133 4.1481 4.7834 5.4190 6.0548 6.6907 9.8718 13.0539 5.28. Эффе! слияния "k 2.0781 1.2352 0.9456 0.7958 0.7029 0.6389 0.5917 0.5553 0.5261 0.4367 0.3892 Vk 0.6723 0.7540 0.7957 0.8213 0.8388 0.8515 0.8613 0.8690 0.8754 0.8954 0.9064 <тнвность каскадного Последовательность величин, для которых существуют идеальные каскадные распределения, аппроксимируется выражением вида С {k, п) = а^ {{2k + 1)/я)". Число перемещений, необходимых для сортировки N {k, п) цепочек, равно nN {k, п) или {п\п N — In а^,)/1п(BА + 1)/я). Число перемещений цепочек, необходимых для сортировки S элементов путем каскадного слияния, приближенно аппроксимируется величиной U^S In S + К^,; значения f/^, и V^ приведены на рис. 5.28. Компромиссные методы слияния. Каскадный, модифицированный каскадный и многофазный методы — это частные случаи семейства компромиссных методов слияния, названных так потому, что они являются компромиссом между каскадным и многофазным методами. В каскадном методе используются этапы слияния по k, {k — 1), {k — 2), ..., 1 пути, а в многофазном методе повсюду используется слияние по k путям. В компромиссном методе вначале осуществляется слияние по k путям, а затем — слияние по {k — 1) пути, как и в каскадном методе. В компромиссном методе циклы заканчиваются на (й — /-+1)-м пути, после чего происходит их повторение. Вид компромиссного метода определяется двумя параметрами {k, г). Компромиссное слияние {k, 1) является многофазным, {k, k) — каскадным, а {k, k — 1) — модифицированным каскадным. Программы для компромиссного слияния имеют следующий вид: def гее compromisecycle г х = if г = О then (reverse х) else let у, z — phase x у ; (comprornisecycle (r — 1) z) def rec compromise г x = let z = compromisecycle г x if all null (t z) then h z else compromise r (reverse z) 238
На рис. 5.29 приведен пример таблицы и дерева для компромиссного E.2)- слияния 33 цепочек. В этом случае требуется всего 83 перемещения цепочек, что сравнимо с 88 перемещениями, необходимыми для многофазной E,1)-сорти- ровки 33 элементов. Способ нахождения относительных эффективностей компромиссных методов аналогичен способам, рассмотренным для многофазного и каскадного методов. Образующая функция для компромиссного {k, г)-слияния равна 7^1 + + Га + • • • + Гд,, где а b с d е f fi  ^ у я о A) +3 -3 -3 -S -3 -J Слияние по 5путям 3 0 3^56- B) +3 -3 -3 -3 -3 Слияние по <^ путям 3 3 0 12 3 C) -/ -/ +/ -1 -J -1 Слияние ло :^питям 2 2 t О 1 Z " Cf) -/-/ +1-1-1 слияние/70 ^питян 1117 0 1 *^ (S) -1 -/ -/ -/+1-/ Слияние ло5п1/тям 0 0 0 0 0 0 ' ' / Z / Z ^ Рис. 5.29. Пример компромиссного E,2)-сли- ямия Т, = ху{Т,+ Т, +■ + Тг) + Т,,,+ 1; Tk-r = ху{Т,+ 7,+---+ Т,) + хТ,- Tt-r.2 = xyiT,+ 7,+---+ 7,_,); П-1 = ху{7, +7,)-, Tk = ху7^. Число перемещений цепочек можно представить в виде UkrS \nS+ Vk,-S, а показатель эффективности оказывается равным ехрA/G^^). Значения Ukr, Vkr и показатель эффективности приведены в таблицах на рис. 5.30—5.32. При й—♦ оо показатель эффективности [k, г)-слияния стремится к значению (г-р !)('■+"/'•. Из этих таблиц видно, что оптимальные значения г для различных k следующие: Ä23456789 10 Г112234677 ■ 239
k k r 2 3 4 5 6 7 8 9 10 15 20 r 2 3 4 5 6 7 8 9 10 15 20 1 1.5037 1.0148 0.8630 0.7958 0.7618 0.7436 0.7337 0.7281 0.7251 0.7215 0.7214 6 0.7029 0.6315 0.5831 0.5494 0.5248 0.4627 0.4465 2 2.0781 1.0232 0.8521 0.7475 0.6911 0.6583 0.6393 0.6269 0.6196 0.6080 0.6969 7 0.6389 0.5870 0.5493 0.5218 0.4529 0.4299 3 1.2352 0.8963 0.7563 0.6747 0.6283 0.5992 0.5806 0.5676 0.5448 0.5415 8 0.5917 0.5521 0.5218 0.4463 0.4184 4 0.9456 0.7728 0.6815 0.6255 0.5855 0.5591 0.5409 0.5051 0.4985 9 0.5553 0.5239 0.4411 0.4104 5 0.7958 0.6905 0.6258 0.5834 0.5532 0.5296 0.4797 0.4683 Каскадное слияние 0.5261 0.4367 0.3892 Рис. 5.30. Значения U для компромиссного слияния k г 2 3 4 5 6 7 8 9 10 15 20 1 0.9920 0.9645 0.9206 0.8635 0.7965 0.7229 0.6459 0.5682 0.4921 0.1655 —0.0715 2 0.6723 0.8199 0.8826 0.8879 0.8681 0.8491 0.8067 0.7696 0.7198 0.4822 0.2821 3 0,7540 0.7994 0.8318 0.8585 0,8712 0.8612 0.8357 0.8188 0.6529 0.4860 4 0.7957 0.8080 0.8304 0.8400 0.8503 0.8623 0.8588 0.7562 0.6182 5 0.8213 0.8213 0.8374 0.8448 0.8461 0.8481 0.8092 0.7084 240
Продолжение рис. 5.31 k г 2 3 4 5 6 7 8 9 10 15 20 6 0.8388 0.8341 0.8457 0.8525 0.8535 0.8488 0.7710 7 0.8515 0.8452 0.8535 0.8598 0.8544 0.8161 8 0.8615 0.8545 0.8606 0.8584 0.8439 9 0.8690 0.8624 0.8721 0.8543 Каскадное слияние 0.8754 0.8954 0.9064 Рис* 5.31. Значения для компромиссного слияния k k г 2 3 4 5 6 7 8 9 10 15 20 Г 2 3 4 5 6 7 8 9 10 15 20 1 1.9445 2.6788 3.1860 3.5136 3.7160 3.8372 3.9081 3.9487 3.9717 3.9987 3.9999 6 4.1481 4.8715 5.5558 6.1724 6.7220 8.6818 9.3915 2 1.6180 2.4773 3.2333 3.8103 4.2501 4.5675 4.7790 4.9296 5.0225 5.1798 5.1947 7 4.7834 5.4928 6.1743 6.7978 9.0991 10.2392 3 2.2470 3.0513 3.7521 4.4022 4.9122 5.3065 5.5972 5.8227 6.2684 6.3391 8 5.4190 6.1177 6.7962 9.3990 10.9165 4 2.8794 3.6470 4.3379 4.9468 5.5171 5.9808 6.3526 7.2423 7.4323 9 6.0548 6.7450 9.6486 11.4373 5 3.5133 4.2553 4.9426 5.5522 6.0957 6.6039 8.0410 8.4592 Каскадное слияние 6.6907 9.8718 13.0539 Рис. 5.32. Эффективная способность компромиссного слияния 241
ссылки и БИБЛИОГРАФИЯ Большинство методов сортировки, рассмотренных здесь в функциональной системе обозначений, можно найти в обширной литературе по сортировке. Обзоры и библиография методов сортировки содержатся в следующих работах: [5—1, 5—3, 5—9, 5—15, 5—16, 5—17, 5—22, 5—24, 5—25, 5—28, 5—34 и 5—35]. Описание методов, изложенных в этой главе, можно найти в следующих работах: схемы упорядочения рассмотрены в [5—2, 5—4, 5—13]; сортировка Шелла [5— 30, 5—36]; деревьям двоичного поиска [5—18, 5—26, 5—39]; быстрая сортировка [5—20, 5—21]; обмен по основанию — [5—19]; упорядочение с помощью дерева сортировки [5—11, 5—12, 5—38]; ленточная сортировка [5—5, 5—6, 5—8, 5— 23, 5—26, 5—31, 5—33, 5—37]. 5-1. Ashenhurst R. L. «Sorting and arranging», (in) Theory of Switching, Report N. BL-7, Harvard Comput. Lab., May 1954, pp. 1.1 — 1.76. 5-2. Batcher K. E. «Sorting networks and their applications», Proc, AFIPS, 1968 SJCC, Vol. 32, Montvale, N. J.: AFIPS Press, pp. 307—314. 5-3. Bell D. A. «The principles of sorting». Computer Journal, Vol. 1, 1958, pp. 71—77. 5-4. Bose R. С and Nelson R. J. «A sorting problem», JACM, Vol. 9, N. 2, 1962, pp. 282—296. 5-5. Bürge W. H. «Sorting tress, and measures of order». Information and Control, Vol. 1, 1958, pp. 181—197. 5-6. Bürge W. H. «An analysis of the compromise merge sorting techniques», Proc. IFIP Cong. 1971, Amsterdam: North Holland, 1972, pp. 454—459. 5-7. Bürge W. H. «An analysis of a tree sorting method and some properties of a set of trees», Proc. 1st USA — Japan Computer Conference, 1972, pp. 372—379. 5-8. Carter W. С «Mathematical analysis of merge-sorting techniques», Proc. IFIP Cong. 1962, Amsterdam: North Holland, 1963, pp. 62—66. 5-9. Davies D. W. «Sorting of data on an electronic computer», Proc. Inst. Elec. Eng. 103B, 1956, Supplement 1, pp. 87—93. 5-10. Plores 1. Computer Sorting, Englewood Cliffs, N. J.: Prentice — Hall, 1969. 5-11. Floyd R. W. «Treesort: Algorithm 113», CACM, Vol. 5, N. 8, 1962, p. 434. 5-12. Floyd R. W. «Treesort: Algorithm 245», CACM, Vol. 7, N. 12, 1964, p. 701. 5-13. Floyd R. W. and Knulh D. E. «The Bose-Nelson sorting problem», (in) A Survey of Combinatorial Theory, J. N. Srivastava, et al. (eds.), Amsterdam: North Holland, 1973, pp. 163—172. 5-14. Ford L. R. and Johnson S. M. «A tournament problem», Amer. Math. Monthly, Vol. 66, 1959, pp. 387—389. 5-15. Friend E. H. «Sorting on electronic computer systems», JACM, Vol. 3, 1956, pp. 134—168. 5-16. Gale D. and Karp R. «A phenomenon in the theory |of sorting», IEEE Conf. Record of the 11th Annual Symposium on Switching and Automata Theory, 1970, pp. 51—59. 5-17. Gollieb, Calvin С «Sorting on computers», CACM, Vol. 6, N. 5, 1963, pp. 194—201. 5-18. Hibbard T. N. «Some combinatorial properties of certain trees with applications to sorting and searching», JACM, Vol. 9, 1962, pp. 13—28. 5-19. Hildebrandl P. and Isbilz H. «Radix exchange — an internal sorting method for digital computers», JACM, Vol. 6, 1959, pp. 156—163. 5-20. Hoare С A. R. «Partition, Quicksort and Find (Algorithms 63, 64 and 65)», CACM, Vol. 4, N. 7, 1961, p. 321, Vol. 6, N. 8, 1963, p. 446. 5-21. Hoare С A. R. «Quicksort», Computer Journal, Vol. 5, 1962, pp. 10—15. 5-22. Hosken J. С «Evaluation of sorting methods», Proc. Eastern JCC, Vol. 8, 1955, pp. 39—55. 5-23. Knulh D. E. «Letters to the editor regarding the ACM glossary of sorting and merging terms», CACM, Vol. 6, N. 10, 1963, pp. 585—587. 242
5-24. Knuth D. E. Sorting and Searching, The Art of Computer Programming, Vol. 3, Reading, Mass.: Addison — Wesley, 1973. 5-25. Lorin H. A. «A guided bibliography to sorting», IBM Systems J., 1971, pp. 244—254. 5-26. Lynch W. C. «More combinatorial properties of certain trees», Computer Journal, Vol. 7. 1964, pp. 299—302. 5-27. Lynch H. A. «The t-fibonacci numbers and polyphase sorting». Fib. Quart., Vol. 8, 1970, pp. 6—22. 5-28. Martin W. A. «Sorting», Computing Surveys, Vol. 3, 1971, pp. 147—174. 5-29. Peterson W. W. «Bouricius' theorem on sorting devices», IBM Research Report IR-00051, 1956. 5-30. Pratt V. «Shell sorting and sorting networks». Ph. D. Thesis, Stanford University, 1971. 5-31. Radke C. E. «Merge-sort analysis by matrix techniques,» IBM Systems Journal, Vol. 5, 1966, pp. 226—247. 5-32. Raney C. N. «Generalization of the Fibonacci sequence to n dimensions», Canadian J. Math., Vol. 18, 1966, pp. 332—349. 5-33. Reynolds S. W. «A generalized polyphase merge algorithm», CACM, Vol. 4, 1961, pp. 347—349. 5-34. Rich R. P. Internal Sorting, Englewood Cliffs, N. J.: Prentice — Hall, 1973. 5-35. Rivest R. L. and Knuth D. E. «Computer sorting, bibliography 26», Computing Rev., Vol. 13, 1972, pp. 283—289. ' 5-36. Shell D. L. «A high-speed sorting procedure», CACM, Vol. 2, N. 7, 1959, pp. 30—32. 5-37. Sobel S. «Oscillating sort-a new sort merging technique», JACM, Vol. 9, 1963, pp. 372—374. 5-38. Williams J. W. J. «Heapsort: Algorithm 232», CACM, Vol. 7, 1964, pp. 347—348. 5-39. Windley P. P. «Trees, forests, and rearranging», Computer Journal, Vol. 3, 1960, pp. 84—88. 06 авторе В. Бердж является научным сотрудником исследовательского центра им. Т.'Д. Волтсона фирмы ИБМ (IBM) вЙорктаун Хайте, штат Нью-Йорк. Летом 1954 г. он начал заниматься программированием для ЭВМ «Трейк» (TREAC), которая представляет собой аналог ЭВМ «Едсэк» (EDSAC) фирмы «Ройал рейдар истеблишмент» (Royal Radar Establishment) г. Грейт Мэлверн, Англия. В 1956 г., окончив Кембриджский университет и получив степень бакалавра математики, он приступил \ к ' исследованиям в организации «Ю-си-эл-эй» (UCLA), где работал над созданием программ численного анализа для ЭВМ «Суэк» (SWAC). Вернувшись в Англию в 1956 г., В. Бердж был принят в штат фирмы «И-эм-ай электронике» (Е. М. I. Electronics Ltd.). Здесь он участвовал в разработке ЭВМ «Имидек-2400» (EMIDEC 2400) и ее программного обеспечения. В это же время он заинтересовался вопросами использования и реализации языков программирования ЛИСП (LISP) и АЛГОЛ-60 и написал компилятор с языка АЛГОЛ-60 для ЭВМ «Имидек-2400»> В 1963 г. В. Бердж перешел 24.3
в отдел «Юнивэк» (UNIVC) фирмы «Сперри рэнд корпорейшн» (Sperry Rand Corporation) в Нью-Йорке, где руководил разработкой программного обеспечения систем. Здесь он возглавил небольшую группу, которая пыталась сформулировать основные требования к семантике языков программирования. С 1965 г. он работает в исследовательском центре фирмы ИБМ (IBM), занимаясь изучением упрош,енных методов программирования, связанных с использованием языков высокого уровня, и методов комбинаторного анализа алгоритмов. В. Бердж является автором большого числа трудов по программам сортировки и языкам программирования. УКАЗАТЕЛЬ ПРОГРАММ 169. 174 08, 109 125 absorbword 139 addr 106 addtree 213 all 31 and 31 anycharacter append 30, append! 133 appendls 133 atleast 159 atomic 103, 115 atomicc 125 В belongs 31 bracket 172 brother 31 bsearch 209 btree 122 btreel 122 btree2 122 cascade 235 cascadecycle 235 cbtree 122 cc 165, 168 ccat 168 cclist 159, 165, 170 close 161 comb 125, 142 combl 125 comb2 125 combine 33, 45, 125 compose, 30 compromise 238 compromisecycle 238 concat 30, 108, 109 concatl 133 244 concatls 115 concatq 164 concats 132 concattss 133 consblock 33 conscond 33 conslist 33 cons Y 33 content 106 crossproduct 31 ctree 117 diag 179 diagram 179 difference 31, 175 div 29 divides 29 double 29 down 144, 211 edit 166, 170, 175 empty 122, 164, 169, emptyc 122 endorder 124 eqch 169 equal 31 equalset 31 etree 122 exactly 159 exists 31, 164 extend 53 extract 45 falsehood 105 filter 31, 131 find 30, 196 flatn 204 174
followedby 164 forest 117, 161 forest 1 117 forest2 117 forests 124 forest4 124 G gen 133 genalt 144 genbtree 136 generate 130 grandfather 31 greater 29 H hs 129 identity 109 identityls 115 incl 31 indref 107 infop 188, 189 inheap 222 insert 193 insertl 215 insertb 204 insertc 195 insertf 204 is 169, 174 integer 107, 171 intersection 31 isfalse 105 istrue 105 mapt 137 max 29 maxpath 222, 223 merge 1 218 merge2 218 merge 194, 197, 198 minus 29 mmerge 194, 198 modcascadecycle 236 mult 29 N next 131 nlist 112, 114 nonatomic 113, 115 nonatomicc 125 nonempty 122 nonemptyc 122 nonnull 108 nonnullc 108 nonzero 107 not 31 notexists 106 null 108 nullc 108 nullist 108 nullists 109 nullistset 169, 174 nullstring 164 0 open 161 or 31 orreln 31 left 122, 127 length 109 length Is 115 less 29 list 107, 108, 159 listl 108, 110, 117 list2 111, 117 listing 117 lists 109 liststructure 160 lookup 53 Is 160 Isl 115 ls2 115 Iss 106 M map 30, 108, 109 map2 31 map21 135 mapl 133 mapls 30, 115 maps 130 parse 168, 178, 185 partition 146, 211 partitions 147 parts 151 perhaps 159 perms 143 phase 230 phi 31 plus 29 polyphase 230 positionlist 35, 53 posn 208 possible 106 postfix 30, 109 postorder 118 power 156 prec 189 predecessor 107 predicates 33 prefix 108 prefixs 129 prescan 145 prodf 31, 155 product 108, 109 245
productls 115 prune 136 sumsquares i09 sumsquaresls 115 qs 210 qualify 159, 171, 175 quicksort 211 R recognize 178 reduce 185 ref 106 related 31 repeat 138 replace 67, 207 reverse 30, 109 reversetree 123 reverseforest 118 reversels 115 reversetree 118 revls 30 revpol 57, 68 rightl22 root 117, 122 rotate 124 searchtree 214 seek 170 select 53, 194 selectls 54 separated 172 shift 224 sift 182 sister 31 sort 137, 194, 195 sp 30 square 29 star 159 stream 1 132 strearn2 132 struct 166 subst 37 successor i 07 sum 30, 80, 108, 109 sumls 30, 115 sumset 31 61 85 05 th 211, 212 thefirst 130 thrice 29 til 134 transition 55 translam 33 trbvs 35 trdef 34 tree 117, treel 117 tree2 117 trees 145, trexp 34 true 105 truthvalue ts 129 tsearch 207 twice 29 и un 169, 174 union 31, 168 unionlist 159 unlist 170 unmap 171, 175 unzip 30 up 144, 169 upchain 221 upto 159 us 132 val 67 value 54, 63 W while 131 whiles 134 zero 107 zip 30, 114 zips 114, 131 64, 69, 96
ПРЕДМЕТНЫЙ УКАЗАТЕЛЬ Л-дерево 117, 137 Л-последовательность 129 Л-комбинация 125 Л-лес 123 Л-список 19, 107, 115, 160 Л-ß-napa 19 Алгоритм Джонсона-Троттера 143 — Черча 40 Атомный компонент 99 В Вставка 193 — двоичная 208 — дерева 212 Выборка 194 Вызов по значению 94 наименованию 94 ссылке 92 Выражение 17, 22, 100 — атомное 100 — в нормальной форме 37 —, вспомогательная часть 23 — выборочное 26 —, главная часть 23 — инфиксное 185 —, левая часть 100 —, нормальная форма 37 — обратимое 37 —, определение 16 —, определяющая часть 23 —, правая часть 100 —, приведение 37 — приводимое 37 —, разложение 37 — рекурсивное 32 — составное 54, 100 — условное 25, 75, 125 , ветвь 26 Выход естественный 81 — неестественный 81 Генерация перестановок 142 Глубина вложения 60 — переменной 66 Грамматический анализатор 148 — разбор 161 Д Дамп 64 — присваивающий 95 Двоичный поиск 204, 209 Декартово произведение 101 Дерево 19, 99, 161 — Л-бинарное 122 — Л-В-бинарное 125, 206 — бинарное 122 —, вес 127 — двоичного поиска 204 —, корень 117 —, прохождение 135 — сортировки 221 Диапазон значений 98 Динамическая цепь 61 И Идентификатор 13, 53 Инструкция вход 72 — выход 72 — загрузки 58 позиции 68 ядра 68 — применения 57 Итерация 152 К Комбинатор 27, 40, 41, 43 — парадоксальный 43 Комбинация 54, 125, 141 Компонент верхнего уровня 87 — непосредственный 87 Конечный автомат 149 Конкатенация ПО Л Лес 123, 161 —, поворот 124 Листинг 21, 32, 117 Локальный минимум 217 Лямбда-выражение 16 Лямбда-преобразование 36 М Магазинный стек 93 Машина Тьюринга 48 Метка, значение 81 Метод прямой вставки 196 — Шелла 202 Модификация данных 87 МП-автомат 161, 182 Н Нуль-компонентная структура 101 О Область действия 98 — значения 11 — определения 11 Обмен 195 Обратная польская запись 49 — структура 118 Объединение 101 Операнд 11, .54 247
Оперативная среда 53 Оператор 11, 54, 100 — инфиксный 12 — постфиксный 12 Операция свертки 182 Определение выражения 16 в стандартной форме 17, 24 вспомогательное 22 одновременное 24 рекурсивное 26 функциональное 24 циклическое 26 —, левая часть 24 —, правая часть 24 П Память типа ttrie» 213 Пара идентификатор-значение 53 — целое-список 19 Переменная локальная 59 —, глубина 66 — свободная 16, 35 — связанная 16, 35 —, положение 66 Перестановка 120, 140 — структурная 161 Побочный эффект 92 Позиция 87 Поле записи 106 Поток 100, 129 Правило Крамера 237 Предикат 12 Преобразование 12 — идентификатор-значение 53 Префикс 19 — потока 129 Признак 193 Прогонка 226 Разбиение 145 Разделитель 18 С Сдвиг 182 Символы нетерминальные 155 — терминальные 155 Синтаксис абстрактный 18, 99, 160 — конкретный 18, 99, 160 Список, длина 120 —, сканирование 111 Слияние 194, 197, 225 — балансное 226 — каскадное 234 модифицированное 236 — компромиссное 238 — многофазное 228 — потоков 136 —, схема 199 —, стратегия 226 Сортировка 193 — быстрая 210 — на лентах 225 Состояние 64 —, дополнение 83 — конечное 149 — начальное 149 Стирающие потоки 138 Схема упорядочения 199 Т Теорема Черча—Россера 38 Точка входа 82 Транзитивное замыкание 155 У Управляющая строка 54, 64 Условие 26 Ф Функция 27, 103 — выборки 87 — генерирующая 104 — подсчитывающая 104 — простая рекурсивная 42 — рекурсивная 42 — симметричная 113 — частично рекурсивная 44 — J 84 — Y 27, 71, 76 — ^-определимая 39 Ц Цикл 26, 134, 228 — двойной 135 Я Ядро 62, 69 — программное 82 Язык АЛГОЛ-60 59, 97, 134, 149, 186, 197 — контекстно-свободный 148, 155 — ЛИСП 97 — регулярный 148 L-значение 87, 91 /^-значение 87, 91 SfCD-машина 49, 64 Х-1-исчисление 38 Х-К-ис'1исление 38