Текст
                    FUNCTIONAL PROGRAMMING
APPLICATION AND
IMPLEMENTATION
Peter Henderson
University of Newcastle upon Tyne
PRENTICE-HALL INTERNATIONAL
ENGLEWOOD CLIFFS, NEW JERSEY
LONDON NEW DELHI
SINGAPORE
SYDNEY TOKYO TORONTO
WELLINGTON
1080


МАТЕМАТИЧЕСКОЕ ОБЕСПЕЧЕНИЕ ЭВМ П.Хендерсон ФУНКЦИОНАЛЬНОЕ ПРОГРАММИРОВАНИЕ Применение и реализация Перевод с английского Л. Т. Петровой под редакцией А. П. Ершова МОСКВА «МИР» 1983
ББК 32.973 Х38 УДК 681.142.2 Хендерсон П. Х38 Функциональное программирование. Применение и реа- реализация: Пер. с англ.— М.: Мир, 1983.— 349 с, ил. Книга английского специалиста по программированию» обобщающая опыт использования функционального программирования. Обсуждаются особенности функциональных языков и возможности их реализации на современных ЭВМ. Изложение иллюстрируется многочисленными примерами. Для программистов, математиков-прикладников, для всех, кто преподает и изучает программирование ., 2405000000-314 БЬК 6Ф 7.8 041 @1)-83 Редакция литературы по математическим наукам 1980 by Prentice-Hall International, Inc., London Перевод на русский язык, «Мир», 1983
ОГЛАВЛЕНИЕ Предисловие редактора перевода ... 5 Предисловие * * i , , 8 Глава 1. Функции и программы * 11 1.1. Программирование при помощи функций 12 1.2. Программирование при помощи процедур 18 Глава 2. Строго функциональный язык , 23 2.1. Символьные данные 23 2.2. Элементарные селекторы и конструкторы 28 2.3. Элементарные предикаты и арифметика 31 2.4. Рекурсивные функции . . * 33 2.5. Другие рекурсивные функции , , 36 2.6. Накапливающие параметры * 43 2.7. Локальные определения * 47 2.8. Функции высших порядков и Х-выражения 50 2.9. Точечная запись выражений 58 Упражнения * « 63 Глава 3. Простые функциональные программы 67 3.1. Анализ размерностей — пример структурированного прог- программирования и структурированной отладки программы 68 3.2. Поиск по дереву — сравнение стратегий с приоритетом по ширине и по глубине ¦ 79 3.3. Программа функции индивидуумы , , 90 Упражнения * » 97 Глава 4. Представление и интерпретация программ 99 4.1. Абстрактная и конкретная формы программ 101 4.2. Связывание 108 4.3. Интерпретатор для варианта языка Лисп 115 Упражнения ...,,, 127
Оглавление 349 Глава 12. Основы машинного обеспечения инструментария функцио- функционального программирования 303 12.1. Хранение списков 308 12.2. Сборщик мусора 313 12.3. Хранение строк 31G 12.4. Построение и отладка инструментальной системы .... 31o 12.5. Раскрутка и оптимизация 325 Приложение 1. Ответы к некоторым упражнениям 32У Приложение 2. Компилятор для инструментального Лиспа 333 Приложение 3. Библиография 336 Алфавитный указатель tft>>>css.....,9. 340
ПРЕДИСЛОВИЕ РЕДАКТОРА ПЕРЕВОДА Функциональное программирование — это способ состав- составления программ, в которых единственным действием является вызов функции, единственным способом расчленения программы на части является введение имени для функции и задание для этого имени выражения, вычисляющего значение функции, а единственным правилом композиции — оператор суперпозиции функции. Никаких ячеек памяти, ни операторов присваивания, ни циклов, ни, тем более, блок-схем, ни передач управления. Алгол, Фортран и Ассемблер сделали свое дело, и тем не- нескольким сотням тысяч людей у нас в стране, которым прихо- приходится писать программы для ЭВМ, пожалуй, будет нелегко представить себе, как можно приступать к программированию, не опираясь на эти, казалось бы, незыблемые конструкции языков программирования. Внимательное чтение этой книги будет на первых порах вызывать у них чувство протеста — они раз за разом будут ощущать себя как спортсмены, которым нужно играть в волей- волейбол с одной рукой, привязанной к телу. Потребуется большое доверие к автору для того, чтобы при чтении книги преодо- преодолеть дебют с такой заметной интеллектуальной форой. На основании собственного болезненного опыта упражнения в функциональном программировании мне хотелось бы преду- предупредить читателей, что ощущение трудности и необычности составления функциональных программ вполне законно и естест- естественно. Мало того, они могут взять себе в союзницы историю науки. Известный американский ученый Алонсо Чёрч, автор лямбда- нотации и тезиса Чёрча, создал на подступах к теории алгорит- алгоритмов лямбда-исчисление, которое сейчас можно считать не чем иным, как теоретической моделью современного функциональ- функционального программирования. Он бился немало месяцев над тем, чтобы запрограммировать в лямбда-исчислении операцию вы- вычитания единицы из натурального числа i 0—1=0
6 Предисловие редактора перевода Чёрч так и не справился с этой задачей и уже уверился в неполноте своего исчисления, но в 1932 г. Стефен Клини, тогда молодой аспирант, предложил следующую, на первый взгляд искусственную, но на самом деле полностью отвечающую сути дела конструкцию: if(x, у, г) = если а: то 0 иначе если у+\*=*х то г иначе F(x, t/+l, 2 + 1) n-l-F(/i, 0, 0) История той же науки — теории алгоритмов и вычислимо- вычислимости — говорит, что лямбда-исчисление, сыграв принципиальную роль в угадывании объема понятия эффективно вычислимой функции, в дальнейшем отошла на второй план, уступив место развитию теории на основе понятия частично-рекурсивных функций и машин Тьюринга. И тем не менее почти через пять- пятьдесят лет эта теоретическая модель снова берется на вооружение в программировании, с надеждой найти в ней ответ на ряд труд- трудных проблем сегодняшнего развития. Если продолжить разговор об исторических предпосылках функционального программирования, то для советского чита- читателя будет особенно интересно узнать, что в Советском Союзе была и существует методологическая школа построения пакетов прикладных программ, которая ведет свой счет времени с 1954 г., предвосхитив и реализовав значительную часть основных по- понятий, относящихся к функциональному программированию. Я имею в виду крупноблочное программирование обработки составных объектов в функциональных обозначениях, предло- предложенное и разработанное Л. В. Канторовичем и его учениками и реализованное в серии конкретных программных систем для ЭВМ Стрела, БЭСМ и М-20 в 50-е и 60-е годы. Глубокая интуиция Л. В. Канторовича позволила ему пред- предвидеть трудности, присущие «блок-схемному» программирова- программированию, ухватить ряд сущностей функционального программирова- программирования и предложить приемы, направленные на повышение эффективности функциональных программ (некоторые из них описаны в этой книге). В то же время начальные варианты подхода школы Канторовича, как сейчас видно, содержали ряд пробелов, а натиск общественного мнения со стороны «тра- «традиционных» программистов, идущих от вовсю критикуемой ныне, но всесильной в то время фон-неймановой архитектуры, поставил представителей школы в положение обороняющихся и сильно затормозил развитие ее идей. Следующий выход в функциональное программирование связан с Лиспом, разработанным в 1961 г. американским уче- ученым Дж. Маккарти. Сам язык и его судьба хорошо известны программистам, однако, несмотря на всю популярность, школа
Предисловие редактора перевода Лиспа долго представлялась «большинству» чем-то вроде сек- сектантского учения. Сам Маккарти хорошо сознавал фундаментальный характер функциональной модели вычислений, лежащей в основе Лиспа, и его анализ этой модели, сделанный в 1965 г. в докладе на Международном конгрессе по обработке информации, привел к построению важного раздела теоретического программирова- программирования — теории рекурсивных программ. В это же время появи- появилась ставшая теперь классической статья английского ученого П. Лэндина об использовании лямбда-исчисления для описания семантики Алгола-60. Хотя эти два направления работ в течение более чем 10 лет были известны только теоретикам, тем не менее они создали предпосылки к выходу функционального подхода в программировании на более широкую арену. Можно рассматривать как некий символ, что новый период функционального программирования начался с работы чело- человека, которому мы, пожалуй, в большей степени, чем кому- либо другому, обязаны за увековечение архитектуры фон Ней- Неймана и «традиционного» программирования. Я имею в виду изобретателя Фортрана научного сотрудника ИБМ Дж. Бэкуса и его Тьюрингову лекцию 1978 г. под названием «Может ли программирование освободиться от бремени фон-неймановского стиля? Функциональный стиль и его алгебра программ». Здесь не место повторять анализ особенностей и потенциальные пре- преимущества функционального программирования, и я скажу только, что понять и принять функциональное программиро- программирование легче, если рассматривать задачу программирования в ее полном контексте, начиная со спецификации задачи и логиче- логического анализа ее разрешимости, побочным продуктом которого является сама программа. Возвращаясь к этой книге, следует воздать должное автору, который педагогически очень тактично вводит и дозирует столь непривычные на первый взгляд приемы функционального про- программирования. Более того! Чувствуя момент, когда у чита- читателя появляются первые проблески уверенности, он честно показывает, что новые меха годятся и для старого вина, де- демонстрируя, как ряд приемов традиционного программиро- программирования с использованием памяти и итерационных процессов можно закодировать в функциональной нотации. С другой стороны, рассказывая о реализации функциональных программ «до конца», т. е. вплоть до аппаратной реализации, автор под- подталкивает активного читателя на поиски новой архитектуры ЭВМ, более непосредственно отражающей рекурсивный харак- характер функциональных вычислений. Академгородок А, П. ЕрШОв октябрь 1982 г.
ПРЕДИСЛОВИЕ Основным мотивом, побудившим меня написать эту книгу, было желание собрать воедино многие важные идеи, которые возникают при написании программ в чисто функциональном, или аппликативном, стиле. Эти идеи проистекают из двух раз- различных областей нашей науки — изучения семантики языков программирования и работ по искусственному интеллекту. Книга полностью охватывает функциональный стиль програм- программирования, структуру функциональных языков, их применение и реализацию и такие современные концепции, как вычисления с задержкой, недетерминированные программы и функции высших порядков. Мой подход является прагматическим и больше касается применений и реализации функционального программирования, нежели его теоретических основ. Так, книга начинается с из- изложения основной методики построения функциональных про- программ, затем описывается, как можно реализовать функцио- функциональные языки на современных машинах и, наконец, дается описание наиболее развитых черт таких языков посредством определения их смысла в терминах реализации. Функциональные языки используются в исследованиях по семантике двояко. Один способ — это описать интерпретатор для изучаемого языка; функциональные языки идеально при- приспособлены для этой роли. Другой способ состоит в том, чтобы для каждой программы на изучаемом языке определить экви- эквивалентную функцию или функциональную программу. В любом случае чрезвычайная простота и мощность функционального языка делают его весьма удобным для таких семантических спецификаций. В работах по искусственному интеллекту приходится опери- оперировать сложными, обычно символьными структурами данных и такими же сложными алгоритмами. Маккарти в разработанном им языке программирования Лисп, имеющем функциональную основу, преодолел многие из этих трудностей. Использование чисто функционального стиля программирования естественно для приложений, характеризующихся многократным получе- получением сложных структур данных из других таких же структур.
Предисловие В настоящее время техника функционального программирования получает широкое распространение, и важность ее как средства, позволяющего продвинуться в развитии языков самого высокого уровня, будет возрастать. По нашему мнению, полное понимание семантики функцио- функционального языка, и вообще всякого языка программирования, достигается только тогда, когда исследована связь между язы- языком и его реализацией. Поэтому книга включает полное описание реализации одного, чисто функционального языка, названного Лисп-инструмент (сокращенный Лисп). Эта реализация важного варианта Лиспа представлена в форме заготовок — набора про- программ, из которого прилежный читатель, имеющий доступ к машине, может легко составить себе единую систему. Два су- существенных компонента этой системы, компилятор в исходной и компилятор в объектной форме, даны в приложении. Все, что требуется для построения системы,— это написать простой имитатор для абстрактной машины и затем ввести в него ком- компилятор. Компилятор будет компилировать себя и любую дру- другую программу на языке Лисп-инструмент, позволяя, таким образом, читателю экспериментировать с программами и язы- языками, описанными в книге. Конструктивные детали содержатся в гл. 11 и 12. Для читателя может оказаться полезным время от времени обращаться к этим главам, чтобы подкрепить свое понимание точного смысла некоторых концепций функцио- функционального программирования. В то время как Лисп-инструмент служит конкретным при- примером реализованного функционального языка, на протяжении всей книги для написания программ используется язык с не- несколько более удобным для чтения синтаксисом. Существуют простые правила транслитерации, так что любую программу из книги или нужную для упражнений можно выполнить в системе Лисп-инструмент. На протяжении нескольких лет я использовал материал этой книги в лекциях по семантике языков программирования для студентов старших курсов. Изложение практически осно- основано на том, что новые понятия можно легко сопоставить с по- понятиями, уже встречавшимися при изучении более традицион- традиционных языков. Кроме того, возможность экспериментировать с описанными здесь интерпретатором и компилятором и втор- вторгаться в семантику изучаемых языков позволяет воспринимать тонкости семантики языков программирования. В целом книга поможет студентам сопоставить и сравнить факты, изучаемые в таких различных областях, как теория программирования, ком- компиляторы, языки программирования, структуры данных и машинная архитектура. Для более старших студентов, специа- специализирующихся в программировании, языках программирования.
10 Предисловие искусственном интеллекте или машинной архитектуре, книга послужит введением во многие существенно важные разделы использования языков как в программировании, так и для целей спецификации. Читателям, заинтересованным в прак- практическом знакомстве с теорией вычислений или с языками весьма высокого уровня для своих экспериментов, построение этой системы также весьма полезно. Чтобы осветить все темы, включенные в книгу, потребуется, вероятно, двухсеместровый курс. Однако, как показывает схема зависимости глав, можно опускать те или иные разделы соот- соответственно тому, на что делается ударение. В моем собственном курсе лекций, который продолжается примерно семестр, я пы- пытаюсь научить функциональному программированию как прак- практическому и полезному навыку и поэтому делаю упор на при- примеры и упражнения, а также на экспериментирование с интер- интерпретатором и компилятором. Для этого используются гл. 2, 4, 5 и 6, а также избранный материал из гл. 8 и 9. Разумеется, студенты имеют доступ к реализации системы Лисп-инструмент как для экспериментов по ее использованию, так и для модифи- модификации самой системы. В более продолжительном курсе можно было бы поручить студентам самим построить для себя такую систему. Это способствовало бы изучению функционального стиля программирования и лучшему пониманию семантики язы- языков программирования. Многие помогали мне в подготовке этой книги. Я получил ценные советы от тех, кто прочитал те или иные разделы руко- рукописи. Особенно значительный вклад внесли Тони Хоар и Саймон Джонс. Очень ценна эффективная реализация системы Лисп- инструмент, выполненная Фредом Кингом. Я благодарен Джулии Леннокс, которая печатала и выверяла рукопись, а также всем моим коллегам по Университету Ньюкасла, создавшим стимули- стимулирующую обстановку для работы. Питер Хендерсон
Глава 1. ФУНКЦИИ И ПРОГРАММЫ В первых машинах и размеры памяти, и быстродействие были невелики по сравнению с современными. Поэтому для програм- программиста самым важным было сделать программу как можно более эффективной. По мере того как увеличивались размеры памяти и быстродействие машин, стало возможным несколько поступить- поступиться эффективностью в угоду большей ясности и выразительности программ. Программы, становясь более легкими для понимания, становились экономичнее при построении и эксплуатации. Были разработаны такие высокоуровневые языки программи- программирования, как Фортран, Алгол, Кобол, ПЛ/1 и Паскаль. Из-за того, что программы, написанные на этих языках, транслиру- транслируются на машинные языки, пользователи теряют в скорости их выполнения от 2 до 10 раз. Эта готовность выиграть в легкости понимания программы пусть даже за счет некоторой потери эф- эффективности является тенденцией, которая значительно уси- усилится в ближайшем будущем по мере того, как мы будем пожи- пожинать плоды технологических усовершенствований в проектиро- проектировании ЭВМ. Современной тенденцией в технологии является разительное снижение стоимости оборудования. Кроме того, быстродействие машин и их размеры постоянно изменяются настолько, что уже в начале восьмидесятых годов можно предвидеть улучшение их характеристик на целые порядки. Таким образом, ввиду повышения эффективности оборудования все для большего числа приложений ясность и легкость понимания программ будет значить больше, чем оптимальное использование оборудования. Можно ожидать поэтому, что уровень языков и их ориентиру- ориентируемость на пользователя будут прогрессивно возрастать. В част- частности, можно надеяться, что языки будут становиться более простыми и единообразными. Для современных языков, таких как уже упомянутые, характерен компромисс между ориенти- ориентированностью на машину и ориентированностью на пользователя, что приводит к большому разнообразию внутренних свойств в каждом языке. Языки, не учитывающие возможностей машины, обладают тем свойством, что их программы выполняются более медленно.
12 Гл. L Функции и программы Язык, о котором пойдет речь в этой книге, меньше ориенти- ориентирован на современные машины, зато доставляет пользователю для общения с машиной средства высокого уровня. Мы говорим об этом языке как о строго функциональном, так как понятие функции является первичным для программ на такого рода языках. Функциональные языки являются наиболее широко изучаемым классом среди языков самого высокого уровня. Программы, которые выглядели бы длинными и трудными на традиционных языках высокого уровня, часто бывают более короткими и ясными в функциональной форме. Таким образом, с уверенностью можно сказать, что функциональные языки имеют более высокий уровень, нежели уже упомянутые совре- современные языки. Ценой, которую приходится за это платить, является то, что их реализация приводит к программам, менее эффективно использующим оборудование. Но, как уже говори- говорилось, тенденции в развитии технологии таковы, что эта эффек- эффективность становится менее важной и для многих приложений в будущем совсем не будет иметь значения. Мы начнем эту главу с рассмотрения математического поня- понятия функции и покажем, что программы могут быть построены как композиции функций. В разд. 1.1 мы определим, что такое функциональный язык программирования, а в разд. 1.2 обсу- обсудим, чего нет в функциональном языке по сравнению с традици- традиционными высокоуровневыми языками. В частности, мы коснемся некоторых свойств машинной ориентированности языков высо- высокого уровня. Остальная часть книги посвящена изучению того, как именно строго функциональный язык может использоваться для широкого круга практических приложений и как он может быть с достаточной эффективностью поддержан современными машинами. Основной тезис, который мы выдвигаем, состоит в том, что функциональные программы яснее выражают свои цели, чем традиционные программы, и поэтому они легче для понимания, эксплуатации и в первую очередь их легче соста- составить правильно. 1.1. Программирование при помощи функций Концепция функции является одним из фундаментальных понятий математики. Интуитивно, функция является правилом, сопоставляющим каждому элементу некоторого класса соот- соответствующий ему единственный элемент из другого класса. Другими словами, для заданных двух классов элементов, соот- соответственно называемых областью определения и областью зна- значений данной функции, каждому элементу из области опреде- определения функция ставит в соответствие в точности один элемент из области значений. Соответствие, изображенное на рис. 1.1,
/./. Программирование при помощи функций 13 определяет функцию, назовем ее /, для которой областью опре- определения является класс элементов {а, 6, с, d) и областью зна- значений — класс {/?, q, r). Важным свойством соответствия, ко- которое характеризует его как функцию, является то, что элементы области значений единственным образом определяются по эле- элементам из области определения. Этот факт позволяет нам обо- обозначать через f(x) тот элемент из области значений, который Рис. 1.1. функция f соотносит элементу х из области определения этой функции. Говорят также, что / отображает х в f(x) или что f(x) является образом х относительно /. При вычислениях чаще го- говорят, что f(x) есть результат применения / к аргументу х. Существует много способов описания простых функций, подобных приведенной выше. Например, можно записать соот- соответствие в виде последовательности пар (а, /?), (fr, q), (с, q)t (d, г) или протабулировать функцию, как показано в табл. 1.1, что будет равносильно предыдущему. Таблица 1.1 Другим способом является запись соответствия в виде ряда равенств: /(а)=/>, f(b)=q, f(c)=q, f{d)=r Каждый из этих способов изображения страдает тем недостат- недостатком, что здесь неявно выражено и поэтому нелегко проверяется то свойство соответствия, что f(x) единственным образом опре- определяется по х. Когда область определения велика, то на первый взгляд не очевидно, обозначает ли некоторая конкретная запись действительно функцию. Когда же область определения бес- бесконечна, мы вовсе не можем использовать такие обозначения.
14 Гл. L Функции ц программы Для этих функций можно использовать такие записи, только чтобы перечислить отдельные образцы соответствия, устанавли- устанавливаемого функцией. Рассмотрим более реальный пример. Функция квадрат имеет областью определения класс всех целых чисел (положительных, нуль и отрицательных), а областью значений — класс всех не- неотрицательных чисел. Элемент области значений функции квад- квадрат, который соответствует элементу х из области определения этой функции, есть хХх. Таким образом, мы имеем, например, квадратC)=9, квадратF)=36, квадрат{—2)=4 Невозможно, разумеется, изобразить все соответствие, так как область определения функции квадрат бесконечна. Однако можно определить все это соответствие полностью, написав такое правило (или определение): квадрат (х)=х X х Здесь точно указано, как вычислить образ х для любого х из области определения функции квадрат. Образ элемента х по- получается просто возведением его в квадрат, т. е. умножением элемента на самого себя. При написании этого определения мы использовали переменную х для обозначения любого элемента из области определения и записали правило вычисления соот- соответствующего элемента из области значений в виде выражения, использующего эту переменную. Такую переменную обычно называют параметром определения. Определяющее правило, или определение, такое, как мы только что рассмотрели для функции квадрат, является тем стандартным способом, которым мы и будем представлять функ- функции. В таком представлении область определения и область зна- значений даны неявно и не всегда очевидны. То, что областью оп- определения для функции квадрат мы предполагаем множество целых чисел, выражается только в дополнительных ограни- ограничениях, а тот факт, что областью значений является в этом случае множество неотрицательных целых чисел, хотя и выводится из вида правила, тем не менее не выражен явно. Мы использовали бы то же самое правило, если бы определяли функцию квадрат для всех вещественных чисел. Вообще для определяемых функ- функций достаточно, если охарактеризовать их области определения и области значений неформально, в виде дополнительных ограничений. В действительности, мы часто будем называть областью определения и областью значений множества даже более широкие, в которых некоторые элементы из названной области определения функции не имеют соответствующего эле- элемента в области значений и наоборот. Например, можно ска- сказать, что функция квадрат отображает целые числа (как область
/./. Программирование при помощи функций 15 определения) в целые (как область значений). Здесь в область значений включаются необязательные отрицательные числа. Аналогично о функции обратная(х) ss — можно сказать, что она отображает вещественные числа в ве- вещественные. Однако вещественное число 0 не входит на самом деле в область определения функции, так как элемент обрат- обратная @) не определен приведенным выше правилом. Говорят, что функция, для которой в качестве области оп- определения задано множество Л, является частичной над А (или частично определенной в множестве Л), если в А существуют элементы, для которых образ посредством этой функции не определен. Функция, которая не является частичной над Л, является всюду определенной в Л, или общей функцией. Мы не будем часто пользоваться этими терминами, но важно отметить, что вообще все функции, которые описываются в книге, будут частично определенными в множествах, которые заданы как области определения. Рассмотрим другой пример. Функция макс, примененная к паре чисел, выдает в качестве результата наибольшее из них. Так, например, макс{\, 3)=3, макс(\,7, — 2,0)=1,7 Правило для этого удобнее всего выражается через два пара- параметра: . . ix, если х^у максах, У)^^у в противном случае Этого громоздкого определения можно избежать, если восполь- воспользоваться условной формой если. . .то. . .иначе. . ., которая зна- знакома нам по традиционным языкам программирования: макс(х, #)=зесли х^у то х иначе у Как и в определении функции квадрат, справа в правиле здесь находится алгебраическое выражение, включающее параметры х и у. В данном случае форма если. . .то. . .иначе. . . требует сначала вычислить предикат х^у и в зависимости от результата (истина или ложь) выбрать подвыражения х или у в качестве результата конкретного применения функции. Например, максA,3), т. е. результат применения функции макс к аргументам A,3), вычисляется по этому правилу так: конструкция применение, называемая также в программиро- программировании вызовом функции, гласит, что параметрам х и у должны быть сопоставлены значения 1 и 3 соответственно. Далее тре-
16 Гл. L Функции и программы буется вычислить х^у, т. е. 1^3, что, дает, разумеется, значе- значение ложь. Соответственно этому выбираем значение у, т, е. 3, в качестве результата применения. Так мы определили, что макс A,3)=3. Начинаем знакомиться с тем, как можно программировать при помощи функций. Мы определяем базовый набор полезных функций, таких как квадрат и макс, и, беря их с соответствую- соответствующими аргументами, вычисляем их результат способом, описан- описанным выше. Функция является по существу программой, которая воспринимает входной сигнал (ее аргументы) и вырабатывает выходной (ее результат). Чтобы иметь возможность конструи- конструировать более мощные программы, необходимо уметь определять новые функции через старые. В конечном счете эти новые функ- функции должны, разумеется, ссылаться при их вычислении только на основные, базовые функции, и очевидно, что этого можно достичь построением иерархии определений. Рассмотрим, на- например, следующее определение: наиб (х, t/, z) = макс {макс (х, у), z) По имени функции легко догадаться, что наиб(х, у, z) вычисляет наибольший из трех ее аргументов. Она делает это, используя функцию макс, сначала применяя ее к первым двум аргументам, а затем к промежуточному результату и третьему аргументу. Определение функции наиб иллюстрирует один из фундамен- фундаментальных способов построения новых функций из старых. Это метод композиции функций. Вложением одного применения функции макс в другое мы добиваемся композиции функции макс с собой же. Вообще функции могут вкладываться одна в другую на произвольную глубину и составлять таким образом произвольно глубокие композиции. Например, наибольшее из шести вещественных чисел а, Ь, с, d, e и / может быть опреде- определено одной из следующих композиций: макс(наиб(ау Ь, с), наиб(а9 е, f)) наиб(макс(а, Ь), макс(с, d)> макс(еу /)) макс(макс(макс(а, 6), макс{су d)), макс(е> f)) Чтобы программировать при помощи функций, нужно иметь возможность определить достаточно богатое множество основ- основных функций и затем использовать композицию, чтобы опреде- определять новые функции в терминах исходных. Таким образом мож- можно построить целую библиотеку функций; некоторые из них, воз- возможно построенные над слоями других, могут представляться достаточно мощными, чтобы называться программами — строго функциональными программами. Возникает вопрос, нужно ли еще что-нибудь, кроме такого набора исходных функций и воз- возможности составлять из них композиции, для того, чтобы оп-
1.1. Программирование при помощи функций 17 ределить библиотеку функциональных программ. Наш ответ гласит, что ничего больше не требуется, и целью этой книги является иллюстрация и подтверждение этой точки зрения. Выбор совокупности основных функций, безусловно, очень важен. Для практических целей, как для достаточно эффектив- эффективной реализации на современных машинах, так и для легкости изображения задачи программой, набор основных функций должен быть весьма мощным. Во второй главе мы введем множе- множество основных функций, которые в функциональном програм- программировании часто являются стандартными и которые адекватно определяют функциональные программы во многих важных приложениях. Возможен другой выбор совокупности основных функций, и это обсуждается далее в десятой главе. В число основных функций необходимо включить арифмети- арифметические операции. Мы привыкли в математике и программиро- программировании записывать алгебраические выражения, включающие арифметические операции, в виде а + Ь х с, Bхх + 3ху)/(х—у) Разумеется, операции +, —, X и / являются функциями, каждая из которых имеет областью определения упорядоченные пары вещественных чисел и областью значений — все вещественные числа. Можно явно определить их как функции: плюс(х, у)^=х + у умн(х, у) = хху минус{ху y)izEzx—y дел(х, у) = х/у Используя вместо операций эти имена функций, можно перепи- переписать приведенные выше алгебраические выражения в виде плюс(а, умнф, с)) дел(плюс(умнB,х), умнC>у)), минус(х, у)) Запись выражений в этой форме показывает то, что Лэндин назвал аппликативной структурой выражений в математическом языке (и языке программирования). Каждое выражение в таком языке может быть разбито на составляющие его части, каждая из которых является либо операцией, либо операндом. Операнд обозначает значение, в то время как операция обозначает функ- функцию. Структура выражения проста: она состоит из операции, применяемой к операндам. Важное понятие, связанное с аппликативной структурой, состоит в том, что значение выражения единственным образом определяется по значениям составляющих его частей. Таким образом, если некоторое выражение дважды встречается в одном и том же контексте, оно имеет одно и то же значение в обоих
18 Гл. L Функции и программы случаях. Язык, в котором это свойство сохраняется для всех его выражений, обычно называют аппликативным. В этой книге мы называем его строго функциональным языком. Вообще ис- используются оба названия, подчеркивая доминантную роль, которую играют функции и применение функций. Прежде чем начать наше исследование функционального программирования на строго функциональном языке, мы дадим в следующем раз- разделе краткий обзор структуры традиционных языков програм- программирования и обсудим те их черты, которые не являются строго функциональными. 1.2. Программирование при помощи процедур Во всех современных языках программирования высокого уровня имеется конструкция, называемая процедурой или подпрограммой, понятие которой в значительной мере бази- базируется на идее функции. Действительно, чаще всего эти процеду- процедуры используются для реализации новых функций в том смыс- смысле, в каком говорилось об этом в предыдущем разделе. В про- программах A.1) — A.3) функция max записана соответственно на Фортране, ПЛ/1 и Паскале. Эти определения реализуют строгие функции. Чтобы определить наибольшее из трех значений, на
1.2. Программирование при помощи процедур 19 Фортране, например, функция может быть вызвана таким опе- оператором присваивания: М=МАХ(МАХ(А, В), С) Однако в этих языках может быть определен только очень ог- ограниченный класс функций, и в практических программах приходится прибегать к более общему использованию процедур. Мы начнем отходить от строгих функций, когда определим процедуры, которые не выдают результат, но присваивают его значение одному из своих параметров. Например, можно опре- определить функцию МАХ так, что вызов МАХ (А, ?, M) вычисляет наибольшее из чисел А и В и присваивает его значение пара- параметру М. Это записано в A.4) — A.6) соответственно на Форт- Фортране, ПЛ/1 и Паскале. i Чтобы вычислить наибольшее из трех значений, теперь нужно запрограммировать два последовательных вызова процедуры: МАХ (А, ?, М) и MAX (My С, М). Можно доказать, что здесь имеют место чисто синтаксические изменения в написании вы- вызова функции, само понятие функции не нарушается. Мы лиши- лишились только некоторых удобств при написании той композиции применений, которая вычисляет наибольшее из трех значений.
20 Гл. L Функции и программы Мы отходим немного дальше от понятия строгой функции, когда определяем МАХ как такую процедуру, которая при вызове МАХ (А, В) вычисляет наибольшее из чисел А и В и присваивает это значение параметру Л, как это сделано в A.7) — A.9). Таким образом, последовательные вызовы МАХ (А, В) и МАХ (А, С) определяют наибольшее из трех значений (оставляя результат в Л), но при этом могут изменить исход- исходное значение параметра А. Снова можно констатировать, что строгое понятие функции не нарушено (только сделано неявным), так как результат вы- вызова MAX(At В) по-прежнему единственным образом опреде- определяется по исходным значениям аргументов. Программы, состав- составленные из чистых функций, просто определяют, как новые значения вычисляются по исходным значениям. Однако процеду- процедуры A.4)— A.9) непросто вычисляют значения, но они имеют также побочный эффект присваивания относительно одного из своих параметров. При составлении программ из таких процедур приходится думать в терминах нарастающих изменений зна- значений переменных при следующих одно за другим присваива- присваиваниях. Несмотря на использование локальных присваиваний, процедуры A.1) — A.3) не имеют побочного эффекта и поэтому реализуют строгие функции. Более сильный пример побочного эффекта дает нам про- процедура, которая изменяет значение переменной, не являющейся ее параметром. В A.10) — A.12) процедура МАХ определена
1.2. Программирование при помощи процедур 2i таким образом, что вызов МАХ(А, В) присваивает наибольшее из двух значений переменной М. Переменная М не является параметром. Наибольшее из трех значений вычисляется теперь двумя последовательными вызовами МАХ (А, В) в МАХ(М, С), при этом результат остается в М. Процедура с такими опреде- определениями, как эти, приводит к программам, которые очень трудны для понимания. Но средства, позволяющие определять такие непонятные, неясные процедуры в традиционных языках про- программирования, включаются в эти языки по той причине, что без них программы не могут достаточно эффективно использо- использовать машину. Строго функциональный язык, который мы определим в сле- следующей главе, не допускает побочных эффектов. Программист может только определять функции, которые вычисляют значения, единственным образом определяемые по значениям аргументов. Соответственно этому в строго функциональном языке отсутст- отсутствуют многие понятия, знакомые по традиционным языкам. Наи- Наиболее важным является отсутствие присваивания. То же самое относится к известному в программировании понятию перемен- переменной, обладающей значением, которое время от времени изме- изменяется присваиванием. Переменные в нашем строго функцио- функциональном языке скорее подобны математическим переменным,
22 Гл. L Функции и программы которые мы использовали в предыдущем разделе как параметры в определениях функций. Они используются, только чтобы дать имена значениям аргументов функции, и связаны с этими по- постоянными значениями на протяжении всего вычисления алге- алгебраического выражения, определяющего результат функции. Зато строго функциональный язык обладает двумя весьма мощными средствами. Первое — это способность структури- структурировать данные. Второе — возможность определять так называ- называемые функции высших порядков. Мощность структурирования данных состоит в том, что целые структуры данных выступают как единые значения. Они могут вводиться в функцию как аргу- аргументы и выдаваться из нее как результаты. Наиболее важно, что однажды построенная структура не может изменяться, ее значение остается неизменным. Разумеется, эта структура мо- может включаться в другие, но до тех пор пока в программе тре- требуется ее значение, оно остается доступным. Этот специальный механизм имеет фундаментальную важность для применимости функциональных языков и требует большой осторожности при реализации, если должна быть получена достаточная эффек- эффективность. Почти то же самое можно сказать о функциях высших порядков. Функции высших порядков имеют другие функции либо своими аргументами, либо результатом. Использование таких функций может привести к более кратким и сжатым про- программам. Их правильная реализация составляет предмет рас- рассмотрения последующих глав. Будущие языки программирования явятся по необходимости компромиссом между языками, на которых программы могут быть ясно выражены, и языками, которые могут быть эффек- эффективно реализованы. Однако стоимость оборудования радикально изменяется, оборудование становится более дешевым и более мощным по всем параметрам. Эта тенденция только усиливается. Поэтому настало время вновь взглянуть на наши языки про- программирования и ответить на вопрос, какие средства высокого уровня можно попытаться включить в них, чтобы облегчить построение некоторых видов программ. Класс языков, на ко- которые мы ссылаемся как на аппликативные, или строго функ- функциональные, языки, широко изучается. Программы на этих языках могут быть на порядок короче, чем программы для решения тех же самых задач, но записанные на традиционных языках. Их, следовательно, легче написать правильно, легче понять и поэтому легче эксплуатировать. Цель этой книги — продемонстрировать эти возможности, описать технику про- программирования на таких языках, сопоставить эту технику с некоторыми теоретическими исследованиями в программиро- программировании и полностью описать способ реализации такого языка на современных машинах.
Глава 2. СТРОГО ФУНКЦИОНАЛЬНЫЙ ЯЗЫК Эта глава посвящается введению абстрактных понятий, ко- которые используются на протяжении всей книги для представ- представления функциональных программ. В предыдущей главе были использованы неформальные понятия и определены функции, которые отображали числовые значения в числовые. Чтобы за- записать интересующие нас программы, потребуются более бо- богатые области определения функций, нежели обычные предста- вимые в машине числа, для определенности будет использована специальная форма символьных данных, введенная Маккарти в языке Лисп. Эта форма данных, которую обычно называют S-выражением, имеет то преимущество, что является одновре- одновременно очень простой и весьма общей. Мы начнем с описания того, как можно записать символьное выражение, и затем введем основные функции манипулирования с ними. Опять-таки мы начнем точно с тех функций, которые являются основными в Лиспе, так что, когда в последующих главах будет введен наш собственный вариант Лиспа, предназначаемый для семанти- семантических спецификаций, мы сможем быть уверены, что опираемся на материал, уже знакомый читателю. Остальная часть главы посвящается методам построения еще более мощных функций. Поэтому мы обсудим различные вопросы, включая рекурсию, функции высших порядков, выбор параметров, чтобы таким образом полностью познакомить читателя с методами и поня- понятиями функционального программирования. Понятия, введен- введенные в этой главе, используются на протяжении всей книги. 2.1. Символьные данные Определим класс символьных выражений, которые назы- называются S-выражениями и образуют область определения для функциональных программ, которые мы будем позднее кон- конструировать. Каждая из следующих строк содержит S-выра- жение: (ДЖОН СМИТ 33 ГОДА) ((ДЭЙВ П) (МЭРИ 24; (ЭЛИЗА БЕТ &)) ((МОЙ ДОМ) ИМЕЕТ (БОЛЬШИЕ (СВЕТЛЫЕ ОКНА)))
24 Гл. 2. Строго функциональный язык Самым очевидным общим свойством, которое можно подметить в этих примерах, является использование скобок. Действитель- Действительно, использование скобок в S-выражениях является основопо- основополагающим. Если изменить положение или количество скобок, то вместе с этим изменится структура и соответственно смысл самого выражения. Этот раздел посвящается описанию правил построения S-выражений в той форме, в какой они преимущест- преимущественно используются в этой книге. Небольшое расширение, которое оказывается полезным в последующих главах, отло- отложено до разд. 2.9. Кроме скобок в приведенных примерах S-выражения состоят из элементов, которые называются атомами. Вот примеры атомов: ДЖОН —127 СМИТ МОЙ 33 АВ13 Атомы бывают двух типов — символьные и числовые. Символь- Символьный атом обычно является последовательностью букв, хотя может содержать и другие символы, включая цифры, но обяза- обязательно должен содержать по меньшей мере один символ, отли- отличающий его от числа. Символьный атом рассматривается как одно неделимое целое. Мы никогда не расчленяем его на состав- составляющие символы. К символьным атомам применяется только одна операция — сравнение, чтобы определить, одинаковые они или разные. Числовые атомы являются последователь- последовательностью цифр, возможно следующих за знаком, и обозначают, как обычно, десятичные числа. Вообще, S-выражение содержит смесь символьных и числовых атомов, хотя у нас будут и S- выражения с однотипными атомами. Простейшей формой S-выражения является отдельный атом. Более сложной формой является простой список атомов. Он образуется заключением в скобки записанной последователь- последовательности атомов. Так, выражение (ДЖОН СМИТ 33 ГОДА) состоит из последовательности четырех атомов. Расположение S-выражения на странице и, в частности, промежутки не имеют значения, кроме того, что необходимо иметь по меньшей мере один пробел между соседними атомами, чтобы отличить их. Другие примеры простых списков даны в B.1). Первый список состоит из восьми символьных атомов. Второй список состоит из пяти разнотипных атомов, третий состоит ровно из одного символьного атома БАНАН. Четвертый пример отличается от третьего, он состоит из пяти символьных однобуквенных атомов.
2.L Символьные данные 25 (ДЖОН СМИТ ОЧЕНЬ ЧАСТО ЕСТ ХЛЕБ С МАСЛОМ) A4 ГРАДУСОВ И 50 МИНУТ) B.1) (БАНАН) (БАНАН) Можно получить S-выражения значительно более сложной структуры, если допустить, что элементами списка могут быть не только простые атомы, но сами S-выражения. Так, запись ((ДЭЙВ 17) (МЭРИ 24) (ЭЛИЗАБЕТ 6)) является S-выражением, так как представляет собой список из трех элементов, каждый из которых сам является S-выражением. Снова напомним, что расположение S-выражения на странице безразлично, однако скобки чрезвычайно важны. Как уже говорилось, выше записан трехэлементный список, каждый эле- элемент которого сам является двухэлементным списком. В то же время запись (ДЭЙВ 17 МЭРИ 24 ЭЛИЗАБЕТ 6) является 6-элементным простым списком, который имеет совер- совершенно другую структуру. Рассмотрим теперь построение более сложного примера. Поскольку S-выражениями являются эле- элементы БОЛЬШИЕ и (СВЕТЛЫЕ ОКНА), то (БОЛЬШИЕ (СВЕТЛЫЕ ОКНА)) тоже S-выражение. S-выражениями яв- являются также (МОЙ ДОМ) и ИМЕЕТ, поэтому ((МОЙ ДОМ) ИМЕЕТ (БОЛЬШИЕ (СВЕТЛЫЕ ОКНА))) тоже есть S-выражение. На практике списки могут вкладываться один в другой на большую глубину и иметь весьма сложную скобочную структуру. Трудности при чтении длинных S-выра- жений перекрываются легкостью их обработки. Общие правила построения S-выражений можно просумми- просуммировать в следующем рекурсивном определении: 1) атом есть S-выражение, 2) последовательность S-выражений, заключенная в скобки, является S-выражением (списком). В разд. 2.9 мы несколько расширим это определение, но в подавляющем большинстве примеров будем использовать S- выражения, определенные приведенными выше правилами. Перед написанием программ сначала рассмотрим, как мы собираемся записывать наши данные в формате S-выражений. Представим себе, например, что мы хотели бы составить про- программу суммирования последовательности целых чисел. В какой
26 Гл. 2. Строго функциональный язык форме представили бы мы эти данные в программе? Вероятно, как список целых чисел. Например, A27 462 45 781 —18 842 96) или A8 17 16 —16 —17 —18) так как эта структура будет простейшей при обработке. Пусть мы хотим выбрать представление в виде S-выражений для простых алгебраических формул. Здесь мы должны разли- различать константы, переменные и бинарные операции. Пусть кон- константы и переменные представляются атомами, а бинарная опе- операция помещается на первом месте в списке и за ней следуют ее операнды. Далее, для операций мы выбираем специальные символьные имена. Мы можем подытожить описание способа представления формул S-выражениями, если приведем для каждой возможной формулы соответствующее ей S-выражение, как это сделано в табл. 2.1. Таблица 2.1 Здесь в столбце формул р и q заменяют части формулы, а в столбце S-выражений — соответствующие этим частям S-выра- жения. Так, из того, что формула 2х*+1 имеет вид pi+qu где рх=2хх и ^х= 1, а рх имеет вид /?2Х(/8, где р2=2 и <72=*> следует, что эта формула представляется S-выражением (ПЛЮС (УМН 2 X) 1) По аналогичным соображениям формула хг+2х—31
2.1, Символьные данные 27 может быть представлена в виде {ПЛЮС {СТЕП X 2) {МИНУС {УМН 2 X) 31)) Она может быть также представлена выражением {МИНУС {ПЛЮС {СТЕП X 2) {УМН 2 X)) 31) и многими другими возможными способами. Любая программа, которую мы напишем для манипулиро- манипулирования с этой формулой, должна обрабатывать все эти различные формы. В качестве третьего, совсем отличного от предыдущих при- примера рассмотрим программу обработки данных, связанных с игрой в шахматы. Во-первых, нуж- нужно уметь обозначать символами название и цвет каждой фигуры, например (БЕЛЫЙ КОРОЛЬ) или (ЧЕРНАЯ ПЕШКА). Затем нужно уметь обозначать позицию каждой фигуры. Мы можем выбрать для представления координат пары целых чисел (предполагая, что строки и столбцы некоторым обра- образом перенумерованы от 1 до 8). Так, клетки шахматной доски мо- могут быть представлены парами D 7), B 1) и т. д. Теперь вся доска может быть представлена списком пар, в котором с каждой фигурой связана ее поаиция. Так, шахматная позиция, изображенная на рис. 2.1, может быть представлена списком B.2). Рис. 2.1. Разумеется, существует много различных способов для пред- представления одних и тех же данных, и, какой мы в действитель- действительности выберем, почти целиком определяется тем, что именно должны мы с этими данными делать. Этот конкретный выбор шахматных данных может быть пригодным для одних целей и быть совершенно непригодным для других.
28 Гл. 2. Строго функциональный язык 2.2. Элементарные селекторы и конструкторы Для того чтобы оперировать с S-выражениями, введем не- некоторые примитивные функции, расчленяющие или составляю- составляющие эти выражения. Затем покажем, как можно определять новые функции в терминах этих примитивных. Например, можно будет определить функцию длина(х), которая, будучи применена к списку ху выдает целое число, равное количеству элементов в этом списке. В табл. 2.2 приводятся значения этой функции для нескольких списков. Таблица 2.2 Приводя примеры результатов функции, будем в дальней- дальнейшем использовать эту форму табулирования. Здесь в столбце х перечислены некоторые простые S-выражения и соответственно в столбце длина(х) — результаты вызова функции длина для этих значений х. Мы видим, что в списке учитываются только элементы верхнего уровня и не учитываются элементы под- подсписков. Функция длина(х) не является примитивной. Прежде чем мы сможем определить ее, нам надо ввести некоторые прими- примитивные функции над S-выражениями. Этим функциям даны их традиционные имена языка Лисп, хотя на первый взгляд они несколько необычны. Чем больше пользоваться ими, тем при- привычнее они станут. Первая функция, которая нам необходима, позволяет выбрать из списка первый элемент. Это функция саг (табл. 2.3). Таким образом, функция саг может применяться к списку и ее результатом является первый (самый левый) элемент в спи- списке. Мы видим, что выбранный элемент может быть либо атомом, либо списком. Функция саг для атома не определена. Наряду с функцией саг имеется связанная с ней функция cdr. Функция cdr от списка есть список, полученный из исходного отбрасы- отбрасыванием его первого члена. Для атома функция cdr не определена. Таким образом, функции саг и cdr являются дополнительными. Если исходный список состоит из единственного атома, то зна- значением функции cdr для этого списка является (специальный) атом НИЛ (NIL).
2,2, Элементарные селекторы и конструкторы 29 При помощи двух этих примитивных функций можно выбрать из списка любой его элемент, зная, что он имеется в списке. Например, третий элемент списка х задается выражением car(cdr(cdr(x))) Так, если х есть список (ABC D), то cdr(x) есть (В С D) и cdr(cdr(x)) есть (С D). Отсюда car(cdr(cdr(x))) есть элемент С, который является третьим в исходном списке. Таблица 2.4 Мы будем допускать введение новых непримитивных функ- функций при помощи функциональных определений. Например, со- соотношение mpemuu(x)==:car(cdr(cdr(x))) определяет новую функцию третий(х), рассмотренную выше. Определяемая функция стоит слева от знака тождества, опре- определение — справа. В определение могут входить примитивные функции, другие определенные функции и параметры определя- определяемой функции. Рассмотрим, например, следующие определения функций, предназначенных для обработки S-выражений, яв- являющихся списками ровно из двух элементов, которые в свою очередь являются такими же списками. Рассмотрим пример ((А В) (С D))
30 Гл. 2. Строго функциональный язык начертание которого подсказывает нам имена, выбранные для наших функций, перечисленных в B.3). Разумеется, эти функ- нии могут быть не определены, если применяются к S-выраже- ниям неподходящей формы. первый(х) S3 саг(х) второй(х) ss car(cdr(x)) верхнийлевый(х) == первый(первый(х)) ,« ~ верхнийправый(х) гг второй(первый(х)) ' ' нижнийлевый(х) ss первый(второй(х)) нижнийправый(х) es второй(второй(х)) Теперь перейдем к примитивному конструктору — функции cons. Эта функция берет в качестве аргументов два S-выражения и соединяет их в единое S-выражение таким образом, чтобы исходные компоненты получались применением к результату функций саг и cdr (табл. 2.5). Таблица 2,5 Вообще, второй аргумент функции cons должен быть списком или специальным атомом НИЛ. Результатом cons тогда будет список, полученный включением первого аргумента в качестве первого элемента этого списка. Отсюда если у есть список длины п, то cons(x, у) будет списком длины п+1, независимо от значе- значения х. В связи с этим удобно и принято рассматривать НИЛ как список нулевой длины, или пустой список. Это символь- символьный атом (НИЛ), но он играет также специальную роль, обо- обозначая пустой список. В качестве примера использования функции cons рассмотрим определения: двучлен(х,у) =а cons{xtcons (у,НИЛ)) квадрат(а,Ьус4) = двучлен(двучлен(а,Ь),двучлен(с,а)) Функция квадрат строит структуру, которая может быть рас- расчленена использованием функций верхнийлевый, верхнийправый, нижнийлевый и нижнийправый. Эти функции даны просто как
2.3. Элементарные предикаты и арифметика 31 пример использования примитивов car, cdr и cons: они не ис- используются далее в книге. Для дальнейшего чтения, однако, важно иметь точное представление о функциях car, cdr и cons. 2.3. Элементарные предикаты и арифметика Для того чтобы обрабатывать списки различной структуры, необходимо: а) уметь распознавать, является ли значение S-выражения атомом или списком, и б) устанавливать равенство атомов. Функция атом(х) называется предикатом, так как она выдает значение истина или ложь. Мы будем обозначать истину атомом И и ложь атомом Л. Функция атом(х) принимает зна- значение И только в том случае, когда х является символьным или числовым атомом. Некоторые примеры даны в табл. 2.6. Таблица 2.6 Мы используем такие предикаты, чтобы проверить символьное значение, прежде чем применить к нему функцию, которая в ряде случаев может быть не определена. Рассмотрим, например, функцию f(x) == если атом(х) то НИЛ иначе саг(х) которая выдает первый элемент списка, если применяется к списку, но выдает НИЛ, если применяется к атому. В то время как функция саг(х) не определена для некоторых S-выражений (частичная функция), f(x) определена для всех выражений (всюду определенная функция). Отметим, что функция атом(х) не различает символьные и числовые атомы. Можно также сравнить два атома и определить, равны они или нет. Это делает предикат равно(х, у), но нужна осторож- осторожность при его использовании. Либо одно, либо другое из срав- сравниваемых значений должно быть атомом. Если оба значения являются списками, результат не определен. Кроме того, ре-
32 Гл. 2. Строго функциональный язык зультат равно{ху у) принимает значение И только в том случае, когда х и у являются одним и тем же атомом, и значение Л, если либо это различные атомы, либо один из аргументов не явля- является атомом (см. табл. 2.7). Обычный способ использования этого предиката иллюстрирует следующее определение: размер(х) эк если равно(х> НИЛ) то 0 иначе если равно(саг (х), НИЛ) то 1 иначе 2 Эта функция выдает значение 0, 1 или 2 соответственно тому, что ее аргумент является списком из 0, 1 или более атомов. Теперь мы вплотную подошли к определению функции длина, которая была введена в начале предыдущего раздела. Сначала, однако, перечислим арифметические функции, которые мы до- допускаем. Будут использоваться арифметические операции +, —, X, ~- (или дел) и ост. Они вычисляют соответственно сумму, разность, произведение, частное и остаток двух операндов, которые яв- являются целыми числами. Операция ост определяется соотно- соотношением х ост у*=х—(х+у)ху т. е. она дает остаток при делении нацело двух целых чисел. Если мы хотим определить функцию, которая выдает дву- двучлен из частного от деления нацело и остатка, то записываем частост(х, у)=двучлен(х-1-у, х ост у) Примеры значений этой функции приведены в табл. 2.8. Мы видим, что частное q от деления нацело и остаток г удов- удовлетворяют соотношению x=qxy+r. Будем также использовать предикат x^iy, чтобы определять, является ли х меньше или равен у (только для числовых атомов).
2.4. Рекурсивные функции 33 Таблица 2.8 Предикат не определен для символьных атомов или списков. В качестве примера использования этого предиката рассмотрим функцию расстояние (х, */)=если *<*/ то у — х иначе х — у значением которой является модуль разности между х и у. Ана- Аналогичные предикаты можно построить для других обычных отношений между числами <, > и ^. Однако в последующих главах D и 6), где вводится наш вариант Лиспа и будет построен для него интерпретатор и компилятор, мы будем ограничиваться в языке только отношением ^, чтобы сократить размеры реали- реализующих программ. Поэтому в книге используются примеры толь- только с отношениями ^ и =. 2.4. Рекурсивные функции Рассмотрим теперь, как можно определить функцию длина (x)f применяемую к спискам и выдающую в качестве результата ко- количество элементов в этом списке. Нам требуется функция, которая, в частности, выдает значения, приведенные в табл. 2.9. Таблица 2.9 Эта последовательность значений подсказывает следующий алгоритм: последовательно берем cdr от л: и считаем, сколько раз это может быть сделано, пока не дойдем до НИЛ. Однако это 2 Ко 929
34 Гл. 2. Строго функциональный язык еще не позволяет нам немедленно программировать алгоритм как рекурсивную функцию. Вообще при оперировании со спи- списками отдельно рассматриваем два случая: х=НИЛ и хфНИЛ. Во втором случае, так как можно взять cdr от х и это будет спи- списком, можно применять функцию, определяемую рекурсивно. Рекурсия обязательно завершится, поскольку с каждым рекур- рекурсивным вызовом список становится на один элемент короче. Этот прием можно применить для построения функции дли- длина^), как показано в B.4). случай A) х = НИЛ длина(х) = 0 случай B) хфНИЛ пусть длина(саг(х)) =п B.4) тогда длина(х) = п-\-\ Случай х=НИЛ тривиален. В случае хфНИЛ мы предполагаем наличие результата рекурсивного вызова, примененного к cdr(x). Здесь предполагается, что длина (cdr (х))=п, и в этом случае длина (х) должна быть на единицу больше. Это приводит нас к записи определения функции длина (х) в следующем виде: длина (х) г= если равно(х,НИЛ) то 0 иначе длина (cdr (x))+1 Можно применить этот метод построения к другой подобной же функции сумма (х), которая берет в качестве аргумента спи- список целых чисел и выдает как результат сумму этих чисел (табл. 2.10). Так как НИЛ представляет пустой список, т. е. список с нулевым количеством чисел в нем, его сумма есть 0. Таблица 2.10 Это очень похоже на функцию длина. Случай х=НИЛ три- тривиален. Случай хфНИЛ приводит к определению суммы от Рассмотрение возможных случаев приводит к анализу, задан- заданному соотношениями B.5).
2.4. Рекурсивные функции 35 списка х в виде саг от х плюс сумма от cdr этого списка. Записы- Записываем определение функции в следующем виде: сумма (*)=если равно(х, НИЛ) то 0 иначе саг(х)+сумма (cdr (x)) Третьим примером будет функция соединить (х, у), которая берет в качестве аргументов два списка х и у и выдает как ре- результат единый список, содержащий последовательно все эле- элементы списка х и все элементы списка у (табл. 2.11). Так как у функции соединить два аргумента и оба — списки, мы должны сделать анализ случаев B.6) для каждого из них. Здесь во всех случаях, кроме B.2), можно сразу выписать результат, а в случае B.2) используется рекурсия. Хотя можно было бы взять cdr как от х> так и от у, на самом деле нужно только cdr от х. Здесь завершение рекурсии гарантируется конечностью х. По ходу этого анализа мы на каждом шаге ис- исследуем, чем располагаем, и смотрим, не можем ли мы прямо написать ответ. Если известно, что некоторый список не НИЛ, то смотрим, не можем ли мы написать неявный ответ в терминах cdr от этого списка. В случае B.2) имеются три кандидата на вычисление значений рекурсивным вызовом: соединить(саг(х), у) соединить(х, cdr(y)) coeduHumb{cdr(x), cdr (у))
36 Гл. 2. Строго функциональный язык Мы обосновали получение требуемого результата согласно первому кандидату. Второй и третий не использовались. Таким образом, мы построили функцию, которую можно было бы записать так: соединить(х, у) s= если равно(х> НИЛ) то если равно(у, НИЛ) то НИЛ иначе у иначе если равно(у, НИЛ) то х иначе cons(car(x), соединить (cdr(x)> у)) Но в таком виде обычно эту функцию не записывают, потому что многие замечают, что при х=НИЛ функция соединить (х, у)=у независимо от того, будет ли у равен НИЛ или нет. То есть под- подвыражение если равно (у, НИЛ) то НИЛ иначе у можно заменить на у, откуда получаем соединить(х, у) = если равно(х, НИЛ) то у иначе если равно(у, НИЛ) то х иначе cons(car(x)> соединить(саг(х)у у) Далее, некоторые предпочитают использовать тот факт, что при хфНИЛ соединить (*, y)=cons(car(x), соединить (cdr(x)y у)) независимо от того, будет ли у равен НИЛ или нет. Это озна- означает, что проверка у может быть опущена и определение запи- записывается в виде соединить(х, у) ==н если равно(х, НИЛ) то у иначе cons(car(x), соединить(саг(х)у у)) Все три версии определяют эквивалентные функции. Все они дают один и тот же результат. Одна может представляться легче для понимания, чем другая. Первую версию можно пред- предпочесть за то, что она явно перечисляет все случаи, а третью — за ее краткость. Вторая и третья версии имеют, однако, сущест- существенно разную эффективность. Вторая версия избегает вычисле- вычислений в случае у=НИЛ ценой излишних проверок, когда уФНИЛ. Третья версия избегает повторных проверок у, однако рекур- рекурсивно перестраивает х даже при у=НИЛ. 2.5. Другие рекурсивные функции Часто бывает удобно при определении функции вводить вспо- вспомогательные функции для облегчения нашей задачи. Напри- Например, при определении функции соединить (х, у) мы могли бы
2.5. Другие рекурсивные функции 37 использовать дополнительную промежуточную функцию соед(хг у), которая соединяет х и у в предположении уФНИЛ. Тогда мы имели бы определение соединить{ху у) = если равному, НИЛ) то х иначе соед(х, у) соед(х9 у) = если равно(х, НИЛ) то у иначе cons(car(x), coed(cdr(x), у)) Отметим, что здесь объединяются достоинства второй и третьей версии функции соединить (х, у) из предыдущего раздела. В функ- функциональном программировании на пути определения главной функции принято определять новые функции в терминах старых, устанавливая таким образом множество функций, определенных одна через другую. Таким образом, функциональная программа состоит из множества функций, одна из которых рассматривается как первопричина других. Это функция, которую программист рассчитывает вычислить, и она как бы является главной про- программой, в то время как остальные функции служат для нее подпрограммами. Проблема выбора подфункций при разработке главной функ- функции является извечной проблемой хорошего структурирования программы. Иногда стандартные подфункции выявляются сами собой, но более часты случаи, когда удачный подбор подфункций специального назначения может значительно упростить струк- структуру множества функций в целом. Если функции, выбранные в этой книге, как кажется, обладают этим качеством, то это потому, что были отвергнуты многие предварительные варианты. И на самом деле, чтобы сделать хорошо структурированную функциональную программу, можно дать только один совет: пытаться непрерывно улучшать то, что имеется. В этом разделе мы рассмотрим построение наиболее разработанных функций. Интересной функцией, с которой мы и далее будем встре- встречаться, является функция обратить (х), которая выдает список с элементами, перечисленными в обратном порядке (табл. 2.12). Заметим, что если элементы списка в свою очередь являются списками, то элементы этих последних не обращаются. По-
38 Гл. 2. Строго функциональный язык скольку х список, мы должны сделать анализ случаев. Если х есть НИЛf то обратить(х) тоже НИЛ. Если х=?НИЛ, то можно использовать обратить(саг(х)). Итак, для начала мы имеем обратить (cdr(x))y и, чтобы завершить задачу, нужно иметь функцию, которая могла бы поместить саг(х) в конец обратить (cdr(x)). С этой точки зрения в разработке полезна функция добавить(х, у) которая получает список х и элемент у и делает у новым послед- последним членом списка х. Несколько примеров таких действий дано в табл. 2.13. Если предположить, что мы можем построить такую функ- функцию, но сделаем это позже, то можно выписать законченное построение B.7) функции обратить(х). случай A) х=НИЛ обратить(х) = НИЛ случай B) х Ф НИЛ B ~ пусть o6pamumb(cdr(x)) = z ' ' тогда обратить(х) = добавить(г,саг(х)) Это можно записать как функциональное определение обратить(х)?з? если равно{ху НИЛ) то НИЛ иначе добавить(обратить(саг(х)), саг(х)) Теперь можно построить функцию добавить. Эта функция имеет два аргумента, первый из которых список и второй — некоторое S-выражение. Поэтому наш анализ слу- случаев касается только первого аргумента. На самом деле это так похоже на нашу предыдущую разработку, что мы можем немед- немедленно выписать построение B.8): случай A) х=НИЛ добавить(х,у) = соп8(у,НИЛ) случай B) х ф НИЛ 2 пусть добавить(саг(х), у) = г v ' тогда добавить(х,у) *=cons(car(x),z)
2.5. Другие рекурсивные функции 39 Это построение приводит к функциональному определению добавить(х9 у) а если равно(х9 НИЛ) то cons(y, НИЛ) иначе cons(car(x)t добавить(саг(х), у)) Теперь определение функции обратить использует добавить как подфункцию, и поэтому эти две функции вместе образуют программу обращения списка. Между прочим, мы могли бы определить функцию добавить через функцию соединить сле- следующим образом: добавить (х, у)=соединить(х, cons (у, НИЛ)) В этом случае программа для обращения списка состояла бы из трех функциональных определений. Однако привычнее вклю- включить вызов соединить непосредственно в функцию обратить и избавиться от функции добавить, что приводит к программе B.9). Для этой последней программы можно провести интересные подсчеты. Функция соединить (х, у) вызывает себя рекурсивно п раз, если длина списка х равна п. Отсюда обнаруживаем, что всякий раз, как мы используем соединить(х, у), будет п раз вызываться cons. Теперь рассмотрим функцию обратить (х). Из аналогичных соображений эта функция вызывает себя ре- рекурсивно п раз, если длина списка х равна п. Отсюда функция обратить делает также п вызовов cons, но она же делает и п вызовов функции соединить. Для каждого из этих п вызовов соединить длина первого ее аргумента равна соответственно О, 1, 2, . . ., п—1. Тогда количество вызовов cons функцией соединить от имени обратить есть 0+1+2+. . .+ (л—1)=л(л—1)/2 Общее количество вызовов cons в функции обратить будет по- поэтому п+п(п—1)/2=п(п+1)/2 что является очень большим числом (табл. 2.14). При обращении списка длины п можно было бы рассчитывать на п вызовов cons. Число излишних вызовов, сделанных функ- функцией обратить, представляется чрезмерным. В гл. 12 мы уви- увидим, что вызовы cons дорого обходятся, но еще прежде в разд. 2.6 фактически будет показано, как можно перепрограммировать
40 Гл. 2. Строго функциональный язык Таблица 2.14 функцию обратить, чтобы избежать лишних вызовов cons, что сделает программу более быстрой, даже если не знать, как дорогостоящи вызовы cons. Обратимся теперь к более типичным применениям S-выраже- ний. Разработаем функцию набор (/), которая получает как аргумент список t атомов и выдает в качестве результата спи- список, в котором каждый атом из / имеет ровно одно вхождение (табл. 2.15). Таблица 2.15 Такое использование списка, который выдает эта функция, является обычным применением S-выражений. Результатом функции Ha6op(t) является множество, представленное в виде списка без повторений. Теперь построим функцию na6op(t) и проведем анализ случаев относительно L Здесь случай t=HHJl тривиален, как обычно. В случае (ФНИЛУ где мы предположили, что список атомов, использо- использованных в cdr(t), есть s, для того чтобы вычислить список атомов, использованных в ty мы должны добавить к s car(t), если этот элемент еще не встречался. Предполагаем, что мы можем оп- определить соответствующую функцию включить (s,a-), которая
2.5. Другие рекурсивные функции 41 делает это. Тогда можно построить функцию набор (/)^если равно (t, НИЛ) то НИЛ иначе включить (набор (cdr (/)), car (/)) Функция включить (s, x) тривиальна, если предположить, что имеется функция, проверяющая, является ли х элементом мно- множества s. Имеем включить (s, *)=если элемент (х, s) то s иначе cons(x, s) Остается построить функцию элемент (х, s). Вспомним, что, хотя s логически является множеством, оно все еще остается и списком, и поэтому можно провести обычный анализ случаев B.11). случай A) s = HMJl элементах,s)= Л случай B) s ф НИЛ пусть 9AeMeHtn(x,cdr(s)) =b B.11) тогда элемент(х^) — если b то И иначе если x — car(s) то И иначе Л Мы написали все правильно, но это описание приводит к не- несколько неэффективной функции элементах, s) г= если paeno(sy НИЛ) то Л иначе если элементах, cdr(s)) то И иначе если равно(х, car(s)) то И иначе Л Простое переупорядочение проверок позволит избежать поиска по всему списку в случае x=car(s). Можно записать элементах, $) = если paeno(s, НИЛ) то Л иначе если равно(ху car(s)) то И иначе если элементах, cdr(s)) то И иначе Л В действительности функция может быть еще несколько под- подправлена и приведена к виду, в котором она обычно известна, если принять к сведению, что выражение если b то И иначе Л имеет всегда то же самое значение, что и b (если b принимает логические значения). элементах, 5)===если равной, НИЛ) то Л иначе если равно(х, car(s)) то И иначе элемент(х, cdr(s)) Мы закончили программу для функции набор (t), которая со- состоит из трех функций: набор, включить и элемент. Кратко рассмотрим обобщение. Расширим функцию набор(г) так, чтобы
42 Гл. 2. Строго функциональный язык она могла действовать и тогда, когда элементами t являются снова списки и вообще списки, вложенные на произвольную глубину. Другими словами, / является общей списочной струк- структурой и ua6op{t) есть список атомов, которые по меньшей мере однажды входят в L Отсюда результат снова является множе- множеством, представленным в виде списка атомов (табл. 2.16). Таблица 2.16 Перестроим функцию набору), как это показано в B.12). случай A) г^НИЛ набору) = НИЛ случай B) t ф НИЛ пусть Ha6op(cdr(t)) =s тогда подслучай B.1) car(t) есть атом B.12) набор(г)= включить^, car(t)) подслучай B.2) car(t) есть список пусть набор(саг(г)) = s' тогда набору) — объединить(s', s) Теперь при гфНИЛ имеем два подслучая. Когда car(t) — атом, имеем тот же результат, что и прежде. Если car(t) — список, то мы должны вызвать набор(саг(г)), чтобы определить множество атомов, входящих в car(t). Теперь у нас два множества: s и s', и мы должны вычислить результат путем объединения этих множеств, т. е. построить множество, которое содержит все элементы как s, так и s'. Следовательно, мы построили функцию на6op(t) = если paeno(t, НИЛ) то НИЛ иначе если amoM(car(t)) то включить (набор (cdr(t))> car(t)) иначе объединить(набор(саг(г)), набор(саг(Щ Чтобы завершить это расширение, необходимо построить функ- функцию объединить{иу v)> где и и v — множества. S^ro делается непосредственно, аналогично тому, как строились многие функ- функции до сих пор. Имеем анализ случаев B.13).
2.5. Другие рекурсивные функции 43 случай A) и— НИЛ объединить(и>ь)—ь случай B) и Ф НИЛ B ^ пусть o6beduHumb(cdr(u)tv) = w * ' тогда объединить(и, v) = включить(ш, саг(и)) Таким образом, если ифНИЛ, вычисляем объединение cdr(u) и v и включаем car(u) в это множество. Это дает нам функцию объединить(и, v) а если равно {и, НИЛ) то u иначе включить(объединить(сс1г(и), u), car(w)) Расширенная программа для функции набору) использует че- четыре функции, определения которых повторно выписаны в B.14—2.17). набору) S3 если равно ((,НИЛ) то НИЛ иначе если amoM(car(t) то включить (Ha6op(cdr(t)),car(t)) B.14) иначе объединить (набор(саг(()), Ha6op(cdr(t))) объединить{и, v) = если равно(и, НИЛ) то и иначе включить(объединить(саг(и)) v), car (и)) B.15) включить^, х) =если элементах, s) то s иначе cons(*, s) B.16) 9AeMenm(x9s) ^=если равно{$уНИЛ) то Л иначе если равно(х, car(s)) то Я иначе B.17) элемент(х, cdr(s)) 2.6. Накапливающие параметры В этом разделе вводится один интересный метод, часто ис- используемый в функциональном программировании. Начнем иллюстрацию этого метода, заново программируя функцию обратить(х). Основная идея состоит в том, чтобы определить вспомогательную функцию с лишним параметром, который используется для накапливания требуемого результата. В слу- случае обращения мы определяем функцию обр(х> у) с тем свой- свойством, что х есть обращаемый список, а у — параметр, накап- накапливающий обращенный список. Имеем определение функции обр(х, у) = если равно(х, НИЛ) то у иначе o6p(cdr(x), cons(car(x)t у)) через которую можно определить функцию обратить(х): обратить(х) = обр(х, НИЛ) Пожалуй, легче увидеть, как действует эта функция, чем опи- описывать это. Когда вызвана функция обр(х, у), список у уже накопил в обратном порядке все рассмотренные к этому моменту элементы обращаемого списка. Следовательно, когда х есть
44 Гл. 2. Строго функциональный язык НИЛ, то у накопил уже весь результат в целом, а если х*?НИЛ, то мы можем накопить в у саг от х и вызвать рекурсивно функ- функцию обр для обработки cdr от х. В табл. 2.17 даны значения х и */, полученные при последовательных вызовах функции обр, если первоначально функция обратить применяется к списку (A BCD). Таблица 2.17 В случае функции обр(х, у) второй параметр у является на- накапливающим. Проблема построения таких функций является проблемой подбора соответствующей подфункции с накапли- накапливающим параметром, который вычисляет требуемый результат. Здесь мы не применяли нашей обычной методики с анализом случаев, но, скорее, взяли обр(х,у) наугад и затем описали, как она работает. Чтобы применить наш анализ случаев, мы должны решить, каким будет общий вид результата обр(ху у) при произвольном выборе значения у. Пусть результатом обр(х9 у) в случае, когда х и у оба являются списками (возможно, пустыми), будет список всех элементов х, взятых в обратном порядке, дополненный всеми элементами у (в их первоначальном порядке). Формально можно написать обр(х, у)*=соединить(обратить(х), у) хотя это не является подходящим определением, так как нам нужно определить функцию обратить. Теперь можно вывести наш проект B.18) из анализа случаев по х. случай A) х=НИЛ обр(х, у) = у случай B) х ф НИЛ пусть obp(cdr(x)y z) = соединить (o6pamumb(cdr(x)), г) ,о 1Я) тогда обр(х, у)— соединить (обратить(х), у)= ' * ' = соединить (обратшпь(саг(х))у cons(car(x), у)) = обр{саг(х), cons(car(x), у)) Это требует некоторых пояснений. Это скорее доказательство правильности функции, данной в начале раздела, чем ее постро-
2.6. Накапливающие параметры 45 ение, поскольку преобразования в случае B) весьма сложны. Рассмотрим случаи по порядку. При х=НИЛ, так как мы требуем, чтобы обр(х, у) = соединить(обратить(х), у) то заключаем просто, что обр(х, у)~у. Однако, когда хфНИЛ, так как по-прежнему требуется выполнение приведенного выше равенства, следует предположить, что o6p(cdr(x), z)=coeduHumb(o6pamumb(cdr(x)), z) для любого z. Это предположение аналогично тому, которое делается в математических доказательствах по индукции, и оно справедливо для таких доказательств. Мы делаем следую- следующее. Предполагая, что можно определить функцию обр для лю- любого первого аргумента, более короткого чем х, показываем затем, что можно определить эту функцию и для х. Теперь, имея соотношение соединить(обратить{х),у) = соединить(обратить(саг (#)), cons (car (х), у)) заключаем, что выбор г=cons (car (x), у) является подходящим для определения функции обр(х, у) через o6p(cdr(x)y z). Чтобы завершить это определение функции обратить, ис- использующее накапливающий параметр, подсчитаем количество вызовов функции cons в этом варианте. Ясно, что функция об- ратить(х) делает все свои вызовы cons в обр(х, НИЛ). Однако обр(х, у) вызывает себя рекурсивно п раз, если п — длина списка х. Таким образом, обр(ху у) вызывает cons ровно п раз, а, следо- следовательно, столько же раз вызывает cons и функция обратить. Сравним это с числом вызовов п(/г+1)/2 в случае первого опреде- определения функции B.9). Имеем огромную экономию. Теперь применим метод накапливающих параметров в более типичном случае. Это случай, когда надо определить функцию, которая накапливает более чем один результат. В одном из предыдущих разделов была определена функция сумма(х), ко- которая вычисляла сумму списка целых чисел. Аналогичную функ- функцию можно определить для вычисления произведения (см. упр. 2.1). Рассмотрим определение функции сумпроизв(хI ко- которая выдает в качестве результата двучлен, у которого первый элемент является суммой, а второй — произведением элементов х. То есть мы хотим определить функцию так: сумпроизв(х) = двучлен (сумма (х), произв(х))
46 Гл. 2. Строго функциональный язык Чтобы сделать это, используя накапливающие параметры, мы введем вспомогательную функцию сп(х, s, р) с двумя дополни- дополнительными параметрами, которые будут накапливать сумму и произведение соответственно. Иначе говоря, попытаемся опреде- определить функцию сп в виде cn(xt s, р) = двучлен^ + сумма(х)> рхпр@изв(х)) и тогда можем определить сумпроизв(х)==сп(х, 0, 1) Функцию сп(х, s, р) строим путем анализа случаев по х B.19). случай A) х = НИЛ сп(х, s, р) = двучлен(8, р) случай B) х Ф НИЛ пусть cn{cdr(x), s', p') = двучлен (s' -\-сумма(саг(х)), р*' Хпроизв(саг(х))) B т тогда сп(х, s, р) = двучлен^ + сумма(х), рХпроивв(х)) к ' = двучлен^ + саг(х) + сумма(саг(х)), рХсаг(х) Хпроизв(саг(х))) = = cn(cdr(x), s -)- саг(х), р Xсаг(х)) Как и прежде, мы провели весьма длинный вывод. При этом нам потребовалось не только выбрать подфункции, но и исполь- использовать простые равенства при хфНИЛ сумма(х) = саг(х) + сумма(саг(х)) произв(х) sss саг(х) х произв(саг(х)) Это приводит нас к определению cn(xt s, р) = если равно(х, НИЛ) то двучлен(з, р) иначе cn(cdr(x), s + car(x), pxcar(x)) Снова исключительно простая и интересная функция. Здесь, избежав двух переборов списка х> необходимых при вычислении функции двучлен(сумма (х), произв(х)) мы избавились также от массы ненужных расчленений и соеди- соединений двучленов, которые были бы необходимы, если бы мы по- попытались программировать без накапливающих параметров. То есть без этих параметров определение выглядело бы примерно так: сумпроизв(х)^ если равно(х, НИЛ) то двунленфу\) иначе накопить(саг (х), сумпроизв(саг(х))) накопить{п, г)= двучлен(п + car(z), п х car(cdr(z))) Версия функции с накапливающими параметрами строит только один двучлен, конечный результат, в то время как последняя версия строит п+1 двучленов, где п — длина исходного списка х.
2.7. Локальные определения 47 2.7. Локальные определения Часто бывает полезно, вычисляя значения выражений, да- давать им имена с тем, чтобы эти значения можно было использо- использовать повторно. Такой пример был у нас в конце предыдущего раздела. Когда мы программировали функцию сумпроизв(х) без накапливающих параметров, мы сочли необходимым определить вспомогательную функцию накопить{пу z), чтобы избежать не- неэффективности такого определения: сумпроизв(х) = если равно(х, НИЛ) то двучлен@>\) иначе двучлен{саг(сумпроизв(саг(х))) + саг(х), саг(саг(сумпроизв(саг(х)))) х саг(х)) При таком определении выражение сумппоизв(саг(х)) вычис- вычисляется дважды. Чтобы распознать это общее подвыражение, компилятор должен быть весьма изощренным. Это дублирование рекурсивных вызовов функции сумпроизв дает экспоненциальный эффект, увеличивая число рекурсивных вызовов с п (для списка а: длины п) до 2п для того же самого списка. Чтобы для решения подобных проблем эффективности дать более удобное средство, нежели введение вспомогательных функций, мы допускаем ло- локальные определения при помощи форм пусть и где. Переписыва- Переписываем приведенное выше определение в форме B.20) и B.21). сумпроизв(х) 5= если равно(х, НИЛ) то двучлен@,\) иначе {пусть г = сумпроизв(саг(х)) B.20) двучлен(саг(г)-\-саг(х)у car(cdr(z)) Xcar(x))} сумпроизв(х) = если равно(х, НИЛ) то двучлен@,\) иначе {двучлен(саг(г) + car(x), car(cdr(z))Xcar(x)) где z = сумпроизв(саг(х))} B21) Выбор между этими вариантами — дело вкуса: оба они имеют тот же смысл. Используя переменную г, вводим локальное опре- определение. Областью действия этого определения является выра- выражение, заключенное в скобки. Форма пусть позволяет записать определение прежде, чем оно используется, форма где вводит определение после его использования. Все, что говорится от- относительно синтаксиса одной формы, применимо и к другой. Здесь z определено как значение сумпроизв(саг(х)), в предполо- предположении, что оно вычисляется только однажды, и затем две части этого результата, частичная сумма саг(г) и частичное произведе- произведение car(cdr{z))y используются в области действия г. Использова- Использование форм пусть и где дает средства, аналогичные блочной струк- структуре в Алголе. Мы дадим более строгое определение правил от- относительно области действия, но отложим это до рассмотрения других примеров.
48 Гл. 2. Строго функциональный язык Можно сделать определение функции сумпроизв (вариант с одной функцией) больше похожим на версию с двумя функциями из предыдущего параграфа за счет введения локального опре- определения для числа л, выбираемого из списка х, как это сделано в B.22). сумпроизв(х) о если равно(х, НИЛ) то двучлен@,\) иначе {пусть п = саг(х) B 22) и г = сумпроизв(саг(х)) к ' ' двучлен{п-\-саг(г), nXcar(cdr(z)))} Определения внутри блока отделены разделителем и. Иногда локальные определения используются, просто чтобы сделать функцию более читабельной, наглядной. Можно сделать это для функции сумпроизв, вводя новые локальные переменные sup для частичной суммы и частичного произведения, содержащихся в г. сумпроизв(х) =зесли равно(х1 НИЛ) то двучлен@,\) иначе {пусть п = саг(х) и г = сумпроизв(саг(х)) /о о^ {пусть s = car(z) {Z'ZO) и p = car(cdr(zj) dey4MH(n-{-st п Хр)}} Здесь необходимо взять в скобки определения s и /?, так как они ссылаются на значение г. Объяснение состоит в том, что, ког- когда в блоке пусть имеется ряд определений, определяемые пере- переменные доступны только в определяемом выражении, но не внутри самих определений. В B.24) используются обозначения хи х2, . . ., xk для ло- локально определяемых переменных, е1у е2, . . ., ek—для выраже- выражений, которые их определяют, и е — для определяемого выраже- выражения. Переменные xlf . . ., xh могут использоваться только в е, где они принимают значения е1у . . ., eh соответственно. Эти средства позволяют построить очень простую функцию для символьного дифференцирования. Будем оперировать только с формулами, в которые входят переменные, константы и операции + и X. Таким образом, требуется реализовать правила диффе- дифференцирования (Dx означает производную по х):
2.7. Локальные определения 49 Dx(x) = l Dx(y) = O уфх {у—константа или переменная) Dx(e,+e2) = Dx(e1) + Dx{e2) Dx (et x e2) = exxDx (e2) + e2xDx (ех) и можно построить нашу программу так, чтобы эти правила вош- вошли в нее почти непосредственно. Сначала нужно решить вопрос с представлением данных, но здесь все ясно после обсуждений в разд. 2.1. Итак, будем использовать представления Сначала запишем две вспомогательные функции, которые ис- используются для формирования сумм и произведений соответ- соответственно. сумма(и, v)-^r.cons{nJlK)Cy cons(u, cons(v, НИЛ))) произв(и, v) = cons(yMH> cons{uy cons(v, НИЛ))) Теперь можем записать функцию дифф(е), которая вычисляет производную е по X. Мы избежали детализации и очевидного анализа случаев при проектировании этой функции. Уместно, однако, сделать неко- некоторые замечания. Функция дифф(е) организована так, что вы- выдает атом ОШИБКА при поступлении неправильного аргумента. На самом деле этот атом может быть включен в результат, если неправильный аргумент затрагивает только вложенные подком- подкомпоненты. Функция построена таким образом, что при использо- использовании локальных определений правила дифференцирования пред- представлены очень кратко.
50 Гл. 2. Строго функциональный язык 2.8. Функции высших порядков и А-выражения Рассмотрим функцию увелич(х) зн если равно(х, НИЛ) то НИЛ иначе сons(car(x) + 1, увелич(саг(х))) Если ее применить к списку целых чисел, то она выдает список такой же длины, но с элементами, на единицу увеличенными по сравнению с исходным списком. Действие функции иллюстри- иллюстрирует табл. 2.18. Таблица 2.18 A 2 3) B 3 4) @ _1 —2) A 0 -1) A27) A28) Аналогично функция ост(х) = если равно(ху НИЛ) то НИЛ иначе cons(car(x) ост 2, ocm((dr(x))) вычисляет по исходному списку х список остатков от деления каждого его элемента на 2 (табл. 2.19). Таблица 2.19 Сходство этих двух функций и наша способность представить себе многие другие подобные функции приводят к рассмотрению некоторой функции общего назначения, где та конкретная опера- операция, которая выполняется над каждым элементом списка, яв- является параметром этой функции. Здесь эта функция общего наз- назначения называется отобразить (или короче отобр) и опреде- определяется соотношением отобр(х% f) =- если равно(х, НИЛ) то НИЛ иначе cons(f(car\x))y omo6p(cdr(x)y f))
2.8. Функции высших порядков и ^-выражения 51 Это в точности та же самая форма, что и в определении функции увелич и ост, но функция, применяемая к каждому элементу спи- списка xf не конкретна, она является параметром /. Мы можем пе- переопределить функции увелич и ост через отобр следующим обра- образом: #ol(z)= г+ 1 увелич(х) г= отобр(х, ув I) ocm2(z) as г ост 2 ост(х) = отобр(х, ост 2) Функции, которые используют другие функции в качестве ар- аргументов, являются примерами того, что мы будем называть функциями высших порядков. В функциональном программирова- программировании разумное использование функций высших порядков может привести к чрезвычайно простым и мощным программам. В качестве другого примера функции высшего порядка рас- рассмотрим операцию редукции (идея и название фактически взяты из языка АПЛ). Мы определяем функцию редукция(х, g\ а), где х — список, g — бинарная функция, а — константа, так что список х— (*i . . . xh) приводится (редуцируется) к значению g(xug(x2 • . .g{xh,a). . .)) Таким образом, если, в частности, мы определим плюс (у, z)=y+z то будем иметь редукция (х, плюс, 0) = сумма (х) Функция редукция строится непосредственным образом: случай A) х = И ИЛ редукция(х, g, a) —а случай B) х Ф НИЛ пусть редукция(саг(х), g, a) =z тогда редущия{х, gt a) = g(car(x)t z) Это приводит к определению редукция(х, gy а) == если равно(х, НИЛ) то а иначе g (car (x)t редукция(саг{х), g, a)) Имея эту функцию высшего порядка, можно тривиально опреде- определить функции сумма и произв: плюс (у, z)=^y + z сумма(х) = редукция(х, плюс,0) умн(у, z) = yxz произв(х) = редукция(х, умн, 1)
52 Гл. 2. Строго функциональный язык Довольно любопытно, что так же обстоит дело и с функцией сумпроизв(х). Мы должны представлять себе, что в этом случае результатом функции редукция является двучлен и поэтому аргументами функции g будут целое число и двучлен, но у нас уже была такая функция в конце разд. 2.6. накопить(п, г) = двучлен(п+саг(г), nxcar(cdr(z))) сумпроизв(х) = редукция(х, накопить, двучлен@,\)) Некоторым недостатком при использовании функций высших порядков является необходимость давать имена функциям, ис- используемым как фактические параметры. Но этот недостаток преодолевается использованием Х-выражений, к описанию кото- которых мы теперь переходим. До сих пор мы вводили функции при помощи определений вида f(xu . . ., xk)^e где / — определяемая функция, хи . . ., xk ее параметры и е— определяющее функцию выражение. Имя / рассматривается за- затем как глобальное (его можно использовать повсюду), в то вре- время как имена хи . . ., xh являются локальными (они могут ис- использоваться только в выражении е). Если бы мы хотели опре- определить функцию, которая не была бы глобальной, у нас не наш- нашлось бы средств сделать это. Х-выражение является таким выражением, значение которого есть функция. Чтобы уяснить это, мы должны понять, что опре- определение функции /, приведенное выше, присваивает / (позднее будем говорить «связывает с /») значение, которое является функ- функцией. Значение, присвоенное приведенным выше определением, является значением, представленным Х-выражением Ч*ь . . ., xk)e Функция является правилом для вычисления значения по неко- некоторым аргументам. Х-выражение заключает в себе это правило, выделяя имена параметров (здесь это хи . . ., xk) и выражение, заданное в терминах этих параметров, ^-выражение Цг)г+1 вычисляет функцию, которая, будучи вызвана при конкретном значении аргумента, выдает это значение, увеличенное на 1. То есть это функция, присвоенная ув\ в определении, данном в начале этого раздела. Отсюда находим немедленное применение для Х-выражений — используем их в качестве фактических пара- параметров функций высших порядков. Мы можем переопределить функцию увелич(х): увелич(х) = отобр(х, k(z)z+\)
2.8. Функции высших порядков и ^-выражения 53 Аналогично можем написать ост(х)=*=отобр(х, X(z)z ост 2) сумма(х) за редукция(х, Х(у, z)y + zy 0) ^-выражение обозначает то же самое функциональное значе- значение, независимо от имен, выбранных для его параметров, лишь бы они были разными для различных параметров. Так, выраже- выражения Цх)х+\ X(z)z + \ обозначают одну и ту же функцию и являются взаимозаменяемы- взаимозаменяемыми, где бы они ни встречались. В частности, можно использовать Х(х)х+1 как фактический параметр функции отобр в определе- определении увелич, хотя это и создает незначительную, но все же неже- нежелательную путаницу из-за двух «иксов». увелич(х) = отобр (х, 1(х) х+1) Будем разрешать использовать ^-выражения повсюду, где требуется функциональное значение. В частности, можно ис- использовать их в комбинации с формами пусть и где в локальных определениях функций. Рассмотрим новый вариант определе- определения функции увелич: увелич(х) sez {отобр(х, ув\) где yel=X(z)z+l} Здесь мы не избежали введения имени ув\ для функционального аргумента функции отобр, но сузили область действия этого имени, сделав его локальным. Действительно, имя ув\ доступно только в самом выражении отобр (х, ув\). Когда локальное определение вводит рекурсивную функцию, следует использовать модифицированные формы пусть и где. Будем указывать эту модификацию ключевыми словами пустьрек и гдерек. Тогда можно переписать определение функции сумпроизв(х), данное в конце разд. 2.6, чтобы сделать функцию сп(ху s, р), имеющую ограниченное применение, локальной: сумпроизв(х)?Е=={сп(х, 0, 1) гдерек cn = X(x, s, p) если равно(х, НИЛ) то двучлен^, р) иначеcn(cdr(x)ys + саг(х)% рхсаг{х))\ Разница между формами где и гдерек (соответственно пусть и пустьрек) заключается в разнице областей действия имен. Ес- Если имеем формы где и пусть, как это показано в B.26), имена хи . . ., хк доступны для использования со значениями
54 Гл. 2. Строго функциональный язык еи . . ., eh только в определяемом выражении е. Однако в случае форм гдерек и пустьрек B.27) имена хи . . ., xh используются для значений еъ е2, . . ., ek и в выражении е, и в выражениях еъ . . ., eh. Если каждое из выражений еи . . ., eh является Я-выражением и, таким образом, каждое из Хи . . ., xk есть функция, то любое из выражений еь . . ., ek может выз- вызвать любую из функций хи . . ., xk. Именно таким способом по- постоянно будем использовать формы гдерек и пустьрек. Сокращение рек указывает на рекурсию и призвано подчерк- подчеркнуть, что локальные определения взаимно рекурсивны. Мы увидим, что это ведет к усложнению интерпретации функцио- функциональных программ. Это может также ввести в заблуждение поль- пользователей. Когда следует использовать рек и когда нет? Суще- Существует хорошее правило использовать рек, только когда это не- необходимо, т. е. когда локально определяются рекурсивные функции и когда есть уверенность, что все параллельные опреде- определения тоже являются определениями функций. То есть для на- надежности все локальные определения в блоках пустьрек и гдерек должны быть определениями функций. Взглянем на странное определение {{х гдерек *=х+1} где *=0} Так как используется форма гдерек, оба х в выражении х—х+1 должны быть одинаковыми. Ясно, что это выражение бессмыс- бессмысленно. С другой стороны, выражение {{х где х=х+\} где х=0} совершенно правильное и имеет значение 1. Здесь различные вхождения х в выражении х=х-\-1 имеют разное значение, х в правой части определяется во внешнем блоке. Как мы видели, функциональная программа обычно состоит из множества взаимно рекурсивных функций, одна из которых
2.8. Функции высших порядков и к-выражения 55 отмечается как главная определяемая. Можно использовать фор- формы гдерек и пустьрек, чтобы сформировать выражение, значе- значением которого является главная определяемая функция и где вспомогательные функции спрятаны внутри выражения. Если множество функций, которые определяются, есть /ь /2, . . ., fh и /, является главной, то определение записывается в виде B.28). Последняя версия функции набор (х), составленная в конце разд. 2.5, может быть представлена в форме B.29). {набор гдерек набор = М0 если равно((, НИЛ) то НИЛ иначе если amoM(car(t)) то включить (Ha6op(cdr(t)), car{t)) иначе объединить (набор(саг(()), na6op(cdr(t))) B.29) и объединить = к (и, v) если равно(и, НИЛ) то v иначе включить(объединить(саг(и, v), car(u)) и включить = k(s, х) если $лемент(х, s) то $ иначе cons{x,s) и элемент ~k(x, s) если равно(ху НИЛ) то Л иначе если равно(х, car(s)) то # иначе элемент(ху cdr(s))} Наконец, в этом разделе мы хотим ввести другого рода функ- функцию высшего порядка — функцию, которая выдает функцию, т. е. функцию, результатом которой является функция. Рассмот- Рассмотрим определение увел{п)^К(г) z+n Здесь увел является, безусловно, функцией, она требует в ка- качестве аргумента целое число п. Однако ее результат тоже функ- функция. Например, увелC) есть функция, которая при вызове вы- выдает ее аргумент, увеличенный на 3. Таким образом, значение выражения {/B)где/=^лC)} есть 5. Аналогично мы можем переопределить функцию увелич: увелич(х) = отобр(х, увел(\)) Заметим, что если бы мы хотели иметь локальное определение функции увел в определении функции увелич (это не очень же- желательно и делается с целью иллюстрации), то мы бы записали
56 Гл. 2. Строго функциональный язык увелич(х) = {отобр(х, увел(\)) где увел = Х(п) {k(z)z + п}\ Здесь ясно показано, что тело функции, присваиваемое увел, само является ^-выражением. Мы использовали лишние скобки, чтобы яснее показать, где начинается и заканчивается каждое выражение. В связи с используемыми здесь обозначениями возникла лю- любопытная проблема. Мы использовали ^-выражение X(z)z+n, не обращая внимания на тот факт, что функция определяется через переменную, которая не является ее параметром, в данном случае это п. Такие переменные называются глобальными (или свободными) переменными данной функции. Здесь эта перемен- переменная не очень глобальна, если посмотреть на контекст, в котором используется X(z)z+n> но тем не менее проблема существует. Эту проблему наглядно иллюстрирует выражение {пусть я = 1 {пусть f = l(z)z + n {пусть п = 2 «3)Ш Значение fC), а отсюда и значение всего выражения есть либо 4, либо 5 в зависимости от того, какое п берется в момент вызова/. Мы представляем себе ^-выражение как выражение, которое вы- вычисляет функциональное значение, и здесь это значение присваи- присваивается /. Будем предполагать, что функциональное значение, вычисленное таким способом, фиксирует все глобальные перемен- переменные. То есть функция такова, какой она получается при замене всех глобальных переменных их значениями в момент вычисле- вычисления Х-выражения. Следовательно, при вычислении приведенного выше выражения мы сначала выполняем локальное определение л+1, затем вычисляем ^-выражение, присваивая / функциональ- функциональное значение rk(z)zJr\. После этого переопределяется л, я=2, и затем вызывается / с фактическим параметром 3. Так как / при- присвоено значение X(z) г+1, этот вызов выдает значение 4. Такая форма вычисления 1-выражений, когда глобальные переменные определяются в месте определения, но не в месте вызова функции, называется простым, или статическим, связыванием (перемен- (переменных). Эта семантическая особенность функционального програм- программирования оказывает значительное воздействие на общий проект его реализации, и поэтому мы будем неоднократно возвращаться к этому на протяжении всей книги. Мы заключим этот раздел определением функции высшего порядка, которая имеет функции и в качестве аргументов, и в качестве результата. Определим KOMn(fyg)^l(x)f(g(x))
2.8. Функции высших порядков и 'К-выраэюения 57 Это так называемая композиция (или произведение) функций. Она берет в качестве аргументов две функции и выдает как ре- результат тоже функцию, которая применяет последовательно обе исходные функции. Таким образом, она применяет / к результату g. Например, увобр = комп (увелич, обратить) является функцией, которая сначала обращает список и затем увеличивает каждый его элемент на 1. Таблица 2.20 Аналогично, если определить, подобно увел(п)у функцию ocm(n)='k(z)z ост п то комп (увел A), ост B)) является функцией, которая дает тот же эффект, что и %(г) (гост 2)+1 так что функция отобр (х, комп (увел A), ост B))) берет каждый элемент списка х и добавляет 1 к остатку от его деления на 2 (табл. 2.21). Таблица 2.21 Функции высших порядков являются очень важной темой, они весьма трудны при реализации строго функционального языка на машине. Вот почему мы их здесь обсуждали и еще неоднократ- неоднократно будем обращаться к ним на протяжении книги. В частности, гл. 9 посвящена приложениям функций высших порядков.
58 Гл. 2. Строго функциональный язык 2.9. Точечная запись выражений Мы подошли теперь к полезному расширению понятия S- выражения, которое в гл. 4 часто будет использоваться при запи- записи S-выражений. Здесь этот раздел включен для полноты. Его изучение можно отложить до тех пор, пока в последующих гла- главах не потребуется точечная запись. Необходимость в этом рас- расширении возникает из-за того, что не существует способа пред- представить значение cons(x> у), когда у есть атом (исключая случай у есть НИЛ). Будем представлять значение cons (А, В) посредством S-выражения (А.В) с точкой между атомами. Отметим, что написанное отличается от значения (AB) которое является результатом выражения cons (Af cons (В, НИЛ)) Теперь мы столкнулись с явлением, не встречавшимся нам до сих пор,— имеются два разных способа представить одно и то же значение. Значение cons {А, НИЛ) может быть представлено либо как (Л), либо как (А.НИЛ). Иными словами, имеются различные способы записи одного и того же значения. Аналогичная ситуация встречается в десятич- десятичном представлении целых, когда мы считаем, что 127 и 0127 представляют то же самое значение, игнорируя 0 перед знача- значащими цифрами. Расширение понятия, которое мы делаем, в действительности имеет более общий характер. Например, представим значение cons (At cons (В, cons (Ct D))) в виде (А В CD) Снова имеем, что (А В С.НИЛ) и (А ВС)
2.9. Точенная запись выражений 59 суть разные представления одного и того же значения. Вообще значение выражения cons (e1, е2) представляется точечной парой где v1 и v2 суть S-выражения для значений ех и е2, независимо от того какого рода эти значения. Таблица 2.22 Мы применяем следующие сокращения: S-выражение (vi.(v2. (vs. . . .(vh.vh+1). . .))) записывается в виде (Vx V2 . . . Vh.Vk+1) Отмечаем единственную точку. Далее, S-выражение (Vi v2 . . . vh.HHJI) записывается в виде (Vi V2 ... Vh) Эти правила можно суммировать так. Точка, за которой непо- непосредственно следует открывающая скобка, может быть опущена, так же как опускаются открывающая и соответствующая закры- закрывающая скобки. Точка, за которой непосредственно следует атом НИЛ, также может быть опущена, это относится и к атому НИЛ. Из этого следует, что значение выражения cons (A, cons (?, cons (С, НИЛ))) представляется структурой {А.{В.{С.НИЛ)))
60 Гл. 2. Строго функциональный язык которая может быть последовательно сведена к структурам (AB.(С.НИЛ)) (А В С МИЛ) (А ВС) На самом деле все S-выражения, изображенные на рис. 2.2, представляют одно и то же значение, т. е. значение приведенного выше выражения. Стрелки показывают, какая форма из какой Рис. 2.2. может быть получена применением одного из правил. Вообще будем всегда использовать кратчайшую форму, т. е. будем исклю- исключать все точки, где это возможно. Будем использовать точечную запись в таких случаях, когда, например, хотим сослаться на первые два элемента списка про- произвольной длины. Запишем U, х2. у) вместо (Xi Х2 Х$ . . . Xk) так что хх обозначает первый элемент, х2— второй, а у обозначает весь остаток списка (здесь это (х3 . . . xh)). Иногда удобнее стро- строить пары, используя функцию cons, а не двучлен. В значительной степени это зависит от того, должны ли эти пары печататься или нет. Например, если мы хотим составить список, состоящий из пар символьных атомов и целых чисел, то можем выбрать либо список двучленов ((X 1) (Y 17) (Z -127)) либо список точечных пар ((Х.1) (К. 17) (Z. -127))
2.9. Точечная запись выражений 61 Выбор является делом вкуса. Точечные пары требуют меньше па- памяти в большинстве реализаций, но если сихмволы образуют пары с общими S-выражениями, а не с атомами, то версия с дву- двучленами может оказаться более четкой. Сравним ((X A) (Y (В С)) (Z НИЛ)) с выражением ((Х.А) (Y В С) (Z)) которое является сокращенной формой списка, помеченного точ- точками. Вообще при реализации для печати выбирается кратчай- кратчайшая возможная форма S-выражения. Так как теперь мы можем представить результат операции cons в общем случае, можно записывать функции от общих S-выражений. S-выражением является либо 1) атом, либо 2) то- точечная пара двух S-выражений (результат операции cons). Следовательно, при построении функций от S-выражений (в противоположность просто спискам) мы имеем другой анализ случаев. Построим функцию B.30), которая вычисляет количест- количество атомов в S-выражении,— назовем ее размер (s). случай A) s—атом размер^) = 1 случай B) s — не атом пусть po3Mep(car(s)) = m B.30) и размер(cdr(s)) = п тогда po3Mep(s) = m-\-n Итак, здесь следует рассмотреть случаи, когда аргумент является атомом и не атомом. В случае если он не является ато- атомом, мы вызываем функцию, которая рекурсивно определена через саг и cdr аргумента. Мы построили функцию размер^) = если amoM(s) то 1 иначе pa3Mep(car(s)) + pa3Mep(cdr(s)) Отметим, что если применять эту функцию к списку или к списку списков и т. д., то элементы НИЛУ которые заключают каждый список, будут также учитываться. Ранее мы отмечали, что предикат равно (х, у) может применять- применяться только в том случае, когда по крайней мере один из его аргу- аргументов является атомом. То есть он не определен, если оба его аргумента являются S-выражениями. Однако можно построить функцию тожд(х, у), которая выдает значение И, если х и у являются тождественными S-выражениями в том смысле, что если мы их сравним, то в них в точности те же самые атомы будут находиться на тех же самых местах. Применим наш анализ слу- случаев.
62 Гл. 2. Строго функциональный язык В случаях A) и B.1) мы знаем, что один из аргументов — атом, и поэтому можем просто применить предикат равно (х, у). В слу- случае B.2), когда ни х, ни у не являются атомами, мы рекурсивно используем предикат тожд для сравнения саг и cdr наших ар- аргументов. Теперь х и у равны, если равны как их саг, так и cdr. То есть хотелось бы написать тожд(х, y)^=t\/\t2 Вместо этого мы пишем тожд(х, */)=если tx то t2 иначе Л что дает, как это легко увидеть, те же самые значения. Это больше согласуется с окончательным построением функции (и отражает тот факт, что мы несколько забегаем вперед с опера- операцией Л — см. гл. 5). Запишем построенную функцию B.32). Мы могли бы записать эту функцию, используя локальные определения для t± и t2 (см. 2.33), но лучше не делать этого. Эта форма предполагает, что сравниваются как саг, так и cdr, гпожд(х, у) зз если атом(х) то равно(х, у) иначе если атом(у) то равно(х, у) иначе {пусть п —тожд(саг(х)у саг (у)) B.33) и t2 = тоже)(cdr(x), cdr(у)) если п то /2 иначе Л) в то время как функция B.32) в случае, если саг различны, не производит ненужного и трудоемкого сравнения cdr. Отметим, что в обеих версиях функции второе применение предиката равно всегда ложно и может быть заменено значением Л.
Упражнения 63 Упражнения 2.1. Опишите рекурсивную функцию произв(х)у которая вычисляет про- произведение списка целых чисел. 2.2. Опишите функцию уменыи{х), которая преобразует список целых чисел х в список, каждый из элементов которого на единицу меньше соответ- соответствующего элемента (табл. 2.23). Таблица 2.23 Обобщите эту операцию таким образом, чтобы величина, на которую уменьшаются элементы, задавалась как дополнительный параметр функции. 2.3. Опишите функцию позиция(х, у), аргументами которой являются атом х и список атомов у. Ее результатом будет положение атома х в списке у, считая с первого по порядку элемента. Предполагается, что х встречается в у (табл. 2.24). Таблица 2,24 2.4. Видоизмените описанную функцию так, чтобы она выдавала значе- значение 0 в случае, если х не встречается в списке у. 2.5. Опишите функцию индекс(i> у), которая воспринимает в качестве аргументов целое число i и список у и выдает элемент списка, имеющий но- номер i. Предполагается, что у имеет длину не меньше L Таблица 2.25
64 Гл. 2. Строго функциональный язык 2.6. Опишите функцию набор (х), аргументом которой является список х с подсписками любой глубины, а результатом — список атомов, обладающий тем свойством, что все атомы, появляющиеся в х, появляются и в набор (х) в том же самом порядке. 2.7. Опишите функцию частоты^), которая берет в качестве аргумента список атомов t и выдает список всех атомов, встречающихся в ty вместе с час- частотой их появления (табл. 2.26). Так как результат представляет собой мно- множество пар, несущественно, в каком порядке эти пары расположены. Таблица 2.26 2.8. Опишите функции, задающие обычные операции над множествами, на списках атомов, используемых для представления множеств таким же об- образом, как в разд. 2.5, т. е. без повторения. Объединение множеств уже рас- рассматривалось. Пересечение множеств выделяет из множеств и и с множество, состоящее из всех атомов, одновременно принадлежащих и и, и v. Разность множеств строит из множеств и и v множество, состоящее из атомов и, кото- которые не лежат в v. Функция симмразность по множествам и и v строит множе- множество, состоящее из всех атомов, которые лежат в и или в u, но не в обоих одно- одновременно, т. е. суммразность(и, v) = разность (объединение (и, v), пересечение (и, v)) 2.9. Опишите функцию индивидуумы @, которая берет в качестве аргу- аргумента список t и выдает в результате список всех атомов, которые встречаются в t ровно один раз. Следует отметить, что, хотя и можно для этой цели исполь- использовать функцию частоты(() и затем выбрать только атомы с единичной час- частотой, но действуя непосредственно, можно получить лучшую программу. 2.10. Перепрограммируйте функцию индивидуумы (f), определенную в упр. 2.9, с использованием накапливающих параметров. На этот раз потре- потребуются два таких параметра: для накопления атомов, встречающихся ровно один раз и более одного раза. Сравните число вызовов функции cons в про- программах с применением и без применения накапливающих параметров. Таблица 2.27 х отобрмнож{ху % (г) г-5-10)
Упражнения 65 2.11. Опишите функцию отобрмнож(х, /), аргументом которой является список х, рассматриваемый как множество, а результатом применения — мно- множество, представленное опять-таки в виде списка, который можно получить применением / к каждому элементу х (табл. 2.27). Следует отметить, что поря- юк элементов в списке, рассматриваемом как множество, не имеет значения. 2.12. Опишите функцию редукция2(х, g, a)y которая, будучи применена к списку х=(хъ . . ., *fc), выдает значение g(. . . g(g(a, хг), х2) . . ., xk). Рассмотрите функцию редукция2(х, к(у, г)\0Ху+гу 0) в случае, когда х — список одноразрядных положительных целых чисел. Какое значение вычисляется? Что получится, если использовать функцию редукция вместо редукция2? 2.13. Сократите следующие S-выражения: ((А.(В.НИЛ)).((С.(й.НИЛ)).НИЛ)) (((А.НИЛ).И ИЛ).В) (А В C.(D Е Р.НИЛ)) ((A.B).(C.D)) 2.14. Перепроектируйте функцию набор (х) из упр. 2.6 так, чтобы она составляла наборы элементов общего S-выражения (табл. 2.28). Следует от- отметить, что при взятии набора от списка в нем появляются элементы НИЛ. Таблица 2.28 Если х — общее S-выражение, набор (х) представляет собой список всех атомов х в порядке, в котором они расположены в списке х. 2.15. Опишите функцию кронтожд(х.у), которая выдает //, если одина- одинаковые атомы расположены в списках х и у в одном и том же порядке незави- независимо от внутренней структуры х и у, и выдает Л в противном случае; дс и f/ — произвольные S-выражения. Вполне корректное, но все же неудовлетвори- неудовлетворительное определение можно дать таким образом: кронтожд(х, у)=2гпожд (набор (х), набор (у)) где функция набор (х) описана в предыдущем упражнении. Общее S-выраже- S-выражение можно изобразить в виде двоичного дерева. Двоичное дерево, эквивалент- эквивалентное (a, b)t выглядит так: Если а и Ь — атомы, они являются листьями дерева, если же это точеч- точечные пары — они являются поддеревьями. На рис. 2.3 двоичные деревья изоб- изображены рядом с соответствующими S-выражениями. Все эти деревья имеют одинаковые кроны. Если мы переименуем или переупорядочим листья, кроны 3 № 929
66 Гл. 2. Строго функциональный язык Рис. 2.3. будут различны. Вычисление функции набор для больших деревьев является очень трудоемкой операцией, нежелательной при сравнении деревьев с раз- различными кронами. Однако избежать этого довольно трудно, в чем и заклю- заключается основная сложность данного упражнения.
Глава 3. ПРОСТЫЕ ФУНКЦИОНАЛЬНЫЕ ПРОГРАММЫ В этой главе будут детально рассмотрены три полностью функ- функциональные программы. Эти программы выбраны так, чтобы каждая из них иллюстрировала какой-либо технический прием; тем не менее они интересны и сами по себе. Первая программа осуществляет специальные символьные вычисления, называемые анализом размерностей. Это процесс проверки формулы или урав- уравнения, направленный на то, чтобы убедиться, что все используе- используемые величины скомбинированы так, чтобы были согласованы фундаментальные единицы массы, длины и времени. Например, недопустимо складывать единицы массы и единицы времени. Этот пример иллюстрирует основные принципы структурирования и продуманное разбиение задач на легко специфицируемые под- подфункции. Это дает возможность сделать некоторые рекоменда- рекомендации по отладке хорошо структурированных программ и ввести понятие ассоциативного списка, общее для многих приложений. Второй пример — поиск вершины дерева (или графа), обла- обладающей заданным свойством. Многие задачи могут быть пред- представлены в этой форме, мы упомянем только некоторые, рассмо- рассмотрев в качестве примера определение поворотов, придающих тетраэдру предписанную ориентацию. Этот пример позволяет обсудить в целом программирование поиска по дереву в функцио- функциональной форме, а также организацию программы, использую- использующей глобальные переменные. Третий пример несколько более абстрактный. Он факти- фактически появился еще в упражнениях к гл. 2 и поэтому, как надеется автор, уже знаком читателю. Это пример определе- определения для произвольной символьной структуры множества атомов, которые встречаются в этой структуре точно один раз (функ- (функция индивидуумы). Этот пример дает возможность ввести по- понятие использования функции для представления множеству (где обычно используются структуры данных) и позволяет об- обсудить эту интересную технику программирования.
68 Гл. 3. Простые функциональные программы 3.1. Анализ размерностей — пример структурированного программирования и структурированной отладки программы В технических науках и прикладной математике формулы, описывающие явления реального мира, выражаются в терминах величин, которые имеют соответствующие размерности. Эти размерности выражаются в терминах фундаментальных единиц массы <М), длины (L) и времени (Т). Таким образом, площадь имеет размерность L2, так как является произведением двух длин, а скорость имеет размерность L71, так как является отноше- отношением длины к времени (например, метры в секунду). Заметим, однако, что эти фундаментальные единицы не принимают в рас- расчет конкретные меры, т. е. неважно, как, например, выражается длина — в метрах, сантиметрах и т. п. Легко можно выбрать систему, которая принимала бы во внимание и такие вещи, но пока не будем этого касаться. Итак, уравнение вида v=u+ft может быть проанализировано следующим образом. Это уравнение относится к прямолинейному движению и го- говорит о том, что если на тело, первоначально движущееся со ско- скоростью и> действует ускорение / в течение времени /, то в конце этого промежутка его скорость будет v. Следовательно, для ве- величин, входящих в формулу, имеем такие размерности: v^LT'1 u=LT~1 f=LT~* t=T Здесь мы видим, что размерность величины /7 есть L7, и по- поэтому размерности по обе стороны равенства сбалансированы. Из этого заключаем, что уравнение правильно по размерности. Другое уравнение прямолинейного движения связывает на- начальную и конечную скорости с путем, пройденным за это время: v*=u2+2fs Предположим, однако, что это уравнение записано неверно: v2=u2+2ft Тогда неправильность этого уравнения помог бы установить следующий анализ размерностей. Размерность и2 есть L2T~2, и размерность 2ft есть L71". Поскольку недопустимо складывать величины разной размерности, формула неверна по размерности. Пусть требуется написать функциональную программу, кото- которая выполняла бы анализ размерностей. Фактически достаточно написать функцию, которая воспринимала бы арифметическое выражение и выдавала как результат его размерность, так как две формулы, данные здесь как примеры, можно представить в
3.1. Анализ размерностей 69 виде O = u + ft—v 0=u* + 2ft—v2 Иными словами анализируется только правая часть уравне- уравнения. Мы должны выбрать представление анализируемых арифме- арифметических выражений и представление размерностей величин. Ограничимся арифметическими выражениями, построенными из переменных, констант и операций +, —, х, -к Фактически мы не различаем переменные и константы, требуя, чтобы и те и дру- другие представлялись атомами и чтобы для них были заданы раз- размерности. Следовательно, имеется класс правильных арифмети- арифметических выражений, приведенных в табл. 3.1, и обычный выбор представления для таких выражений рассматривался в разд. 2.1. Таблица 3.1 Отсюда выражения u+ft—v и u2+2ft—v2 представляются в виде (ПЛЮС U {МИНУС)(УМН F Т) V)) {ПЛЮС(УМН UU) [МИНУСАМИ 2 (УМН F Т)) (УМН V V))) Теперь рассмотрим структуру данных для представления раз- размерностей. Вообще размерность имеет форму Mn*Ln*Tn\ где целые ль /г2, п3 могут быть положительными, отрицательными или нулем. Следовательно, можно представить размерность трех- трехэлементным списком целых. Так, например, скорость LT~l бу- будет представлена списком @ 1 —1), а сила MLT~2 — списком A 1 —2). Имея в виду это простое представление, приступим к построению нашей функции анализа, которая окажется не за- зависящей от выбора представления, что мы и продемонстрируем позже, выбрав более гибкое представление. Остается еще одно важное для построения решение. Как опре- определить размерности входящих в формулу переменных? Безусло^-
70 Гл. 3. Простые функциональные программы но, эти размерности должны быть заданы как аргументы функции анализа, так что нужно предусмотреть структуру этих аргумен- аргументов. Так как каждой переменной соответствует единственная размерность, этот факт может быть отражен списком пар, каждая из которых представляет переменную и связанную с ней размер- размерность. Такой список называют обычно ассоциативным. Вообще имеем структуру (Mi) (М2). • .(Mfc)) для ассоциативного списка с k элементами, где vu • • •> vh— пере- переменные (атомы), a dly . . ., dh— их размерности. Основная функция, при помощи которой манипулируют с таким ассоциа- ассоциативным списком, есть ассоц (и, а), где v — переменная, значение которой (в нашем случае это размерность) требуется выдать, и а — ассоциативный список. Если предположить, что v входит в список, имеем ассоц(р, а) ж если paeno(v, car(car(a))) то car(cdr(car(a))) иначе ассоц(и, cdr(a)) Однако, если не предполагать, что v обязательно есть в списке, можно выдавать некоторое стандартное значение, отражающее тот факт, что никакой ассоциативной связи не обнаружено. Для этой цели мы выбираем атом НЕОПР (или другое обозна- обозначение _[_). ассоц(р, а) ^ если равно(а, НИЛ) то НЕОПР иначе если равно(ю, car(car(a))) то car(cdr(car(a))) иначе ассоц(ь, cdr(a)) Теперь мы можем отложить описания некоторых других подфункций, пока мы не рассмотрим построение главной функ- функции анализа. Назовем эту функцию разм(е, а), где е является выражением, которое должно быть проанализировано, а а — ассоциативным списком. Нужно рассмотреть пять случаев, соответствующих пяти формам правильных арифметических вы- выражений, как показано в C.1). случай A) е — переменная / азм(р,а) = ассоц(е,а) случай B) е имеет форму (ПЛЮС е1 е2) пусть разм(е1, a) = dx и разм(в2у a)=d2 Тогда A) dx и d2 должны быть равны B) разм(е,а)=а1 случай C) уме^орму^ „^ B) (ЗЛ)
3.1. Анализ размерностей ?_} случай D) е имеет форму пусть разм(е1,а) = а1 (УМН ех е2) и разм(е2, a)=d2 тогда разм(е, a)=yMH(dit d2) случай E) в имеет форму пусть разм(еъ a) = d1 (ДЕЛ ех е2) и разм(е2> a)=d2 тогда разм(е, а)—дел(аъ d2) Здесь случаи B) — E) аналогичны в том отношении, что раз- размерность сложного выражения является функцией размерно- размерностей его компонент. В случае операций УМН и ДЕЛ соответ- соответствующие функции умн (du d2) и дел {du d2) еще не определены. Функция, которая используется в случае операций ПЛЮС и МИНУС для определения совпадения размерностей, также еще не определена. Назовем ее совпад (dl, d2), и тогда можно написать определение C.2) функции разм(е, а). разм(е, а) если атом(е) то ассоц(е,а) иначе {пусть d\ = разм(саг(саг(е)), а) и d2 = pa3M(car(cdr(cdr(e))), а) если саг(е)=ПЛЮС то если coenad(d\, d2) то d\ иначе _[_ C.2) иначе если саг(е) = МИНУС то если coenad(dl, d2) то d\ иначе _L иначе если саг(е) — УМН то умн(а\, d2) иначе если саг(е)=ДЕЛ то дел(&\, d2) иначе JJ Заметим, что здесь использована форма, например, саг(е) = ПЛЮС вместо строгой формы равно (саг(е)у ПЛЮС). С этого момента будем использовать в подобных текстах обе эти формы вперемежку. Способ действия этой функции весьма прямолинеен. Фактиче- Фактически она присваивает сначала размерности самым внутренним подвыражениям, затем определяет размерности более внеш- внешних подвыражений и, наконец, определяет размерность всего выражения. Мы обращаемся с константами как с переменными, и поэтому константы должны быть представлены в ассоциативном списке. Теперь следует построить функции совпаду умн и дел. Если предположить прежнее представление для размерности, прихо- приходим к следующим определениям C.3) — C.7). coenad(d\, d2)=s если d\ =_L т0 Л иначе если d2 = J_ то Л иначе если Macca(d\) = Macca(d2) то C.3) если длина(а\)=длина(а2) то время (d\)=epeMn(d2) иначе Л иначе Л
72 Гл. 3. Простые функциональные программы масса(й) зз car(d) длина{а) as car(cdr(d)) C.4) время(й) = car(cdr(cdr(d))) yMH(d\, d2)s= если dl=J_ то J_ иначе если с/2 = _L то J_ иначе тройка (Macca{d\)~\-Macca(d2), C.5) длина(а 1) + длина(а2), время(а\) -j- в/?ежя(сB)) дел (dl, d2) ?э если dl == J_ то J_ иначе если d2 = J_ то J_ иначе тройка (Macca(d\) — масса(а2), C.6) длина(а 1)—длина(а2), время(а\) — время(а2)) тройка(ху у, z) = cons(x,cons(y,cons(z, НИЛ))) C.7) Эти определения говорят сами за себя. Рассмотрим теперь, как все эти функции справляются с выражением (ПЛЮС U (МИНУС (УМН F T)V)) при ассоциативном списке ((U @ 1 -1)) (У@ 1-1)) (F@ 1-2)) (Г @ 0 1))) Функция разм(еу а) строит размерности, приведенные в табл. 3.2. Таблица 3.2 При отладке всей программы следует отладить каждую под- подфункцию независимо. Здесь функции масса, длина, время и тройка достаточно тривиальны, чтобы можно было избежать для них такой проверки. Однако функции совпад, умн и дел несколь- несколько менее тривиальны, и их независимая отладка имеет то пре- преимущество, что когда мы переходим к отладке функции разм,
3.1. Анализ размерностей 73 то можем полагаться на правильную работу подфункций. Функ- Функция ассоц, конечно, должна быть отлажена независимо, и тогда главную программу можно отлаживать последовательно, снача- сначала с простыми данными и затем с каждой формой выражения поочередно. Мы вернемся к обсуждению отладки программы пос- после того, как несколько детализируем программу. Предположим, что вместо использования фундаментальных единиц М, L и Т мы хотим пользоваться обычными абсолютны- абсолютными единицами. Анализируя нашу формулу, определим, согла- согласованы ли все абсолютные единицы. Так, например, если длина измеряется в метрах, а время в секундах, то скорость — в мет- метрах за секунду, а ускорение — в метрах за секунду за секунду. Формула ' и + ft—v согласована, если и и v суть скорости в метрах за секунду, / — ускорение в метрах за секунду за секунду и t — время в секун- секундах. Если f —есть время в минутах, мы можем использовать преобразование t=ct\ где t есть время в секундах, ас — кон- константа перехода (=60), которая имеет размерность секунды за минуту. Заметим, что если бы использовались фундаментальные единицы, такая константа была бы безразмерной. Таким обра- образом, мы видим, что можно выполнять некоторый полезный до- дополнительный анализ формулы. В контексте нашей книги это представляет интерес, поскольку выдвигает проблему представ- представления данных. То представление данных, которое имелось, конечно, не уст- устраивает нас, так как у нас будет больше одного вида единиц, например длины, и поэтому придется иметь дело более чем толь- только с тремя единицами. Расширение представления данных до п- членного списка целых, где каждая позиция отводится под опре- определенную предписанную единицу, тоже не может быть успешным по той причине, что здесь предполагается как то, что можно пе- перечислить все единицы, которые когда-либо будут использоваться нашей программой, так и то, что этот список будет достаточно коротким. Вместо этого попытаемся оперировать с единицами более явно. Заметим, что размерность может быть записана как отношение произведений единиц. Так, имеем метры за секунду — м/сек метры за секунду за секунду — м/сек2 килограммы на квадратный метр — кг/м2 Следовательно, можно представить размерность как пару (т. е. двучленный список) списков, соответственно числитель и зна- знаменатель этого отношения. Приведенные выше размерности при- примут вид
74 Гл. 3. Простые функциональные программы ((М) (СЕК)) ((М) (СЕК СЕК)) ((КГ) (ММ)) Это представление обладает тем свойством, что оно может выра- выразить и все экстремальные случаи, которые нам требуются. На- Например, ((М) НИЛ) — базовая единица, метры (НИЛ (СЕК)) — единица за секунду (НИЛ НИЛ) — единица безразмерной константы С другой стороны, имеется незначительная трудность, связанная с возможной избыточностью записи, например, размерность ((М) (М СЕК)) лучше записать в виде (НИЛ (СЕК)) «сокращая» на М. Будем говорить, что представление, в котором невозможны такие сокращения, имеет приведенную форму, и запишем функцию привести (d), которая по данной размерности d выдает ее приведенное представление. При разработке функции привести (d) будем предполагать, чтоd либо является атомом _[^,либо имеет форму (tb)> где числи- числитель t и знаменатель Ь являются списками атомов. Функция при- привести будет просто брать поочередно каждый атом числителя и смотреть, нельзя ли его вычеркнуть из знаменателя. Заметим, что достаточно таким образом рассмотреть только числитель, так как если атом может быть вычеркнут, он должен быть пред- представлен в числителе до начала сокращения. По симметрии с рав- равным успехом можно было бы проделать эту процедуру и со зна- знаменателем. Но дело в том, что нужно рассмотреть только один из вариантов. Сначала рассмотрим случай, когда d есть J_> и затем передадим остальные вычисления подфункции: привести (б^^если d=J_ то _]_ иначе npue(d) Теперь рассмотрим случаи C.8), с которыми имеет дело функ- функция npue(d), случай A) d имеет форму (ИИЛ Ь) уже приведенная форма случай B) d имеет форму ((u.l)b) пусть d" = npue(d'). где d' = (t b) подслучай B.1) C.8) и входит в знаменатель d"\ тогда результатом является d" с вычеркнутым из знаменателя и
3.1, Анализ размерностей 76 Мы рассмотрели два главных случая, зависящие от того, пуст или не пуст числитель. Если числитель есть НИЛ, ясно, что размерность уже имеет приведенную форму. Если числитель не пуст, можно рекурсивно вызывать функцию прив для приведе- приведения размерности исключением первого атома числителя. Таким образом, в соотношениях C.8) размерность d" имеет приведен- приведенный вид и почти является приведенной формой размерности d, исключая возможную поправку при помощи атома и, отложен- отложенную до рекурсивного вызова. Далее, у нас есть две возможности: и входит или не входит в знаменатель d". Если входит, то вычер- вычеркиваем его там и получаем результат. Если не входит, то вклю- включаем в числитель и получаем результат. Итак, мы построили функ- функцию, описанную соотношениями C.9). Приводить можно любую размерность, не обращая внимания на то, приведена она или нет. Применение функции прив к при- приведенной размерности не причинит вреда, но оно весьма расточи- расточительно, поэтому постараемся избежать этого. Мы так организуем нашу программу, что как только будет вычислена новая размер- размерность, она сразу будет приводиться. Напомним, что функция разм(е, а) вызывает для обработки размерностей функции сов- nad(d\y d2), умн(а\, d2) и дел{а\> d2). Фактически конкретное представление размерностей касается только этих функций. Что- Чтобы модифицировать программу для работы с нашим новым пред- представлением, необходимо только перепрограммировать эти три функции. Самая простая из них — умн(A1, d2)\ мы просто соединяем списки исходных числителей, чтобы получить новый числитель, и списки знаменателей — для получения нового зна- знаменателя. Эта новая размерность может быть неприведенной, и пс^этому следует ее привести. Наконец, необходимо принять во внимание случай, когда один из аргументов не определен. Новая функция задана соотношениями C.10).
76 Г л, 3. Простые функциональные программы умн(а\ 42) = если dl=J_ то J_ иначе если d2 = J_ то J_ иначе привести(пара(соединить(числ (d\), C.10) числ(а2)),соединить(знам(A\), знам(а2)))) Деление двух размерностей выполняется аналогично, исклю- исключая «перекрещивания» числителя и знаменателя, т. е. числитель результата получается из числителя d\ и знаменателя d2, а зна- знаменатель результата — из знаменателя dl и числителя d2. deA(d\,d2) «если gu=J_ to _L иначе если d2 = J_ то J- иначе привести(пара(соединить(числ(а\)% C.11) знам(а2)), соединить(знам(а\), числ(а2)))) Разработка функции совпад приводит к интересному програм- программированию. Из-за того что мы не принимаем во внимание поря- порядок единиц в списке при формировании числителя и знаменателя размерности, мы не можем просто проверить числитель и зна- знаменатель на равенство. Например, следующие размерности все равны: ((КГ) (М М СЕК)) ((КГ) (М СЕК М)) ((КГ) (СЕК М М)) Все, что сейчас требуется,— это сравнить списки атомов и посмот- посмотреть, не равны ли они безотносительно к их упорядоченности. Один способ сделать это заключается в том, чтобы брать пооче- поочередно элементы первого списка и вычеркивать их из второго и посмотреть потом, не станут ли оба списка пустыми одновремен- одновременно. Это напоминает «сокращение» и наводит на мысль о том, нельзя ли для этой работы использовать функцию привести. Можно сделать это, заметив, что две размерности равны, если их отношение безразмерно. То есть если вычислим дел(а\, d2) и получим (НИЛ НИЛ), то dl и d2 суть одинаковые размернос- размерности, так как каждый атом из одной размерности сокращается с та- таким же атомом из другой. Как обычно, нужно позаботиться об особом случае, когда атом есть _]_.
3.1. Анализ размерностей 77 Для того чтобы обсудить вопросы отладки программы, по- полезно построить схему вызовов (рис. 3.1). На этой схеме, являю- являющейся графом, вершины которого помечены именами функций, дуга из одной вершины в другую отражает тот факт, что функ- функция, стоящая у вершины, из которой стрелка исходит, вызывает функцию, стоящую у вершины, в которую входит стрелка. Три- Тривиальные функции числ, знам и пара не изобра- изображены на схеме. На самом деле имеют- имеются две альтернативные реализации функций сов- совпаду дел и умн, исполь- использующие два различных представления данных. Предполагая, что мы ре- решили реализовать толь- только второе, более гибкое представление, имеем тем не менее два различ- различных способа отладки. Можно производить от- отладку полностью мето- методом снизу вверх или можно включить элемен- элементы анализа сверху вниз. Здесь мы дадим набро- набросок того и другого под- подхода, чтобы показать, как именно такая отладка использует преимущества структуры программы. При отладке снизу вверх сначала исследуем функции, кото- которые не вызывают других функций, затем те функции, которые вызывают только уже отлаженные функции, и т. д. Тестовые дан- данные выбираются таким образом, что при неудаче отладки есть все основания сомневаться в функции более высокого уровня, которая является новой для этого теста. Например, пусть при упомянутой выше структуре вызовов независимо отлажена функция ассоц. Если отлажена функция ассоц с аргументами и затем отлаживать функцию разм с теми же самыми данными, то успешная отладка функции ассоц и неудача с отладкой функции разм подтверждают наши сомнения относительно функции разм. Следовательно, мы локализуем ошибку. Последовательность от-
78 Гл. 3. Простые функциональные программы ладки функций легко определяется из приведенной схемы вызо- вызовов. Функцию следует отлаживать только тогда, когда отлажены все подфункции, которые она вызывает, и, где это возможно, эти подфункции должны оперировать с теми аргументами, которые дает им вызывающая функция. Рекурсия несколько запутывает эту ситуацию, но при некотором размышлении можно так вы- выбрать данные, чтобы постепенно осваивать рекурсивную функ- функцию по частям, заставляя ее вызывать себя 0, 1 и более раз. Ана- Аналогично и множество взаимно рекурсивных функций можно по- постепенно отлаживать при соответствующем выборе данных. На- Например, если / вызывает g и g вызывает / следующим образом: /(...) = если ... то ... ?(...) ••• иначе... #(...)== если ... то .../(...) ... иначе... то мы должны отлаживать / и g вместе, но можем, вероятно, вы- выбрать данные так, чтобы сначала / не вызывала g*, потом вызыва- вызывала бы g один раз, a g в свою очередь сначала бы не вызывала / и т. д. В нашем примере, который рассматривается в этом разделе, разумно предположить, что функции соединить, элемент, ассоц и вычерк предварительно уже отлажены, если же не делать этого предположения, то эти функции можно отладить независимо. Затем, вероятно, будем отлаживать вместе функции привести и прив, так как функция привести сравнительно проста. Когда станет известно, что привести работает, если хотим продолжать отладку снизу вверх, следует отладить умн, дел и затем совпад. Наконец, может быть отлажена функция разм путем последова- последовательной проверки ее ветвей. Элементы отладки сверху вниз, о чем упоминалось ранее, появляются при реализации более про- простых версий функций умн, дел и совпад для отладки функции разм. Отладив функции разм и привести, можно объединить их и, вводя соответствующие данные, отладить более сложные вер- версии функций умн, дел и совпад в этой объединенной программе, а не независимо. В этом разделе были затронуты многие проблемы: структури- структурирование программ и соответствующий выбор подфункций, выбор представления данных и организация отладки программ. В по- последующих разделах, когда эта техника будет использоваться повторно, мы не будем на ней останавливаться. Это позволит быстрее осваивать имеющиеся программы и концентрировать внимание на тех чертах, которые они призваны продемонстри- продемонстрировать.
3.2. Поиск по дереву 79 3.2. Поиск по дереву — сравнение стратегий с приоритетом по ширине и по глубине Сначала отметим, чего нет в этом разделе: в нем нет програм- программирования эффективного поиска. Хотя речь идет о поиске вер- вершины дерева, которая обладает определенными свойствами, здесь не будет затронута очень важная концепция такой организа- организации поиска, чтобы в первую очередь исследовались наиболее подходящие вершины. Скорее нас просто интересует то, как мож- можно написать функциональные про- программы для двух основных стра- стратегий поиска — «сначала вширь» и «сначала вглубь» — и структура этих программ. Сначала мы долж- должны подготовить почву и показать, откуда возникает такой поиск. Для этой цели воспользуемся эле- элементарным примером, связанным с ориентацией тетраэдра. Пред- Представленные здесь разработки осно- основаны на некоторых идеях, пред- представленных в книге Нильсона A971), где обсуждаются многие очень интересные приложения. Рассмотрим правильный тетраэдр, изображенный на рис. 3.2. Это треугольная пирамида, в которой каждая из ее четырех гра- граней является равносторонним треугольником. Займемся поворо- поворотами тетраэдра, которые меняют местами его грани. Можно рас- рассматривать как стандартное такое положение, когда грани об- обращены соответственно на юг, северо-запад, северо-восток и вниз. Если пометить грани буквами a, ft, с и d и поместить тет- тетраэдр в плане, как это изображено на рис. 3.3, можно предста- представить его стандартное положение состоянием abed, т. е. перечис- перечисляя грани в указанном выше порядке. Одним из возможных преобразований над тетраэдром явля- является поворот его на 120° по часовой стрелке при том, что его основание остается по-прежнему внизу. Обозначив эту операцию через /?, имеем подграф изменения состояния, изображенный на рис. 3.4; здесь каждое изменение состояния получается круго- круговой перестановкой первых трех имен граней. Другая операция, назовем ее F, выбранная для упрощения графа, который мы должны изобразить, должна поменять мес- местами основание и южный скат. Эта операция отображает abed в deba и вообще изменяет порядок имен в состоянии на обрат- обратный. Однако эта операция делает доступными только 12 состоя- состояний, а 12 других остаются недостижимыми (имеется 24 переста- Рис. 3.2.
80 Гл. 3. Простые функциональные программы новки из 4 имен). Если взглянуть на тетраэдр, изображенный выше (рис. 3.2), можно понять в чем дело. Поскольку положение двух граней фиксировано, две другие тоже определены. Но это потому, что тетраэдр считается жестким. Если представить его сделанным из тонкой резины и с дыркой, можно было бы вы- вывернуть его наизнанку и преобразовать состояние abed в aebd. Рис. 3.5. Назовем это преобразование С. Оно меняет местами грани, об- обращенные на северо-запад и северо-восток, но не затрагивает основание и грань, обращенную на юг. Теперь просто, хотя это
3.2. Поиск по дереву 81 и несколько утомительно, убедиться в том, что граф состояний тетраэдра имеет вид, показанный на рис. 3.5, где изображены только 12 состояний, а остальные 12 могут рассматриваться в параллельной плоскости. Графы в этих двух плоскостях име- имеют одинаковую форму, за исключением того, что стрелки R имеют обратное направление. Соответствующие состояния в каж- каждой плоскости связаны с преобразованием С. Преобразование F показано ненаправленным, так как оно действует в обе стороны. Интересно отметить, что по этому графу из вершины abed Рис. 3.6. в вершину cadb (которая лежит в другой плоскости, над верши- вершиной edab) не существует более короткого пути, чем из пяти пре- преобразований, например RRFRC или RFCRF. Хотя нашей главной целью является демонстрация того, ка- какого вида графы могут строиться для представления проблем реального мира в символьной форме, для читателя, который не полностью освоился с преобразованиями тетраэдра, может ока- оказаться полезным построить этот тетраэдр в натуре. Вместо того чтобы строить тетраэдр, который можно выворачивать наизнан- наизнанку, легче построить два тетраэдра (назовем их положительный и отрицательный), используя чертежи, приведенные на рис. 3.6. Если склеить тетраэдры, оставляя буквы снаружи, то с этими моделями можно манипулировать на плоскости, как об этом го- говорилось выше. Операции имеют тогда такой смысл: R — поворот тетраэдра на 120° по часовой стрелке на его основании, F — поменять местами основание и южную грань, С — заменить тетраэдры так, чтобы имена основания и юж- южной грани сохранились.
82 Гл. 3. Простые функциональные программы Ненадолго отойдем от проблемы тетраэдра. Сначала обоб- обобщим задачу так, чтобы можно было обсуждать проблему поиска изолированно от частных особенностей этой задачи. Обобщение основано на понимании того факта, что для мно- многих задач характерен граф состояний, аналогичный тому, кото- который изображен на рис. 3.5 и служит для переориентировки тет- тетраэдра. Это детально описано в книге Нильсона A971). Здесь мы сосредоточимся только на наиболее элементарных формах гра- графа состояний, рассмотренных Нильсоном. Предположим, что можно характеризовать задачу, построив представление состо- состояния (объекта реального мира), и что может быть вычислен ре- результат применения к состоянию некоторой операции, опреде- определяющий новое состояние. В нашем примере состояние может быть представлено списком атомов, задающих ориентацию сто- сторон фигуры. Легко видеть, как могут моделироваться другие задачи при соответствующем выборе состояний. Чтобы обобщить понятие применения операции, предположим, что нам доступна функция CAed(s), которая, будучи применена к состоянию s, выдает список всех непосредственно следующих за s состояний. Например, в нашем графе тетраэдра имеем функцию следова- следования s cAed(s) (А В С D) -> ((С А В D) (D С В А) (А С В D)) Вообще для данного начального состояния s функция след, при- применяемая последовательно, генерирует дерево. Ветка дерева за- заканчивается в некотором состоянии s, только если след (s)=HHJI. Дерево, которое генерируется посредством функции след для Рис. 3.7. тетраэдра, является бесконечным, начало его показано на рис. 3.7. Разумеется, оно бесконечно только потому, что каждое со- состояние в конце концов повторяется. Предположим, что функция CAed(s) такова, что если она применяется повторно, то она выда- выдает НИЛ* т. е. что дерево конечно. Когда функция след в конкрет-
3.2. Поиск по дереву 83 ной задаче не обладает этим свойством, будем искусственно мо- модифицировать ее, чтобы добиться этого свойства. Задача, за которую мы хотим взяться, заключается в том, чтобы для заданного начального состояния найти достижимое из него состояние, обладающее некоторым свойством. Пусть свойство, которым должно обладать состояние, реализовано как предикат (логическая функция) p(s). Например, относительно специальной ориентации тетраэдра можем иметь p(s)=paeHo(s9(C A D В)) Существуют две принципиально различные стратегии, которые применяются при поиске состояния s, удовлетворяющего пре- предикату p(s). Стратегия «сначала вглубь» исследует состояния в таком порядке, что, когда формируется список преемников, для каждого элемента рассматривается сначала все исходящее из него дерево, прежде чем перейти к следующему элементу списка. С другой стороны, при стратегии «сначала вширь» ис- исследуются все вершины, находящиеся на одном расстоянии (в терминах числа применений функции след) от начальной вер- вершины, прежде чем перейти к более далеким вершинам. Вообще считается, что программирование поиска «сначала вглубь» более естественно при использовании рекурсивных функ- функций, поэтому начнем с него. Определим функцию найти (s), ко- которая, отправляясь от начального состояния s, ищет состояние, удовлетворяющее p(s). Используем вспомогательную функцию среди (S), которая берет в качестве аргумента список состояний S и применяет функцию найти (s) к каждому состоянию из этого списка до тех пор, пока будет найдено состояние, удовлетворяю- удовлетворяющее предикату р. найти (s) = если p(s) то s иначе среди(след (s)) cpedu(S) see если 5 = НИЛ то НЕТ иначе {пусть s — Haumu(car(S)) C.I3) если s — HET то cpedu(cdr(S)) иначе s} Здесь видим, что функция найти (s) сначала исследует s, и толь- только в том случае, если состояние не удовлетворяет предикату p(s), рассматривает состояния след (s), используя функцию сре- среди. Если функция cpedu(S) обнаруживает, что множество со- состояний S пусто, то она выдает специальное значение НЕТ, указывающее на то, что никакой результат не может быть полу- получен. Если S не пусто, то функция найти применяется к первому элементу списка S. Если функция найти не выдает значения НЕТ, то результат, который она выдает, и есть требуемое состоя- состояние. В противном случае мы исследуем оставшиеся члены 5, Совершенно естественно используя рекурсию. Престо опреде-
84 Гл. 3. Простые функциональные программы лить, что действительно осуществляется поиск «сначала вглубь», так как функция найти применяется последовательно к первому элементу каждого множества преемников, прежде чем рассмат- рассматриваются остальные члены. Разумеется, порядок элементов в списке, который выдает функция след, безразличен (хотя это может быть решающим фактором для эффективности поиска), и мы выделяем саг, в противополож- противоположность всем другим элементам толь- только для удобства. В дереве (про- (произвольном), изображенном на рис. 3.8, вершины занумерованы в том порядке, в каком они исследуются при поиске «сначала вглубь». Несмотря на естественность функции найти в форме C.13), полезно перепрограммировать ее рис. 3.8. в другом виде, чтобы можно было сравнить ее с поиском «сначала вширь». На этот раз воспользуемся тем фактом, что функция cpedu(S) манипулирует с элементами списка S в определен- определенном порядке. Мы ухитримся заменить элемент в 5 на его преемников так, чтобы функция среди исследовала состояния в порядке, характерном для стратегии «сначала вглубь». Имеем Haumu(s) S3 cpedu(cons(st НИЛ)) cpedu(S) =если S^HHJl то НЕТ иначе если p(car(S)) то car(S) иначе cpedu(coeduHumb(cAed(car(S)), cdr(S))) Здесь вся работа выполняется функцией среди. Аргументом функ- функции среди является список состояний, которые еще нужно ис- исследовать. Если список пуст, то решения не существует. Если первый элемент списка удовлетворяет /?, то это требуемый ре- результат, в противном случае определяются все его преемники и помещаются в начало списка, который должна исследовать функция среди. Здесь более явно представлен порядок, в каком должны исследоваться состояния, и легко видеть, что он отве- отвечает стратегии «сначала вглубь». Эта версия имеет некоторые недостатки. Использование функции соединить может обусло- обусловить значительные накладные расходы, как это обсуждалось в разд. 2.5, и вложение рекурсивных вызовов теперь пропорцио- пропорционально не глубине искомой вершины, а числу вершин, отбро- отброшенных до того, как нужная вершина была найдена. Но зато это делает тривиальным описание поиска «сначала вширь». Чтобы осуществлять поиск «сначала вширь», можно модифицировать
3.2. Поиск по дереву программу, просто изменив аргументы вызова функции соеди- соединить. Вызов принимает вид coeduHumb(cdr(S),след (car (S))) и мы видим, что для конкретного вызова функции cpedu(S) элементы S берутся в порядке последовательных рекурсивных вызовов и за последним элементом 5 в аргумент для функции среди встраиваются все преемники элементов из S. Таким обра- образом становятся доступными по порядку все состояния следующе- следующего уровня. Далее, при таком поиске, при котором последние две програм- программы сравнимы, т. е. когда в случае неуспеха выдается НЕТ, про- программа поиска «сначала вширь» намного медленнее программы «сначала вглубь». Это потому, что в среднем cdr(S) намного длиннее, чей след (car(S)), и, таким образом, функции соединить требуется выполнять гораздо больше работы. Поэтому представ- представляется более важным исключить функцию соединить из про- программы поиска «сначала вширь», чем из другой программы. Это нетривиально, но тем не менее оставляется как упражнение чи- читателю, так как нашей главной целью является использование этих программ в нашем конкретном приложении к тетраэдру. Конкретная задача, которую мы хотим решить, состоит в том, чтобы по заданным двум состояниям тетраэдра найти последова- последовательность преобразований, переводящих первое состояние во вто- второе. На первый взгляд программирование функции найти не относится к этой проблеме. Предполагая, что начальное состоя- состояние есть х, а искомое состояние — у, положив p(s)^moMd(s, у) и вызвав найти (х), получаем один из двух результатов: либо НЕТ, что означает недостижимость у из х, либо у. Но способ преобразования х в у не выдается. Поскольку функция найти выдает только состояние, обход этот незначительной трудности очевиден: необходимо закодировать в состоянии не только ори- ориентацию объекта, но также и последовательность операций, при- примененных к начальному состоянию х, чтобы достичь данной ориентации. Например, если начать с ориентации тетраэдра (А В С D) и затем применить последовательно операции /?, F и С, то получаем ориентацию (D А В С) Можно записать эту последовательность преобразований состояниями {(A BCD) НИЛ) ((С А В D) (R)) ((D В А С) (F /?)) ((D А В С) (С F Щ) Здесь состояние определяется парой списков — списком граней и списком операций, примененных к начальной ориентации для
Гл. 3. Простые функциональные программы достижения данной. Для удобства мы перечислили преобразо- преобразования в порядке, обратном их применению. Сначала определим в C.14) функцию прим(опу s), которая берет в качестве аргументов атом (один из R, F и С) и состояние и применяет соответствующую операцию к состоянию. прим (on, s) ев {пусть opueHtn = car(s) и преобр — car(cdr(s)) {пусть а = саг(ориент) и b = саг(саг(ориент)) C.14) и с — car(cdr{cdr(opueHtn))) и d = car(cdr(cdr\cdr(opueHm)))) если on = R то пара (четверка(с, а, Ь, d)} cons(on, преобр)) иначе если on = F то пара (четверка(ау с, bt a), cons(on, преобр)) иначе пара (четверка(а> с, bt d), cons(on, преобр))}} При построении ориентации используются функции пара и четверка с очевидными определениями пара(х, y)^cons{x, cons(y, НИЛ)) четверка(и), х> у, z) ^cons(wt cons(x, cons(y, cons{z, НИЛ)))) Объявив локальные переменные a, bt с, d для именования частей исходной ориентации, получаем ясную запись программ для трех операций. Используя эти функции, совсем просто определить функцию CAed(s). Это просто трехэлементный список состояний, получен- полученных применением операций /?, F и С соответственно. Однако следует обратить внимание на то, что CAed(s) для тетраэдра стро- строит бесконечное дерево и его нужно искусственно обрезать. Для этого просто отсекаем все ветви на некоторой глубине, которую назовем максглуб (для тетраэдра соответствующее значение рав- равно 6). Для функции следования имеем CAed(s) г* если dAuna(car(cdr(s))) ^ максглуб то НИЛ иначе tnpouKa(npuM(R, s), npuM(F, s), прим(С> s)) Длина преобразования в записи состояния служит мерой теку- текущей глубины. Если она равна (или больше) максимально допу- допустимой глубине, то функция CAed(s) не выдает преемников. Наконец, необходимо определить предикат /?, которому бу- будет удовлетворять искомое состояние. Это тривиально: просто проверяем, не равна ли ориентация состояния искомой ориен- ориентации p(s)=mo9fcd(car(s),y) Теперь можно собрать все части вместе, чтобы построить про- программу, которую определим как функцию поиск (х, у, максглуб)
3.2. Поиск по дереву 87 где пользователь задает начальную и конечную ориентации и предельную глубину дерева поиска. Так как начальной ориен- ориентацией является х, начальное состояние есть пара (х, НИЛ), и поэтому имеем определение поиск (х, у> максглуб)^найти (пара(х, НИЛ)) Очевидно, что выбор версии будет определять, какой резуль тат мы получим, потому что поиск «сначала вширь» будет нахо- находить кратчайшую из возможных последовательностей преобра- преобразований, в то время как поиск «сначала вглубь» будет смещен в сторону применения операции R, так как мы произвольно вы- выбрали ее для применения в первую очередь. Если требуется най- найти из (ABC D) ориентацию (С В D А) с глубиной поиска 6, эти алгоритмы выдадут соответственно (F R R) и (R R R F R R). Результат поиска по ширине обеспечивает то, что не суще- существует последовательности меньше чем из трех преобразований, переводящей состояние (А В С D) в состояние (С В D Л). Ре- Результат поиска по глубине (более дешевый в получении) не дает такой уверенности. Однако найденные результаты и относитель- относительная эффективность алгоритмов нас сейчас не интересуют. Вер- Вернемся к обсуждению структуры программы. Программируя поиск, мы определили функции найти (s) и среди (S). Они были определены через функцию след (s) и завер- завершающий предикат p(s). Однако, когда мы перешли к определе- определению конкретных преемников и функции завершения для проб- проблемы тетраэдра, мы определили эти функции через переменные максглуб и у соответственно. Эти переменные не являются пара- параметрами функций след и /?, они являются глобальными для них. Фактически это параметры главной функции поиск (х, у, макс- максглуб), так как их значения управляют конкретным поиском. Чтобы функции след и р получили доступ к переменным макс- максглуб и у, они должны быть объявлены локальными в функции поиск. Таким образом, структура программы должна иметь вид C.15) или подобный этому.
88 Гл. 3. Простые функциональные программы Отметим, что ссылки на переменные максглуб и у из функций след и у соответственно являются единственными ссылками на эти переменные во всей программе. Эта структура программы не отличает функции общего на- назначения найти и среди от функций специального назначения след, р и прим, которые используются для реализации представ- представления состояний для проблемы тетраэдра. Можно исправить это обстоятельство, изменив описание функции поиск, как это по- показано в соотношениях C.16). поиск — А(*, у у максглуб) {{найти(пара(х, НИЛ)) гдерек найти = ... и среди = ...} C.16) гдерек след = ,. .максглуб... и р = ...у... и прим =...} Такая организация программы, давая доступ, например, из функции найти к функции след и из след к переменной макс- глуб, которые требуются по логике программы, ясно устанавли- устанавливает, что вызовы найти и среди возможны только из самого внут- внутреннего блока и что никакие из ориентированных на применение функций, подобных прим, не вызывают их. Это было не очевид- очевидно из прежней организации. Однако эта вторая структура не запрещает явно другого. Это касается того, что для функций найти и среди доступны не толь- только след и /?, но также и прим. То, что они не используют и не должны использовать эту последнюю функцию, может быть по- показано путем новой перестройки, на этот раз такой, чтобы эта вспомогательная функция оказалась локальной для той функ- функции, которая ее использует, как это показано в C.17). поиск = к(х, у, максглуб) {{найти (пара (х, НИЛ)) гдерек найти—... и среди = ...} C.17) г де след = {... максглуб... где прим —...} и р = ...у...} Эта структура представляется более проработанной и дета- детализированной. В ее пользу кроме указаний на явное отсутствие некоторых видов доступа говорит также то, что здесь можно ис- использовать конструкцию где вместо конструкции гдерек в пре- предыдущей организации. Там она была необходима, чтобы иметь доступ к функции прим, теперь локальной, которая была объяв- объявлена на том же самом уровне, что и след. Использование конст-
3.2. Поиск по дереву 89 рукции гдерек, которая говорит о взаимной рекурсивности мно- множества функций, если можно избежать этого, расценивается как плохой стиль программирования. Однако ценой, которую при- приходится платить, является более глубокая вложенность програм- программы. К тому же и преобразование не всегда бывает таким простым. Здесь нам просто повезло, что функции р и след не имеют общих подфункций. Если бы, например, предикат р тоже использовал функцию прим, мы должны были бы либо дублировать ее, либо поместить ее в новый (охватывающий) блок для того, чтобы обе функции имели к ней доступ. Если и дальше придерживаться этого подхода, делая функ- функции доступными только для тех функций, которые их вызывают, пришлось бы действительно закончить очень глубоко вложен- вложенными программами. Даже хотя функции длина, соединить и т. д. не вызываются функциями найти и среди, то, что мы сде- сделали их доступными для этих функций, не затруднило понима- понимания функций. Это последнее замечание требуется немного пояс- пояснить. Когда мы пытаемся понять программу, написанную кем- то другим, мы сначала должны очертить независимые части про- программы. Как авторы программы, чтобы сделать ее понятной для других, мы должны насколько возможно облегчить эту задачу. Поэтому мы не будем предоставлять функциям ненужного им до- доступа к переменным и другим функциям. Тогда, следуя этому принципу, мы придем к очень глубоко вложенным программам. Если поместить функции общего назначения, такие как длина, во внешнем блоке, не очень трудно установить тот факт, что, на- например, функция найти не вызывает функцию длина. На самом деле единственным способом установить этот факт является изу- изучение текста функций найти и среди. По этой причине вопрос о том, помещать ли функцию, подобную длине, одним или несколь- несколькими блоками прежде, чем она явно потребуется, является делом вкуса. Один из аргументов в пользу этого состоит в том, что та- такие функции, будучи очень простыми и имея общее назначение, могут быть вызваны отовсюду (и это только по случайности не так) и поэтому должны помещаться в самом внешнем блоке. Фак- Фактически мы это и проделали с функциями длина, соединить, тожд, пара, тройка и четверка. Последнее, несколько иное преобразование нашей программы приводит к альтернативной структуре. Мы строим вспомогатель- вспомогательную функцию в C.18), почти ту же самую, что и найти, только в ней функции р и след объявлены параметрами, и это явно ука- указывает на то, что найти и среди используют р и след. Теперь, как представляется, мы достигли желаемого разделе- разделения, так как определения функций р, след и искать, являясь ветвями конструкции где, безусловно, не ссылаются друг на друга, а функция найти, являясь локальной для функции ис-
90 Гл. 3. Простые функциональные программы катъ, очевидно, недоступна никакой другой функции. Далее, если мы поместим функцию искать в тот же блок, что и поиск, то мы закроем для нее доступ к параметрам функции поиск, в частности к у и максглуб. Но так как такой доступ и не требует- требуется, ясно, что это будет улучшением программы. Это обсуждение показало, в каких широких пределах может варьироваться глобальная организация функциональной про- программы и какое влияние это может оказать на ясность программы. Говоря о том, что разработанная здесь структура программы бы- была постепенно улучшена преобразованиями, примененными к ней, мы не считаем, что данные конкретные преобразования улучшат любую программу. Организация программы остается все еще очень субъективной областью. Поскольку в той же мере, как и другие языки, функциональные программы допускают значительное разнообразие вариантов, для построения более яс- ясной программы все еще необходим трезвый расчет. 3.3. Программа функции индивидуумы В этом разделе мы приступаем к решению задачи, которая уже знакома по упражнениям к гл. 2 (упр. 2.8 и 2.10). Задача состоит в определении для общего S-выражения множества ато- атомов, которые встречаются в этом выражении в точности один раз. Будем называть эту функцию индивидуумы^). Помимо но- новой демонстрации общих методов функционального программи- программирования и сравнения альтернативных вариантов решения наша основная цель — показать, как функция может использоваться вместо структуры данных, и обсудить достоинства этого приема Читатель, знакомый с упражнениями, знает, что существует мно- много способов взяться за эту задачу и преодолеть конкретные труд- трудности. Чтобы удержаться в разумных пределах краткости, мы не будем обсуждать все эти трудности, а только те, которые яв- являются новыми для этой задачи. Функция, которую мы хотим определить, имеет значения, данные в табл. 3.3. Результат рассматривается как множество, и поэтому порядок значений в нем безразличен. Отметим, однако, что, так как аргу- аргумент является общим S-выражением, элемент НИЛ в конце спис- списка может появиться в результате, как это показано в первом при-
3.3. Программа функции индивидуумы 91 мере таблицы. В предположении, что мы начинаем непосредствен- непосредственно определять функцию индивидуумы (s) при помощи обычного анализа случаев, приходим к следующему: случай A) s есть атом индивидуумы^) sscons(s, НИЛ) случай B) s не атом пусть tmdueudyyMbi(car(s)) =» A и UHdueudyyMbi(cdr(s))~ В тогда UHdueudyyMbi(s) = ? Первый случай, когда s — атом, является тривиальным. Мно- Множество атомов, встречающихся в s, состоит в точности из од- одного элемента. Во втором случае, когда s не атом, предполагаем, что множество индивидуумов может быть вычислено для car(s) и cdr(s). Но как вычислить индивидуумы (s)? А и В нельзя про- просто объединить, так как они могут иметь общие элементы, кото- которые входят в s по крайней мере дважды. Это не может быть и симметрической разностью А и В (множеством элементов, кото- которые входят либо в Л, либо в ?, но не принадлежат обоим одно- одновременно,— см. упр. 2.8). Чтобы убедиться в этом, нужно рас- рассмотреть только случай, когда элемент встречается один раз в списке car(s) и поэтому входит в Л и дважды встречается в спис- списке cdr(s) и поэтому не входит в множество В. Так как этот атом трижды встречается во всем аргументе в целом, он не должен входить во множество индивидуумы (s), но мы не можем опреде- определить это только по А и В. Фактически, чтобы вычислить инди- индивидуумы (s), нам нужно взять те элементы из Л, которых нет в списке cdr(s), и те элементы из В, которых нет в списке car(s). Программа уже обещает быть чрезвычайно неэффективной, но мы не допустим, чтобы это обстоятельство помешало нам за- завершить ее построение, так как это позволит нам получить более глубокое представление о задаче, а опыт, накопленный в первой попытке, вероятно, послужит успеху последующих. Эта програм- программа, будучи завершенной, может помочь нам убедиться в правиль- правильности некоторой более эффективной версии. Если эта более эф- эффективная версия является и более сложной, а это, по всей ве- вероятности, так и будет, то может оказаться легче убедиться, что
92 Гл. 3. Простые функциональные программы она эквивалентна менее эффективной, чем проверить правиль- правильность ее самой. Если эта менее эффективная версия легче прове- проверяется на правильность, то это составляет существенную часть документации программы. Чтобы закончить описание, предположим существование трех функций. Множество атомов, входящих в S-выражение s, вычис- вычисляется функцией mhom(s). Если хну суть списки, представ- представляющие множества атомов, то объединение^, у) есть список, представляющий множество атомов, входящих либо в х, либо в у, а разность (х, у) есть список, представляющий множество тех атомов, которые входят в х, но не входят в у. При помощи этих функций можно завершить описание второго случая (см. 3.19). случай B) s не атом пусть индивидуумы(саг(8)) = А и uHdueudyyMbi(cdr(s)) = В C.19) и MHOdic(car{s)) — С и MHODic(cdr(s)) = D тогда индивидуумы 8 — объединение(разность(А, D), разность (В, С)) Отметим, что, так как каждый атом из А должен быть также и в С и аналогично каждый атом из В должен быть в D, множе- множества разность (Л, D) и разность (В, С) не имеют общих элемен- элементов. Поэтому нет необходимости формировать их симметричес- симметрическую разность, хотя это бы и дало тот же самый результат. Функ- Функция, которую мы описали, имеет следующий вид: индивидуумов) = если amoM(s) то cons(s, НИЛ) иначе одъединение(разность(индивидуумы(саг(8)), MH0M(cdr(s))),pa3HOcmb(uHdueudyyMbi(cdr(s)), MHOM(car(s)))) Необходимо построить вспомогательные функции, но не будем здесь этого делать. Однако приведем их определения лишь для того, чтобы иметь возможность обсудить эффективность про- программы. Соответствующие определения даны соотношениями C.20) — C.23).
3.3. Программа функции индивидуумы 93 Эта программа демонстрирует несколько видов неэффектив- неэффективности. Если бы можно было предположить, что множества, ге- генерируемые в процессе вычисления функции, невелики, т. е. что одни и те же атомы встречаются снова и снова и что индивиду- индивидуумы нетипичны, то это оправдывало бы использование основных функций манипулирования с множествами. Однако использова- использование функции mhom(s) не может быть оправдано на этом осно- основании, так как ее эффективность целиком зависит от разме- размеров анализируемого S-выражения. Проблема не в том, как запрограммирована сама функция множ (s), а в том, как она вызывается функцией индивидуумы^. Неэффективность возни- возникает в конкретном случае, который можно проиллюстрировать, рассмотрев подвыражение o6beduHeHue(pa3HOcmb(uHdueudyyMbi(car(s)),MHO9fc(cdr(s)))y разность(индивидуумы{саг(8)),множ(саг{$)))) Рассмотрим вычисление этого выражения в случае, когда s не атом. Исследуя вызов индивидуумы(саг(з)), видим, что он в свою очередь вызывает вычисление множ(сйг (car(s))) и множ (car(car(s))). Аналогично, если рассмотрим вызов множ(саг($))у то видим, что он тоже вычисляет множ(cdr(car(s))) и множ(саг (car(s))). Таким образом, одни и те же очень длинные вы- вычисления выполняются дважды. Разумеется, если рассмотреть все вычисления в целом, можно увидеть, что функция множ(б) будет вычисляться многократно для одного и того же аргу- аргумента, в зависимости от глубины вложенности структуры, к ко- которой она применяется. Эту неэффективность можно преодолеть, используя накапли- накапливающие параметры. Определим функцию undue(s, a, &), которая имеет вспомогательные параметры, накапливающие требуемые множества. Фактически параметр а будет использоваться для накопления множества атомов, которые встречаются более од- одного раза, а параметр Ъ будет накапливать множество атомов, встречающихся только один раз. Значением функции UHdue(sy ау Ь) будет пара множеств. Первое из этих результативных мно- множеств будет содержать атомы, встречающиеся в структуре более одного раза, а второе — атомы, встречающиеся только один раз. Опишем функцию undue(sy a, b) в соотношениях C. 24) пу- путем уже знакомого анализа случаев, однако на этот раз выбирая случаи, более ориентированные на задачу. Рассмотрим подслу- чаи по порядку.
94 Гл. 3. Простые функциональные программы Случай A.1) имеет место, когда атом s уже неоднократно встре- встречался прежде. Мы игнорируем его, так как запись атома в мно- множество а означает, что он для нас не представляет более интереса. Случай A.2) означает, что атом встречается второй раз. Нужно вычеркнуть его из множества Ь и включить в множество а. Слу- Случай A.2.2.) означает, что атом встречается первый раз. Вклю- Включаем его в множество /?. Случай B) имеет дело с еще не препариро- препарированной структурой. Вызов tiHdue(car(s)y а, Ь) используется для накопления множеств однократно и многократно встречающих- встречающихся в car(s) атомов, и затем они добавляются соответственно к b и а, образуя множества d и с. Затем очевидным образом вызы- вызывается undue{cdr(s), с, d). Таким образом определено множество функций C.25). Приведем очевидное определение функции вы- черк и предполагаем обычное определение функции элемент (см. разд. 2.5). UHdueudyyMbt(s)*scdr (uHOue(s, НИЛ, НИЛ) индиец, а, Ь) э== если amoM(s) то если элемент^, а) то cons(a, b) иначе если элемент^, Ъ) то cons(cons(st а), вычерк^ь, Ь)) иначе cons(a, cons(s, b)) иначе {пусть p = undue(car(s)y a, b) C.25) UHdue(cdr(s), car(p), cdr(p))} вычерк(х, у) es если равно(х, саг(у)) то cdr(y) иначе cons(car(y)t вычерк(х, cdr{y))) элемент(х, y)ss ... Теперь можно для полноты дать формальное определение зна- значения, которое выдает функция индив (s, а, Ь). Пара (cd), кото- которая выдается как значение этой функции, состоит из двух мно- множеств end. Множество с содержит: 1) все элементы множества а, 2) все атомы, многократно встречающиеся в s, 3) все однократно встречающиеся в s атомы, которые явля- являются элементами множества Ь.
3.3. Программа функции индивидуумы 96 Множество d содержит: 1) все однократно встречающиеся в s атомы, которые не вхо- входят ни в одно из множеств а и Ь, 2) все элементы множества Ь, которые не встречаются в s. Таким образом, видно, что вызов undue(st НИЛ, НИЛ) вы- выдает пару, состоящую из множеств с (все многократно встречаю- встречающиеся в s атомы) и d (все однократно встречающиеся в s атомы). Поэтому определение индивидуумы^) zEEcdr(undue(s, НИЛ,НИЛ)) корректно. Теперь посмотрим на использование множеств а и b в опре- определении функции индив (s, а, 6). Множество а исследуется функ- функцией элемент и пополняется при использовании функции cons. В множестве Ь аналогично производятся поиск и пополнение, но оно также иногда и уменьшается при использовании функции вычерк. В конечном счете нас интересует только множество fr, так как множество а используется лишь как вспомогательное средство, облегчающее вычисление множества 6. Это отражено в том факте, что окончательно выдается только cdr(uHOue(s, НИЛ, НИЛ)). По этой причине мы свободны в выборе представ- представления множества а. Рассматривая операции, которые применя- применяются к s, можно посмотреть также, не существует ли таких пред- представлений структуры s, которые бы сделали выполнение этих операций более эффективным. Конкретное представление, кото- которое мы рассмотрим, недоступно в большинстве языков програм- программирования. Это представление множества посредством функции. До сих пор множество атомов представлялось списком без повторений. Предположим, что s является таким списком, пред- представляющим множество. Можно определить функцию /, обла- обладающую тем свойством, что f(x) = элемент (xf s), и использо- использовать / для представления множества вместо s. Разумеется, будет невозможно перенести все операции над множествами на /, но для некоторых операций это можно сделать. Для множества s, когда нужно добавить к нему новый элемент ху про который из- известно, что он не входит в s, используется просто cons(x, s). Однако, когда для представления множества используется функ- функция /, нужно построить новое функциональное значение, кото- которое ведет себя как функция элемент для расширенного множест- множества. Соответствующее выражение имеет вид к (у) если равно(у, х) то И иначе f(y) Рассмотрим, почему это так. Это Х-выражение обозначает функ- функцию, которая при применении ее к аргументу у выдает Я, если
96 Гл. 3. Простые функциональные программы у есть либо х9 либо элемент множества, представленного функ- функцией /. Поэтому Х-выражение представляет множество, получен- полученное расширением множества, представленного функцией /, за счет включения в него нового элемента х. Пустое множество бу- будет представлять функция Х(у)Л, так как никакой атом не яв- является элементом пустого множества. Можно очень легко моди- модифицировать определение функции undue(sy a, fc), используя функциональное представление множества а, как это сделано в C.26). Здесь требуются только три изменения. Пустое множество НИЛ заменяется функцией Х(у)Лу вызов элемент (s, а) заменя- заменяется вызовом a(s), и вызов cons(s, а) заменяется на {^(у) если равно(уу s) то И иначе а(у)}. В паре, которая теперь выдается как значение функции undue(sy a, &), первый элемент является функцией. Эта возможность включать функции в структуры дан- данных и использовать их вместо структур данных является очень мощным средством функционального программирования. Предположим, что множество, представленное функцией, вначале пусто, а затем последовательно пополняется атомами Л, В, С. Тогда последовательность функциональных значений, ис- используемых для представления этих множеств, будет следую- следующей: а, = КУ)Л а2 = если равно(у, А) то И иначе аг(у) а3==если равно(у, В) то И иначе а2(у) а4 = если равно(у, С) то И иначе а3(у) Можно видеть, например, что вызов а^(А) порождает вызов as(A), который порождает а2 (Л), в результате чего выдается И. Аналогично вызов aJJ)) порождает вызовы a9(D), aa(D), ujfD), и в результате выдается значение Л. Отсюда видно, что а4 ведет себя так, как это свойственно функции, представляющей множество элементов Л, ?, С. В гл. 6 мы возвратимся к обсуж- обсуждению такого использования функций и увидим, что это кон-
Упражнения 97 кретное использование функции несколько более эффективно по скорости вычислений, чем использование списка и функции эле- элемент. Упражнения 3.1. Рассмотрите логические выражения, построенные из переменных и операторов и, или и не. Опишите представление логических выражений в виде S-выражений и определите функцию, выдающую значения таких вы- выражений по ассоциативному списку, ставящему в соответствие каждой пере- переменной выражения логическую величину. 3.2. Опишите функцию HaumunocA(s)t которая, как и функция найти (s), определяет состояние s\ удовлетворяющее p(s'), в дереве, описанном функцией cyied(s), но выдает список состояний (st, . . ., sfe), где sl=s и sk=sf, а для всех i от 2 до k s/ — элемент след fa ^. Иными словами, результатом применения этой функции является список состояний, соединяющих s и s'. Покажите, каким образом ее можно использовать для решения проблемы тетраэдра, не прибегая к хранению преобразований. Какой подход лучше? 3.3. Опишите представление ориентации куба. Какое ограничение глу- глубины является естественным для дерева куба? 3.4. Перепроектируйте поиск с приоритетом по ширине, описанный в этом разделе, так, чтобы избежать употребления функции соединить (х, у). Опреде- Определите, как часто встречаются вызовы cons в программах с использованием функции соединить и без использования, имея в виду наихудший случай, когда результат — НЕТ, и предполагая наличие преемников на каждом уровне и ограничение по глубине d. 3.5. Рассмотрите программу анализа размерностей алгебраической фор- формулы, данную в разд. 3.1. Обдумайте области действия переменных, встречаю- встречающихся в этой программе, и определите организацию функциональных описа- описаний, составляющих программу. 3.6. Рассмотрите функцию отобр(х, /) es если х — НИЛ то НИЛ иначе cons(f(car(x))> omo6p(cdr(x), /)) Покажите, как ее можно построить иначе, избегая постоянного «перехода» параметра /. Изучите структуру этого видоизмененного определения с точки зрения эффективности и легкости чтения. 3.7. Переработайте первый вариант функции индивидуумы (s), не исполь- используя накапливающих параметров, но таким образом, чтобы программа выда- выдавала пару, состоящую соответственно из множества индивидуумов и мно- множества всех атомов, встречающихся в s. Назовем эту функцию индмнож(ь). Тогда справедливо индмнож(8) = cons(uHdueudyyMbi(s) ,множ(8)) undueudyyMbi(s) —саг(индмнож{в)) Решает ли этот вариант проблему неэффективности по отношению к вызовам функции множ^у из-за которой была отвергнута первая программа? 3.8. Опишите функцию послед (ху у), которая по атому х и списку у выдает множество всех атомов в у, непосредственно следующих за каждым вхожде- вхождением х в у. 3.9. Опишите функцию после(х, у), которая по атому х и общему S-выра- 4 ja 929
98 Гл. 3. Простые функциональные программы жению у выдает множество всех атомов у> непосредственно следующих за вхождениями х. При этом скобки не принимаются во внимание, т. е. после (*, у)=послед (х.набор (у)) 3.10. Опишите функцию добавить (*, /), которая по атому х и функции /, представляющей множество атомов 5, выдает функцию, которая представляет множество 5, расширенное элементом х. 3.11. Опишите выч(х, /), которая выдает функцию, представляющую множество вычерк(ху S), где х и / —- из предыдущего упражнения. 3.12. Покажите, каким образом множество S можно представить с по- помощью функции /, обладающей тем свойством, что f(O)=n есть мощность мно- множества, а /A), . . ., f{n) — его элементы. Покажите, каким образом множество можно расширить и как можно определить принадлежность. Сравните до- достоинства этого представления с достоинствами представления, используе- используемого в основном разделе.
Глава 4. ПРЕДСТАВЛЕНИЕ И ИНТЕРПРЕТАЦИЯ ПРОГРАММ Эта глава является началом наших исследований по семан- семантике языков программирования. В основном она посвящается описанию интерпретатора для варианта языка Лисп. Основные операции Лиспа — car, cdr, cons, атом и равно — введены во второй главе и уже знакомы читателю. Вообще Лисп-программа — это система функций, определенных в терминах этих основных операций, но в действительности форма ее представления в ма- машине отличается от той, которая выбрана для представления программ в этой книге. Лисп-программы записываются в виде S-выражений, чтобы облегчить их машинную обработку. Точно так же как арифметическая формула переписывается в виде S-выражения, например, хху + г^(ПЛЮС{УМН X Y)Z) для того, чтобы сделать функциональные программы, оперирую- оперирующие с ними, проще в написании, так и Лисп-программы записы- записываются в виде S-выражений, чтобы упростить их интерпретацию. Фактически будут даны правила для транслитерации в S-выра- S-выражения всего строго функционального языка, который введен во второй главе и используется на протяжении всей книги. Это дает нам вариант Лиспа, очень похожий по форме и по смыслу на на- настоящий Лисп, но несколько более простой, отличающийся от исходного языка незначительными деталями. Язык, описанный в книге, называется Лисп-инструмент, или инструментальный Лисп, чтобы отличить его от настоящего Лиспа. Но обычно для краткости мы будем называть его просто Лисп, за исключением тех случаев, когда важно сохранить это различие. Все детали ре- реального Лиспа затруднили бы нашу простую концепцию и услож- усложнили бы все рассмотрения. Интересующемуся читателю можно рекомендовать обширную библиографию по реальному Лиспу. Ссылаясь на программу, «написанную на Лиспе», мы будем иметь в виду, в частности, представление этой программы в виде S-выражения. Следующее выражение является определением на строго функциональном языке уже знакомой нам функции сое-
100 Гл. 4. Представление и интерпретация программ динить; {соединить гдерек соединить = Х(х, у) если х = НИЛ то у иначе cons(car(x), соединить(саг(х), у))} Записав это на инструментальном Лиспе, т. е. в виде S-выраже- ния, получаем форму D.1). (ПУСТЬРЕК СОЕДИНИТЬ (СОЕДИНИТЬ ЛЯМБДА (X У) (ЕСЛИ (РАВНО X (КОД НИЛ)) У D.1) (CONS (CAR X) (СОЕДИНИТЬ (CDR X) У))))) На предыдущее представление программы соединить будем ссылаться как на абстрактную форму, а на последнее — как на конкретную форму этой программы. Следующий раздел посвя- посвящается простым и очевидным правилам перехода от абстрактной к конкретной форме. Конкретное представление Лисп-программы в виде S-выраже- ния позволяет записывать функциональные программы, для ко- которых эта конкретная программа будет аргументом. В частности, можно написать простые программы анализа: например, опреде- определить множество переменных, используемых в программе. После обсуждений подобных вопросов в предыдущей главе ясно, как это можно сделать. Менее очевидным, однако, является то, что можно написать функциональные программы, которые будут дей- действовать как компилятор или как интерпретатор с Лиспа. Ком- Компилятор должен транслировать Лисп-программу на другой язык, тогда как интерпретатор должен определять для конкретного описания функции и конкретных аргументов результат примене- применения данной функции к данным аргументам. Третий, и последний, раздел этой главы посвящен такому интепретатору. Компилятор, транслирующий наш вариант Лиспа на язык машины специаль- специального назначения, описан в гл. 6. Представим себе функциональную программу, определяющую функцию применить (/, л:), где / есть Лисп-программа в форме S-выражения, определяющая функцию, такая как приведенная выше программа для функции соединить, а л: есть представление в виде S-выражения списка фактических параметров для этой функции. Тогда применить (/, х) будет представлением в виде S-выражения результата применения функции, которую пред- представляет /, к аргументам, которые представляет х. Некоторые примеры приведены в табл. 4.1. Функция применить (/, х)> разумеется, должна проанализиро- проанализировать структуру программы / для того, чтобы вычислить значение
4.1. Абстрактная и конкретная формы программ 101 Таблица 4.1 Конкретная форма ((А В C)(D E F)) (А В С D E F) программы соединить ((А В С) НИЛ) (А В С) D.1) (((Л В))((С D))) ((A B)(C D)) /, примененной к х9 и по этой причине она называется интерпре- интерпретатором (или интерпретирующей функцией). В известном смысле этот интерпретатор задает семантику языка, на котором написана программа / (в нашем случае это Лисп). Действительное значе- значение этого станет ясным, когда мы осознаем, что эту семантику можно изменить очень простыми добавлениями в определении функции применить. Мы вернемся к обсуждению того, что за- задается и что не задается интерпретатором, когда закончим оп- определение функции применить (f, х). Прежде, однако, следует изложить два важных соображения. В следующем разделе будет описано преобразование абстракт- абстрактной формы функциональных программ в конкретную и обратно. Интерпретатор будет записан в абстрактной форме, как и осталь- остальные программы в этой книге, в то время как интерпретируемая программа на инструментальном Лиспе будет записана в виде S-выражения (т. е. в конкретной форме). Но поскольку мы сво- свободно можем переходить от одного представления к другому, можно отметить два простых факта. 1. Задается не только семантика Лиспа, но и семантика на- нашей абстрактной формы записи функций. 2. Поскольку определение функции применить может быть переписано на Лиспе, получается, что интерпретатор с Лиспа записывается на самом Лиспе. Читатель, которого несколько смущает очевидная простран- пространность этих выводов, будет прав, так как существуют некоторые ограничения на то, что может быть задано. Тем не менее в этой главе будет ясно продемонстрирована мощь функционального программирования в точном формальном описании семантики языка программирования. В следующей главе эта техника с та- таким же успехом будет применена к алголоподобному языку. 4.1. Абстрактная и конкретная формы программ Требуется показать, как любая программа, записанная на строго функциональном языке программирования, может быть представлена в виде S-выражения. Для этого нужно определить
102 Гл. 4. Представление и интерпретация программ то, что называют правильно составленным или правильным вы- выражением в данном языке. В инструментальном Лиспе правиль- правильными являются как раз те подвыражения программы, с которыми связываются определенные значения. В рассмотренных ранее определениях функций можно найти такие примеры правиль- правильных выражений: х х+1 car(cdr(s)) если х = 0 то 1 иначе х xf (х—1) Каждое из этих выражений является законченным в том смысле, что если известны значения входящих в него свободных перемен- переменных, то можно вычислить и значение всего выражения. Напро- Напротив, следующие подвыражения не являются правильными про- просто потому, что они незаконченные. х + car(cdr если х = 0 то 1 Даже если известны значения входящих в эти выражения пере- переменных, невозможно связать с таким подвыражением никакое значение. Например, какое значение имеет третье подвыраже- подвыражение в случае, когда хфО? Если взглянуть на функциональные программы, написанные ранее, то все они построены из вложенных друг в друга правиль- правильных выражений. Как мы видели в разд. 2.8, программа сама может быть записана как единое правильное выражение, обла- обладающее функциональным значением. Правильное выражение всегда можно расчленить на главный оператор и его операнды, являющиеся в свою очередь правильными выражениями. Так, например, в сложном выражении если л;=0то 1 иначе xxf(x—1) главным оператором является форма если ... то ... иначе, а правильные выражения х = 0 1 XXf(x-l) являются его операндами. Второй из операндов не является сос- составным и не расчленяется дальше. Однако в выражении х=0 главным оператором будет=, а х и 0 — его операнды, оба пра- правильные. Аналогично в выражении xxf(x—1) главным операто- оператором будет х , а х и f(x—1)—его правильными операндами и т. д. Выделим все виды правильных выражений в нашем абстракт- абстрактном строго функциональном языке и сопоставим каждому из них
4.1. Абстрактная и конкретная формы программ 103 соответствующее S-выражение. Это S-выражение, которое будет называться конкретной формой, наследует свойство правиль- правильности, и поэтому, ссылаясь на правильное выражение, можно иметь в виду либо абстрактную, либо конкретную его форму. Чтобы показать, как это делается, рассмотрим различные виды правильных выражений, которыми мы пользовались при опре- определении функций. Напомнив эти определения, мы преобразуем их в S-выражения, и их легко отличить, так как все S-выра- жения в конкретной форме записываются прописными буквами. Самыми элементарными правильными выражениями в опреде- определениях функций являются константы и переменные. Переменные представляются символьными атомами, например х, у, z и соот- соответственно X, Y', Z. Чтобы константы всегда можно было от- отличить от переменных, они помечаются ключевым словом КОД. Таким образом, формируется двухэлементный список, первым элементом которого является слово КОД, а вторым — сама константа. Так, константы 127, НИЛ, (А В) и X будут пред- представлены соответственно S-выражениями (КОД 127), (КОД НИЛ), (КОД (А В)) и (КОД X). Из этого правила нет исключений: константы всегда помечаются ключевым словом КОД, перемен- переменные не помечаются никогда. Фактически только по этому приз- признаку их и различает интерпретатор. Вообще всякое правильное выражение в конкретной форме представляется списком, первым элементом которого является атом, воспринимаемый как ключевое слово. Слово КОД явля- является первым таким примером. С вызовом функции поступаем аналогично. Вызов f(eu . . ., eh) преобразуется в конкретную форму вида (/ ег ek), т. е. в список из ?+1 элементов, первый из которых — имя вызываемой функции, а остальные — соответ- соответствующим образом преобразованные параметры функции. Так, вызов саг (х) преобразуется в конкретную форму (CAR X) а вызов cons (car(x), соединить (cdr(x), у)) в форму (CONS (CAR X) (СОЕДИНИТЬ (CDR X) Y)) Здесь необычно то, что имя вызываемой функции стоит внутри скобок, а не снаружи. S-выражения, будучи очень чувствитель- чувствительными к расположению скобок, не допускают в этом никаких синтаксических вариаций. Это отличительная черта Лиспа. Все
104 Гл. 4. Представление и интерпретация программ базисные функции (и фактически все функции, определяемые пользователем) преобразуются подобным образом. Чтобы уточ- уточнить ключевые слова для базисных функций, перечислим их в соотношениях D.2). car(e) (CAR e) Ч — Ч (МИНУС ех е2) cdr(e) (CDR е) ехХе2 (УМН ех е2) cons(eu е2) (CONS ех е2) ех ч- е2 (ДЕЛ ех е2) (* ^ атом(ё) (АТОМ е) ех ост е2 (ОСТ ех е2) к 'А) равно(еъ е2) (РАВНО е) e1<le2 (MP ег е2) е± + е2 (ПЛЮС р, е2) Другие примеры применения этих правил приводятся в табл. 4.2. Таблица 4.2 Ни условная форма, ни ^-выражение не вызывают затрудне- затруднений при переходе к конкретной форме. Абстрактному определе- определению функции если ех то е2 иначе еъ отвечает конкретная программа вида (ЕСЛИ е1е2ед) т. е. четырехэлементный список с ключевым словом ЕСЛИ. Таким образом, выражение если t<0 то cons@—i, НИЛ) иначе cons(i, НИЛ) принимает вид (ЕСЛИ (МР I (КОД 0)) (CONS (МИНУС (КОД 0) /) (КОД НИЛ)) (CONS I (КОД НИЛ))) ^-выражение 4*1, . . ., хк)е
4J. Абстрактная и конкретная формы программ 105 преобразуется к виду (ЛЯМБДА (хи . . ., xh) e) т. е. к трехэлементному списку с ключевым словом ЛЯМБДА, списком параметров и выражением, используемым для вычисле- вычисления значения функции. Например, функция К (х, у) cons (x} cons (у, НИЛ)) запишется в виде {ЛЯМБДА (X Y) (CONS X (CONS Y (КОД НИЛ)))) Теперь можно посмотреть, как преобразуется Х-выражение, определяющее функцию соединить: Цх, у) если равно(х, НИЛ) то у иначе cons(car(x), соединить(сс1г(х), у)) переходит в форму (ЛЯМБДА (X Y) (ЕСЛИ (РАВНО X (КОД НИЛ)) Y (CONS (CAR X) (СОЕДИНИТЬ (CDR X) Y)))) Наконец, следует рассмотреть конструкции пусть и где и их рекурсивные аналоги. В конкретной форме допускается только один способ локальных определений, который является гибрид- гибридной формой конструкций пусть и где. Используются ключевые слова ПУСТЬ и ПУСТЬРЕК, но определяемое выражение по- помещается перед локальными определениями, как и в обычной форме где. Каждое из выражений D.3) преобразуется к виду D.4). Аналогично выражения D.5) переходят в D.6).
106 Гл. 4. Представление и интерпретация программ Таким образом, блок с k определениями представляется спис- списком из &+2 элементов, где первым элементом является ключе- ключевое слово, вторым — определяемое выражение, а остальные эле- элементы — локальные определения. Каждое локальное определе- определение представляется точечной парой. Обычно значение, присваи- присваиваемое локальной переменной, является выражением, заключен- заключенным в скобки, и на практике по нашим правилам точка вместе с этими скобками опускается. В то время как буквальное при- применение правил переводит выражение {пусть s = car(z) и p = car(cdr(z)) napa(s + n, pxri)} в форму {ПУСТЬ (ПАРА (ПЛЮС S N) (УМН Р N)) (S.(CAR Z)) (P.(CAR(CDR Z)))) лучше записать это без точек в виде (ПУСТЬ (ПАРА (ПЛЮС S N) (УМН Р N)) (S CAR Z) (Р CAR (CDR Z))) Это объясняет, в частности, почему в определении функции сое- соединить в D.1) перед словом ЛЯМБДА отсутствуют скобки, т. е. определение имеет форму (СОЕДИНИТЬ ЛЯМБДА (X Y). . .) В качестве завершающего примера преобразуем в конкрет- конкретную форму программу дифференцирования, описанную в самом конце разд. 2.7.
4.1. Абстрактная и конкретная формы программ 107 Ниже перечислены все S-выражения, которые являются пра- правильными для программ, написанных на инструментальном Лиспе. Здесь хъ . . ., xk— атомы, а еи . . ., ek— вложенные правильные выражения; идентификатор s обозначает некоторое S-выражение. Каждое из этих правильных выражений, за исклю- исключением переменных и констант, содержит вложенные в него другие правильные выражения. Например, условная форма содержит три вложенных в нее правильных выражения, соот-
108 Гл. 4. Представление и интерпретация программ ветствующих проверке и альтернативе истина или ложь. В этом списке имеется только одно необычное выражение — это вызов функции. Из (k+\) вложенных в него правильных выражений первое должно иметь значением функцию, а остальные являются параметрами этой функции. Так, выражение ((КОМЛ ОБРАТИТЬ УВЕЛИЧ) (КОД A 2 3))) является правильным. Это двухэлементный список, соответ- соответствующий вызову функции с функцией (КОМП ОБРАТИТЬ УВЕЛИЧ) и единственным аргументом (КОД A 2 3)) Оба вложенных выражения правильные. Форма (КОМП ОБРАТИТЬ УВЕЛИЧ) правильная и снова отвечает вызову функции. Во всех схемах правильных выражений, перечисленных выше, значением k может быть 0. Вообще это приводит к вырожденным случаям, таким как блок без определений или функция без параметров, но тем не менее это правильные выражения. В разд. 4.3 будет определен интерпретатор для инструмен- инструментального Лиспа в форме функции применить (/, #), которая тре- требует, чтобы f было правильным выражением, значением которого является функция, а х должен быть списком аргументов для этой функции. Во время интерпретации выделяются и вычисля- вычисляются все правильные подвыражения f, часто не один раз. Дей- Действительно, только правильные выражения являются значения- значениями, только для них можно определить значения, и поэтому структура всего интерпретатора продиктована множеством пра- правильных выражений, перечисленных в этом разделе. Интерпре- Интерпретатор как раз и анализирует случаи правильных подвыражений в программе. 4.2. Связывание Во второй главе неформально обсуждался вопрос о том, ка- каким образом в строго функциональном языке значения связы- связываются с переменными. Вообще это делается прямо в лоб и отра- отражает наше интуитивное представление о роли параметров в опре- определении функции. Некоторые тонкости, однако, возникают тогда, когда в определении функции допускаются вхождения свобод- свободных (или глобальных) переменных, а также локальных перемен- переменных (не параметров), введенных блоками где или пусть. Чтобы объяснить правила связывания, введем понятие контекста.
4.2. Связывание 109 Каждое выражение в функциональной программе вычисляется относительно контекста (или в некотором контексте). Контекст как раз и является соответствием между переменными и значе- значениями (которые обычно являются S-выражениями). Будем записывать связи в форме переменная ->- значение и, таким образом, контекст будет множеством таких связей, обычно изображаемым в виде таблицы. Например, контекстом является запись х-+(А.В) У —(С D) 2—127 В этом контексте выражения, перечисленные в табл. 4.3, прини- принимают значения, указанные в той же таблице. Таблица 4.3 Блоки пусть и где используются, чтобы установить новые связи. Такой блок всегда появляется вложенным в некоторый контекст. Выражение, определяющее значение новой локальной переменной, вычисляется в этом контексте. Определяемое выра- выражение тогда вычисляется в новом контексте, полученном расши- расширением внешнего контекста за счет добавления новых связей, связывающих каждую локальную переменную с ее значением. Так, если выражение {пусть и = саг(х) И 0=2 + 1 cons (и, cons(v, у))} встретилось в контексте, приведенном выше, то при вычислении выражений саг(х) и г+1 получаются значения А и 128 соответ- соответственно, как и показано в табл. 4.3. Затем определяемое выраже-
ПО Гл. 4. Представление и интерпретация программ ние cons (и, cons(v, у)) вычисляется в расширенном контексте х-^(А.В) и—* А y-+(CD) и—128 г—127 и это дает значение (А 128 CD). Когда локальные переменные имеют те же самые имена, что и переменные в контексте, в котором вычисляется данный блок, то старые связи вытесняются новыми. Так, если бы выражение {пусть х — саг{у) cons(x, НИЛ)} вычислялось в окружении х— (А В) у-+(С D) то новая связь для х имела бы вид х-+С и окружение, в котором вычислялось определяемое выражение, было бы таким: х-»С У-(С D) Результатом было бы выражение (С). Связь х-> (А В) полностью исчезает из определяемого выражения. Теперь это правило проясняет смысл локальных определений, в которых одно и то же имя переменной входит и в левую и в правую части определения. Например, пусть имеем выражение {пусть х==х + 1 х^-2) Оно имеет смысл только в контексте, где переменная х уже свя- связана с числовым значением. Например, в контексте *-И27 это выражение имеет значение 64. Это согласуется с понятием локальности новой переменной, введенной блоками пусть или где. Так как имена локальны в блоке, значение блока не изменится, если эти имена заменить на другие, не встречающиеся в блоке. Разумеется, такая замена должна быть согласованной, т. е. заменить нужно все вхождения данной переменной. В приведен- приведенном выше блоке замена его на выражение {пусть у = х + 1
4.2. Связывание 111 не изменяет его значения. Это значение становится более очевид- очевидным после такой замены, но тем не менее нужно уметь определять значение и в прежней форме. При обсуждении проблем связывания в контексте функций начнем с таких, в теле которых нет свободных (или глобальных) переменных. Функции обычно вводятся л-выражениями, и функ- функциональные значения обычно связываются с переменными по- посредством блоков, которые были рассмотрены выше. Например, в выражении {пусть / = /.(,*, уJх х + /B, 3)} вводится функция, которая вычисляет конкретную арифметиче- арифметическую функцию от двух переменных, и это значение связывается с переменной /. Когда функция / вызывается, вычисляются ее фактические параметры и каждое значение связывается с соот- соответствующим формальным параметром. Таким образом, тело функции вычисляется в контексте, включающем связи каждого из формальных параметров с соответствующими значениями фак- фактических. В нашем примере тело функции 2хх-\-у вычисляется в контексте х->2 У —3 что приводит к результату 7. В более общем случае имеем {пусть / = >.(*, уJхх + у /B, 3) + fC, 2)} Здесь функция вычисляется дважды: один раз в прежнем кон- контексте, а другой раз в контексте х—3 У-2 Значение /C, 2) равно 8. Отсюда значением всего выражения будет 15. Когда функция определяется, как это было сделано выше, механизм блоков дает ей имя, и это имя связывается затем с функ- функциональным значением. Теперь рассмотрим подробнее, что та- такое функциональное значение. В случаях, описанных выше, когда тело функции не содержит глобальных переменных, л-выражение само служит таким значением. Однако, когда к-вы- ражение содержит свободные переменные, это будет некорректно, так как вхождение этих переменных требует, чтобы они были
112 Г л, 4. Представление и интерпретация программ связаны с теми значениями, которыми они обладают в тот момент, когда вычисляется Ji-выражение, чтобы получить конкретное функциональное значение. Одним из способов обойти эту труд- трудность является использование в качестве функционального зна- значения модифицированного ^-выражения, в котором все вхожде- вхождения глобальных переменных заменены их значениями. Так, если выражение {пусть f = X(xJxx+y {пусть у = 4 fB)}} вычисляется в окружении то сначала / связывается с функцией %(х) 2ХХ+3 а затем у связывается с 4 и образуется контекст f-+X(xJxx + 3 {/—4 в котором вычисление /B) дает 7. Эквивалентный способ описания функциональных значений получается при использовании понятия замыкания. Вместо подстановки в тело функции формируется сложный объект, называемый замыканием Х-выражения (или просто замыканием), состоящий из ^-выражения и контекста, определяющего значе- значения глобальных переменных. В рассмотренных выше вычисле- вычислениях, когда встречается вызов /B), мы имели бы контекст f—[X(xJxx + y, {у — 3}] У-+4 где замыкание обозначается конструкцией [^-выражение, контекст] Теперь вызов функции интерпретируется следующим образом. Вычисляются фактические параметры в контексте вызова, за- затем контекст замыкания (определяющий контекст) пополняется за счет связывания каждого формального параметра с соответ- соответствующим ему значением, и в этом окружении (в этой вычисли- вычислительной обстановке) вычисляется тело функции. Так, в нашем примере тело 2хх+у вычисляется в контексте х — 2 несмотря на связь #->4 в месте вызова.
4.2. Связывание 113 Существует простая и поучительная аналогия между бло- блоками пусть и ^-выражениями. Рассмотрим более общую форму вызова функции е(е19 . . .,ек) где вызываемая функция является значением выражения е, которое может быть просто переменной, но может быть и более общим выражением. Параметры, приданные этой функции, суть еъ . . ., ек. Например, в гл. 2 встречалась функция увел(п) с оп- определением увел(п)^к(г)г+п которое выдает как результат функцию, которая увеличивает свой аргумент на п. Используя общую форму, можно записать вызов (увелA))(х) в котором функциональное значение, полученное вычислением увел(\), применяется к аргументу х. Рассмотрим эту общую форму вызова в том случае, когда вызываемая функция задается Я-выражением. Вызов имеет вид {Цхи . . ., хк)е) (еи . . ., ек) где функция Х(хи . . ., xh)e применяется непосредственно к аргументам еи . .., ек. В этой форме выражение имеет то же самое значение, что и представление D.8). {пусть х1 = е1 и х2 — е2 D.8) и xk = ek Эту аналогию между блоком пусть и Х-выражением (их экви- эквивалентность) мы и хотели как раз продемонстрировать. Это по- помогает пояснить некоторые правила связывания. Как показы- показывает приведенная аналогия, блок пусть не является необходимым средством языка. Однако разделение формальных и фактических параметров в форме с ^.-выражением делает форму с блоком пусть более легкой и удобной на практике. Перейдем теперь к более трудным понятиям — к блокам пустьрек и гдерек, в которых определения взаимно рекурсивны. {пустьрек *i = ei и х2 = е2 D.9) и хк=ек
114 Гл. 4. Представление и интерпретация программ Форма D.9) неявно содержит то семантическое условие, при ко- котором в противоположность простым блокам пусть и где вхожде- вхождения хи . . ., хк во все выражения еи . . ., ек интерпретируются как вхождения локальных переменных. Отсюда утверждение о том, что лгь . . ., хк определяются через взаимную рекурсию. Трудность заключается в том, что выражения е1у . . ., ек должны вычисляться в окружении, в котором переменные хи . . ., хк уже связаны. Но значения, с которыми должны быть связаны эти переменные, суть значения этих самых выражений еи . . ., ек. Вообще будем рассматривать только такие блоки, в которых все выражения еи . . ., ек являются ^-выражениями. Тогда значе- значением каждого е( является замыкание [eh а], где а — контекст, в котором каждая переменная из хи . . ., хк связана с соответ- соответствующим значением еи . . ., ек, т. е. а^{х1-^[е1У а] **-*[**> а]\ Отметим еще раз, что здесь предполагается, что каждое из е( есть Х-выражение. С этим необычным рекурсивным контекстом фактически легко обращаться человеку и ненамного труднее — машине. Мы просто отмечаем тот факт, что контекст имеет имя а, и не беспокоимся о том, какие значения он включает. Когда во время вычисления е (или любого из выражений еи . . , ек) мы наталкиваемся на ссылку на переменную xh то берем в качестве ее значения замыкание [eh а]. Таким образом, последовательные вхождения xlt . . ., хк в е( примут значения в контексте а, что мы и имели в виду. Чтобы увидеть, как это работает, рассмотрим определение {пустьрек f = X(x) если атом(х) то х иначе f(cdr(x)) вычисляемое в контексте 2^ (А В.С) Для вычисления f(z) берем контекст a s== {/ —* [X (х) если атом(х) то х иначе f(cdr(x)), a] z-+(A В.С)} Отсюда вызов f(z) заставляет нас войти в функцию с контекстом х-+(А В.С) f —+[к(х) если атом(х) то х иначе f(cdr{x)), a] г-+ {А В.С) Теперь, поскольку х не атом, делается рекурсивный вызов f(cdr{z)).
4.3. Интерпретатор для варианта языка Лисп 115 Значение / остается прежним, а контекст а пополняется новой связью за счет фактического параметра функции f(cdr(x)). Таким образом, мы повторно вызываем функцию с контекстом х-»(В.С) f—+[k(x) если атом(х) то х иначе f(cdr(x)), a] г-+(А В.С) Так мы оказываемся в том же самом состоянии, что и прежде, и убеждаемся, что рекурсия работает должным образом. В следую- следующем разделе, где определяется интерпретатор для инструмен- инструментального Лиспа, будет рассмотрена конкретная реализация этих правил связывания. 4.3. Интерпретатор для варианта языка Лисп Существенной чертой Лиспа является та легкость, с которой он применяется для записи своего собственного интерпретатора. В действительности это является одним из главных его предназ- предназначений. В этом разделе описывается такой интерпретатор для инструментального Лиспа. Это только один из возможных интер- интерпретаторов, и очень многое можно узнать о Лиспе, рассмотрев другие варианты. Читая этот раздел, имеет смысл пролистать другие публикации по интерпретаторам; в частности, интересен оригинальный интерпретатор Маккарти [1960] и его пересмотрен- пересмотренная версия [1962]. Такое изучение убедит в том, что семантика языка может быть очень тонкой и, кроме того, что при реализа- реализации очень трудно бывает уловить различные нюансы языка. Однако главная цель этого раздела — продемонстрировать ос- основную технику непосредственной интерпретации исходных программ на инструментальном Лиспе, и мы только вкратце коснемся того, в какой мере семантика языка представлена в этом интерпретаторе. Случаи, которые здесь анализируются, в точности повторяют перечень правильных выражений, которые и могут входить в программу на инструментальном Лиспе. Имеются 18 различных типов выражений, перечисленные в разд. 4.1. Если е есть пра- правильное выражение, то нужно определить функцию вычислить, которая выдает значение этого выражения. Выражение е может содержать свободные вхождения переменных, и поэтому его значение определено только в том случае, если имеется контекст, связывающий значение с каждой свободной переменной из е. Понятие контекста было введено в предыдущем разделе, где рассматривалось связывание переменных. Ассоциативный список, который рассматривался в разд. 3.1, является одной из возмож- возможных реализаций такого контекста. Фактически это реализация,
116 Гл. 4. Представление и интерпретация программ использованная Маккарти в его оригинальном интерпретаторе. Здесь, однако, будет использована несколько более разработан- разработанная реализация, которая согласуется с концепцией, принятой при реализации компилятора, который мы опишем в гл. 6. Построим список имен, в котором перечислены все перемен- переменные, входящие в программу. Список имен имеет структуру списка из списков атомов, каждый атом является именем переменной. Например, ((X Y Z) (А X) (FN)) является списком имен. В программе на инструментальном Лиспе каждое подвыражение входит в некоторый контекст, где действу- действуют некоторые из переменных в том смысле, что это выражение находится в области действия этих переменных. Эти доступные переменные упорядочены в списке имен таким образом, что более локальные находятся в нем ближе, а глобальные — дальше. Интерпретатору необходим не только список имен, но и конгру- конгруэнтный ему (т. е. равносоставленный) список значений. Например, список значений (A (А В) -127) ((CD) 3) (((ЛЯМБДА...)))) конгруэнтен списку имен, приведенному выше, и, таким обра- образом, эти два списка представляют контекст X -^1 Y -+(АВ) Z -^ —127 A ^(C.D) FN-»((ЛЯМБДА...)) Для доступа в список значений в соотношениях D 10) опреде- определяется функция ассоц(х, п, v), которая по имени х из списка имен п выдает соответствующее значение из списка значений v. ассоц(х, гс, v) = если элементах у саг(п)) то место(х, car{n), car(v)) иначе ассоц(х, cdr(n), cdr(v)) место(х, /, m) == если равно(х, сагA)) то саг(т) D.10) иначе место{х, cdr(l), cdr(m)) элемент(х, I) === если равноA, НИЛ) то Л иначе если равно{ху саг{1)) то И иначе элемент{ху cdr(l)) Видим, что функция ассоц проверяет каждый подсписок в п поочередно, чтобы определить, не является ли х элементом этого подсписка. После того как такой подсписок найден, функ- функция место определяет позицию и извлекает из списка v соответ-
4.3. Интерпретатор для варианта языка Лисп 117 ствующее значение. Отметим, что функция ассоц предполагает, что х обязательно есть в списке п. Если х не входит в список я, результат функции ассоц не определен. Будем избегать услож- усложнений, связанных с выявлением ошибочных ситуаций, подоб- подобных этой. Условимся, что в случаях, когда ошибка может быть отмечена, функция, которая может определить ошибку, не делает этого, а просто не имеет определенного значения. Теперь можно приступить к определению функции вычислить (е, п, v). Эта функция, которая будет определять значение пра- правильного выражения, имеет своими параметрами правильное выражение е, список имен п и конгруэнтный ему список значе- значений v. Списки п и v вместе образуют контекст, в котором проте- протекает вычисление. Нужно построить функцию и записать ее определение в виде вычислить(е, п, t;)^ ... Сделаем это при помощи анализа случаев, исходя из структуры правильного выражения е. Чтобы сделать описание построения более легким для понимания, запишем каждый случай в виде уравнения. Результат применения функции вычислить к аргу- аргументам е> п и v будем записывать в виде вычислить[еуп&] где используются квадратные скобки, чтобы отличить их от ско- скобок S-выражения. Так, например, можно написать уравнение выншмить[(САЯ Х),((Х)ША.В)))]=А чтобы указать значение выражения (CAR X) в контексте, свя- связывающем переменную X со значением (А.В). Разумеется, обыч- обычно мы будем писать более схематичные уравнения, где математи- математические переменные, записанные малыми буквами, обозначают произвольные S-выражения. Например, следующее уравнение определяет значение (CAR e) для любого правильного выражения е: если вычислить[е, п, v] = (a.b) то вычислить[(СAR e),n,u] = а Чтобы облегчить чтение, будем аккуратно выбирать имена переменных. Правильные выражения будем обозначать через е, ?ъ • • .i Zk\ х> *и . . м Хь, обозначают атомы, используемые в качестве переменных в правильных выражениях; п, п1у п2, . . . обозначают списки имен, и v, vl9 v2, . . . — списки значений; наконец, другие буквы, но чаще a, b и s будут использоваться для обозначения произвольных S-выражений. Это согласуется с обозначениями в списке правильных выражений, приведенном
118 Гл. 4. Представление и интерпретация программ в разд. 4.1. Рассмотрим поочередно каждое из этих правильных выражений. Опустим арифметические выражения, так как здесь не требуется никаких новых рассмотрений. Значением переменной х является как раз то значение, ко- которое можно извлечь из структуры «список имен — список зна- значений», т. е. вычислить [x,n,v] = ассоц(х,п&) Значением правильного выражения (КОД s) является само s9 независимо от контекста, в котором выполняется вычисление. Отсюда вычислить[(КОД s),n,v]=s Рассмотрим теперь основные функции CAR, CDR, CONS, АТОМ и РАВНО. Каждая из них имеет определенное число аргументов, и значение всего правильного выражения, в котором главный оператор является одной из этих базисных функций, определяется через значения этих аргументов. Например: если вычислить[еип,Ь] = а и вычислить[е2,п^] = Ь то ebi4UCAumb[(CONS е1 e2),niv\ = cons(ay Ь) Здесь мы сделали несколько больше, чем просто определили CONS через cons. Здесь указано, что аргументы должны быть вычислены и затем функция cons применяется к значениям. Ясно, что каждая из базисных функций может быть определена подоб- подобным образом. Правилом для (CAR e) будет если вычислить[е,п,и] = а то вычислить[(СAR e),n,v] = car(a) а правила для CDR и АТОМ аналогичны. Правило для РАВНО аналогично правилу для CONS. Условная форма (ЕСЛИ ег е2 е3) требует некоторых размышле- размышлений. Здесь не предполагается, что обе компоненты е2 и е3 вы- вычисляются заранее, поэтому их можно не вычислять, пока не будет вычислено ег. Правило можно записать в виде если вычислить[еип^] = а то вычислить[(ЕСЛИ ел е2 е3),п^] = если а то вычислить[е2,п^] иначе вычислить[еч,п^] что согласуется со способом, каким определены базисные функ- функции. Здесь фактически вычисляется только одно из выражений е2 или ?8, что связано с тем обстоятельством, что этим свойством обладает форма если ... то ... иначе . . • . Если переписать это
4.3. Интерпретатор для варианта языка Лисп П9 правило в другом виде: если ebi4UCAumb[elyn,v] = a то вычислить[(ЕСЛИ ег е2 e3)yn,v] = вычислить[еслп а то е2 иначе еЗУпуи] то, может быть, станет более очевидным, что вычисляется только одно из выражений е2 или е3 и фактически это не зависит от того, что этим свойством обладает форма если ... то ... иначе .... Целям интерпретации служит любое из этих определений. Интерпретация ^-выражения (ЛЯМБДА (х1у . . . xk) e) нес- несколько труднее, но зато и интереснее. В предыдущем разделе объяснялось, как нужно рассматривать значение-функцию, что- чтобы связать ее свободные переменные со значениями, которые имеются в момент определения функции. Рассматривались два альтернативных способа сделать это: либо подставить в е вместо всех переменных, кроме х1у . . ., xh, те их значения, которые име- имеются в контексте в данный момент, либо построить для пред- представления функции специальное сложное значение, называемое замыканием. Выберем последнюю альтернативу. Для представле- представления замыкания выберем простую комбинацию ^-выражения и контекста. Замыкание должно содержать не только тело ^-выражения, но также имена связанных переменных и контекст, в котором должно вычисляться тело функции. Произвольным образом вы- выберем следующую структуру этой информации: вычислить[(ЛЯМБДА у е), п,и] = cons(cons(yfe)f cons (n,v)) где у есть список связанных переменных формы (х±, . . ., xh)- Назначение хранения всей этой информации станет понятным при анализе вызова функции. Вызов функции обозначается правильным выражением (е €i • • • €и)> гДе выражение е имеет своим значением функцию (замыкание), а значения выражений еи . . ., eh являются аргу- аргументами этой функции. Чтобы вычислить аргументы, определим функцию вычсписA, n, v) з если / = НИЛ то НИЛ иначе сопь(вычислить(сагA)у п, и), вычспис(сагA)9 п, v)) и, чтобы избежать недоразумений, примем по отношению к этой функции то же самое соглашение о квадратных скобках. Теперь можно записать определение вызова функции следующим обра-
120 Гл. 4. Представление и интерпретация программ зом: если вычислить[е, п, v] = ((y.e').(n'.v')) и вычепис[(ег . ¦. ek), п, v] = z то вычислить[(е ех.. .ek)yn,v\ = вычислить [е\ (у.п')> (z.v')] Это требует некоторых пояснений. Сначала предполагается, что вычисление выражения е дает замыкание, структура которого указана на рис. 4.1, и что вычисление аргументов (ех . . . ек) дает список значений г. То есть z будет /^-элементным списком, Рис. 4.1. конгруэнтным списку у связанных переменных. Имея эти значе- значения, можно определить значение вызова функции (е ех . . . ек) как значение, полученное при вычислении тела функции в кон- контексте, списком имен которого является (у.п') и списком значе- значений — (z.v'). Ясно, что это тот самый способ, который мы себе обычно представляем при вызове функции, когда тело функции вычисляется при условии, что значения фактических параметров связываются с формальными, а другие переменные имеют те значения, которые были у них в момент определения функции. Как это и можно было ожидать, простой блок ПУСТЬ очень похож на вызов функции: если вычспис[(ег ... ek)>n,v\~z то вычислить[(ПУСТЬ е (х1.е1).. .(xk.ek)), n, v] =вычислить[е, ((хх ... xk).n)y (z.v)] Здесь видно, что определения (ех . . .eh) группируются вместе и вычисляются функцией вычспис. Это делается просто для удоб- удобства. Результативный список значений z выставляется затем в общий список значений на позицию, соответствующую списку определяемых переменных (хъ . . . Хъ). В этом расширенном контексте вычисляется тело блокам. Очевидно, что правило уста-
4.3. Интерпретатор для варианта языка Лисп \2\ навливает эквивалентность {ПУСТЬ e(x1.e1)...(xk.ek)) <s ((ЛЯМБДА(х1 ...хк)е)ег... ек) в том смысле, что оба этих выражения принимают одинаковые значения в любом контексте. Переходим, наконец, к рекурсивному блоку (ПУСТЬРЕК е (хг.ег) . . . (xh.eh)) Как можно ожидать, здесь будут некоторые трудности, так как определения еи . . ., eh должны вычисляться в окружении, где имеется доступ к определяемым переменным хъ . . ., xh. И дей- действительно, здесь будет дана ограниченная, но в значительной мере адекватная интерпретация рекурсивного блока, а полная ин- интерпретация будет отложена до гл. 8. Введем псевдофункцию, которая обычно пишется на Лиспе, а здесь определяется в ог- ограниченном виде. Эта псевдофункция называется завершение (х, у), и ее действие состоит в замене саг от х на у. В момент вызова завершение (х, у) значение саг(х) должно быть специальным зна- значением й, на которое ссылаются как на «незавершенное». За- Завершение используется для замены Q на предназначенное для этой цели значение, когда оно будет вычислено. Результатом вызова завершение (х9 у) будет х с учетом того, что значение Q в саг(х) будет заменено на предназначенное значение у. Незавер- Незавершенное значение просто рассматривается как временное, которое представляется вместо того значения, которое в конечном счете его заменит. Если программа пытается иметь доступ к незавер- незавершенному значению прежде, чем оно будет заменено, то действие программы не определено. Рассмотрим выражение {пусть b = cons(Q, В) завершение (Ь,А)\ его значением будет (А .В). В этом смысле завершение (х, у) имеет точно то же значение, что и cons (у, cdr(xj). Однако завершение имеет одну важную возможность, которую мы и будем использо- использовать. Она может строить «циклические» структуры. Пусть имеем {пусть b**cons(Qy В) завершениефу Ь)\ Разумеется, значением этого выражения является S-выражение, cdr которого есть В. Однако саг этого S-выражения тоже явля- является S-выражением. Таким образом, построена структура а, значением которой является (а.В). Так, например, если х=
122 Гл. 4. Представление и интерпретация программ = (а.В), то cdr (car (car (car(x))))=B и фактически, сколько бы раз мы ни брали саг от х, всегда полу- получим значение а. Способ действия функции завершение можно вполне понять, зная только, как S-выражения представляются в машине. Практически функция cons генерирует запись с двумя Рис. 4.2. Рис. 4.3. указателями, один из которых указывает на саг этой структуры, а другой — на cdr. S-выражение затем представляется указателем на эту запись cons. Когда cons вызывается первый раз в выраже- Рис. 4.4. нии cons(Qt ?), то строится запись, изображенная на рис. 4.2, и когда затем вызывается завершение с этим значением а в ка- качестве обоих аргументов, то й в этой записи просто заменяется на а (рис. 4.3). Если между созданием записи а с Q и заменой Q делается копия а, то, так как копируется только указатель, а Рис. 4.5. не сама запись, замещение остается в силе. Например, список (А В С D) может быть представлен в виде, изображенном на рис. 4.4. Тогда выражение {пусть b = cons(Q,HHJI) завершение(Ьусоединить((А В С D), Ь))} генерирует структуру, указанную на рис. 4.5, которая обозна- обозначает бесконечное S-выражение ((Л В С D (А В С D (А В С D (. . .)))))
4.3. Интерпретатор для варианта языка Лисп 123 Это возможность строить циклические структуры будет ис- использована при интерпретации рекурсивного блока. Вычисление определяющих выражений еи . . ., eh выполняется со списком значений, в котором значения хъ . . ., xk еще не завершены. Таким образом, на использование еи . . ., eh в блоке ПУСТЬ- РЕК накладывается такое ограничение, при котором вычисле- вычисление ev . . .yek не требует немедленного доступа к хи ..., xk. Имеем следующее правило: если v' = (Q.v) и nf = ((x1 ... xk).n) и вычспис[(ех .. ek)y n\ v'] = z то вычислить[(ПУСТЬРЕК е (хх.ех)... (xk.ek)),n,v] ~вычислить^,п\завершение\ргу г]] Здесь введен фиктивный список значений v\ конгруэнтный списку имен пг=((х1 . . . xh).n)y за исключением того, что на первом месте в нем стоит незавершенное значение Q, которое в конечном счете будет заменено списком значений еи . . ., ek. Этот фик- Рис. 4.6. тивный контекст используется при вычислении списка опреде- определений (ег . . . ek) таким образом, что, когда некоторое конкрет- конкретное et есть ^-выражение, значением, определяемым для него, будет замыкание, содержащее v'. Перед вычислением тела блока мы заменяем саг от v' на z — список значений определений. Например, по этому правилу при вычислении правильного выражения (ПУСТЬРЕК е (F ЛЯМБДА у, ех) (G ЛЯМБДА у2 е2)) сначала строится список значений, изображенный на рис. 4.6, а затем конструируются два замыкания для функций F и G (рис.4.7). Отметим, что оба они ссылаются на v'. Когда переходим к вы- вычислению еу то Й в списке v замещается, и теперь имеет значе- значение, изображенное на рис. 4.8. Прослеживая эту схему, видим, что ссылки из е на функции F или G приводят к значениям а и ? соответственно. Аналогично, когда интерпретируется тело F или G, т. е. ех или е2у формируе-
124 Гл. 4. Представление и интерпретация программ мый при этом контекст строится из v\ и поэтому ссылки на F или на G из этих выражений также приводят к а и ?. Таким обра- образом, интерпретация основана на том факте, что для получения значения Х-выражения не требуется вычислять его тело, это от- откладывается до вызова функции. Можно определить вычисление блока ПУСТЬРЕК без ис- использования псевдофункции завершение, и с математической точ- точки зрения именно так стоило бы сделать. Соответствующая кон- Рис. 4.8. струкция трудна для понимания, и это до некоторой степени увело бы нас от нашей цели дать здесь точное описание семантики этого средства на интуитивном уровне. В гл. 8 мы вернемся к проблеме более полной реализации блока ПУСТЬРЕК, когда будут рассматриваться вычисления с задержкой. Фактически разработка интерпретирующей функции законче- закончена, остается только выписать соответствующее описание функции. Нам потребуются функции перем(а) и выр(а)у которые расщепля- расщепляют список переменных и определений ((Xi.ei) . . . (xk.ek)) и формируют отдельно список переменных (хг . . . xh) и список определений {ех . . . eh). Эти функции тривиальны по сравнению с теми программами, что рассмотрены раньше, но мы приведем их для полноты. перем(а) г если d = НИЛ то НИЛ иначе cons(car(car(d))ynepeM(cdr(d))) выр(а) гэ если d= НИЛ то НИЛ иначе cons(cdr(car(d)),ebip(cdr(d))) Фиктивный элемент Q, указывающий на незавершенность значе- значения, в программе будем обозначать атомом ПОКА. Определение
4.3, Интерпретатор для варианта языка Лисп 125 функции вычислить(е, я, v) получается почти буквальным переписыванием разобранных выше уравнений (см. 4.11). вычислить(е,п^)« если атом(е) то ассоц(е,п^) иначе если саг(е)~КОД то car(cdr(e)) иначе если car(e) — CAR то саг(вычисАить(саг(саг(е)),п,и)} иначе если car(e) = CDR то саг(вычислить(саг(саг(е))уп,у)) иначе если саг(е) = АТОМ то атом(вычислить(саг(саг{е)),п^)) иначе если car(e)=zCONS то cons(ebi4UCAumb(car(cdr(e)),nyvI ebi4UCAumb(car(cdr(cdr(e))) ,n,v)) иначе если саг(е)~ РАВНО то равно(вычислить(саг(саг(е)),п,у)у вычислить (car (cdr(cdr(e)))yn,v)) иначе если саг(е) = ЕСЛИ то {вычислить (если вычислить (e[tnfv) то е2 иначе еЗ,/г,а) где el ~car(cdr(e)) и e2 = car(cdr(cdr(e))) D.11) и e3 = rar(cdr(cdr(cdr(e))))} иначе если саг(е)=ЛЯМБДА то co/2s(cons(ca/-(cdr(^)), car(cdr(cdr(e))))t cons(ntv)) иначе если саг(е) = ПУСТЬ то ^4ucAumb(car(cdr(e)),cons(y,n)tcons(z,v)) где y = nepeM(cdr(cdr(e))) и z = ebi4cnuc(ebip(cdr(cdr(e)))in1v)} иначе если саг(е)=ПУСТЬРЕК то {{вычислить (car (cdr(e)),cons(y>n),заве ршение (vf %z)) где г = вычспис(выр(саг(саг(е))),соп5(у,п)^')} где # = nepeM(cdr(cdr(e))) и i/=cotts(tfO7^,i;)} иначе {вычислить(саг(саг(с))> cons(car(car(c))t car(cdr(c)))t cons(z>cdr(cdr(c)))) где с — вычислить(саг(ё).п,у) и г = вычспис(саг(е),п^)} Внимательное сравнение этого определения с рассмотренными ранее уравнениями убедит нас в том, что это точное повторение принятых ранее решений. Смысл такого переписывания заключа- заключается в том, что так яснее стала форма, в которой может выпол- выполняться интерпретация на машине. Чтобы согласовать это с тем, что говорилось об интерпрета- интерпретаторе раньше, требуется определить функцию, которая будет не вычислять выражения, а применять функцию к некоторым аргу- аргументам. Программа на инструментальном Лиспе является пра- правильным выражением /, значением которого является функция, и обычно требуется применить эту функцию к списку аргументов а. Этот образ действий характеризуется в разд. 4.1 выражением применить (/, #). Определим функцию применить (/, х) следующим образом: применить^\х) в {вычислить(саг(саг(с))у cons(car(car(c)), car(cdr(c)))>cons(x, cdr(cdr(c)))) где с = вычислить($,НИЛ\НИЛ)} Иными словами, / вычисляется, чтобы образовать замыкание
126 Гл. 4. Представление и интерпретация программ с= ((у.ё). (n.v)), а затем вычисляется значение выражения вычислить[е,(у.п),(х.ь)}. Кратко обсудим вопрос о том, что именно делает такой интер- интерпретатор и в какой степени он определяет семантику рассматри- рассматриваемого языка. Первое, что мы должны отметить,— это то, что смысл, придаваемый интерпретатором базисным функциям CONS, CAR, CDR, ATOM и РАВНО, определяется не интер- интерпретатором, а нашим предварительным пониманием смысла функций cons, car, cdr, атом и равно. Задается лишь то, что эти операции в исходной программе применяются к вычисленным значениям аргументов, а не просто к буквенным аргументам. Было бы, вероятно, бессмысленно приписывать базисным функ- функциям другие определения, отличные от тех, что им даны перво- первоначально. Но если бы мы расширили исходный язык просто добавлением правильного выражения {НУЛЬ е) и расширили бы интерпретатор, включив в него фразу .. .если равно (саг(ё),НУЛЬ) то нуль (вычислить) car (cdr (e)),n,v)) иначе... то можно было бы спросить: каков смысл этого правильного вы- выражения? Ответ гласил бы, что НУЛЬ означает то, что значит нуль, и мы можем выбрать все, что захотим. Аналогичное явление наблюдалось с правильным выражением ЕСЛИ. Единственной причиной того, почему первая форма пра- правила, которое мы дали, обладала требуемым свойством, а именно что вычисляется только одна ветвь условия, был факт, что этим свойством обладала форма если...то...иначе..., которой мы поль- пользовались в определении. В данном случае, однако, мы сумели найти форму, в которой не полагались на эту взаимозависимость свойств. Эта форма оказалась предпочтительнее, так как была более ясной. Разумеется, было бы просто написать другой интерпретатор для Лиспа, который придал бы тот же самый смысл всем кон- конструкциям языка, а затем поэкспериментировать с ним, изме- изменяя смысл конструкций и расширяя язык. Аналогично можно написать интерпретаторы для совсем других языков, даже для императивных, таких как Алгол, хотя, чтобы сделать это, нужно обычно переписать интерпретируемый язык в форме S-выраже- ний. Поскольку практическая ценность такой реализации была бы невелика из-за ее неэффективности, легко допустить такую транслитерацию. Первый раздел гл. 5 посвящается такой се- семантической спецификации. В гл. 6 будет дано другое определение Лиспа по сравнению с тем, что дано здесь — в некотором смысле более удовлетворительное, и поэтому дальнейшее обсуждение семантики Лиспа отложим до тех пор. В гл. 8 мы вернемся к вопросу о более полном определении рекурсивного блока.
Упражнения 127 Упражнения 4.1. Рассмотрите концепцию связывания в контексте функций высших порядков, в частности рассмотрите функцию отобр, определенную в разд. 2.8. Покажите, что описанное здесь понятие контекста не вызывает недоразуме- недоразумений, например, в том случае, когда функциональный аргумент функции отобр содержит свободную переменную, как это имеет место в вызове отобр (yyk(z)z-\- 4.2. Альтернативой рассмотренному здесь простому связыванию является так называемое текущее связывание. При текущем связывании свободные пере- переменные не связываются в момент определения функции, т. е. значением функ- функции является буквальное А,-выражение. Тогда при вызове функции свободные переменные принимают значения, которые имеют переменные с такими име- именами в контексте вызова. Приведите простой пример, когда такое правило связывания отличается от правила простого связывания. Покажите, что если функция отобр определена, как в разд. 2.8, то вызов в упр. 4.1 неверен из-за путаницы jc-в. 4.3. Функция факториал записана в виде фак гдерек фак-'к(п) если равно{п,0) то 1 иначе пХфак(п—1) Можно переписать ее так, чтобы она имела дополнительным параметром функ- функцию, которая применяется к аргументу (п—1) вместо фак. Тогда при вызове, когда значением этого дополнительного параметра является сама функция фак, должно получиться то же самое. Перепишите функцию фак таким об- образом и покажите, что рекурсивный блок гдерек не является необходимым. Можно ли обобщить это преобразование на случай, когда рекурсивный блок вводит несколько новых переменных? 4.4. Расширьте интерпретатор, данный в этой главе, включив интерпре- интерпретацию стандартной условной формы Лиспа с ключевым словом УСЛ, которая удовлетворяет следующему соотношению эквивалентности: (УСЛ(рх ег) . . .(pk ек))^(ЕСЛИ рг ех(ЕСЛИ...(ЕСЛИрк ek G))) где Й есть некоторое выражение, значение которого не определено. 4.5. Модифицируйте интерпретатор, данный в этой главе, так, чтобы зна- значение функции представлялось не замыканием, а просто ^-выражением. То есть реализуйте текущее связывание, описанное в упр. 4.2. 4.6. Перепишите рассмотренный в этой главе интерпретатор так, чтобы вместо явного следования контексту, представленному списком имен и спи- списком значений, во время вызова функции (или входя в блок) тело модифици- модифицировалось бы подстановкой значений формальных параметров (эти значения могут быть помечены ключом КОД). Тогда функции вычислить никогда не требовалось бы вычислять выражения со свободными переменными. Разу- Разумеется, реализация блока ПУСТЬРЕК здесь труднее всего остального и при первом чтении ее можно опустить. 4.7. Рассмотрите следующее расширение языка, интерпретируемого по- посредством функции вычислить (е, n, v). Смысл правильного выражения (ЗНАЧ ё) определяется следующим расширением функции вычислить: ...если равно(саг(е),ЗНАЧ) то ассоц(вычислить(саг(саг(е)),п^),п^) иначе... Что делает ЗНАЧ? Как это можно использовать? Желательно ли такое рас- расширение Лиспа? 4.8. Допустив списки имен и списки значений более общей структуры, так чтобы они представляли в действительности дерево имен и конгруэнтное дерево значений, можно обеспечить более общие списковые структуры пара-
128 Гл. 4. Представление и интерпретация программ метров, чем просто список переменных. Выполните такое расширение и об- обсудите его достоинства. 4.9. Интерпретатор в форме уравнений было бы проще спроектировать и понимать по сравнению с формой выполнения. Можно определить версию Лиспа, которая позволила бы писать программы, более близкие форме урав- уравнений. Основная идея заключается в том, чтобы ввести условную форму вы- выбора по образцам вида выбор е из (КОД а): ... (CAR е')\ ... (ЛЯМБДА у е')\ ... конец Здесь вычисляется выражение е и его форма непосредственно сравнивается с каждой из меток оператора выбора. Если подходящий образец найден, то устанавливается соответствие между переменными метки и компонентами е. Определите такое расширение Лиспа и напишите для него интерпретатор. 4.10. Разработайте интерпретирующую функцию для следующего просто- простого языка. В языке имеются только логические выражения. Правильные вы- выражения (их семь типов) приводятся в табл. 4.4. Таблица 4.4 Кванторы напоминают ^-выражения в том отношении, что каждый из них вводит переменную х, при этом интерпретируемые правильные выраже- выражения не должны содержать свободных переменных. Примерами интерпрети- интерпретируемых правильных выражений являются: (ВСЕ Р(ИЛИ Р (НЕ Р))) (СУЩ Р (И (ВСЕ Q (ИЛИ Р <?)) Р)) (ИЛИ (СУЩ Р Р) (ВСЕ Р Р)) Вычисление таких выражений всегда приводит к значению И или Л. Для операций Я, ИЛИ, НЕ используются обычные правила вычисления. При вычислении формы ВСЕ выражение е вычисляется дважды, сначала переменная х получает значение Я, а затем Л. К результатам применяется операция И. Аналогично форма СУЩ также требует вычисления выражения при двух различных значениях переменной. К результатам применяется за- затем операция ИЛИ. Такой интерпретатор является вычислительной формой пропозиционального исчисления. Подробнее об этом см. в книге Манна [1974], гл. 2,
Упражнения 129 4.11. Разработайте программу анализа правильных выражений в инстру- инструментальном Лиспе, вычисляющую «арность» (см. следующее определение) встречающихся в них переменных (см. список правильных выражений на с. 107). Арность переменной есть число аргументов, которые она требует. Отсюда простая переменная имеет арность 0, «переменные» CAR, CDR и др. имеют арность 1, CONS имеет арность 2 и т. д. Для начала можно не рассмат- рассматривать правильные выражения, построенные с использованием конструкций ЛЯМБДА, ПУСТЬ и ПУСТЬРЕК, так как здесь имеются свои трудности. Однако нужно попытаться обобщить свою программу, чтобы справиться по крайней мере с самыми простыми из этих случаев. 4.12. Постройте программу анализа правильного выражения в инстру- инструментальном Лиспе, определяющую тип каждого подвыражения. Предпола- Предполагается, что с каждой переменной в правильном выражении связан тип /, где t есть: 1. АТОМ, который указывает на то, что соответствующая переменная имеет атомарное значение. 2. ВЫРАЖ, которое указывает на то, что соответствующая переменная имеет неизвестную структуру. 3. (CONS ti t2), где tx и t2 — типы выражений; этот тип указывает на то, что соответствующая переменная имеет сложное значение, составленное из значений типа гл и t2 соответственно. Блочные конструкции и ^.-выражения можно не рассматривать, во всяком слу- случае для начала. Если переменная X связана с типом (CONS ВЫ РАЖ (CONS ВЫ РАЖ АТОМ)), то выражения (CAR X), (CAR (CDR X)) и (CDR (CDR X)) все будут правильными по типу, а выражение (CAR (CAR X)) неправильное (так как ВЫРАЖ может быть атомом), неправильным будет также и (CAR (CDR (CDR X))). Расширьге основные области типов, включив возможность различать символьные и числовые атомы, а также атом НИЛ. 5 х. 929
Глава 5. СООТВЕТСТВИЯ МЕЖДУ ФУНКЦИОНАЛЬНЫМИ И ИМПЕРАТИВНЫМИ ПРОГРАММАМИ Эта глава посвящается неформальному введению в методы описания семантики языков программирования и анализа про- программ. Наша цель — показать сильную зависимость между эти- этими методами и уже изученными нами методами функционального программирования. И в самом деле, можно указать множество прямых связей между алголоподобными языками и тем строго функциональным языком, который рассматривается в этой книге. Термин «императивный» будет использоваться для описания тех программ или языков программирования, которые вычисляют посредством некоторого действия, а не просто вычисляют значе- значения. Вернемся к рассуждениям, приведенным в гл. 1. Прин- Принципиальной особенностью написания программ на Алголе яв- является то, что переменные обладают значением, которое может изменяться оператором присваивания. Вычисления на Алголе вы- выполняются путем последовательного изменения значений пере- переменных, определяющего новое значение для каждой переменной как функцию от текущих значений некоторых или всех програм- программных переменных. Таким образом, программа на Алголе состоит из последовательности императивных операторов, каждый из которых выполняет простые или сложные изменения значений программных переменных. Как уже объяснялось в гл. 1, именно отсутствие такой возможности изменять значения переменных характерно для строго функционального языка. В разд. 5.1 будет введен фрагмент алголоподобного языка, который с этого момента будет использоваться как воплощение сущности императивных языков. Мы дадим точное формальное описание семантики этого фрагмента, определив для него интер- интерпретатор. Там же будет введено также очень важное понятие абстрактного синтаксиса. В разд. 5.2 на основе интерпретатора будет развит более точный и мощный метод описания семантики каждой конструкции нашего фрагмента Алгола. Здесь с каждой конструкцией Алгола будет связана функция; при этом требу- требуется, чтобы эта функция указывала в некотором смысле, что именно вычисляет данная конструкция. Это является основой теперь уже установившейся области математической семантики. Однако мы даем только краткое введение в эту область, отсылая
5.1. Интерпретатор для императивного языка 131 читателя, интересующегося этим предметом, к соответствующей литературе. В разд. 5.3 мы продолжим обзор способов, с помощью которых императивная программа может быть преобразована в функциональную. В некотором смысле это повторение рассужде- рассуждений разд. 5.2, но на более интуитивном уровне, и это поможет нам установить строгое соответствие между некоторыми типами императивных и функциональных программ. В частности, будет показано, что обычный способ использования цикла пока соот- соответствует, как правило, функциональной программе, использую- использующей накапливающий параметр. Чтобы закончить эту главу и подготовить базу для последую- последующего изложения, в разд. 5.4 обсуждается способ вычисления выражений, основы функциональных программ, с использова- использованием стека. В некотором смысле это завершает картину соответ- соответствия между императивными и функциональными программами, показывая, как можно описать семантику функционального язы- языка, используя императивный. Вся гл. 6 посвящается императив- императивному языку и транслятору с Лиспа на этот язык, и поэтому здесь будет дано только краткое введение в основы трансляции. 5.1. Интерпретатор для императивного языка В нашем императивном языке будут использоваться обозначе- обозначения, аналогичные алгольным. Текст E.1) является простой императивной программой, записанной в этих обозначениях и предназначенной для вычисления частного q и остатка г от деления у на х. Программа находит результат последователь- г: = у\ q := 0; пока г ^х цк ,с п г :=г—х; EЛ) q :=</+1 кц ным вычитанием. Как видим, здесь допускаются арифметические и логические выражения, операторы присваивания и простые циклы. Кроме того, разрешены пустой и условный операторы. Сначала перечислим правильные выражения в нашем фрагмен- фрагменте языка. Рассмотрим только одну арифметическую и одну ло- логическую операцию, так как другие совершенно аналогичны. Переменная (записанная как идентификатор) является правиль- правильным выражением. Константа (записанная в виде целого значения или как И или Л) есть тоже правильное выражение. Если ех и е2— правильные выражения, то правильными являются и вы- выражения ег-\-е2, ?i~e2 и т. д. Это соответственно арифметические и логические выражения. При определении интерпретатора нам понадобятся функции, которые выдают значение любого пра- 5*
132 Гл. 5. Функциональные и императивные программы вильного выражения. Наш способ введения правильных выра- выражений вызывает здесь некоторые затруднения, которые требуется пояснить. Это легче проиллюстрировать на операции вычитания. Выражение х—у—г будет правильным в двух различных случаях, связанных с выражениями (х—у) —z их—(у—z). Если не указы- указывать, какой из этих случаев имеется в виду, налицо будет то, что называется синтаксической неоднозначностью. Чтобы избежать этого, будем предполагать, что в правильном выражении всегда имеется достаточно скобок, чтобы оно было однозначным. Теперь перечислим правильные операторы фрагмента. Пустой оператор пусто является правильным. Выполнение этого опера- оператора не приводит ни к каким действиям. Оператор присваивания х : =е является правильным, если х есть переменная, ае — пра- правильное выражение. Выполнение этого оператора приводит к вычислению е и затем к замене текущего значения на вычислен- вычисленное значение еу т. е. к обновлению значения х. Если Sx и S2— правильные операторы, то Sx; S2 тоже правильный оператор. Действие оператора Si; S2 состоит в выполнении сначала опе- оператора S2 и затем S2. Снова имеется синтаксическая неоднознач- неоднозначность в составной форме Sf, S2; S8, но, так как анализ этой формы в разных случаях приводит к одному и тому же результату, эту неоднозначность можно игнорировать. Если е есть правиль- правильное выражение, a Si и S2— правильные операторы, то форма если е то Si иначе S2 все является правильным оператором. Выполнение оператора если е то Sx иначе S2 все состоит в вычис- вычислении е и затем в выполнении оператора Sx или S2 в зависимости от того, какое значение получено — И или Л. Аналогично, если е есть правильное выражение, a S — правильный оператор, то форма пока е цк S кц является правильным оператором. Выпол- Выполнение этого оператора состоит в вычислении е, и затем, если это дает Я, выполняется S и снова весь оператор повторяется сна- сначала. Если вычисление е дает Л, выполнение оператора счита- считается законченным. Таким образом, оператор пока е цк S кц вы- вызывает 0 и более раз повторное выполнение S до тех пор, пока вы- вычисление е не приведет к значению Л. Теперь переходим к одной очень полезной при обращении с языками программирования технике — к использованию по- понятия абстрактного синтаксиса. Для каждого типа правильных выражений введем предикат, который будет истинным, если применяется к выражению данного типа, и ложным, если приме- применяется к любому другому правильному выражению. Затем для каждого сложного правильного выражения введем селекторные функции, которые будут выделять правильные компоненты этого выражения. Для нашего фрагмента языка выбраны предикаты и селекторы, приведенные в табл. 5.1, где е есть правильное выра- выражение.
5.1. Интерпретатор для императивного языка 133 Используя эти предикаты и селекторы, можно написать, напри- например, следующую простую функцию, которая вычисляет арифме- арифметические выражения, состоящие только из констант и сложений: элемзнач(е) =з если этоконст(ё) то число(е) иначе если этосум(е) то элемзнач(арг 1 (е)) + элемзнач(арг2(е)) иначе ОШИБКА Аналогично вводятся предикаты и селекторы для правильных операторов, указанные в табл. 5.2. Это позволяет оперировать с синтаксисом фрагмента Алгола при минимуме ссылок на его действительное представление. Однако для закрепления этих идей было бы полезно рассмотреть вкратце некоторое представ- представление для синтаксиса фрагмента и затем определить предикаты и селекторы для этого представления. Таблица 5.2
134 Гл. 5. Функциональные и императивные программы Естественно для представления фрагмента выбрать S-выражения. Для правильных выражений можно выбрать списки с ключевыми атомами на первом месте: х {ПЕРЕМ х) с (КОД с) ег + е2 (ПЛЮС гх е2) ех = е2 (РАВНО ег е2) Тогда имеем этоперем(ё) = равно(саг(е),ПЕРЕМ) имя(е) = car(cdr(e)) этоконст(е) = равно(саг(е)>КОД) число(е) = car(cdr(e)) и т. д. Для правильных операторов фрагмента можно взять сле- следующие представления: пусто (ПУСТО) х :== е (ПРИСВ х е) Sx; S2 (РЯД St S2) если е то Sx иначе S2 все (ЕСЛИ е Sx S2) пока е цк S кц (ПОКА е S) Тогда имеем такие определения: 9tnoHyAb(S) =s paeHo(car(S),ПУСТО) smonpuce(S) ss paeHo(car(S),nPHCB) Aee4dcmb(S) я car(cdr(S)) npae4Ctcmb(S) set car(cdr(cdr(S))) и т. д. Читатель без труда может для себя пополнить этот спи- список. Отметим, что выбранные здесь функции являются частично определенными, и нужна некоторая осторожность при их ис- использовании. Например, сначала нужно применить предикат, чтобы убедиться в том, что соответствующие селекторы будут определены для данной формы. Следующим шагом в разработке интерпретатора будет реше- решение вопроса о том, как представить вычислительную обстановку, т. е. контекст, который дает значения всем переменным. Можно выбрать ассоциативный список, как это сделано в задаче о раз- размерностях в разд. 3.1, или структуру «список имен — список значений», использованную в интерпретаторе с Лиспа в гл. 4, или воспользоваться многими другими возможностями. Тот выбор, который мы делаем здесь, идеально служит нашим целям: он прост и без изменений может быть использован в последую-
5.1. Интерпретатор для императивного языка 136 щих разделах. Это представление аналогично структуре «спи- «список имен — список значений», использованной в интерпретаторе, но имеет только один уровень имен и значений. Составим список всех переменных, встречающихся в интерпретируемой програм- программе, назовем его списком имен, а затем построим конгруэнтный список значений каждой из этих переменных. Используя поня- понятие 5-выражения и имея в виду наши обычные соглашения о записи переменных атомами (заглавными буквами), можно запи- записать список имен и список значений для приведенного в начале этого раздела примера (в некоторой точке выполнения программы) в виде (X Y Q R) C 7 14) Чтобы манипулировать представленной таким образом вы- вычислительной обстановкой, нам потребуются две функции. Пер- Первая из них, ассоц(х, пу v), находит в списке значений то значе- значение, которое соответствует переменной (предполагается, что х входит в список имен): ассоц(х, пу и) = если равно(саг(п),х) то car (v) иначе ассоц(х, cdr(n)t cdr(v)) Это совершенно тривиальная функция, напоминающая функцию с тем же именем, использованную в разд. 3.1 и 4.3. Другая функ- функция, которая нам потребуется, должна заменять (т. е. обнов- обновлять) значение, соответствующее переменной х, на новое зна- значение у: заменить(п> v> х, у) = если равно(саг(п)у х) то cons(yy cdr(v)) иначе cons(car(v), заменшпь(сйг(п), cdr(v)y x, у)) Снова очень знакомая функция. Здесь предполагается, что х входит в список л, что списки п и и одинаковой длины и выдается список, совпадающий с v, за исключением того, что значение, соответствующее х, заменяется на у. С приведенными выше зна- значениями списков п и v вызов заменить (п, v, Q, 2) привел бы к списку C 7 2 4). Теперь мы в состоянии написать интерпретатор для нашего фрагмента Алгола. Запишем его в виде двух функций. Первая из них, знач(е, пу v), выдает значение правильного выражения е в вычислительной обстановке, представленной списками п и v (см. 5.2).
136 Гл. 5. Функциональные и императивные программы знач(е,п,ь) == если этоперем(е) то ассоц(имя(е),п,у) иначе если этоконст(е) то число(е) иначе если этосум(е) то {х + у где х = знач(арг\(е),п,ь) г5 ^ и у = знач(арг2(е),п,и)} иначе v ; если эторав(е) то {равно (х,у) где х = знач(арг\(е),пуи) и у = знач(арг2(е),п^)} иначе... Все делается прямо в лоб, аналогично интерпретатору с Лис- Лиспа. Если е — переменная, то находим ее значение в списке v\ если е — константа, то она есть ее собственное значение; если е — сумма, то вычисляется каждый из ее операндов и затем скла- складываются результаты. Аналогично, если е есть сравнение на ра- равенство, вычисляется каждый из операндов и сравниваются их значения. С другими арифметическими и логическими операция- операциями фрагмента поступаем так же. Вторая функция, которая нам требуется,— это выполнить (S, п, v). Ее результатом, как показано в E.3), является список значений той же длины, что и v, отражающий результат выпол- выполнения правильного оператора 5 в исходной вычислительной об- обстановке, представленной списками п и v. выполнить(8,пуь) s= если этонуль(8) то v иначе если 9tnonpuce(S) то заменить(п^,левчасть(8)у энач(правчасть (8) ,n,v)) иначе если эторяд(8) то выполнить (emopou(S),n, выполнить (первый(S),n,v)) иначе если 9тоусл(8) то ,- «ч выполнить (если энач(если{8)>п^) то mo(S) * ' иначе иначе(S),n,v) иначе если этоцикл(8) то {w(v) гдерек w~X(v) если знач(усл(8),п>Ь) то w (выполнить (тело(8)уп,1*)) иначе и} иначе ОШИБКА Эта функция требует некоторых пояснений. Как видим, она имеет дело поочередно с каждым из пяти видов правильных опера- операторов. В случае пустого оператора выдается просто список v, так как никаких изменений в v не происходит. В случае оператора присваивания сначала посредством вызова знач(правчасть{8)у n, v) вычисляется правая часть, а затем обновляется вычисли- вычислительная обстановка, так что переменная, указанная в левой части оператора, будет теперь связана со значением, полученным при вычислении правой части. Следующей обрабатывается по- последовательность операторов. Здесь видим, что сначала вычисля- вычисляется выполнить (первый (S), n, v), чтобы получить список зна- значений v\ обновленный при выполнении первого из двух опера-
5.1. Интерпретатор для императивного языка 137 торов последовательности. Затем вычисляется выполнить (вто- (второй E), п, с;'), определяя таким образом вторично обновляемый список значений, на этот раз за счет выполнения второго опера- оператора в вычислительной обстановке, полученной после выполне- выполнения первого оператора. Отметим, что это очень тонкое семанти- семантическое определение порядка вычислений. В случае условного оператора выбирается одна из ветвей mo(S) или иначе (S) соот- соответственно тому, выдает ли предикат ecAu(S) значение истина или нет. Наконец, обрабатывается оператор цикла. Здесь уста- устанавливается, что результатом является w(v), где w есть локально определенная рекурсивная функция. Фактически w(v) опреде- определена таким образом, что если предикат ycA(S) выдает ложь, то результатом является vy а если значением уел (S) является исти- истина, то вызывается w (выполнить (тело (S)y /г, v)). Таким образом, происходит однократное выполнение тела цикла и полученный в результате этого список значений подставляется в w. Еще раз бегло просмотрев описание этой ветви интерпретатора, убежда- убеждаемся в том, что оно является семантическим определением цикла пока. В заключение рассмотрим другой способ записи интерпре- интерпретатора. Запишем его в форме уравнений (см. E.4)) аналогично тому, как это делалось для интерпретатора с Лиспа. Эта форма имеет то преимущество, что позволяет обойтись без явных имен для предикатов и селекторов абстрактного синтаксиса. Тем не менее мы будем все так же иметь дело с синтаксисом в абстракт- абстрактной форме. Будем обозначать переменную через х, константу — через су правильные выражения — через е, еъ е2у . . ., пра- правильные операторы — через 5, Su S2y . . . . Текст на фрагменте Алгола будем заключать в фигурные скобки, чтобы отличить его от остальной части уравнения. знач({х},п,у) = ассоц(х,п^) внач({ег-\-е2},п,и) =знач({е1},п,и) -{-знач({е2) уп,и) знач({е1 — e2},n,v) — равно CHa4({ei}n,v)f 3Ha4({e2},n,v)) выполнить({пу сто} ,n,v) = v выполнитьЦх := е}Уп,у) = заменить(п,и,х,знач({е},п,у)) ebinoAHumbUS^S2},nyv) =выполнить({52},п, выполнить^Б^уПуи)) E.4) выполнитьЦеслн е то Sx иначе 52 Bce},n,f) = выполнить(если 3Ha4({e},nyv) то {Sx} иначе {S2},n,v) выполнить({пош е цк S кц},/г,у) = w(v) гдерек w = )*(v) если знач({е},п,и) то w(eunoAHumb({S},n,v)) иначе v Так записанный интерпретатор часто бывает легче понимать, чем полностью функциональное определение. Чтобы связать эти две формы, полезно представить текст, заключенный в фигурные
138 Гл. 5. Функциональные и императивные программы скобки, в виде S-выражений. Тогда эти уравнения можно рас- рассматривать просто как перезапись интерпретатора, представ- представленного функциональной программой. 5.2. Функциональные эквиваленты императивных программ В этом разделе будет показано, что каждому правильному оператору может быть сопоставлена функция. Поскольку мы го- говорим, что эта функция определяет значение выражения или действие оператора, ее можно рассматривать как определение семантики этих конструкций. Функциональные эквиваленты каждой конструкции тесно связаны с той частью интерпретатора, которая имеет дело с данной конструкцией. Функции, которые здесь выводятся, тесно связаны также с функциональной про- программой, рассматриваемой в следующем разделе, и поэтому ма- материал данного раздела полезен для перехода к последующему материалу. Однако при желании можно перейти непосредственно к следующему разделу, минуя настоящий. Рассмотрим сначала правильные выражения. Простейшим правильным выражением является константа. С каждым пра- правильным выражением свяжем функцию, которая применяется к списку значений данной вычислительной обстановки и выдает значение данного выражения в этой обстановке. Так, с констан- константой с связывается функция X (v) с. Это как раз то, что и можно бы- было ожидать: это функция, которая применяется к некоторому списку значений v и выдает постоянное значение с. Аналогично правильному выражению, которое является переменной х, со- соответствует функция X (v) ассоц(х, п, v) где п — список имен данной вычислительной обстановки. Это интересная функция. Она предполагает уже заданные значения х1] и пи поэтому будет всегда выбирать фиксированную по- позицию в списке значений, который является ее аргументом. Это, разумеется, и есть то самое значение, которое принимает пере- переменная: некоторая позиция в списке значений данной вычисли- вычислительной обстановки. Для всякого правильного выражения е обозначим через е§п функцию, сопоставленную е при данном списке п. Будем заклю- заключать в фигурные скобки только сложные правильные выражения. Так, хотя мы будем писать {?i+e2} § пу можно не писать {ег\ § л, поскольку из более простой формы е1 § п ясно, что слева от сим- символа § должно стоять правильное выражение. Однако постара- *> Надо иметь в виду, что значением аргумента х является имя некоторой переменной.— Прим. ред.
5.2. Фукциональные эквиваленты императивных программ 139 емся писать достаточно скобок, чтобы избежать недоразумений. Итак, уже имеем с § п = X (v) с x§n = X(v) ассоц(х, п, v) Отметим, что форма е § п указывает на то, что функциональный эквивалент е определяется не только выражением et но и списком имен я. Рассмотрим сложное правильное выражение ех+е^ Имеем {ei+e2}§n='k(v)e1§n(v)+e2§n(v) То есть функциональный эквивалент ех+е2 есть функция от v9 которая применяет функциональные эквиваленты ех и е2 к v и затем складывает полученные результаты. Аналогично имеем {ei=e2}§п=Х(v) равно(е1 §п(v), e2§n(v)) Напомним, что е§п есть функция, и поэтому e§n(v) есть резуль- результат применения этой функции к аргументу v. Перейдем к правильным операторам. Снова будем через S § п обозначать функциональный эквивалент правильного оператора 5. Очевидно, что пусто §n=k(v) v Таким образом, функциональным эквивалентом пустого опера- оператора является тождественная функция. Для оператора присва- присваивания имеем {х := e}§n=X(v) заменить(nf v, x, e§n(v)) Определенная здесь функция выдает список значений, в котором значение, соответствующее переменной х, заменено на значение, полученное при вычислении выражения е (т. е. при вычислении его функционального эквивалентае§п(и)). Так, например, функ- функция {у := у-{-1}§п выдает список значений, в котором значение, соответствующее переменной у, увеличивается на 1. Форма для последовательности операторов выводится тривиально: {Su S2}§n=X(v) S2^n(S1§n(v)) Фактически эта форма является композицией функций, которая обсуждалась в разд. 2.8, и поэтому можно написать {Sx; S2}§n=KOMn(S2§n, Sx§n) Это семантическое определение подтверждает, что действие опе- оператора Si; S2 состоит в выполнении оператора S2 в той вычисли- вычислительной обстановке, которая получена после выполнения опе- оператора Si.
140 Гл. 5. Функциональные и императивные программы Перейдем наконец к двум операторам управления. Во-пер- Во-первых, условный оператор имеет следующее семантическое опреде- определение: {если е то Si иначе S2 все} §n=A,(u) (если е§п(р) то Sx§n иначе S2§n)(v) Таким образом, эквивалентная функция выбирает между S^n и S2§n в зависимости от значения е§п (v). Аналогично для оператора цикла имеем следующее семантическое определение: {пока е цк S кц}§/г = ш гдерек w = X(u) если e§n(v) то w(S § n(v)) иначе v Это очень близко к форме, данной в интерпретаторе. Функция w, соответствующая правильному оператору пока е цк S кц, определяется рекурсивно. Если значением e§n(v) является ложь, то w (v) есть просто v> а если значением е§п (и) является истина, то вычисляется v'=S§n(v) и затем вычисление w(v) сводится к вы- вычислению w(v'). Это означает, что список значений w(v) получа- получается из v путем многократного (возможно, нулькратного) при- применения функции S§n> до тех пор пока е§п не выдаст значение ложь. Точно такое семантическое определение и было дано для оператора цикла. Таким образом, для каждого правильного выражения и каж- каждого правильного оператора нашего фрагмента языка мы вы- вывели эквивалентную функцию. Можно утверждать, что семантика фрагмента описывается этими функциями в том смысле, что каж- каждая правильная конструкция имеет значение, определяемое этой функцией для некоторого конкретного аргумента. Разу- Разумеется, при этом мы только свели понимание смысла программы к пониманию смысла функции. Читатель может отметить, что именно сами правила перехода передают информацию о семан- семантике фрагмента, а не те функции, которые можно было бы полу- получить по этим правилам, применяя их к конкретной программе. Тем не менее для освоения этих правил читателю можно поре- порекомендовать вывести функцию, эквивалентную какой-нибудь простой программе. Метод сопоставления функции каждой правильной конструк- конструкции языка программирования называется математической се- семантикой (или денотационной семантикой). Здесь только вскользь затронута эта тема. Изучающие математическую се- семантику сталкиваются с очень важной проблемой установления того факта, что математическая область, в которой они работают, т. е. множество функций, которые порождаются этими прави- правилами, является корректно определенной. Соответствующим ма- математическим аппаратом является сложный аппарат современ- современной алгебры (отсюда встречающиеся иногда ссылки на этот под-
5.3. Преобразование императивных программ 141 ход как на алгебраический). Тем не менее эта область важна и для теории вычислений. Когда работы в этой области будут более продвинуты, может оказаться, что удобное формальное семанти- семантическое описание языков программирования в целом, основанное на этом методе, будет таким же общепринятым, как бэкусовская нормальная форма описания синтаксиса. 5.3. Преобразование императивных программ в функциональные В предыдущем разделе было показано, как каждой импера- императивной программе может быть соотнесена конкретная функция, которая, будучи применена к соответствующей вычислительной Рис. 5.1. Рис. 5.2, Рис. 5.3. обстановке, выдает в качестве результата вычислительную об- обстановку, отражающую действие выполнения этой программы. Здесь мы повторим эти рассуждения, только на более интуитив- интуитивном уровне, выводя функциональные программы, которые не- непосредственно связаны с императивными программами. Обра- Обратимся на время к блок-схемам, поскольку таким способом го- гораздо легче описываются преобразования, которые мы хотим обсудить. Позднее мы дадим правила преобразования в терминах фрагмента Алгола. Рассмотрим сначала блок-схемы с единственной переменной х. Предположим, что все присваивания имеют форму, изображенную на рис. 5.1, и все проверки условий имеют форму, показанную на рис. 5.2, где f(x) и р(х) — простые функции. Например, прог- программа, приведенная на рис. 5.3, повторно удваивает я, пока результат не окажется большим 100. Определим функцию от х, которая вычисляется между входом и выходом на рис. 5.3. Сделаем это, последовательно помечая стрелки на блок-схеме функцией, вычисляемой между этой
142 Гл. 5. Функциональные и императивные программы стрелкой и выходом из схемы. Ясно, что выходная стрелка поме- помечается тождественной функцией (рис. 5.4). Пусть стрелка, исходящая из оператора присваивания х : = f(x), помечена функцией g. Тогда меткой стрелки, веду- ведущей в этот оператор, будет Х(х) g(f(x)) (рис. 5.5). Аналогично, если стрелки, исходящие из проверки условия р(х), помечены соответственно функциями g и Л, то меткой вхо- входящей стрелки будет функция, указанная на рис. 5.6. Теперь из-за того, что в нашей программе есть цикл, этот процесс будет продолжаться бесконечно, если только его каким- нибудь образом не оборвать. Сделаем это, выбрав некоторую точ- Рис. 5,6. Рис. 5.7. ку в каждом цикле и приписав имя функции, вычисляемой между этой точкой и выходом. Прогоняя эту функцию по циклу, при- приходим к стрелке, уже помеченной некоторым выражением е, значением которого является функция. Тогда записываем здесь определение ШЕ==е, где w есть имя, выбранное для функции в точке разрыва цикла, включая таким образом цикл в рекурсивную функцию. Применим этот процесс в нашем примере, выбрав точку разрыва непосредственно перед проверкой. Обозначим через q функцию, вычисляемую от этой точки до выхода (рис. 5.7). От- Отметим, что разрыв цикла приводит к схеме, являющейся по форме деревом, в котором помечаются теперь все стрелки. Если завер-
5.3. Преобразование императивных программ 14 шить процесс пометки по рассмотренным выше правилам, то полу чается схема, приведенная на рис. 5.8. ¦ Рис. 5.9. Таким образом, имеем определение <7(л;)==если х>100 то х иначе qBxx)
144 Гл. 5. Функциональные и императивные программы Так как меткой q помечен вход в нашу схему, то q(x) и явля- является функцией, вычисляемой между входом и выходом всей схемы. Ясно также, что эта функциональная программа вычисляет то значение, которое принимает х после выполнения императивной программы. Этот метод преобразования императивной программы путем определения функционального эквивалента очевидным образом распространяется на программы, в которых имеется более одной Рис. 5.10. переменной. Мы просто конструируем функции, в которых пара- параметрами являются все переменные программы. Если предполо- предположить, что нас интересуют значения всех переменных на выходе схемы, то помечаем выход функцией, которая формирует список значений всех переменных. Однако если для нас интересно только некоторое подмножество, то некоторые переменные в списке мо- могут быть опущены. Продемонстрируем это на примере. На рис. 5.9 изображена блок-схема программы, с которой мы начали главу. Помечаем выход функцией, указывающей интересующие нас программные переменные (функция четверка просто формирует список значений всех аргументов), и разрываем цикл непосредст- непосредственно перед проверкой, обозначив функцию, которая вычисля- вычисляется, начиная с этого места, через w. Таким образом, можно ожи- ожидать вывода определения w в виде w(y, х> q, /-)= .... Выполне- Выполнение всех пометок по указанным правилам приводит к схеме на рис. 5.10 и отсюда к рекурсивному определению и>(У, х> Ц> г) = если г^х то w{y9 x, q+l9 r—x) иначе четверка (*/, х, q, r)
5.3. Преобразование императивных программ 145 Аналогично можно определить, что вся программа вычисляет функцию Ь(У> *> Q> r) w(y> *> 0, у) Наше соглашение о перечислении всех переменных программы в качестве аргументов всех функций привело к странному и не- ненужному появлению q и г в последнем определении. В определе- Рис. 5.11. нии функции w ненужным был бы параметр у, если сделать w локальной по отношению к главной функции. Но к этому мы вернемся позднее. А сейчас займемся преобразованием програм- программы, в которой больше одного цикла. Программа, изображенная на рис. 5.11, вычисляет наиболь- наибольший общий делитель (НОД) двух чисел х и у, оставляя резуль- результат в х. Подпрограмма, обведенная пунктиром на рис. 5.11, вычисля- вычисляет остаток от деления у на х, оставляя результат в г. Внешний цикл реализует алгоритм Евклида (рис. 5.12) для нахождения наибольшего общего делителя х и у, последовательно заменяя
146 Гл. 5, Функциональные и императивные программы у и х соответственно нал; и у ост х. В этой программе выбираем точки разрыва, помеченные на схеме буквами / и g. Можно выб- выбрать сколько угодно точек разрыва, но так, чтобы в каждом цикле была по крайней мере одна. Применяя обычный процесс пометки стрелок, приходим к следующим определениям функций. Предполагается, что все функции имеют аргументы у, х и г и что на выходе нас интересует только значение х. Поэтому выход помечается функцией k(y, х, г)х (рис. 5.13). Определения имеют вид f(y,x* r)=sg(yyxyy) g {у, х, г) = если г > х то g (у, х, г—х) иначе если г Ф О то / (ху гу г) иначе х Это не очень естественные функции, но они служат для иллю- иллюстрации процесса преобразования. Использование имени для обозначения функции, вычисляе- вычисляемой от точки разрыва цикла, аналогично использованию метки в императивном языке. Рекурсивные вызовы функций fug ведут себя в точности как оператор goto, заставляя перехо- переходить к телу функции с указанным именем. Ана- Аналогия становится пол- полной, если мы заметим, что рассмотренная пара функций такова, что они попеременно вызывают одна другую, пока g не выдает окончательный результат (значение х), при этом все предыду- предыдущие вложенные вызовы Рис. 5.12. Алгоритм Евклида. функций завершаются без применения каких- либо других функций. Чтобы завершить этот раздел и поставить его в один ряд с другими, разработаем правила преобразования для фрагмента Алгола. Сделаем это, используя следующую технику. Если 5 есть правильный оператор фрагмента, а е — правильное выра- выражение фрагмента или строго функционального языка, то кон- конструкция S результат е
5,3. Преобразование императивных программ 147 является правильным выражением во временно расширенном функциональном языке. Замысел состоит в том, что выполнение оператора S внутри выражения 5 результат е является полностью локальным. Значением выражения S результат е является зна- Рис. 5.13. чение е в контексте, полученном после вычисления ~S. Так, на- например, значение выражения {х := х+1; у :=у+1} результат хху то же самое, что и значение выражения (х+1)Х(у+1). Можно взять любой правильный оператор фрагмента и по- построить из него выражение, добавив конструкцию результат е для некоторого произвольным образом выбранного выражения е, которое отражает нашу заинтересованность в исходе S. Напри- Например, к программе для частного и остатка, рассмотренной ранее, можно было бы добавить конструкцию результат четверка (у, х,
148 Гл. 5. Функциональные и императивные программы <7, г), получив выражение E.5). {г := у; q : = О, пока г^х цк г : = г—х\ E.5) * := <7+1 кц} результат четверка(у,Х4,г) Приведем теперь правила преобразования, указав, каким образом из функциональной программы последовательно уда- удаляются содержащиеся в ней выражения вида S результат е. Будем писать чтобы обозначить, что е можно заменить на е\ где е содержит выражение вида 5 результат е\ а ег либо не содержит таких вы- выражений вовсе, либо содержит более короткие выражения такого вида. Тогда последовательное применение таких правил удалит из программы все выражения вида S результат е. Простейшее правило преобразования связано с пустым опера- оператором: пусто результат е -*- е В случае оператора присваивания можно поступать двояко. Можно использовать форму где {х := е} результат е' -> е' где х=е Здесь сохраняются вхождения как е, так и х. В другом способе в выражении е' все вхождения переменной х заменяются на вхож- вхождение е. Это записывается в виде {х :=е} результат е' —> е' \ех Так, например, выражение {х := 2Хх+1} результат хХу мож- можно преобразовать либо к виду хХу где x=2jc+1 либо к виду Bх+1)Ху Отметим, что в последнем случае следует добавить скобки, чтобы сохранить правильный порядок выполнения операций. Предпо- Предполагаем, что наш процесс подстановки делает это в нужных слу- случаях. Последовательность {5Х; S2} имеет следующее правило пре- преобразования: {Si; S2} результат e-+St результат {S2 результат е\
5.3. Преобразование императивных программ 149 Таким образом, 52 результат е можно преобразовать в выраже- выражение е\ и тогда в свою очередь преобразуется Sx результат е'. Пример E.6) иллюстрирует это правило. [х := х+\; у := У+Ц результат хХу -+х := х+\ результат^ := у-{-\ результат хХу} ^ fi, -+х := х+\ результат хХ(у+1) к ' -+(х+\)Х(у+\) Преобразование условной формы особенно просто, как это показывает соотношение E.7). В E.8) приводится пример. {если е то S± иначе S2 все} результат е' —> если е то {Sx результат е'} иначе {S2 результат е'} E.7) {если х^О то пусто иначе х:=—х все} результат л: —> если х^О то {пусто результат д:} иначе {д:: = — х результат х) —> если х^О то х иначе —х E.8) Наконец, переходим к наиболее интересному случаю — к оператору цикла. Сначала запишем правило преобразования E.9), а затем обсудим его. {пока е цк S кц} результат е' —*ш(*ь ¦.., xk) гдерек E.9) w — X(xu ..., Xk) если е то w (S результат хъ ..., S результат дг&) иначе е' Это правило очень сложное, но оно очевидным образом вы- вытекает из правил, приведенных для циклов в предыдущих разде- разделах. Здесь рекурсивно определяется функция, названная w, хотя как мы ее обозначаем, совершенно несущественно. Эта функция имеет параметрами переменные хи. . ., xh. На время достаточно предположить, что это все переменные, используемые в операторе пока е цк 5 кц. Тело функции w вычисляет е. Если значение е ложно, то как результат вычисляется ё\ если же вы- вычисление е приводит к истине, делается рекурсивный вызов wy при этом каждый из параметров заменяется на значение, кото- которое присваивается ему оператором S. В качестве примера рас- рассмотрим E.10). {пока г^хх цк г := г—х кц} результат г —> w (г, х) гдерек w = X(r, x) если г^х то w(r :=r—x результат г, г := г—х результат *) иначе г E.10) —>w(r, x) гдерек w = X(r, x) если г^х то w(r—x, х) иначе г
150 Гл. 5. Функциональные и императивные программы Выбор параметров для функции w может быть не таким широ- широким, чтобы включать все переменные, используемые в операторе пока е цк S кц; необходимо включить только все переменные, упомянутые в 5. При этом следует проявить должную осторож- осторожность по отношению к вложенным определениям. С другой сто- стороны, не будет вреда, если включить в список параметров любую другую переменную. Определение наименьшего возможного множества параметров функции w оставляется в качестве упраж- упражнения читателю. Процесс преобразования императивной программы в функцио- функциональную теперь завершен. Для императивной программы S сначала выбирается выражение е, которое знаменует собой ре- результат 5, затем конструируется форма S результат е и исполь- используются правила постепенного исключения связки результат, что приводит к эквивалентному выражению е'. Наконец, кон- конструируется функция к(хи. . ., xk)e\ где хи. . ., xk — свобод- свободные переменные е'. В заключение преобразуем простую програм- программу для вычисления факториала E.11). {/:=1; «:=1; пока 1<>п цк /: = fXt; E.11) кц} результат / Сначала рассмотрим только цикл, который преобразуется в E.12). Отметим, что последнее выражение в E.12) должно быть пока 1<,п цк / : = fxi; i := «+1 кц результат / —+w(i,f) гдерек ш = А,(?, /) если i^n то ({/:=/xt; / := c-f-1} результат i, {f:=fXi; i:=i+\} результат /) E.12) иначе / —> w (I, f) гдерек w = X(it f) если f<n то w(i-\-\, fxi) иначе / инициировано через операторы присваивания / := 1; i : = 1, подставляющие 1 вместо свободных переменных f и i9 которые как раз и являются такими в теле рекурсивного блока. Таким образом, получаем E.13). {/: = 1; i := 1; пока i^n цк / : = /Xi; i := i+1 кц} результат f —*{/:=1; i:=\] результат {w(ij) гдерек w = X(iJ) если i<^n Tow{i+l, fxi) E.13) иначе /} —> a;(l, 1) гдерек w=X{i,f) если i^n то ш(/+1, fxi) иначе /
5.3. Преобразование императивных программ 151 Наконец, следует превратить это выражение в функцию от свободно входящих в него переменных, в нашем случае это п. Отсюда выводим эквивалентную функциональную программу E.14). Я(л)шA, 1) гдерек w-X(iJ) если i^n то w(i+\,fxi) E.14) иначе / В этой функциональной программе, выведенной из программы, которую можно назвать стандартной императивной программой вычисления факториала, видно, что параметр / используется как накапливающий. Обычное использование оператора цикла пока состоит в итеративном присваивании значений множеству пере- переменных, а соответствующим стилем в функциональном програм- программировании является использование накапливающих параметров. Рассмотрим приведенное выше определение функции w(i9 /). Эта функция вычисляет значение f X i X (t+l)x. . .X (n-l)xn и поэтому w(l, l)—n\ — 1х2х. . .Х/г. Другой способ выраже- выражения этого свойства w вытекает из наблюдения, что каждый вызов w удовлетворяет следующему ограничению. При вызове w(i9 f) имеет место равенство / = 1 х2х. . . X (t — 1), и отсюда w(i, f) вычисляет п\ для всех /и/с этим свойством. В частности, шA, 1) вычисляет п\. Тот факт, что эта программа работает также и при л=0, может быть установлен проверкой. Мы завершим этот раздел кратким обзором того, что нами достигнуто. В предыдущих разделах функциональные программы использовались для определения семантики императивных прог- программ двумя способами. Во-первых, путем вывода интерпретатора, который явно идентифицирует каждую конструкцию в импера- императивной программе и дает правило ее выполнения, чтобы опреде- определить ее действие. Во-вторых, путем вывода функции, формально эквивалентной каждой конструкции императивного языка. В этих разделах мы придавали особое значение формальному се- семантическому описанию каждой правильной конструкции импе- императивного языка. Следовательно, в этих разделах для изучения предназначались именно семантические правила. В последнем разделе мы сосредоточили внимание на выводе более интуитивной функциональной программы из императивной, и поэтому здесь акцент был сделан не на отдельных языковых конструкциях, а на всей программе в целом. Уместно отметить, что техника трех этих разделов тесно переплетается: первые два раздела сконцент- сконцентрированы на семантике языка, третий — на семантике программ.
\52 Гл. 5. Функциональные и императивные программы 5.4. Аппаратное обеспечение функциональных программ В качестве дополнительного изучения других связей между функциональными и императивными программами и прежде чем перейти к полному изложению машинной архитектуры для функ- функциональных программ, предварительно приведем здесь обзор ос- основных способов использования стека для вычисления выраже- выражений и рекурсивных функций. В разд. 5.2 рассматривалось преоб- преобразование императивной программы в эквивалентную функцио- функциональную. Возникает вопрос, можно ли преобразовать любую функциональную программу в эквивалентную императивную. Прежде чем ответить на этот вопрос, его следует пояснить, так как ответ будет отрицательным при одной интерпретации вопроса и положительным — при другой. В конечном счете будет пока- показано, что это можно сделать, но полученная программа будет, к сожалению, далеко не удобочитаемой. Это служит основой реа- реализации функционального языка программирования на совре- современных машинах. Следующая глава посвящается полному опи- описанию такой реализации. Неудобочитаемость этих программ не будет нас касаться, так как мы связаны только с формальным соответствием. Это означает, что преобразования из функциональ- функциональной формы в императивную скорее выражают семантику функ- функциональной формы, чем преобразованной программы. Можно было бы начать с вопроса о том, почему не могут быть обращены правила преобразования, изложенные в разд. 5.3. Это происходит потому, что функциональные программы, кото- которые по этим правилам могут быть выведены из императивных, имеют очень специальную форму. Если рассматривать процесс преобразования блок-схемы, где разрывается каждый цикл и вводится новое имя функции, то множество полученных функ- функциональных определений обладает тем свойством, что каждый вызов определенной функции является самым внешним. Говорят, что вызов функции / является самым внешним в выражении, если это выражение имеет вид f(ely е2,. . ., ek) или является условным выражением если е то ё иначе е", где вызов / является самым внешним в ё или в е", т. е. в одной из ветвей условия. Функциональная программа, состоящая из множества определе- определений функций, в которой все вызовы определяемых функций являются самыми внешними, называется итеративной и имеет итеративную форму. Такие функциональные программы могут быть преобразованы в императивные просто применением обра- обращенных правил разд. 5.3. Однако не все функциональные программы имеют итератив- итеративную форму. Программа вычисления факториала факт(п) = если п = 0 то 1 иначе п х факт (п — 1)
5.4. Аппаратное обеспечение функциональных программ 153 не является итеративной, так как вызов факт(п— 1) является вложенным, поскольку это операнд умножения. Эту программу можно преобразовать к итеративной форме, а затем от нее пе- перейти к императивной программе факт(п) е~ / (п— 1) /(/г, т) =если п = 0 то /п иначе / (п—1, тхп) Это преобразование использует технику накапливающего пара метра. Существует другой способ преобразования неитеративной функциональной программы в эквивалентную императивную Рис. 5.14. путем добавления внешнего счетчика для подсчета глубины рекурсивной вложенности. Несмотря на все эти методы, суще- существуют тем не менее функциональные программы, которые не могут быть преобразованы в императивные без добавления неопределенного числа переменных. В полном объеме эта тема обсуждается в книге Манна A974), гл. 4. Но так или иначе на практике мы должны допустить необходимость средства, позво- позволяющего использование неуказанного числа дополнительных переменных. Таким средством, при этом адекватным и естест- естественным для этой цели, является стек. Чтобы описать способ применения стека для вычисления строго функциональных программ, реализуем стек, используя массив стек и указатель (индекс) верш (рис. 5.14). Будем предпо- предполагать, что элементы стек[1], стек[2]у .. ., стек1верш] помеща- помещаются (вталкиваются) в стек именно в этом порядке. Поэтому опе- операция помещения в стек нового элемента х описывается так: верш := верш + 1; стек[верш] := х Напротив, операция выталкивания элемента из стека описы- описывается следующим образом: х := стек[верш\\ верш := верш— 1 Строго говоря, чистый стек допускает только эти две операции, и о таком порядке действий говорят как о порядке «последним пришел — первым ушел». Эти операции более естественны, если их оформить как вызовы процедур, скажем встек(х), изстека(х). Здесь, однако, возможен доступ к элементам, находящимся в
154 Гл. 5. Функциональные и императивные программы стеке глубже первого элемента, но только для определения зна- значения этих элементов, без их сдвига или изменения. К тому же есть возможность перевычислять значение переменной верш, сдвигая вершину на разные позиции путем многократного приме- применения процедуры изстека. Хотя можно было бы ограничиться только операциями встек и изстека, будет более наглядным пока- показать операции в их полной форме, так как читатель, в частности, может проследить за значением переменной верш. Мы не будем заботиться о переполнении или исчезновении стека, а также об инициализации его, так как все эти тривиальные вопросы на- нарушили бы простоту представления. Опишем для каждого правильного выражения функциональ- функционального языка последовательность операторов, манипулирующих со стеком и обладающих тем свойством, что после их выполнения значение выражения остается в вершине стека. Это очень важ- важное свойство соответствующей императивной программы, и поэ- поэтому дадим ему специальное название — свойство чистого резуль- результата (свойство 4P). Свойство чистого результата. Последовательность операто- операторов, выведенная из правильного выражения, такова, что после выполнения этих операторов в вершине стека остается единствен- единственное значение, а остальная часть стека, имевшая место до вы- выполнения этих операторов, остается неизменной. Единственным значением, которое вталкивается в стек, будет значение, вы- вычисленное для этого правильного выражения. Простейшим правильным выражением является константа с. Эквивалентные операторы имеют вид верш := верш + 1; стек[верш\ := с Легко видеть, что эти операторы обладают свойством чистого результата. Аналогично помещается в стек значение переменной. Рассмотрим типичное правильное выражение с бинарной опера- операцией, например ех + е2у где ег и е2 — правильные выражения. В этом случае выводится следующая последовательность опера- операторов: el; el; стек[верш — 1 ]: = стек[верш — 1 ] + с тек[верш]; верш :== верш—1 В этой последовательности е\ и е\ обозначают соответствующие последовательности операторов, полученные для правильных выражений ех и е2. Так, например, выражение х + у транслиру- транслируется в следующую последовательность операторов: верш : = верш -f- 1; стек[верш]: == х\ верш : = верш -f 1; стек[верш]: = у\ стек[верш— 1 ]: = стек[верш— 1 ]+ стек[верш]\ верш : = верш— 1
5.4. Аппаратное обеспечение функциональных программ 155 где первая строка получена для подвыражения х, вторая — для у. То, что весь оператор в целом, полученный для выраже- выражения ех + е2у обладает свойством чистого результата, вытекает из следующих рассуждений. Подвыражение еи будучи правиль- правильным, приводит к оператору со свойством чистого результата; отсюда после вычисления значение ех оказывается в вершине стека, а в остальном стек не изменяется. Аналогично оператор el> соответствующий выражению е2, после вычисления оставляет стен Рис. 5.15. значение е2 в вершине стека. Таким образом, два верхних эле- элемента стека суть значения ех и е2 соответственно и, кроме двух этих добавленных значений, стек не изменился (рис. 5.15). Теперь, складывая эти два верхних значения, помещая резуль- результат в стек[верш — 1] и уменьшая на 1 значение вершины стека, мы гарантируем, что последовательность операторов, соответст- соответствующая выражению ег + е2у обладает свойством чистого резуль- результата. Это достаточно просто, и ясно, что такие же правила можно использовать для любой бинарной операции. Следует проявить осторожность с такими операциями, как минус, для которых важен порядок операндов (некоммутативные операции). С условными выражениями дело обстоит также просто. Пра- Правильное выражение если e1w e2 иначе е3 имеет три правильных подвыражения. Обозначая соответственно через е\, е\ и е\ после- последовательности операторов, полученные для этих подвыражений, можем вывести для условного выражения следующую последо- последовательность: е\\ если стек[верш] = И то верш := верш—1; el иначе верш := верш—1; el все Эта последовательность обладает свойством чистого результата, так как при любом выборе ветви оператор верш := верш— 1 вытеснит значение проверки условия из стека. Затем, разуме- разумеется, вычисляется только одно из выражений е2 или е3, и его значение остается в вершине стека. Поскольку каждый из опе- операторов e*lt e*2 и el обладает свойством чистого результата, то и вся последовательность операторов, соответствующая условному выражению, обладает этим свойством. Теперь мы можем опери- оперировать с произвольно вложенными выражениями, составленными из простых операторов и условных выражений. Остается только показать, как поступать с определениями и вызовами функций.
П>6 Гл. 5. Функциональные и императивные программы Ограничимся одним простым типом определений рекурсивных функций. Предположим, имеется множество взаимно рекурсив- рекурсивных функций, каждая из которых определяется уравнением вида f(xu. . ., хп) ^е где определяющее выражение е содержит ссылки только на пе- переменные хи. . ., хп и вызовы только тех функций (помимо примитивных), которые определяются этими уравнениями. Та- Таким образом, в частности, исключаются функции, ссылающиеся на глобальные (т. е. непараметрические) значения, и передача функций в качестве параметров. Главная проблема, как и пред- предвидит читатель, связана с рекурсивными вызовами определяемых функций. Если ни одна из определяемых функций не вызывается рекурсивно, т. е. она не начинается, пока не закончится ее незавершенный вызов, то тогда можно назначить по одной пере- переменной на каждый параметр каждой функции и поступать с вызо- вызовом следующим образом. На входе в функцию каждой перемен- переменной, представляющей параметр вызываемой функции, присваи- присваивается значение соответствующего действительного параметра. Тело функции как правильное выражение представляется после- последовательностью операторов со свойством чистого результата; следовательно, при выполнении этих операторов со ссылками на соответствующие переменные вычисляется значение, помещае- помещаемое в вершину стека. Проблема возникает, когда во время выпол- выполнения этой последовательности операторов непосредственно или косвенно снова вызывается та же самая функция. Поскольку в этом случае значения переменных, используемых для хранения параметров, должны быть перевычислены, то они будут не опре- определены при возвращении к более раннему выполнению тела. Очевидным решением этой проблемы является назначение раз- различных множеств переменных для представления параметров каждого вызова функции. В свою очередь очевидным способом реализации таких переменных является использование стека. Соответственно этому мы так упорядочим эти множества, что при выполнении операторов, соответствующих телу функции, некоторый участок стека вниз от вершины будет резервным мно- множеством для хранения значений параметров. Этот участок стека будет определяться указателем акт (рис. 5.16).
5.4. Аппаратное обеспечение функциональных программ 157 Позиции стека стек [акт], стек [акт + 1], . . ., стек [акт + +k — 1] будут использоваться для хранения значений парамет- параметров функции с k параметрами. Сегмент стека, содержащий эти параметры и некоторые другие значения, которые объясним позднее, называется активной записью, поскольку активной яв- является функция, тело которой выполняется в настоящий момент. Отсюда сокращение акт. Таким образом, при выполнении опе- операторов, полученных для тела функции с определением f(xu. . . . . ., xk) = е, мы ссылаемся на переменную х( посредством обра- обращения стек [акт + i — 1]. Тогда правильное выражение xt порождает последовательность операторов верш := верш + 1; стек [верш] := стек [акт + i — 1] Отметим, что это простое средство дает нам доступ только к одной активной записи; отсюда сделанные нами ограничения на вид определения функции. Рассмотрим вызов функции. Выражение f(eu. . ., ek) является правильным, если / есть имя функции, а еи. . ., eh — правильные выражения, доставляющие значения параметрам. Последовательность операторов ё[\ е\\ . . .; е\, поскольку все параметры правильные, увеличит стек на k значений, являющих- являющихся по порядку значениями соответствующих выражений. Таким образом, для приведенного выше вызова выводится последователь- последовательность операторов E.15). е\; е2; ...; ek верш : = верш + 1; стек[верш] : = г; на /; E.15)' г: верш := верш—k — 1; стек[верш] : = стек[верш + k + 1 ] Мы видим, что эта последовательность сначала помещает в стек k значений параметров, затем помещает значение г (метку), представляющее точку возврата функции / после ее завершения. Затем применяется оператор перехода на /, включающий последо- последовательность операторов, полученную для тела функции /, что мы рассмотрим немного позже. Если последовательность операто- операторов, построенная для тела определения /, обладает свойством чистого результата (свойством 4P), то значение вызова останется в вершине стека. При возвращении управления метке г стек находится в состоянии, изображенном на рис. 5.17. Для позиций стека выбраны обозначения t, t+ 1 и т.д., где через / обозначено начальное положение вершины перед началом вызова. Адрес возврата г помещается на позиции
1б§ Гл. 5. Функциональные и императивные программы стек It + k + 1], и тогда в результате выполнения тела / значе- значение /(вь. . ., eh) остается на позиции стек U + & + 2]. Затем опе- оператор с меткой г, т. е. верш := верш — k — 1, делает вершиной позицию t + 1, и тогда оператор присваивания стек [верш] : = стек [верш + k + 1] сдвигает значение f(el9. . ., eh) в новую вершину стека. В результате стек, полученный к началу вызова, остался без изменений, добавилось только единственное значе- значение в вершине, и соответственно на одну позицию сдвинулась Рис. 5.17. сама вершина; таким образом, свойство 4P для вызова функции установлено. Рассмотрим теперь последовательность операторов, порож- порожденную телом функции с определением f(xu. . ., xk) = е. Эта последовательность должна обладать свойством 4P, а также должна правильно использовать и изменить значение указателя акт во время вызова. Последовательность E.16) удовлетворяет этим требованиям. /: верш := верш-\-1; стек[верш] := акт\ акт := верш—k—1; е*\ верш : = верш — 1; акт : — стек [верш]; E.16) стек[верш] := стек[верш-\-1]; на стек[верш — 1] Последовательность E.16) помечена именем функции, что согласуется с оператором на / в программе вызова функции. При входе в / текущее значение указателя акт помещается в стек, а затем значение акт изменяется так, чтобы оно указыва- указывало на первый параметр. Верхушка стека изображена на рис. 5.18 при сохранении прежних индексов. Далее выполнение последовательности операторов е* приве- приведет к помещению значения е на позицию стек [t + k + 3], и вершина примет значение t + k + 3, это обусловлено свойством 4P для е*. Операторы верш := верш — 1; акт := стек [верш] восстанавливают прежнее значение акт, а затем оператор стек [верш] := стек[верш + 1] сдвигает значение /(еь. . ., eh) на одну позицию ниже. Так устанавливается формат стека, предусмотрен- предусмотренный меткой г\ на эту метку можно и перейти непосредственно,
Упражнения 159 выполнив оператор на стек [верш — 1], который говорит о том, что значение метки, на которую делается переход, берется из стека. Читатель может счесть для себя полезным рассмотреть прос- простой пример рекурсивной функции и вывести для нее эквивалент- эквивалентную императивную программу. Это закрепило бы его понимание свойства чистого результата и того, как это свойство обеспечи- Рис. 5.18. вает правильное вычисление функции. В частности, читатель мог бы рассмотреть вопрос о том, как сохраняются значения парамет- параметров процедуры, когда делается рекурсивный вызов из тела этой процедуры, и как они восстанавливаются при выходе из этого вы- вызова. Упражнения 5.1. Рассмотрите еще один язык программирования. Он полностью иден- идентичен используемому фрагменту Алгола, за исключением того, что в нем кроме доступа к текущим значениям переменных существует дополнительная возможность доступа к предыдущим значениям. Если переменная предва- ряется символом пред, восстанавливается значение, которое она имела до предыдущего присвоения. Таким образом, пред х — правильное выражение, в котором х является переменной. Например, х := у\ у := пред х переставляет значения хну. Напишите интерпретатор для этого языка. 5.2. Определите функции ассоц и заменить для следующего альтернатив- альтернативного представления вычислительной обстановки. Вычислительная обстановка а это функция, которая по заданной переменной х выдает значение этой переменной. Покажите, что простой перенос уравнений, приведенных в конце разд. 5.1, если опустить переменную п и заменить v на а, определяет вполне удовлетворительный интерпретатор (см. программу индивидуумы, разд. 3.3). 5.3. Рассмотрите более реалистическую модель вычислительной обста- обстановки, в которой переменные связаны с адресами, а адреса — со значениями. Эту модель можно очень просто получить с помощью пары конгруэнтных спис- списков, связывающих переменные с адресами, и еще одной пары списков, свя- связывающей адреса со значениями. Пересмотрите интерпретатор, описанный в разд. 5.1, с использованием такой вычислительной обстановки и затем опре- определите семантику правильного оператора х :— (/(где х и у — переменные), который означает присвоение х того же адреса, что и у. 5.4. Логическое выражение ех и е2 имеет значение «истина», только если и ег, и е2 истинны. Оно имеет значение «ложь», только если и ev и е2 опреде- определены и одно из них ложно. Иными словами, как ех, так и е2 определены. Мы используем форму ех ии е2, чтобы выразить форму и в случае, когда если ех
160 Гл. 5. Функциональные и императивные программы ложно, то е2 не определено. Таким образом, ег ии е2 истинно только тогда, когда истинны и е1у и е2. Однако^ ии е2 ложно, если либо ех ложно, либо ег истинно, а е2 ложно. Приведите интерпретирующие семантические определения этих двух операторов, чтобы яснее показать их различие. 5.5. Определите абстрактный синтаксис и расширьте интерпретирующую функцию из разд. 5.1, чтобы задать семантику правильного оператора цк Sf пока е\ S2 кц который имеет структуру управления, показанную на рис. 5.19. Рис. 5.19. 5.6. Обдумайте возможности формального интерпретирующего определе- определения семантики следующей пары правильных операторов: 1) рекначало S конец — это правильный оператор, определяющий опе- оператор 5 как «рекурсивно возобновляемый»; 2) рекур — правильный оператор, вызывающий «рекурсивное возобнов- возобновление» блока рекначало, в котором он встречается. Таким образом, факториал можно записать как в E.17). Семантику конструкций рекначало и рекур можно неформально описать, указав, что оператор рекур ведет себя точно таким образом, как будто непо- непосредственно включающий его блок рекначало копируется и заменяет собой вхождение рекур.
Упражнения 161 5.7. Выведите функцию, эквивалентную правильному оператору E.18). Соответствующей вычислительной обстановкой будет являться двухэлемент- двухэлементный список. 5.8. Запишите q§n как вычисляемую функциональную программу пара- параграф (q, л), где q — либо правильное выражение, либо правильный оператор. Далее, поскольку (параграф (е, п)) (с/)=з«ач (etn,v) и (параграф (S ,n)) (v)=выполнить (S,n,u) понятно, что параграф (qy n) является некоторой альтернативой интерпрета- интерпретатору. Действительно, так как параграф (q, n) не зависит от списка значений, к которым применяется, то эта функция более напоминает компилятор, чем интерпретатор. Исследуйте этот факт. 5.9. Подберите подходящие функции для блок-схемы, показанной на рис. 5.20, и таким образом постройте соответствующую функциональную программу. Рис. 5.20. 5.10/Сделайте то же самое для блок-схемы на рис. 5.21, где внутренний блок используется для вычисления т := fxi последовательным сложением. 5.11. Используйте правила удаления связки результат для преобразова- преобразования программы, реализующей алгоритм Евклида в разд. 5.3. Для того чтобы написать эту программу с использованием операторов пока, необходимо повторять часть программы, находящую остаток. Обозначим эту повторяю- повторяющуюся часть через ОСТ; тогда получим программу E.19). 6 jv, 929
162 Гл. 5. Функциональные и императивные программы 5.12. Перепишите программу из упр. 5.9 на фрагменте Алгола и преоб- преобразуйте ее в функциональную программу. Сравните программу, построенную в этом упражнении и упражнении 5.9. Рис. 5.21.
Упражнения 163 5.13. Трудности, возникающие при использовании правил удаления связки результат и имевшие место в предыдущем упражнении, существуют из-за вложенности цикла пока. Правила, действующие в таком виде, как они введены, ведут к построению различных функций для выполнения вложенного цикла по каждой переменной. Правила, которым не свойственны эти труд- трудности, можно получить, расширяя функциональный язык таким образом, чтобы список выражений являлся правильной конструкцией. Предположим, что (е1у . . ., ek) — список выражений. Тогда необходимо задать для каждого правильного оператора 5 правила, удаляющие связку результат из конст- конструкции 5 результат (et, . . ., ek) Вывести эти правила. Покажите, что в общем случае использование преобра- преобразования S результат^, . . ., efe)->(S результат е1} . . ., 5 результат е^ приведет к тем же функциям, выведенным по правилам, приведенным в ос- основном разделе. 5.14. Расширив функциональный язык, как в упр. 5.13, постройте рас- расширение императивного языка, позволяющее ввести параллельное присваи- присваивание хъ . . ., хк := ev . . ., ek которое вычисляет каждый член последовательности е1у . . ., ек и припи- приписывает результирующее значение соответствующему члену последователь- последовательности *!,..., xk. Выведите правила преобразования этого расширения к фраг- фрагменту. 5.15. Покажите, что каждый оператор фрагмента Алгола можно преобра- преобразовать к однократному параллельному присваиванию. 5.16. Функция последовательность при определении описывалась таким образом: вначале преобразовывался оператор S2 результат еу а затем резуль- результирующее выражение «проталкивалось» через S1. Напротив, можно было «про- «протолкнуть» S2 результат е без предварительного преобразования. Всегда ли в результате возникает одна и та же функциональная программа независимо от того, какую из этих интерпретаций мы используем? 5.17. Опишите минимальное множество переменных, которые должны стать параметрами рекурсивной функции, представленной преобразованием оператора пока. 5.18. Покажите, что указатель стека верш не является строго необходи- необходимым и что его значение в любой точке последовательности операторов можно определить как фиксированное смещение значения переменной акт. 5.19. Определите последовательность операторов со свойством 4P для блок-выражения пусть хг=ег и ... и xk=ek e так, чтобы единственными переменными, доступными в еу были х±у . . ., хк. Рассмотрите возможность изменить правила доступа таким образом, чтобы в случае, когда блок-выражения вложены одно в другое или в некоторые функции, был возможен доступ из вложенных выражений к переменным внешних блоков. 6»
Глава 6. АРХИТЕКТУРА МАШИНЫ ДЛЯ ФУНКЦИОНАЛЬНЫХ ПРОГРАММ 6.1. Эскиз машины В гл. 4 был приведен интерпретатор для варианта языка Лисп в форме функции применить (/, а). Здесь / является S-выраже- нием, представляющим Лисп-программу, а S-выражение а пред- представляет список аргументов этой программы. Например, / может быть программой функции соединить: (ПУСТЬРЕК СОЕДИНИТЬ (СОЕДИНИТЬ ЛЯМБДА(Х Y) (ЕСЛИ (РАВНО X (КОД НИЛ)) Y (CONS (CAR X) (СОЕДИНИТЬ (CDR X) Y))))) Тогда возможным значением а может быть ((А В С D) (Е F G Н)) т. е. список значений, которые являются соответственно значения- значениями параметров X и Y. Значением применить (/, а) тогда является S-выражение, представляющее результат применения / к а. В нашем примере результатом будет список (А В С D Е F G Н). Таким образом, можно принять функцию применить (/, а) в качестве формального определения семантики языка Лисп, так как эта функция определяет значение любой Лисп-программы для любого заданного множества аргументов. Может случиться при этом, что некоторые свойства, которые мы хотели бы определить для языка, окажутся неопределенными или будут определены неверно или что некоторые конструкции, которые мы предпочли бы оставить неопределенными, приобретут произвольный смысл. Это можно обойти таким определением интерпретатора приме- применить (/, а) в качестве семантики языка, чтобы тот смысл, кото- который он предписывает (или не предписывает) программе, был бы полностью предусмотрен и верен. Однако, даже если мы сде- сделаем это, определение остается в некотором смысле неудовлетво- неудовлетворительным. Главное критическое замечание, которое мы вкратце высказали в конце разд. 4.3, заключается в том, что смысл определяемого языка (в данном случае это инструментальный Лисп) зависит от нашего понимания того языка, на котором напи- написан интерпретатор (в данном случае это строго функциональный
6.1. Эскиз машины 165 язык). Разумеется, это всегда справедливо, но это критическое замечание здесь тем более уместно из-за близости семантики определяемого и определяющего языка. В той мере, в какой мы правильно понимаем наш строго функ- функциональный язык, мы можем согласиться, что интерпретатор из гл. 4 придает верный смысл правильным выражениям в Лиспе. Если, однако, наше понимание строго функционального языка будет иметь некоторые изъяны, но так, что интерпретатор будет придавать правдоподобный смысл каждой конструкции Лиспа, каким образом мы сможем узнать, что наше понимание Лиспа тоже имеет изъяны? Аналогично, если существуют некоторые аспекты строго функционального языка, которые мы недопони- недопонимаем, можем ли мы использовать интерпретатор для определения эквивалентных аспектов Лиспа? Ответ часто бывает отрицатель- отрицательным. Например, предположим, что мы хотим определить, вычис- вычисляются ли фактические параметры до входа в функцию или только когда они необходимы для вычислений (т. е. вызываются ли они по значению или по наименованию — подробнее мы поговорим об этом в гл. 8). Предположим, рассматривается функция {ЛЯМБДА (X) (КОД 1)) Эта функция, вызванная с фактическим параметром, который не определен, например таким как (CAR (КОД 2)), будет неопре- неопределенной в одном случае и выдавать результат 1 — в другом. Понятно, что интерпретатор из гл. 4 может придать конструкции любое из этих значений. Проблема возникает из-за того, что уровень определяющего языка очень высок и поэтому возможно множество различных интерпретаций. Чтобы попытаться пре- преодолеть эти трудности, в настоящей главе мы определим преобра- преобразование Лисп-программы в программу на языке гораздо более низкого уровня, близком к обычному машинному языку. В за- защиту интерпретатора следует, однако, сказать, что он имеет огромное значение для описания высокоуровневых аспектов Лиспа. Например, он придает ясный смысл идее использования функции как параметра или как результата другой функции (так называемые функции высших порядков) без использования этих свойств в самом интерпретаторе. Аналогично полные правила связывания явно находятся вне интерпретатора, так что могут быть проанализированы наиболее сложные случаи, хотя сам ин- интерпретатор использует только простейшие зависимости перемен- переменных. Итак, если мы в какой-то степени правильно понимаем функ- функциональные программы и этого достаточно для правильного понимания интерпретатора, то интерпретатор сможет действи- действительно расширить это понимание. Определения настоящей главы разбиваются на две части.
166 Гл. 6. Архитектура машины для функциональных программ Здесь определяется абстрактная машина, которая существенно использует стек таким же образом, как он использовался для вычисления функциональных программ в гл. 5. Затем определя- определяется транслятор (компилятор), который транслирует Лисп в про- программы, выполняемые на этой машине. Абстрактная машина, рассматриваемая в следующем разделе, называется SECD-ма- шиной (название объясняется ниже) и была введена Лэндином. Приведенная здесь машина конкретного вида использует наше конкретное представление данных, т. е. S-выражения. В част- частности, программа для этой машины является S-выражением. Например, после трансляции функции соединить, приведенной в начале этого раздела, ее программа имеет вид (ФОРМ ВДК НИЛ ВДФ (ВВОД @.0) ВДК НИЛ. ..)...) Всего будет около 60 атомов. Каждый из атомов, фигурирующих в этом S-выражении, является либо машинной командой (опе- (операцией), либо операндом машинной команды. SECD-машину можно себе представить как функцию выполнить, которая вос- воспринимает скомпилированную версию функции /, назовем ее /* (она является S-выражением, аналогичным приведенному выше), а также S-выражение, представляющее аргументы а, и выдает S-выражение, представляющее результат применения / к а. Таким образом, выполнить (f*, а) = применить (/, а) Это означает, что SECD-машина по данным S-выражениям ском- скомпилированной функции и ее аргументов выполняет скомпилиро- скомпилированную на машинном языке программу, чтобы вычислить резуль- результат применения этой функции к заданным аргументам. Определив машину, перейдем к определению компилятора. Иначе говоря, мы определим функцию компилф, такую что компил (f) = /* На самом деле обе функции, выполнить (f*, а) и компил (/), будут заданы не обычными функциональными определениями, а совокупностью правил. Однако, обратив правила для компилA) в программу на строго функциональном языке, мы сможем затем переписать ее на Лиспе. Из этого материала мы сможем сгенерировать компил *, программу на машинном языке, обла- обладающую тем свойством, что выполнить (компил *, /) = /* для всех /. Это составит основу инструментария, описанного в гл. И и 12; фактические S-выражения, представляющие функцию компил и программу компил *, приводятся в приложении 2.
6.1. Эскиз машины 167 Функция выполнить (f*, а) реализована так, что для вычисле- вычисления вызовов функций она оперирует со стеком, как это описано в гл. 5. Поскольку программа/* является S-выражением и данные тоже представлены S-выражениями, естественным представлением состояния этой стековой машины будет тоже S-выражение. Мож- Можно реализовать стековую структуру в виде S-выражения, просто представив элементы стека как элементы списка, вталкивая и выталкивая элементы с левой стороны этого списка. Так, напри- например, список (А В С D) может представлять стек из четырех элементов. Чтобы выявить структуру стека, часто будут использоваться точечные обозна- обозначения. Так, если мы хотим явно выделить атом стека, находя- находящийся в его вершине, можно записать, например, (x.s). Тогда можно использовать х для обозначения атома в вершине и s —¦ для обозначения всех остальных атомов стека. В рассмотренном примере через х обозначается атом Л, а через s — весь остальной список (В С D). Аналогично, если мы хотим выделить два верх- верхних элемента произвольного стека, можно написать, например, (х y.s). В рассмотренном примере теперь х обозначает атом Л, у — атом ?, a s — список (С D). Так, например, можно описать сумму двух верхних элементов стека, сказав, что стек вида (х y.s) заменяется на стек вида (x+y.s). При использовании стека в разд. 5.4 требовалось, чтобы при каждом вызове функции для хранения параметров вызова отводилась новая область стека. Она называлась активной записью. Во время вычисления в стек должны помещаться про- промежуточные результаты, и, если эти вычисления требуют другого вызова функции, начинается новая область стека. Этот механизм реализован в SECD-машине при помощи двух стеков. Один стек используется для хранения промежуточных значений. Другой стек (называемый складом) используется для хранения значений первого стека и другой информации, которая относится к отло- отложенной активной записи. В описании SECD-машины читатель легко увидит использование стека в качестве механизма вычисле- вычисления. Установление точного соотношения между SECD-машиной и абстрактной машиной из разд. 5.5 оставляется в качестве упраж- упражнения читателю. Функция выполнить (f*, а) такова, что для начала ее вычисле- вычисления значения /* и а должны быть введены в регистры SECD-ма- SECD-машины. Затем, анализируя программу /*, SECD-машина дейст- действует так, что состояние машины преобразуется согласно семанти- семантике каждой машинной команды, которая встречается в программе. В конечном счете значение выполнить (f*, а) вычисляется и по свойству, аналогичному свойству чистого результата, помеща-
168 Гл. 6. Архитектура машины для функциональных программ ется в вершину стека. Следующий раздел посвящается описанию изменения состояний машины при выполнении каждой машинной команды. В разд. 6.5 мы вернемся к вопросу о точной реализа- реализации функции выполнить (/*, а) на этой машине. 6.2. SECD-машина SECD-машина, первоначально введенная Лэндином, полу- получила свое название по начальным буквам ее основных регистров: S (stack — стек) используется для размещения промежу- промежуточных результатов во время вычисления выражений, Е (environment — окружение, вычислительная обстановка) используется для хранения значений, связываемых с переменными во время вычисления, С (control list — список управления) используется для раз- размещения выполняемой машинной программы, D (dump — склад, хранилище) используется как стек для хранения значений других регистров при вызове новой функции. Каждый из регистров содержит S-выражение. Как мы увидим в гл. 11, где рассказывается о моделировании машины, регистры будут содержать указатели на структуры данных, представлен- представленные S-выражениями. Для наших целей в этой главе, однако, удобнее представлять себе, что все S-выражение целиком содер- содержится в регистре. Состояние машины полностью определяется содержимым этих четырех регистров. Таким образом, каждая команда машины мо- может быть описана заданием состояния машины до и после выпол- выполнения команды. Это изменение состояния мы назовем пере- переходом и будем записывать правила перехода в виде s е с d -> s' e' с' &' т. е, четыре S-выражения слева от стрелки задают состояние машины до выполнения команды и четыре S-выражения спра- справа задают состояние после выполнения команды. Чтобы указать, что некоторые S-выражения остаются после выполнения команды без изменений, используются одни и те же буквы слева и справа от стрелки. Например, переход при вводе константы командой (ВДК) показан на рис. 6.1. Рис. 6.1.
6.2. SECD-машина 169 Программа перед выполнением команды имеет вид (ВДК х.с), т. е. выделяются три отдельные части списка управления (про- (программы). Код операции ВДК, заданный здесь в мнемонической форме, на практике будет числовым атомом, но это не важно нам вплоть до гл. 11. Следующий атом в списке управления является операндом команды ВДК, константой, которая должна быть введена. Переменная с обозначает остаток списка управле- управления, поэтому здесь использована точка. Таким образом, если бы в действительности список управления имел вид (ВДК (А.В) CAR ВДК А РАВНО ВОЗВР) то переменной х соответствовало бы выражение (A.B)t ас — (CAR ВДК А РАВНО ВОЗВР). В состоянии после выпол- выполнения команды мы видим, что в регистре управления оставле- оставлена только эта хвостовая часть списка, и, таким образом, выпол- выполнение команды ВДК вызывает удаление части списка управле- управления — самой команды и ее операнда. Операнд вталкивается в стек s, как указывает S-выражение (x.s), означающее, что все предыдущие элементы стека сохраняются, но х помещается перед ними. Окружение и склад остаются без изменений. От- Отсюда, если, например, машина находится в состоянии ((А.В) 127) (C (С))) (ВДК 17 ВОЗВР) НИЛ то после выполнения команды ВДК она перейдет в состояние A7 (А.В) 127) (C(С))) (ВОЗВР) НИЛ Машина организована так, что программа в ее регистре управ- управления всегда является списком с кодом команды на первом месте. Этот код команды определяет, какое изменение состояния должно быть выполнено. Так, машина выполняет свою програм- программу, проходя через последовательные переходы, вызванные ко- командами в списке управления. Так как некоторые команды вызывают повторный ввод списка управления, влияя на вызов подпрограмм, эта последовательность переходов может быть весьма длинной, даже если сам список управления иногда может быть совсем коротким. Выполнение программы заканчивается, когда встречается команда СТОП. Формально она определяется переходом s е(СТОП) d-+s e (СТОП) d которое после однократного применения прекращает дальнейшие вычисления. Ниже приводится полный список команд, включаю- включающий мнемоническое изображение и название команды:
170 Гл. 6. Архитектура машины для функциональных программ Далее в этом разделе будут даны правила перехода для каждой из этих команд и приведены некоторые примеры их использования. Начнем с правил перехода для арифметических операций. Для сложения это будет правило {a b.s) е (ADD.c)d-+ (b + a.s)e с d Здесь запись (a b.s) обозначает, что стек имеет по меньшей мере два элемента, а запись (b + a.s) — что эти два элемента (которые должны быть числами) заменяются на их сумму. Так, если ма- машина находится, например, в состоянии G 6.s) e (ПЛЮС.с) d то на следующем шаге она переходит в состояние A3.s) e с d Правила для остальных арифметических операций имеют точно такую же форму:
В последних двух случаях логические значения замещают в стеке арифметические и являются одним из атомов И или Л, обозна- обозначающих соответственно истину или ложь. Теперь можно продемонстрировать переходы машины во время выполнения программы. В табл. 6.1 каждая строка полу- получается из предыдущей путем применения одного из перечислен- перечисленных выше правил перехода. Отметим, что использованная здесь программа могла бы быть результатом компилирования выраже- выражения 1—2x3=4 или, более точно, списка {РАВНО {МИНУС {КОД 1) {УМН {КОД 2) {КОД 3))) {КОД 4)). Здесь ясно вид- видно, как используются в машине стек и список управления. Спи- Список управления содержит программу на машинном языке, пер- первым элементом которой всегда является машинная команда. Правило перехода определяется исключительно этой командой. Таблица 6.1 Стек содержит соответствующие результаты во время вычисле- вычисления выражения. Имеются машинные команды для каждой из примитивных операций над S-выражениями, CAR, CDR и CONS. Они воздей- воздействуют на значения в вершине стека и, как можно ожидать, помещают туда же свой результат. Правила перехода таковы: (ab.s) e {CONSx) d-+((a.b).s) е с d {{a.b).s) e (CARx) d—>(a.s) e с d ((a.b).s) e {CDRx) d-+(b.s) e с d Обозначив сложное значение в вершине стека через {а.Ь)у мы смогли точно показать действие каждой команды. Существует, 6.2. SECD-машина 171
172 Гл. 6. Архитетура машины для функциональных программ разумеется, аналогичное правило перехода для предиката, кото- который проверяет, является ли значение атомарным или нет. Соот- Соответствующая машинная команда называется АТОМ. (a.s) е (АТОМ.с) d-> (t.s) e с d где t = И у если а есть атом, и t = Л, если а не является атомом. Эта специальная SECD-машина предназначена для того, чтобы на ней реализовать инструментальный Лисп. И остальные машинные команды характерны для этого языка. Первая из них реализует доступ к значениям, связанным с переменными Лисп- программы. Чтобы описать эту команду, необходимо определить структуру вычислительной обстановки состояния SECD-машины и то, как эта структура соотносится с переменными Лисп-про- Лисп-программы. Вычислительная обстановка в SECD-машине имеет ту же самую структуру, что и список значений, использованный при определении интерпретатора в гл. 4, т. е. он является списком списков. Например, структура (C 17) ((А В) (С О))) возможна в качестве окружения. Доступ к вычислительной об- обстановке осуществляется командой ВВОД, имеющей в качестве операндов пару целых чисел, называемых индексной парой. Подсписки вычислительной обстановки нумеруются числами 0,1,2,. . .; каждый элемент подсписка также занумерован числами 0,1,2,. ... По индексной паре (i.j) выбирается /-й элемент t-ro подсписка. Элементам приведенного выше окружения соответст- соответствуют индексные пары, указанные в табл. 6.2. Можно определить функцию, которая выдает n-й элемент списка s: индекс(п, s) = если равно (л, 0) то car(s) иначе индекс (п — 1, cdr(s)) Теперь можно определить функцию, которая для любой индекс- индексной пары i = (b.ri) выдает соответствующее значение вычисли- Таблица 6.2
6.2. SECD-машина 173 тельной обстановки: взятьA> е)^=~{индекс(п, индексф, е)) где b = car (i) и n = cdr(i)\ Имея эту функцию, можно, наконец, определить правило пере- перехода для команды ВВОД, которая имеет один параметр, индекс- индексную пару, и копирует значение, связанное с этой парой, в вер- вершине стека: s е (ВВОД i.c) d -+ (x.s) e с d где х=взятьA, ё) Для определенности проследим за такой последовательностью переходов: НИЛ (C 7) B 0)) (ВВОД @.1) ВВОД A.0) ПЛЮС) d G) (C 7) B 0)) (ВВОД A.0) ПЛЮС) d B 7) (C 7) B 0)) (ПЛЮС) d (9) (C 7) B 0)) НИЛ d Здесь выбраны два значения из вычислительной обстановки и по- получена их сумма. Команда ВВОД используется для доступа к значениям, свя- связанным с переменными в Лисп-программе. Каждой переменной соответствует определенная позиция окружения. Фактически нулевой подсписок окружения соответствует описаниям в блоке (или функции), непосредственно содержащем переменную, первый подсписок отвечает блокам, охватывающим нулевые блоки, и т. д. Более подробно мы ознакомимся с этими схемами доступа при описании компилятора в следующем разделе. Для реализации условной формы имеются две команды. Команда ВЫ Б выбирает подсписок управления в зависимости от значения в вершине стека. Команда ВОССТ возвращает управле- управление главному списку. Так, на практике предполагается, что управление имеет вид (...ВЫБ (...ВОССТ) (...ВОССТ)...) I t t Cf Со С и выполняется а или с0 в зависимости то того, находится ли в вершине стека значение И или Л. После того как выбран под- подсписок с0 или си в ходе его выполнения команда ВОССТ обеспечи- обеспечивает возвращение управления с. Этому отвечают следующие правила перехода для этих команд: (x.s) e (ВЫБ сг со.с) d-+s e cx (cd) s e (ВОССТ) (cd) -+s e с d
174 Гл. 6. Архитектура машины для функциональных программ Здесь видно, что команда ВЫБ проверяет значение х в вершине стека и в зависимости от этого выбирает соответствующий под- подсписок {сг при х = И и с0 при х = Л). Остаток управляющего списка помещается в хранилище d. Если выбранный список пра- правилен, т. е. оставляет стек d в том виде, в каком он был на входе в этот список, то в момент выполнения команды ВОССТ элемент в вершине стека d является остатком первоначального (главного) списка управления. Команда ВОССТ восстанавливает этот главный список, и вычисления продолжаются. Рассмотрим спи- список управления {ВВОД @.0) ВЫБ {ВВОД @.1) ВОССТ) {ВВОД @.2) ВОССТ) СТОП) При выполнении этой программы в вычислительной обстановке, имеющей некоторые значения на позициях @.0), @.1) и @.2), результатом будет значение, введенное в стек с позиции @.1) или @.2), в зависимости от того, будет ли значение на позиции @.0) истиной {И) или ложью (Л). Команды, используемые при вычислении вызовов функций, несколько сложнее. Фактически эти же команды участвуют при компиляции блоков пусть. Особенно важна команда ПРИМ. Принципиальным является способ, каким устанавливаются зна- значения в вычислительной обстановке. Мы увидим, что, когда определенная пользователем функция применяется к ее аргумен- аргументам, значения этих аргументов размещаются в окружении как подсписки, так что они доступны в теле функции через команду ВВОД, Сначала, однако, нужно рассмотреть представление теку- текущих значений функции. В разд. 4.2 при обсуждении правил свя- связывания в инструментальном Лиспе использовалось понятие кон- контекста. Было определено, что значение функции представляется замыканием, состоящим из Х-выражения, определяющего функ- функцию, и копии контекста, в котором сделано это определение. В SECD-машине эквивалентом контекста является вычислитель- вычислительная обстановка, а эквивалентом вычисляемого выражения — список управления. Таким образом, замыкание в текущий момент представлено парой, состоящей из списка управления и вы- вычислительной обстановки. Такая пара строится командой ВДФ (ввод функции), которая имеет своим аргументом список управ- управления. Правило перехода для команды ВДФ имеет вид s е {ВДФ c'.c)d-+ {{c\e).s) e с d Таким образом, замыкание {с' .е) есть просто cons операнда ко- команды ВДФ и вычислительной обстановки. Например, если машина находится в состоянии @) (C 7) (Л)) {ВДФ {ВВОД A.1) ВОЗВР) ВВОД @.1)) НИЛ
6.2. SECD-машина 175 то применение упомянутого правила перехода даст нам состояние (((ВВ0ДA Л) В03ВР).(C 7)(А))H) (C 7)(А)) (ВВОДфЛ)) НИЛ где замыканием является ((ВВОДУ Л)В03ВР).(C 7)(А))). Вооб- Вообще замыкание, созданное однажды, не применяется немедленно, но находит свое место в вычислительной обстановке и может вызываться неоднократно командой ВВОД, так что оно может применяться к различным аргументам. Когда замыкание вызывается и применяется посредством команды ПРИМ, оно должно находиться в стеке непосредственно над списком фактических параметров. Команда ПРИМ формиру- формирует вычислительную обстановку и помещает код из замыкания в регистр управления для выполнения. Хранилище используется для сохранения содержимого всех регистров так, чтобы оно могло быть восстановлено командой ВОЗВР в конце программы из замыкания. Команда ПРИМ имеет следующее правило пере- перехода: ((c'.e')v.s) е (ПРИМх)а^ H4JI(v.e')c'(s e с А) Это самое сложное правило из всех, что были до сих пор. Команда ПРИМ требует, чтобы элемент в вершине стека был замыканием (с'.е'), а второй элемент v — списком значений для параметров функции, представленной замыканием. Вычисление функции на- начинается с занесения программы с' в регистр управления и по- построения окружения (v.ef). Эта форма вычислительной обстановки означает, что параметры функции расположены на позициях @.0), @.1), @.2) и т. д., а свободные переменные — на позициях A.0), A.1.), A.2) и т.д. Выполнение программы замыкания начина- начинается с пустого стека. Непосредственно после выполнения коман- команды ПРИМ хранилище имеет вид (s ее А), означающий, что цели- целиком сохраняется все состояние, которое было перед выполнением этой команды. Как пример выполнения команды ПРИМ рассмот- рассмотрим состояние (((ВВОДA Л) ВВОД @.0) ПЛЮС ВОЗВР).(C 7) (А))) F) 0) (B В)) (ПРИМ СТОП) d которое, согласно правилу, переходит в следующее состояние: НИЛ (F) C 7) (А)) (ВВОДУ Л) ВВОДф.О) ПЛЮС ВОЗВР) (@) (B5)) (СТОП) А) Взглянув на программу, помещенную в регистр управления, видим, что она реализует доступ к позиции @.0), вводя первый (и единственный) фактический параметр, и к позиции A.1), вводя значение 7 — глобальное значение, требуемое в замыкании. Команда ВОЗВР дополняет команду ПРИМ в том смысле, что она возвращает машину к состоянию, хранимому в регистре d.
176 Гл.6, Архитектура машины для функциональных программ Поэтому программа, помещаемая командой ПРИМ в регистр управления, должна заканчиваться командой ВОЗВР. Правило перехода для команды ВОЗВР имеет вид (х) ё (ВОЗВР) (s e cd) -> (x.s) e с d Мы видим, что команда ВОЗВР рассчитывает иметь в стеке единственное значение х. Она вталкивает его в восстановленный стек s. Например, после выполнения вводов и сложения в приве- приведенном выше состоянии получаем A3) (F) C 7) (А)) (ВОЗВР) (@) (B В)) (СТОП)Л) что после выполнения команды ВОЗВР дает A3 0) (B В)) (СТОП) d и значение 13 оказывается в вершине стека в качестве результата вызова функции. Чтобы описать оставшиеся две команды SECD-машины, ФОРМ и РЕК, снова потребуется лисповская псевдофункция завершение(х, у). Эта псевдофункция, которая рассматривалась в разд. 4.3, означает замену саг от х на у. Здесь скорее осуще- осуществляются побочные вычисления, а не вычисление значения. Однако функция должна иметь значение. Ее значением является jc, за исключением того, что car x заменяется на у. Напомним, что вводилось ограничение — замена car x разрешается лишь в том случае, если car x имеет специальное значение Q, которое называется незавершенным. Мы будем использовать функцию завершение при реализации рекурсивных блоков пустырек, где требуется, чтобы локальные определения вычислялись в контексте, включающем их собствен- собственные значения. Сделаем это, создав номинальное формальное окружение с незавершенными локальными определениями, вы- вычисляя определения, используя это формальное окружение и затем последовательно заменяя незавершенные части окружения на значения определений. Таким образом, в каждое замыкание, создаваемое при вычислении определений, будет включаться фор- формальное окружение. После замены незавершенной части эти замы- замыкания становятся циклическими, каждое из них содержит окру- окружение, которое содержит само замыкание как значение. Все это полностью объясняется в конце следующего раздела и связано как раз с командами ФОРМ и РЕК. Команда ФОРМ создает номинальную вычислительную об- обстановку, первым подсписком которой является Q. Отсюда любая попытка доступа к значениям этого подсписка остается неопре- неопределенной, пока не заменен элемент Q. Правило перехода для команды ФОРМ имеет простой вид: s е (ФОРМ.с) d-+ s (Q.e) с d
6.3. Компилятор для варианта Лиспа 177 Команда РЕК почти тождественна команде ПРИМ, не считая того, что вместо операции cons, включающей фактические пара- параметры в вычислительную обстановку, здесь используется функ- функция завершение, заменяющая символ й, внесенный командой ФОРМ: ((c'.e')v.s){Q,.e){PEKx)d-+ НИЛ завершение(ef', v)c' (s e cd) На первый взгляд это правило кажется несколько необычным. В момент применения команды РЕК вычислительная обстановка имеет всегда вид е' = (Q.e). Таким образом, замыкание в верши- вершине стека содержит окружение, тождественное окружению в теку- текущий момент. С другой стороны, команда РЕК во многом сходна с командой ПРИМ. Она берет программу с1 из замыкания и пере- переносит ее на регистр управления, а окружение е', заменив его саг на список значений v9 вносит в регистр окружения. Стек, окру- окружение и список управления сохраняются в регистре d. Что ка- касается окружения, то, так как оно было расширено командой ФОРМ, сохраняется только его cdr. Сравнивая правила перехода для команд ПРИМ и РЕК и имея в виду то, что при выполнении команды РЕК окружение уже расширено элементом Q, легко видеть, что эти команды сходны. Использование этих команд станет яснее, когда в следующем разделе будем изучать програм- программу, получаемую компилятором. 6.3. Компилятор для варианта Лиспа Чтобы описать компилятор для нашего варианта Лиспа, рас- рассмотрим отдельно каждое правильное выражение в Лиспе, следуя определениям разд. 4.1. Для каждого правильного выражения будем строить список команд на языке SECD-машины, обладаю- обладающий следующим свойством. Чистый результат правильного выражения. Программа, ском- скомпилированная для правильного выражения, такова, что после ввода в регистр управления SECD-машины и выполнения она оставляет значение выражения в вершине стека. Программа требует, чтобы вычислительная обстановка была введена струк- структурой, содержащей на соответствующих позициях значения сво- свободных переменных выражения. Она оставляет стек без измене- изменений, за исключением вталкивания значения выражения в вер- вершину стека. Окружение и хранилище имеют одни и те же значения до и после выполнения программы. Кратко, если с есть программа, построенная для правильного выражения, и е есть окружение, содержащее значения перемен- переменных правильного выражения, то для любого стека s и любого хранилища d выполнение программы с по правилам предыдущего
178 Гл. 6. Архитектура машины для функциональных программ раздела приводит к такому изменению состояния: seed-* (x.s) e НИЛ d где х есть значение, вычисленное программой с. Мы используем стрелку ->, чтобы указать, что применены одно или более правил перехода из предыдущего раздела. Более общо, если обозначить через сх\с2 результат соединения списков управления сг и с2, имеем s е с \с' d ->- (x.s) e cr d Например, если с = (ВДК 1) с' = (ВОЗВР) то с\с' = (ВДК 1 ВОЗВР) и имеем s е (ВДК 1 ВОЗВР) d-+ (l.s) e (ВОЗВР) d Из этого правила следует, что если оба списка сх и с2 построены для правильных выражений и они вычисляют соответственно значения хг и х2, то s e Cxlcjc7 d-+ (x2 xlts) e с' d Иначе говоря, результатом последовательного выполнения двух программ для правильных выражений будет вталкивание значе- значений этих выражений в стек и устранение программ из списка уп- управления, а в остальном содержимое регистров машины не изме- изменяется. Это сокращенное обозначение для объединения двух списков управления будет часто использоваться в дальнейшем, и поэтому уместно хорошенько усвоить его сейчас. В действительности х\у есть не что иное, как выражение соединить (х, у), записанное в краткой форме. Однако мы используем тот факт, что соединить (х, соединить (у, z)) = соединить (соединить (х, у) z) разрешая писать x\y\z вместо {х|#}|г или х|{#|2}. Так, например, запись (ВДК НИЛ) | (ВДК 1) | (CONS) упрощается до вида (ВДК НИЛ ВДК 1 CONS) и поэтому запись (ВДК НИЛ) | с | (CONS) является одним из способов задания программы, которая вы- вычисляет значение cons(x, НИЛ) (т. е. список с одним элементом), если с вычисляет значение х (в вершине стека).
6.3. Компилятор для варианта Лиспа 179 При анализе каждого правильного выражения мы будем строить список переменных, объявленных в Я-выражении и блоках внутри его. Этот список будет иметь форму, конгруэнтную форме вычислительной обстановки в текущий момент, т. е. это будет список списков. Как и в интерпретаторе, назовем этот список именным. Список ((А) (X Y) (СОЕДИНИТЬ ОБР ДИР)) является примером именного списка. Где бы ни встретился но- новый блок или функция, переменные, объявленные в них, будут добавлены к именному списку в качестве нового первого под- подсписка. Приведенный выше именной список предполагает глуби- глубину вложения равной трем. Например, этот список мог быть обра- образован для программной структуры F.1) в момент анализа отме- отмеченного стрелкой выражения. По данному именному списку можно определить индексную пару, используемую для доступа к каждой переменной. Это достигается нумерацией 0,1,2,. . . подсписков и элементов в каж- каждом подсписке. Так, приведенный выше список имеет сле- следующие индексы: ((А) (X Y) (СОЕДИНИТЬ ОБР ДИР) т г t t it @.0) A.0) A.1) B.0) B.1) B.2) Эти индексные пары легко вычисляются функцией место (х, п), где х — рассматриваемая переменная и п — именной список. Сначала определим функцию позиция(х, о), которая по данному списку атомов а выдает индекс атома х в этом списке (предпола- (предполагая, что он там имеется): позиция(х, а) = если равно(х, саг (а)) то 0 иначе \+позиция(х, cdr(a)) Теперь функцию место можно определить так: место(х, п) = если 9лемент(х, саг (п)) то cons@, позиция(х, саг(п))) иначе {cons (car (z)+ 1, cdr(z)) где z = Metmo(xycdr(n))\
180 Гл. 6. Архитектура машины для функциональных программ Используемая здесь функция элемент определена в разд. 2.5. Мы видим, что функция место выдает пару @.^), если перемен- переменная х встретилась в первом подсписке на позиции^. Если, одна- однако, она не встречается в первом подсписке, то функция место вызывается рекурсивно для cdr(n) и определяется индексная пара (p.q). Выдается значение (p+l.q). Функция место(ху п) не опре- определена, если х не встречается в л, но здесь мы игнорируем эту возможность. Теперь приступим к описанию программы, которая строится для каждого правильного выражения. Еслие — правильное вы- выражение и п — именной список, то будем обозначать через е*я программу, которая строится при компиляции е по отношению к именному списку п. Программы для двух самых элементарных правильных выражений, переменной и константы, тривиальны: х*п = (ВВ0Д i) где i: = место(х, п) (КОД s)*n = (BMKs) В обоих случаях образуется одна команда, которая вводит зна- значение в стек. Для переменной это команда ВВОД, которая из- извлекает значение переменной из вычислительной обстановки. Для константы соответствующая команда есть ВДК, которая просто вводит свой операнд в стек. Легко видеть, что обе эти программы удовлетворяют свойству чистого результата пра- правильного выражения. Рассмотрим теперь правильное выражение {ПЛЮС ех е2) Если при компиляции ег и е2 получены программы сх и с2 соот- соответственно, то программа для арифметического выражения имеет вид Сх\с2\(ПЛЮС) так как сг и с2 должны обладать свойством чистого результата DP). Сначала значение ех вычисляется программой сх и оставля- оставляется в стеке, затем значение е2 вычисляется программой с2 и заносится в стек, и, наконец, команда ПЛЮС складывает эти два значения и по свойству 4P оставляет в стеке одно это значе- значение — (ПЛЮС ег е2). Записав это одним общим правилом, имеем (ПЛЮС е1е2)*п=е1т\е2*п\ (ПЛЮС) В записи таких правил считается, что символ * указывает на бо- более сильную связь по сравнению с вертикальной чертой |. Однако обычно такому правилу может быть придан только единственный разумный смысл. Здесь, например, операция | приемлема только между списками-программами, а операция * — между правиль- правильным выражением и именным списком. Поэтому можно, не обращая
6.3. Компилятор для варианта Лиспа 1*Н внимания на старшинство операций, установить правильный смысл этого правила. Остальные правила для арифметических, структуральных выражений и отношений полностью аналогичны правилу для операции ПЛЮС. Это следующие правила: (МИНУС е, е%)*п=ег*п \ е2т \ (МИНУС) (УМН ех ejm^e^n \ е2т \ (УМН) (ДЕЛ ех е2)т=ехт \ е2т \ (ДЕЛ) (ОСТ ех е1)*л=в1*л | е2т \ (ОСТ) (РАВНО ех е2)т=е1*п \ е2т \ (РАВНО) (МР ех е2)т = ех*п \ е2т \ (МР) (CAR e)*n = em \ (CAR) (CDR ё)т=е*п \ (CDR) (CONS ех е2)т = е2т \ er*n I (CONS) (АТОМ ё)*п = em | (АТОМ) Из-за принятого способа определения SECD-машины команда CONS требует свои аргументы в «обратном» порядке. Это не представляет никаких трудностей, и на этом не стоит задержи- задерживать внимание. Для закрепления этих правил рассмотрим компиляцию выра- выражения в F.2). Это форма вычисления арифметического выраже- выражения, когда перед применением операции ее аргументы вычисля- вычисляются и помещаются в стек. (ПЛЮС(САЯ Х)(КОД 1 ))*((* К)) = (CAR X)*((X У))\(КОД 1)*((Х У))\(ПЛЮС) = X*((X Y))\(CAR)\ (ВДК \)\(ПЛЮС) F.2) = {ВВОД @.0))\(САЯ)\(ВДК 1) | (ПЛЮС) = (ВВОД @.0) CAR ВДК 1 ПЛЮС) Когда операция появляется, как здесь, после программы для опе- операндов, говорят, что программа имеет постфиксную, или обрат- обратную, форму. Это в точности то же самое, с чем мы сталкивались в разд. 5.4. Теперь перейдем к программе, соответствующей условной форме. Условная форма имеет вид (ЕСЛИ ех е2 е3) и поэтому, разумеется, нужно вычислить каждое из выражений ?ь ^2 и е9. Спидки программ, полученных для е2 и е8, должны иметь в конце команду ВОССТ. То есть для е2 строится програм- программа е2*п | (ВОССТ)
182 Гл. 6. Архитектура машины для функциональных программ и аналогично для еь. Отсюда общая программа для условной формы имеет вид (ЕСЛИ ег е2 е3)т = ехт \ (ВЫБ е2т \ (ВОССТ) еъ*п | (ВОССТ)) Здесь видно, что программы для е2 и е3 вложены как операнды ВЫБ, в то время как программа для ег предшествует ВЫБ. Программа обладает свойством чистого результата. Действи- Действительно, когда после выполнения программы для ех значение этого выражения находится в вершине стека, команда ВЫБ выталки- выталкивает это значение и выбирает для исполнения один из подсписков. Так как каждый из этих подсписков заканчивается командой ВОССТ, то, как говорилось при введении операций ВЫБ и ВОССТ, по свойству чистого результата значение е2 или е3 остав- оставляется в стеке, а в остальном состояние машины не изменяется. Как пример рассмотрим выражение (ПЛЮС Y (ЕСЛИ (МР X Y) X (КОД 1 )))*((* Y)) для которого строится следующая программа (программа услов- условного выражения отмечена стрелками): I (ВВОДфА) ВВОД @.0) ВВОД @.1) МР ВЫБ (ВВОД @.0) ВОССТ) (ВДК 1 ВОССТ) ПЛЮС) г Проследив за вычислениями по этой программе, видим, что после выполнения команды МР в вершине стека находится зна- значение И или Л. В зависимости от этого значения команда ВЫБ выбирает один из подсписков, согласно чему в стек вводится либо значение с позиции @.0), соответствующее переменной X, либо константа 1, после чего команда ВОССТ так перестраивает управление, что может быть выполнена команда ПЛЮС. По свой- свойству чистого результата для условной формы заключенная между стрелками программа оставляет в стеке единственное значение, которое является вторым аргументом операции ПЛЮС. При компиляции ^-выражения возникают некоторые новые проблемы. В частности, поскольку здесь вводятся новые пере- переменные, следует согласовать именной список. Рассмотрим ком- компиляцию ^-выражения (ЛЯМБДА (х, xh) e) по отношению к именному списку п. Ясно, что мы должны вы- вычислить еу но не по отношению к списку п. Нужно нарастить п параметрами (хи. . ., xh). Соответствующий именной список бу-
6.3. Компилятор для варианата Лиспа 183 дет иметь вид ((*ь. . ., xk).n) Это означает, что теперь на переменные в списке п будем ссылать- ссылаться при помощи индексной пары, где первый индекс увеличен на 1. Это в точности согласуется с теми требованиями, которые возни- возникают, когда мы переходим к применению функций; там будет использоваться команда ПРИМ, которая подставляет значения фактических параметров в вычислительную обстановку по пози- позиции с индексами вида @./), и поэтому глобальные переменные будут в ней на смещенных позициях. Далее, построив программу с = е*((хх. . .xh).n) мы должны добавить к ней команду ВОЗВР и сделать ее пара- параметром команды ВДФ. Таким образом, полная программа для Х-выражения имеет вид (ВДФ с | (ВОЗВР)) Правило компиляции запишется так: (ЛЯМБДА {хх. . .хк) е)т = (ВДФе*((х±. . .xk).n) | (ВОЗВР)) Например, (ЛЯМБДА(Х) (ПЛЮС X Y))*((X F)) = (ВДФ (ВВОД @.0) ВВОД A.1) ПЛЮС ВОЗВР)) Результатом выполнения программы для ^-выражения будет помещение замыкания, содержащего тело функции, в вершину стека. Свойство чистого результата для этого правила выпол- выполняется автоматически. Правильное выражение (е е1т . ,ek) интерпретируется как вызов функции. Компилируется каждая из его компонент и строятся списки программ с, с1у. . ., ch. Необходимо так ском- скомбинировать эти программы, чтобы во время выполнения по- построить в стеке список значений, вычисляемых программами с1%. . ., Cfc, и замыкание, вычисляемое программой с. Список значений может быть получен программой (ВДК НИЛ) \ch\ (CONS) | ck^ I (CONS) \ . . .\cx\(CONS) где при вычислении значений в обратном порядке получаем в результате список элементов в нужной последовательности. Если программы си. . ., ch вычисляют значения vu. . ., vhy оставляя каждое из них в вершине стека (так как еь. . ,, ek — правильные выражения), то приведенная выше конструкция оставит в вершине стека значение (ui. . .vh). Поскольку програм- программа с = е*п оставляет в вершине стека замыкание вызываемой
184 Гл. 6. Архитектура машины для функциональных программ функции, имеем следующую программу для вызова функции: (е ех ... ен)*п = (ВДК НИЛ) \ek*n\ (CONS) |... \ex*n\CONS\e*n\(nPHM) Свойство чистого результата этой программы вытекает из пра- правильности тела функции, скомпилированной в замыкании, и дей- действия команды ВОЗВР. Как пояснялось выше, программа при ее выполнении сначала формирует в стеке список значений факти- фактических параметров и затем поверх них помещает замыкание. Это в точности структура стека, которая требуется для команды ПРИМ у запоминающей в стеке состояние всех регистров машины (за исключением замыкания и значений параметров). Затем вы- выполняется программа из замыкания, и так как она компилирова- компилировалась для правильного выражения, результирующее действие программы состоит в том, что значение выражения помещается в стек. Команда ВОЗВР возвращает машину в то состояние, кото- которое было до начала вызова, добавив в стек единственное значение тела функции. Таким образом, программа вызова функции обла- обладает свойством чистого результата. В качестве примера рассмотрим вызов (УВ (КОД 1)) вычисляемый в контексте, где УВ является единственной пере- переменной и ее значение получается из Я-выражения (ЛЯМБДА (X) (ПЛЮС X (КОД 1))) Замыканием, которое представляет Х-выражение в момент вы- вычисления, будет $>=((ВВОД @.0) ВДК 1 ПЛЮС ВОЗВР).НИЛ) t Т программа окружение Компиляция вызова дает (УВ (КОД 1))*((УД)) = (ВДК НИЛ) | (ВДК 1) | (CONS) | (ВВОД @.0)) | (ПРИМ) = (ВДК НИЛ ВДК 1 CONS (ВВОД @.0)) (ПРИМ)) Эта программа выполняется в вычислительной обстановке, со- содержащей замыкание, как показано в табл. 6.3. Строки таблицы описывают последовательные состояния машины, каждое состояние получается из предыдущего приме- применением одного из перечисленных в предыдущем разделе правил перехода. Здесь наглядно демонстрируется свойство чистого результата программы.
Теперь компиляция простого блока ПУСТЬ выполняется прямо в лоб, если воспользоваться эквивалентностью между та- таким блоком и вызовом функции, которая установлена в разд. 4.3. Имеем {ПУСТЬ e(xve^. . .(хк.ек))^({ЛЯМБДА(хг. . ,xk)e) ех. . .ek) и поэтому для блока ПУСТЬ строится программа только что рассмотренного вида, т. е. (ПУСТЬ е (x^)... (**¦**))*" = (ВДК #tf,/7)K*n|(CCWS)| ... K*rt|(CONS)| (ВДФ е*т \ (ВОЗВР) ПРИМ) где т=*((хг ...xft).n) Здесь важно отметить, что если определяемое выражение вы- вычисляется относительно именного списка, дополненного локально определенными переменными, то выражения, определяющие значения этих локальных переменных, вычисляются относи- относительно исходного, недополненного списка. Соответственно этому и программа имеет такой вид, что во время вычисления значений выражений еъ . . ., ек вычислительная обстановка такова, что позиции для переменных хи . . ., Хъ еще не установлены. Они устанавливаются только командой ПРИМ для вычисления определяемого выражения. Так как программа та же самая, что и при композиции ^-определения и вызова функции, то, она, очевидно обладает свойствам чистого результата. Переходим, наконец, к рекурсивному блоку. Программа, которая строится в этом случае, очень похожа на программу для простого блока, но имеет два небольших изменения. Во- первых, выражения, определяющие значения локальных пере- переменных, вычисляются относительно пополненного именного списка, а не исходного, как в простом блоке. Во-вторых, про- 6.3. Компилятор для варианата Лиспа 185 Таблица 6.3
186 Гл. 6. Архитектура машины для функциональных программ грамма для этих выражений вычисляется в формальной вычис- вычислительной обстановке, создаваемой командой ФОРМ. Соот- Соответственно вместо команды ПРИМ используется РЕК. Имеем (ПУСТЬРЕК е (х^е,) ... (**.**))# л = (ФОРМ ВДК НИЛ) \ek*m\ (CONS)\ ... |ех*m\(CONS)\ (ВДФ e*m \ (B03BP) РЕК) где т = ((хг ... xk).n) Чистота результата этой программы следует из тех же рас- рассуждений, что проводились для простого блока и анализа ко- команд ФОРМ и РЕК в предыдущем разделе. Чтобы продемон- продемонстрировать эту программу в действии, рассмотрим простой случай рекурсивного определения. Рассмотрим правильное выражение (ПУСТЬРЕК (ФАК (КОД 6)) (ФАК ЛЯМБДА (X) )) где ФАК определяется обычным способом, как функция факто- факториал. Вычисление этого выражения относительно именного списка НИЛ дает нам программу (ФОРМ ВДК НИЛ ВДФ с CONS ВДФ (ВДК НИЛ ВДК 6 CONS ВВОД @.0) ПРИМ ВОЗВР) РЕК) где с есть программа для тела определения функции. Если ввести в регистр управления SECD-машины эту программу, а в осталь- остальные регистры ввести НИЛ, то получим такую последователь- последовательность состояний: НИЛ НИЛ (ФОРМ ... РЕК) НИЛ НИЛ а (ВДК НИЛ ... РЕК) НИЛ где фиктивная вычислительная обстановка а имеет структуру а=@.Я//Л). Вычисления продолжаются: (НИЛ) а (ВДФ с... РЕК) НИЛ ((с. а) НИЛ) a (CONS ... РЕК) НИЛ (((с. а))) а (ВДФ (ВДК НИЛ ... ВОЗВР) РЕК) НИЛ (((ВДК НИЛ.. .ВОЗВР). а) ((с.а)))а (РЕК) НИЛ Теперь в вершине стека находится замыкание для определя- определяемого в блоке выражения. Ниже находится список значений, определяемых в этом блоке, в нашем случае это одно значение — замыкание, представляющее функцию ФАК. Оба замыкания содержат фиктивную вычислительную обстановку. Далее, когда
6.4. Программирование компилятора 187 выполняется команда РЕК, это фиктивное окружение подправ- подправляется таким образом, чтобы саг этого списка содержал список определяемых значений. Таким образом, машина переходит в состояние НИЛ а (ВДК НИЛ... ВОЗВР) (НИЛ НИЛ НИЛ.НИЛ) где теперь из-за функции завершение а удовлетворяет соотно- соотношению а=(((с.а)).НИЛ). То есть а является вычислительной обстановкой с единственной позицией @.0), где хранится зна- значение (с.а). Поэтому, когда в списке управления встречается команда ВВОД@.0), для функции ФАК будет введено правиль- правильное замыкание, а когда делается рекурсивный вызов из про- программы с, через команду ВВОД A.0), это замыкание будет снова восстановлено, так как а действительно содержится как окружение в этом замыкании. На этом заканчивается описание машинных команд, которые строятся для каждого правильного Лисп-выражения. В сле- следующем разделе будет показано, как можно записать функцию, которая берет в качестве аргумента правильное выражение и выдает как результат соответствующий список-программу. 6.4. Программирование компилятора Мы хотим определить функцию компил(е), которая по дан- данному правильному выражению е вычисляет программу, опреде- определенную для е в предыдущем разделе. Можно просто определить функцию, которая реализует операцию *, как это определено выше, скажем комп\(е, п)=е*п и тогда будем иметь компил (е)=комп 1 (е, НИЛ) Однако при этом будет многократно применяться функция соединить. Избежать этого можно, используя накапливающий параметр с, в который по частям поступает программа. Функция комп (е, п, с) определяется так, что комп(е, п, с)=е*п\с т. е. если комп (е, /г, с) вычисляется для правильного выражения е, именного списка п и списка-программы с, то строится список, содержащий команды для е, за которыми следуют команды программы с. Тогда можно определить компил (е)=комп (е, НИЛ, НИЛ)
188 Гл. 6. Архитектура машины для функциональных программ Функция комп используется специальным образом. Напри- Например, так как (ПЛЮС ег е^т^е^п^^п^ПЛЮС) то если е имеет вид (ПЛЮС ех е2), значением комп (е, п, с) будет комп(еъ п, комп(е2, п> cons (ПЛЮС, с))) Отметим, что команды ПЛЮС, е2*п и е&п встраиваются в спи- список перед программой с. Чтобы определить комп, потребуются вспомогательные функции. Во-первых, это сложные селекторы cadr(x) г« car(cdr(x)) caar(x) s= car(car(x)) cddr(x) =3 cdr(cdr(x)) caddr(x) = car(cdr(cdr(x))) cdar(x) ss cdr(car(x)) cadddr(x) = car(cdr(cdr(cdr(x)))) Затем нужны функции, которые позволяют выбирать из списка определений ((xi.ei). . .(xh-ek)) по отдельности переменные (хг. . . . . ,xk) и выражения (ег. . .ek). Имеем перем(а) s= если равно(а,НИЛ) то НИЛ иначе cons(caar(d), nepeM(cdr(d))) выр(а) ss если равно(а,НИЛ) то НИЛ иначе cons (cdar (d), ebip(cdr(d))) Эти функции использовались в интерпретаторе гл. 4. Наконец, используется функция компспис(еу я, с) для компиляции списка выражений е= (ех. . ,ek) и построения программы (ВДК НИЛ)\ек*п\(С0Ы8)\. . .\ех*п\(CONS)\c Как и раньше, до конца этой главы будем пользоваться символь- символьными названиями команд машины. В приложении 2, где компи- компилятор дан в виде, предназначенном для ввода в машину, на- названия команд заменены соответствующими числовыми кодами: еынспис (е, п, с) = если равно(е,НИЛ) то cons (ВДК, cons (НИЛ, с)) иначе компспис(саг(е), п, комп(саг (е), п, cons(CONS, с)) При помощи этих вспомогательных функций программиро- программирование функции комп выполняется совсем просто, как это пока- показано в F.3). комп(е, п, с) 5S если атом(е) то cons(BBOU, cons(Mecmo(e, п), с)) иначе если равно(саг(е), КОД) то cons(BMK, cons(cadr(e), с)) иначе если равно(саг(е), ПЛЮС) то комп(сааг(ё), п, комп(саай,(е), л, cons(FIJIIOC, с))) иначе ... аналогично для МИНУС, УМН, ДЕЛ, ОСТ, РАВНО, МР, ...
6.5. Завершение описания семантики 189 если равно(саг(е), CAR) то комп(сааг(е), cons(CAR, с)) иначе F.3) ... аналогично для CDR, АТОМ, ... если равно(саг(е), CONS) то KOMn(caddr(e), n, комп(сааг(е), п, cons(CONSt с))) иначе если равно(саг(е), ЕСЛИ) то \комп(сааг(е), я, cons(BbIB, cons(mo, сопБ(иначе, с)))) где mo — KOMn(caddr(e), п, (ВОССТ)) и иначе = KOMn(cadddr(e), n, (ВОССТ))} иначе если равяо (саг(е), ЛЯМБДА) то {соп8(ВДФ, cons(tne.io, с)) где meAO = KOMti(caddr(e), cons(cadr(e), /г), (ВОЗВР))} иначе если равно(саг(е), ПУСТЬ) то \{компспис(арг, п, сопьфДФ, cons(meAO, cons(TlPuM, с)))) где тело = комп(сааг(е), mt(B03BP))} где m = cons(nepeM(cddr(e))y n) и арг = выр(сааг(е))} иначе если равно(саг(е),ПУСТЬРЕК) то {{co/zs@OPA^, компспис(арг, т, сопз(ВДФ, cons (тело, cons(PEK, с))))) где тело = комп(сааг(е), т, (ВОЗВР))} где m = cons(nepeM(cddr(e)), n) и apa = eup(cddr(e))} иначе компспис(саг(е), л, комп(саг(е), л, corcs (ПРИМ, с))) Полный компилятор можно определить единым выражением, записав его в виде гдерек компил = к(е) комп(еМИЛ,{ПРИМ СТОП)) и комп = Х(еу п, с)... и компспис = k(et п, с)... и вб//? = X (d)... и /2е/?еи* = A, (d)... и л*есто = А,(х, /г)... и т. д. Необходимость конструкции (ПРИМ СТОП) объясняется в следующем разделе. Наконец, этот компилятор может быть пре- преобразован в конкретной форме по правилам гл. 4. Компилятор в этом виде дан в приложении 2 и составляет существенную часть инструментальной Лисп-системы, описанной в гл. 11 и 12. 6.5. Завершение описания семантики В предыдущих разделах этой главы было дано формальное описание абстрактной машины (SECD-машины) и функции, которая компилирует программу для выполнения на этой ма- машине. Было точно определено, какое значение придается каж- каждому правильному выражению в языке Лисп. Этим задается формальное описание семантики языка. Возникает вопрос, все ли определено, что мы должны были определить, и все ли пра- правильные выражения имеют тот смысл, который нас устраивает.
190 Гл. 6. Архитектура машины для функциональных программ Ответ на оба этих вопроса отрицательный. В этом разделе будет рассмотрена ограниченность приведенных здесь определений и проанализированы причины, по которым не даются более силь- сильные определения. Подытожим то, что определено. Функция компил(е) берет правильное выражение е, т. е. Лисп-программу в конкретной форме, и выдает в качестве результата список с. Выражение еу которое компилируется функцией компил(е), имеет своим зна- значением функцию, и список с имеет вид (. . . программа ввода замыкания функции... ПРИМ СТОП) т. е. это программа, которая во время ее выполнения вводит замыкание в стек и применяет его. Эта программа требует, чтобы в стеке находились соответствующие значения фактиче- фактических параметров функции е. Таким образом, если SECD-машина запускается из состояния (v) НИЛ с НИЛ и работает по правилам перехода состояний, то в результате этого функция с будет применена к значениям и и по свойству чистого результата результат этого применения останется в стеке. То есть конечное состояние имеет вид (х) НИЛ (СТОП) НИЛ где х является результатом применения функции. Например, если компилируется функция соединить, приведенная в разд. 6.1, то получается программа рассмотренного выше вида. Если эту программу ввести в машину, в стеке которой находится список (((А В С D) (Е F G Н))) машина начнет вычислять и закончит работу со стеком ((А В С D Е F G Н)) Этот процесс выполнения скомпилированной программы можно поручить функции выполнить (с, и), таким образом, чтобы с и v использовались для задания начального состояния ма- машины, как это указывалось выше, а результат функции оста- оставался бы после окончания выполнения программы в вершине стека. То есть выполнить (с, v)=x. Имея это, можно определить семантику всякой правильной Лисп-программы. Лисп-программа является функцией. Резуль- Результат применения этой функции / к списку аргументов а является значением выполнить (компил([), а)
6.5. Завершение описания семантики 191 как это обсуждалось в разд. 6.1. Другими словами, если SECD- машина заканчивает работу и получает некоторое значение в вершине стека, то это значение является результатом приме- применения / к аргументам а. Имеется ряд замечаний, которые следует сделать относи- относительно этого определения, если воспринимать его буквально. В частности, как об этом говорилось в начале раздела, нужно исследовать некоторые из наиболее тонких сторон этого опре- определения семантики. Мы приведем здесь только некоторые при- примеры и не будем пытаться дать полный перечень достоинств и недостатков этого определения; при этом наша цель скорее объ- объяснить, чем проанализировать этот метод. Приведем примеры четырех конкретных случаев. 1. Определение точно относительно наиболее важных свойств языка. Например, определен механизм обработки параметров, обычно называемый вызовом по значению. 2. Многим правильным выражениям придается значение, даже если оно не предусматривалось. Например, вызов функ- функции с излишними фактическими параметрами. 3. Некоторым правильным выражениям не придается зна- значения, даже когда это и возможно. Например, локальные оп- определения блока ПУСТЬРЕК. 4. В некоторых специальных случаях правильных выраже- выражений, значение которых не может восприниматься интуитивно, этим определением дается разумное или приемлемое значение. Например, функция без параметров. Для этих четырех случаев, можно, разумеется, привести много других примеров. Выбранные нами примеры подкреплены последующим материалом, но у читателя остается возможность исследовать это определение семантики подробнее в упражне- упражнениях к настоящей главе. Рассмотрим определение, данное для вызова функции: (е ех ... ек)*п = (ВДК НИЛ) \ek*n \ (CONS) |... | ех * п | (CONS) \e*n\ (ПРИМ) Каждый из фактических параметров еи . . ., eh вычисляется до входа в функцию. Формальные параметры функции ведут себя как локальные переменные, значениями которых являются зна- значения соответствующих фактических параметров. Это механизм, который в Алголе определен как вызов по значению. Термин «вы- «вызов» здесь неудачен, но традиционен. Он используется для обо- обозначения способа, которым параметры передаются в функцию или процедуру. Слово «передача» было бы здесь лучше, но мы воспользуемся принятой терминологией. Вызов по значению противопоставляется альтернативному методу, вызову по имени.
192 Гл. 6. Архитектура машины для функциональных программ При вызове по имени выражения фактических параметров под- подставляются вместо формальных параметров. Здесь возникает проблема со свободными переменными в выражениях фактиче- фактических параметров, состоящая в том, что контекст, необходимый для вычисления тела функции, может не содержать значений для этих переменных или, даже хуже, может содержать зна- значения для локальных переменных с теми же самыми именами, это так называемая коллизия имен. Мы не будем рассматривать эту проблему. Классическим примером различия в значениях при вызове по значению или по имени является следующее определение функции: f(at b, с)==если а то Ь иначе с Рассмотрим вызовы f(amoM (х), х, саг(х)) !(равно(уу 0), 1, 100 4-0) Если они интерпретируются в инструментальном Лиспе согласно данной здесь семантике, каждый из фактических параметров функции / должен быть вычислен до входа в функцию. Таким образом, даже в том случае, когда х является атомом, мы по- попытались бы взять его саг и получили бы неопределенный ре- результат. Аналогично, даже если у есть нуль, мы попытаемся вычислить \00-±-у и снова придем к неопределенному резуль- результату. Однако, если бы использовался вызов по имени, подста- подстановка невычисляемых на самом деле выражений для фактиче- фактических параметров вместо формальных привела бы к тому, что эти два вызова давали бы те же самые значения, что и выражения если атом(х) то х иначе саг (х) если равному, 0) то 1 иначе 100 ч- у совершенно правильно определенные оба. Однако это не явля- является недостатком определения. Определен механизм вызова по значению, именно он и имеется в виду. Для функционального программирования этот механизм намного полезнее других и применяется в большинстве случаев. Мы вернемся, однако, к механизму передачи параметров в гл. 8, где рассматривается более мощный, хотя и реже используемый механизм. Серьезным недостатком нашего определения семантики яв- является его безразличие к ошибочным программам на инструмен- инструментальном Лиспе. Во многих случаях проверка в компиляторе или на машине могла бы выявить неопределенность, когда ни- никакое значение не может иметься в виду. Единственной при- причиной отсутствия таких проверок является стремление к крат- краткости определения. Вследствие этого придается значение очень
6.5. Завершение описания семантики 193 необычным конструкциям. Например, следующее выражение имеет значение А: (ПУСТЬ (ФН (КОД А) (КОД В)) (ФН ЛЯМБДА (X) X)) Лишний фактический параметр подается в (тождественную) функцию ФН и проходит совершенно незамеченным. Таким образом, как определение, так и последующая возможная реа- реализация инструментального Лиспа окажутся совершенно инерт- инертными относительно ошибочных программ. Понимая всю важ- важность проблемы — поскольку пользователь системы вправе ожидать, что система чувствительна к ошибкам, которые она может заметить,— мы тем не менее сосредоточим внимание на том, чтобы придавать правильные значения верным выраже- выражениям, и в основном игнорируем значения неверных выражений. Определение блока ПУСТЬРЕК является частичным. Так как программа, данная для компиляции блока (ПУСТЬРЕК е (Jti.^i). . . (Xfe.efc)), вычисляет каждое из определяемых выра- выражений еъ . . ., ek в фиктивном окружении (fi.—), то любое оп- определяемое выражение, которое во время вычисления имеет доступ к какой-либо из переменных хъ . . ., xky приведет к ошиб- ошибке. Это потому, что, пока не заменен символ Q, не может быть выполнен ВВОД (O.t). Отсюда единственной верной формой для ?ъ . • ., Сь. будет та, где считается, что хи . . ., xk находятся внутри ^-выражений, в которые не входят до начала вычисления определяемого выражения. Таким образом, простые случаи, подобные следующему: (ПУСТЬРЕК Y (Y CAR X) (X КОД (ABC D)) приводят к неопределенному результату. Простое правило, которое ограничивает инструментальный Лисп, но гарантирует по крайней мере, что все блоки ПУСТЬРЕК начинают вычис- вычисление своих определяемых выражений, состоит в том, что каж- каждое определяемое выражение должно быть Х-выражением. Это означает, что все локальные переменные, вводимые рекурсивным блоком, должны иметь своим значением функцию. Для такого ограниченного инструментального Лиспа наше определение семантики корректно. Рассмотрим, однако, блок (ПУСТ ЬРЕК (В СОЕДИНИТЬ (КОД A23)) В)) То есть мы хотим создать вычислительную обстановку, в кото- которой переменная В связывается с циклическим списком, изобра- 7 cVi 929
194 Гл. 6. Архитектура машины для функциональных программ женным на рис. 6.2. Обидно, что ПУСТЬРЕК не сможет это правильно реализовать. Позднее мы рассмотрим, как расширить Лисп, чтобы это было возможно Рис. 6.2. В заключение наших разрозненных замечаний рассмотрим такие случаи, когда наше определение приводит к значениям выражений, которые не только приемлемы, но и полезны. Рас- Рассмотрим функцию без параметров. Синтаксис языка позволяет такие записи. Например, запись (ПУСТЬ (ФН ЛЯМБДА НИЛ )) допустима, и вызов функции ФН внутри определяемого выра- выражения может осуществляться также вполне допустимой синтак- синтаксически конструкцией (ФН). Если рассмотреть построенную программу и ее выполнение, то видно, что, когда вызывается функция, в окружении устанавливается соответствующий пу- пустой список значений фактических параметров и затем при вычислении тела функции глобальные переменные занимают правильные позиции (i./), где ь^\. Хотя функция без парамет- параметров и не представляется очень полезной, ее можно использовать в случае задержки вычисления выражения, когда это вычисление дорогостоящее и в то же время не необходимое. К этому мы также вернемся в упражнениях и последующих главах. Другой простой пример использования функции без параметров: пусть требуется вычислить значение выражения / и это выражение не является функцией. Тогда можно скомпилировать программу для функции (ЛЯМБДА НИЛ е) и эта программа будет правильно оперировать с пустым списком фактических параметров. То есть значение выражения е дается функцией выполнить(компил((ЛЯМБДА НИЛ е)), НИЛ) Наиболее важным свойством определения Лиспа, данного в этой главе, является то, что оно дает точное описание любой реализации, сделанной для обеспечения инструментального Лиспа. И на самом деле реализация, приведенная в гл. 11, пол- полностью воспроизводит определенную здесь семантику. Разуме- Разумеется, при создании конкретной формы этого определения име-
Упражнения 195 лась в виду именно эта реализация, и фактически это уже аб- абстрактная версия реализации. По этой причине описание, по- подобное приведенному, называется операционным. Преимущест- Преимуществом определения, носящего более строгий математический ха- характер, где значением Лисп-выражения являются более респек- респектабельные в математическом отношении объекты, чем программа на языке SECD-машины, возможно, будет его простота или на- наглядность. Оно послужило бы солидным базисом для доказа- доказательства корректности Лисп-программ или реализации инстру- инструментального Лиспа. Оно, вероятно, привело бы к проникно- проникновению в суть некоторых вещей, скрытых от нас деталями нашего определения. Однако неоспоримая польза операционного оп- определения состоит в том, что оно разрешает многие проблемы реализации Лиспа, и это перевешивает преимущества более математического определения в свете нашей конечной цели. Это определение рассчитано на интуицию программиста именно потому, что оно операционное, в то время как математик может считать более наглядным другой тип определения. Упражнения 6.1. Введите машинную команду ПРИМО, которая применяет функцию, не имеющую аргументов. Она требует только замыкания в стеке. Покажите, что программа (программа ввода замыкания ПРИМО) имеет тот же эффект, что и (ВДК НИЛ программа ввода замыкания ПРИМ) 6.2. Введите машинную команду ПРИ MX, которая применяет функцию с одним аргументом. Она требует замыкания и аргумент, но аргумент не встраивается в одноэлементный список. Какая программа с командой ПРИМ будет эквивалентна следующей: (программа ввода аргумента программа ввода замыкания ПРИМ\) 6.3. Спроектируйте машину, в которой регистр t объединяет функции стека s и хранилища d. Проект такой машины служит доказательством того факта, что стек и хранилище синхронизированы так, что могут быть реализо- реализованы как единый стек. Проект требует, чтобы были реализованы все команды SECD-машины. 6.4. Пусть имеются команды ПРИМО и ПРИ MX, описанные в упр. 6.1 и 6.2. Опишите списки программ в специальных случаях вызова функции с одним аргументом и без аргументов, используя эти команды. 6.5. Расширьте Лисп, включив правильное выражение (ИХ ех е2) с тем свойством, что его значением является результат логической операции «Я» над ег и е2, т. е. это значение есть И только в том случае, если и е1у и е2 имеют значение Я, но при этом если ег есть Л, то е2 даже не вычисляется. Какая программа строится для этого выражения? То есть какой вид имеет (ИХ ег e2)*ri> Аналогично определите программу для выражения (ЯЛЯ1 ег е2), ко- которое есть Л, только если как elt так и е2 оба имеют значение Л, и е2 не вычис- вычисляется, если ei есть И (см. упр. 5.4).
196 Гл. 6. Архитектура машины для функциональных программ 6.6. При каких обстоятельствах возникает при компиляции последова- последовательность ПРИМ ВОЗВР? Почему она избыточна? Введите команду, заме- заменяющую эту последовательность. Когда при компиляции возникает такая команда? 6.7. Пусть мы расширили инструментальный Лисп, включив конструк- конструкцию (МЕТКА х е), которая имеет то же самое значение, что и (ПУСТЬРЕК х (х.е))> где х — переменная, а е — правильное выражение. Определите (МЕТКА х ё)*п и результирующее действие этой программы на SECD- машине. 6.8. Вызов по имени можно моделировать в Лиспе, включая параметры, которым требуется этот механизм, в функцию и вызывая их всякий раз, когда они требуются в теле функции. То есть вычисление фактических параметров задерживается маскировкой в Я-выражении. Покажите, что функцию/(a, bt с) из этой главы можно переопределить и получить функцию, имеющую такие значения, которые получались бы при механизме вызова по имени. 6.9. Рассмотрите следующее определение факториала через функцию /: фак (л)з=/ (я= 0,1 ,лХ фак (п—\)) В чем здесь ошибка? Почему эта функция не выдает ожидаемое значение? 6.10. Определите факториал аналогично упр. 6.9, но через модифициро- модифицированную функцию f из упр. 6.8. Рассмотрите, как интерпретируется и вычис- вычисляется на SECD-машине факF). Сколько раз выполняется операция МИНУС, возникшая при компилировании выражения п—1?
Глава 7 НЕДЕТЕРМИНИСТСКИЕ ПРИМИТИВЫ И ПРОГРАММЫ С ВОЗВРАТАМИ В этой главе начинается исследование, которое будет продол- продолжено в двух следующих главах. Его цель — найти более выра- выразительные формы для некоторых типов программ. Другими словами, здесь определяются новые примитивы в нашем чисто функциональном языке или новые методы использования уже существующих примитивов, так что программы, которые без этого были бы длинны и трудны для понимания, станут короче и относительно понятнее. Наша цель — показать, что опреде- определение соответствующих примитивных операций может в зна- значительной степени упростить задачи в специальных областях применения ЭВМ. Однако при определении точной семантики этих небольших расширений демонстрируется также мощность операционных определений гл. 6. Чтобы придать точный смысл каждому из новых примитивов, добавленных в язык, соответст- соответствующим образом расширяется SECD-машина 7.1. Недетерминистские примитивы Рассмотрим задачу о включении в список атома. Если спи- список длины я, то имеется п~\-\ различных позиций, куда может быть включен атом. Предположим, мы хотим определить функ- функцию, которая по заданным списку и атому включает атом на произвольную позицию списка и выдает этот список в качестве результата. Нам безразлично, какой из возможных (п+1) ре- результатов мы получим. Такая функция — назовем ее включить (х, а), где х является атомом, а а — списком,— легко определя- определяется. Если а=НИЛ> то возможен единственный результат cons(x, НИЛ). Однако если афНИЛ, то можно получить либо cons(x, а), либо cons(car(a)y включить(ху cdr(a))). То есть либо х помещается на первое место, либо рекурсивно применяется включить, чтобы разместить произвольно х в списке cdr(a) и добавить этот список к саг (а). Для того чтобы записать такую функцию, нужно уметь обозначать произвольный выбор между двумя альтернативами. Если обозначить через гх либо е2
198 Гл. 7. Недетерминистские примитивы правильное выражение, результатом которого является либо е1у либо еъ но неизвестно, какое именно из них, тогда определение функции включить (х, а) может быть записано следующим об- образом: включить (ху я) = если равно(а,НИЛ) то сопь(х,НИЛ) иначе {cons (x, а) либо cons (car (а), включить (ху cdr (а)))} Для иллюстрации использования этой функции рассмотрим, как можно выполнить произвольную перестановку элементов списка. Следующая функция выдает по исходному списку спи- список, полученный произвольной перестановкой его элементов: перестф) = если равноф, НИЛ) то НИЛ иначе включить(саг(Ь)уперест(саг(Ь))) Ясно, что если список Ь пуст, то и результатом является пустой список. Если же Ь не пуст, то выполняется произвольная пере- перестановка списка cdr(b) и затем включается сагф) на произволь- произвольное место этой перестановки. Аналогичным примером является следующая функция, ко- которая выдает произвольное целое число в промежутке от 1 до п: выбор(п) =i если п = 1 то 1 иначе {выбор (п — 1) либо п) Эта простая функция позволяет нам проиллюстрировать осо- особенно неприятные качества такого расширения нашего чисто функционального языка. Функция пара(п), определенная ниже, выдает произвольную пару целых чисел, дважды вызывая функ- функцию выбор (п): пара (n)~cons (выбор (п), выбор (п)) Возникает вопрос, равны ли числа, составляющие пару. До сих пор в нашем чисто функциональном языке, если некоторое подвыражение дважды встречалось в одном и том же контексте, мы могли с уверенностью предполагать, что оно имеет одно и то же значение. Таким образом, мы могли извлечь это значение и сделать его значением локальной переменной без изменения смысла программы в целом. Если придерживаться этого свой- свойства, нужна уверенность, что оба вызова функции выбор (п) в определении функции пара(п) дают один и тот же результат. Тогда можно было бы написать пара(п) = {пусть х = выбор(п) cons(x, х)\ как альтернативное определение функции пара(п). Иначе го- говоря, функция пара(п) выдает пару одинаковых чисел.
7.1. Недетерминистские примитивы 199 Это приводит к дилемме. Как можно определить функцию, которая выдает произвольную пару чисел из множества всех п2 возможных случаев? Конечно, можно было бы вернуться к ис- исходным принципам и определить функцию прямо через опера- операцию либо, например пара(п) == если /2=1 то cons( 1,1) иначе {пусть р = пара(п—1) р либо cons(car {p),?) либо cons{n, cdr (p)) либо cons(n,n)\ Это определение является более узким и гораздо менее удов- удовлетворительным по сравнению с первоначальным. Мы всегда предполагали, что подстановка определяющего выражения (е пусть х=е ё) вместо каждого вхождения пере- переменной х9 которой оно присваивается, не изменяет смысла про- программы. Если придерживаться этого свойства подстановки, то нужно согласиться с такими определениями, как эта более узкая вторая версия функции пара(п). Чтобы не делать этого, до конца настоящей главы будем придерживаться такой интер- интерпретации выражения ех либо е2, когда выбор его значения точно не определен в том смысле, что, даже зная значения ех и e2i ни- ничего другого нельзя сказать о значении ех либо е2> как только то, что оно является одним из этих двух. Этот факт лежит в основе нашей дилеммы. Значение выражения в нашем чисто функциональном языке до сих пор всегда определялось только значениями составляющих его выражений. Теперь мы отказы- отказываемся от этого свойства, но только до окончания этой главы. Обычно при вычислении ег либо е2 действует такое рассуждение. Сначала вычисляются как еъ так и е2, а затем подбрасывание монеты решает, какое из этих значений выбрать. Таким обра- образом, например, вычисление {1 либо 2}— {1 либо 2} требует двух бросаний монеты, и поэтому нет гарантии, что результатом будет нуль. Решение выбрать такую интерпрета- интерпретацию означает, что мы отказываемся от возможности вынесения за скобки (от свойства подстановочности) во всем расширенном языке, так как нет уверенности в том, что то или иное выражение не требует подбрасывания монеты. Это серьезная потеря, и мы вернемся в конце главы к обсуждению того, восполняет ли ее наше расширение языка. Возвратимся к проблеме построения произвольной переста- перестановки. Предположим, что требуется ограничить класс допусти- допустимых перестановок. Например, мы хотим, чтобы результат пе- реет (а) удовлетворял бы некоторому предикату р. Можно было
200 Гл. 7. Недетерминистские примитивы бы написать функцию, которая проверяла бы перест(а) и, если результат не удовлетворял предикату /?, снова вызывала бы функцию перест(а)} в надежде, что новое подбрасывание монеты будет более удачным. Однако из-за произвольного характера такого подбрасывания нет гарантии, что такой процесс приведет когда-нибудь к перестановке, удовлетворяющей предикату р. Следует еще немного расширить наш язык, чтобы преодолеть эту трудность. Добавим в наш функциональный язык правильное выражение нет, которое имеет следующий смысл. Если при выполнении программы существует последовательность выборов, исключаю- исключающая необходимость вычисления подвыражений вида нет, то именно такие выборы и должны реализоваться. Другими сло- словами, хотя результат каждого бросания монеты, необходимого для определения значения выражения ех либо е2, и произволен, окончательный эффект всей программы будет таков, что исход этих бросаний исключает необходимость вычисления нет. При таком соглашении можно записать нашу функцию ограничен- ограниченных перестановок в следующей форме: огрперест{а) = пусть b = перест(а) если р (Ь) то Ь иначе нет Это очень необычная программа. В соответствии с определением нет получаем, что если имеется значение перест{а), которое удовлетворяет предикату р, то оно и должно быть выдано в качестве результата функции огрперест(а). Чтобы лучше понять использование нет в соединении с либо, рассмотрим следующий возможный способ интерпретации про- программы, использующей эти конструкции. Предположим, имеется абстрактная машина, приспособленная для выполнения таких функциональных программ. Когда такая машина наталкивается на выражение ег либо е2, она порождает свою копию с таким же точно состоянием, исключая только то, что первая машина действует так, как будто она натолкнулась на еи а вторая ма- машина действует так, как будто она натолкнулась на е2. Таким образом, в процессе вычислений количество машин увеличива- увеличивается. Если такая машина натолкнется на выражение нет, она отключается, так как последовательность выборов, приведшая к ее созданию, не удовлетворяет нашим условиям. Если неко- некоторая машина заканчивает свои вычисления, то эта машина определяет возможный результат всей программы, он и может быть взят в качестве окончательного результата, а остальные машины отключаются. Возможно, конечно, что ни одна из машин не закончит свою работу таким образом, но тогда программа не имеет смысла.
7.L Недетерминистские примитивы 201 В качестве простого примера этих недетерминистских прими- примитивов рассмотрим генерирование перестановок с тем свойством, что два последовательных элемента перестановки удовлетворяют предикату q{x, у). Можно использовать функцию огрперест(а), определив функцию р(Ь) (см. 7.1). р (Ь) = если равноф, НИЛ) то И иначе если равно(саг(Ь), НИЛ) то И иначе G.1) если q(car(b), car(cdr(b))) то p(cdr(b)) иначе Л Однако другой способ сделать то же самое, когда каждая пара последовательных значений проверяется по мере их вы- выбора, иллюстрирует ту важную черту использования надтерми- нистских примитивов, что нет может быть включено в вычисля- вычисляемое выражение на произвольную глубину. Определим нашу функцию соотношением G.2). огрперест(Ь) е= если равноф, НИЛ) то НИЛ иначе огрвключ(саг (Ь),огрперест(саг(Ь))) G.2) огрвключ(х, а) е= если равно(а, НИЛ) то cons(xf НИЛ) иначе {встроить(ху а) либо встроить(саг(а), огрвключ(х> cdr(a)))} встроить^, у) = если q(xy car(y))io cons(x, у) иначе нет Здесь мы видим, что огрперест (Ь) и огрвключ (х, а) имеют ту же самую форму, что и наши первоначальные функции перестф) и включить (х, а), за исключением того только, что они вызы- вызывают для конструирования перестановки функцию встроить (х, у), а не cons(x, у). Здесь функция встроить (х, у) может опираться на тот факт, что у непустой список, и просто проверять, удовле- удовлетворяется ли предикат q(x, car (у)). Если это условие не выпол- выполняется, тогда должно вычисляться нет. Наши правила для либо и нет таковы, что вычисление нет по возможности исклю- исключается, и, таким образом, перестановка, удовлетворяющая нашим ограничениям, будет построена, если только она сущест- существует. В этом примере следует обратить внимание на то, что нет может встречаться на произвольной глубине рекурсивных обра- обращений к функциям. Правила, приведенные нами, позволяют это. В самом деле, если бы это было не так, то это было бы со- совсем бесполезное расширение. Внимательный анализ точного определения смысла примитивов либо и нет в следующем раз- разделе покажет, что эта возможность принята во внимание. 7.2. Интерпретация недетермикистских примитивов Можно дать очень точную интерпретацию недетерминист- недетерминистских примитивов, включив их в инструментальный Лисп и затем расширив SECD-машину так, чтобы она могла выполнять
202 Гл.7. Недетерминистские примитивы скомпилированную программу. Выберем очевидные синтакси- синтаксические формы для расширения Лиспа, т. е. правильными выра- выражениями теперь становятся (ЛИБО ег е2) (НЕТ) Как обычно, подвыражения формы ЛИБО тоже должны быть правильными. Теперь необходимо добавить две команды SECD- машины, которые соответствовали бы этим двум формам в ис- исходном языке. Эти команды имеют имена ЛИ Б и ТУП (тупик). Название ЛИБ созвучно команде ВЫБ и наводит на мысль об их сходстве, а также отличает машинную форму (ЛИБ) от ис- исходной (ЛИБО). Как правило, процесс интерпретации имеет две стадии: сначала даются правила трансляции исходных форм в команды SECD-машины, а затем правила изменения состояния SECD- машины при выполнении двух новых команд. Сначала рассмот- рассмотрим правила компиляции (ЛИБО е, е2)*п = (ЛИБ ^ * я | (ВОССТ) е2*п\ (ВОССТ)) (НЕТ)*п = (ТУП) Мы видим, что компиляция правильного выражения (ЛИБО ех е2) по отношению к списку имен п дает список управления в форме (ЛИБ сг с2), где сх и с2 просто сгруппированные списки управ- управления, по одному на каждый из возможных выборов. Каждый из этих подсписков заканчивается командой ВОССТ, в этом отношении ЛИБ напоминает ВЫБ. Компиляция правильного выражения НЕТ тривиальна, порождая список управления из одной команды, содержащий другую новую операцию. Теперь SECD-машину нужно расширить, добавив новый регистр, который будем обозначать через г и на который будем ссылаться как на регистр возобновления. Этот регистр будет использоваться как дополнительная память для хранения со- состояния машины в целом. Действительно, целью, которой он служит, является сохранение состояния машины в случае, когда команда ЛИБ делает выбор, приводящий к ТУПику. Когда это случается, состояние машины должно быть возвращено из г, и команда ЛИБ должна сделать другой выбор. Добавление этого нового регистра не затрагивает правила для других ма- машинных команд, они остаются без изменений. Формально, если раньше правила имели вид s е с d-+ s' ё с' df то теперь перепишем их в виде s е с d г ->- s' ё ё d! r чтобы указать, что значение г слева остается без изменений.
7.2. Интерпретация недетерминистских примитивов 203 Теперь можно дать правила для наших новых команд. Сна- Сначала рассмотрим ЛИБ: s е (ЛИБ d c2.c)d r-+ s e et (cd) (s e c2 (cd).r) Если ЛИБ встречается в правильной машинной программе, управляющий список имеет структуру (ЛИБ а с2х). Это озна- означает, что операция ЛИБ имеет в качестве операндов два управ- управляющих списка сх и с2, а остальная программа представлена списком с Если бы мы сделали произвольный выбор между сх ис2и затем хотели бы продолжить вычисления, то следовало бы перевести регистры s, е, с и d в одно из состояний s e cx (cd) или s е с2 (cd). Нужно предусмотреть, чтобы как et, так и с2 оканчивались бы командой ВОССТ. И тогда, поскольку с помещается в начале списка, при встрече команды ВОССТ вычисления будут продол- продолжены, как это и требуется, начиная с этого с Теперь, так как у нас нет уверенности, что выбор сг не приведет к тупику, мы должны сохранить состояние, включающее с2, чтобы соответст- соответствующим образом прореагировать, если тупик действительно встретится. Таким образом, ЛИБ устанавливает состояние s e d (cd) (s e c% (cd).г) которое позволяет продолжать с си но может, если это необхо- необходимо, вернуть машину в исходное состояние и продолжать с с2. Если все выборы, сделанные таким образом, приведут к яв- явному выполнению команды СТОП, то машина останавливается с результатом в вершине стека. Теперь рассмотрим, что делает машина, если встречается команда ТУП. В этом случае обычно предполагается, что предварительно сохраненное состояние находится в списке возобновления. Состояние машины изме- изменяется следующим образом: s е (ТУП) d (s* е' с' d'.r) -+ s' e' с' d! r Это означает, что при встрече ТУЯика состояние регистров s, е, с и d полностью разрушается. Значения s', e\ с1 и d', ко- которые были помещены в регистр возобновления г командой ЛИБ, извлекаются оттуда и машина переходит в состояние, достигнутое на предыдущем шаге, но с альтернативным управ- управляющим списком. Если команда ТУП встречается, когда регистр возобновления г пуст, машина останавливается без результата. Этот факт отвечает переходу состояния s е (ТУП) d НИЛ -> НИЛ е (ТУП) d НИЛ который аналогичен переходу состояния при команде СТОП, исключая только то, что в рассматриваемом случае разрушается стек 5. Рассмотрим теперь способ, которым расширенная SECD-
204 Гл.7, Недетерминистские примитивы машина интерпретирует правильное выражение {ЛИБО ех е2). Ее поведение базируется на процессе, описанном в предыдущем разделе, т. е. на порождении отдельных абстрактных машин для реализации всех возможных выборов и остановке, когда одна из машин достигает успешного завершения. Однако в действительности расширенная SECD-машина будет специаль- специальным образом моделировать вычисления этих абстрактных машин, и позднее мы увидим, что имеются ограничения на смысл наших недетерминистских примитивов. При вычислении выражения {ЛИБО ег е2) SECD-машина ведет себя так, как будто бы ей встретилось просто выражение ех. Если это приведет к успеш- успешному завершению программы, то все в порядке. Если, однако, этот выбор ег приведет к выполнению {НЕТ), то SECD-машина ведет себя так, будто никакого ех не было, и выбирает вместо этого е2. Конечно, это снова может привести к вычислению {НЕТ), и тогда программа может не иметь результата. Разница между только что описанным фактическим поведе- поведением SECD-машины и действием коллектива абстрактных ма- машин, описанных в предыдущем разделе, возникает тогда, когда мы имеем дело с незавершающейся программой. Если недетер- недетерминированная программа такова, что некоторое выражение {ЛИБО ег е2), встречающееся в ней, обладает тем свойством, что выбор ех приводит к бесконечной работе SECD-машины, то по приведенным здесь правилам мы получим незавершающуюся программу. В то же время выбор е2 дал бы нам удовлетворитель- удовлетворительный результат. Если бы мы могли следовать правилам преды- предыдущего раздела и сгенерировать отдельные машины, одну для ех и другую для е2) вместо {ЛИБО ег е2), то машина для е2 оста- остановилась бы, а машина для ег была бы отключена. Не так трудно привести правила, которые реализовали бы эту более широкую интерпретацию, но это довольно длительный процесс, и поэтому мы оставляем это в качестве упражнения читателю (упр. 7.8). 7.3. Программы с возвратами Процесс, описанный в предыдущем разделе и состоящий в запоминании целиком состояния вычислений в момент, когда делается выбор, для того чтобы это состояние могло быть пол- полностью восстановлено, если в дальнейшем окажется, что выбор был некорректным, является хорошо известной техникой про- программирования и называется программированием с возвратами. Обычно это используется в программах поиска. Недетерминист- Недетерминистские примитивы, введенные в разд. 7.1, позволяют естественным образом описывать программы с возвратами. В данном разделе мы проиллюстрируем это, рассмотрев решение задачи, типичной для техники программирования с возвратами. Это известная
7.3. Программы с возвратами 205 задача о восьми ферзях. В шахматах ферзи атакуют в любом направлении: по горизонтали, по вертикали или по диагонали шахматной доски. Задача состоит в том, чтобы найти такое раз- размещение 8 ферзей на доске 8x8, чтобы никакие два ферзя не атаковали друг друга. Оказывается, что существует 12 раз- различных решений (исключая симметричные решения, полученные Рис. 7.1. вращением или отражением доски), и поэтому наш поиск не будет бесплодным. Поскольку это не вызывает дополнительных трудностей, будем фактически решать задачу для п ферзей и доски пХп. Решение, которое мы дадим, можно назвать стан- стандартным, оно известно многим и обычно получается каждым программистом, который берется за задачу. Оно почти не тре- требует пояснений. Сначала занумеруем вертикали и горизонтали доски числами от 1 до п. Затем отметим, что сумма номеров столбцов и строк постоянна на диагонали СВ — ЮЗ и линиях, параллельных ей. В свою очередь разность этих номеров постоянна на диагоналях направления СЗ — ЮВ. Это показано на рис. 7.1 для случая п=Ъ. Это дает нам простой метод определения того, не лежат ли квадраты на одной диагонали. Можно представить расположение 12 3 4 Рис. 7.2.
206 Гл. 7. Недетерминистские примитивы ферзей на доске списком пар чисел, где каждая пара есть номер строки и номер столбца. Так, позиция, изображенная на рис. 7.2, представляется списком (A.2) B.4) C.1) D.3)) Разумеется, порядок пар в этом списке безразличен. Для такого представления можно легко определить предикат атака(i, /, позиция), который истинен только в том случае, если квадрат с номером горизонтали i и номером вертикали / атакован одним из ферзей, находящихся на позиции. Этот пре- предикат записан в G.3). атака (i, /, позиция) = если равно(позиция, НИЛ) то Л иначе {пусть V —car(car (позиция)) G Ъ и j'=cdr(car (позиция)) * ' если i' = i то И иначе если /'=/ то И иначе если *-}-/ = *' + /' то # иначе если i—/ = i'—/' то И иначе атакаA> j, саг(позиция))} Здесь мы видим, что, если список ферзей исчерпан, квадрат /, / не атакован. В противном случае проверяем первого из ферзей в этом списке, размещенного в квадрате i", /". Если квад- квадраты лежат на одной горизонтали, или на одной вертикали, или на одной диагонали (направления СВ — ЮЗ или СЗ — ЮВ), то предикат атака (i, /, позиция) выдает значение И. Если ис- исходный квадрат не атакован первым ферзем, то вызывается атака (i, jtcdr (позиция)) для анализа остальных ферзей. Теперь можно определить функцию ещеферзьA,п, позиция), которая по данному размещению (i—1) ферзя на доске пхп пытается разместить новую фигуру на t-й горизонтали. Это де- делается очень просто. Сначала при помощи функции выбор (п) выбирается произвольная вертикаль для ферзя на i-й горизон- горизонтали. Функция выбор (п) определена в разд. 7.1. Если выбранный квадрат атакован одной из прежних фигур, следует использо- использовать конструкцию нет, которая указывает на то, что один из выборов, приведших к этой ситуации, некорректен. Если же квадрат не атакован, размещаем на нем очередного ферзя, и если находимся еще не на последней, n-й горизонтали, продол- продолжаем добавлять ферзей на следующие горизонтали. Эта функ- функция определена соотношениями G.4). ещеферзьA, п> позиция)^ {пусть 1 = выбор(п) если amaKa(it /, позиция) то нет иначе ,7 .v {если i = n то новпозиция иначе * ' ещеферзьA-\-\, я, новпозицчя) где новпозиция ~ cons(cons(ij), позиция)}}
7.3. Программы с возвратами 207 Рассмотрим определение функции выбор (п): выбор(п) = если п=1 то 1 иначе {выбор (п—1) либо п} Это то место в программе, где вводится недетерминизм. Мы видим, что возможен любой выбор целого от 1 до я, и поэтому если существует решение задачи пхп, оно может быть найдено этой программой по правилам, которые даны для наших при- примитивов. Программу можно дополнить главной функцией решение (п)=ещеферзь A, я, НИЛ) которая начинает с пустой позиции и просто пытается ввести ферзя на первую горизонталь. Отсюда вызов решение (8) выдает решение задачи 8x8. Теперь написанная нами программа имеет вполне естест- естественный вид. Можно даже сказать, что она в высшей степени ес- естественна. Насколько это позволяет наш функциональный язык, алгоритм выражается очень непосредственно. Алгоритм доста- достаточно прост и состоит в размещении одной фигуры на каждой из п горизонталей поочередно и так, чтобы никакие два ферзя не атаковали друг друга. Разумеется, произвольный выбор позиции для каждого ферзя не очень хорош. Программа организует про- процесс поиска подходящей позиции при помощи так называемой процедуры с возвратами. SECD-машина реализует возвраты очень простым и очевидным способом. Новая фигура размеща- размещается на доске на позиции, которая не атакована прежними фи- фигурами. Если такое размещение ферзя невозможно, то пересмат- пересматривается позиция предыдущей фигуры и перемещаются эта фигура и все следующие за ней. Если рассмотреть способ, каким интерпретируется конструкция либо в SECD-машине, то видно, что, если частное значение, выданное функцией выбор (п), есть / и это приводит к нет, выберется /+1. Таким образом, если перемещение ферзя не приводит к решению, то сдвигается вправо последняя из размещенных фигур, которая допускает это, а все последующие позиции перевычисляются. Возникает вопрос, является ли эта программа более простым описанием процесса по сравнению с другими, которые мы можем получить на функ- функциональном языке без использования недетерминистских при- примитивов. Чтобы ответить на этот вопрос, нужно построить такую программу. Эта программа будет явно описывать поиск с возвратами. Это только один из возможных способов описания поиска, но, как полагает автор, он не труднее других. Сначала отметим, что заменить следует функцию ещеферзьA,пу позиция), так как именно она использует недетерминистские примитивы. Факти- Фактически мы переопределим ее без этих примитивов и оставим
208 Гл. 7. Недетерминистские примитивы остальную программу нетронутой. Функция npo6a(i, j,n, позиция) = если атакаA,\,позиция) то НИЛ иначе {если i = n то новпозиция иначе ещеферзьA-\-1 ,п, новпозиция) где новпозиция = cons (cons (t, 1),позиция)} отличается от функции ещеферзь (i^n,позиция) только в двух отношениях. Во-первых, в то время как ещгферзь выбирает зна- значение / внутри, функция проба требует его как параметр. Во- вторых, если нет решения, ещеферзь обращается к нет, а проба выдает НИЛ. Далее, эта функция исследует только одну позицию на i'-й горизонтали, и нужна функция, которая будет пробовать их поочередно. Функция гор из (i,j,n,позиция) как раз и делает это: горизA,\ ,пу позиция)^ {пусть новпоз-=пробаAу\,п,позиция) если новпозфНИЛ то новпоз иначе если / = /г то НИЛ иначе горизA,j -f-l,n>позиция)} Здесь исследуется /-я позиция. Если решение не получено, о чем говорит НИЛ, выданный функцией проба, то исследуется (/+1)-я позиция. Если /=л, то функция гориз выдает НИЛ, в противном случае она рекурсивно вызывает себя для иссле- исследования позиции /+1. Имея эту функцию, можно очень просто переопределить функцию ещеферзь (i,n,позиция) следующим об- образом: ещеферзь A,п,позиция)=горизA,1 ,п, позиция) То есть можно добавить ферзя на i-ю горизонталь, пробуя все позиции от 1 до п. Отметим, что вызов функции ещеферзь внутри функции проба делает эти три функции взаимно рекурсивными. Обсуждение их поведения убедит читателя в том, что они явно описывают рассмотренный ранее процесс поиска с возвратами. Имеются другие способы программировать процессы с воз- возвратами. Общим для них является использование функций, которые либо выдают результат, либо восстанавливают преды- предыдущие состояния структуры данных, представляющей в нашем случае размещение ферзей на доске. Интересная альтернатива нашей программе возникает в связи с возможностью абстраги- абстрагироваться от нее и использовать функцию общего назначения (применимую и в других случаях). Эта функция просто пробует каждое из значений от / до п в качестве аргумента функции / до тех пор, пока не будет получен, если это возможно, резуль-
7.3. Программы с возвратами 209 тат, отличный от значения НИЛ: некийЦ, п, f) {пусть s = /(/) если s Ф НИЛ то s иначе если / = п то НИЛ иначе некийЦ+Х, n, f)} Теперь можно переопределить функцию гор из (i,jyn,позиция): eopu3(if /, я, позиция)^некий(\, пу k(j)npo6a (i, /, п,позиция)) Первоначальная недетерминистская форма более непосредственно соотносится с этой, а функция некийЦ, п, f) тесно связана с функцией выбор (п). Сравнение двух типов программ с недетерминистскими при- примитивами и без них субъективно, и читатель должен прийти к самостоятельному заключению. То, что недетерминистский ва- вариант избегает одного уровня рекурсивных определений, ил- иллюстрируется диаграммами вызовов, изображенными на рис. 7.3. Однако, если учесть, что функция некий (j, n, /) сравнима (по трудности понимания) с функцией выбор (я), этот аргумент Рис. 7.3. Рис. 7.4. становится менее состоятельным, так как можно записать диа- диаграмму вызовов, приведенную на рис. 7.4. Что касается программирования с возвратами, то задача восьми ферзей, разумеется, весьма проста. В этом случае име- имеется всего один источник недетерминизма — определение функ- функции выбор (п). В более сложных задачах, где недетерминизм вводится в нескольких местах, непосредственная формули- формулировка в терминах недетерминистских примитивов будет иметь существенные преимущества перед явным программированием
210 Гл. 7. Недетерминистские примитивы возвратов. Преимущества вытекают из возможности рассуждать в терминах произвольного выбора, зная, что будет сделан наи- наиболее предпочтительный выбор. Это мощное средство, но оно легко забывается, если приходится программировать поиск самому. Ценой, которой приходится расплачиваться за столь высо- высокую выразительность, является, как уже упоминалось в начале этой главы, важное свойство подстановочности. Поскольку вы- выражения в функциональном языке могут произвольно обращать- обращаться к выбору, мы теперь не можем быть уверены, что выражение вида f(x) — f(x) будет нулем. Поэтому обсуждение таких про- программ требует других предпосылок, нежели в случае строго детерминированных программ. Только на основе своего собст- собственного опыта читатель может судить о том, не будет ли цена слишком велика и не является ли простой вид алгоритмов лишь иллюзией, которая может привести нас к неверным рассужде- рассуждениям и выводам. Упражнения 7.1. Определите функцию монотон(х), которая истинна только в том слу- случае, когда список х целых чисел является возрастающей последовательно- последовательностью. Затем определите функцию, которая генерирует произвольную моно- монотонную перестановку. 7.2. Определите функцию дерево(х), которая требует в качестве аргумента список атомов. Ее результатом будет произвольное бинарное дерево, листьями которого являются атомы из х, причем каждый атом встречается только од- однажды. Напомним, что бинарное дерево представляется либо атомом, либо как cons двух бинарных деревьев. Листья в дереве должны идти (слева на- направо) в том же порядке, в каком они перечислены в списке х. Отсюда на- набор (дерево (х))=х, где функция набор определена в упр. 2.14. 7.3. Покажите, что (ех либо нет) либо (нет либо е2) есть то же самое, что ег либо е2. (Указание: рассмотрите реализацию на абстрактной машине.) 7.4. Определите функцию подмнож(х), которая берет в качестве аргу- аргумента список атомов, представляющий множество х (т. е. порядок атомов безразличен и они взяты без повторений), и выдает как результат произволь- произвольное подмножество множества х. 7.5. Расширьте интерпретатор гл. 4, придав конструкциям (ЛИБО ег е2) и (НЕТ) тот самый смысл, который придан им в настоящей главе. Это можно сделать двумя способами, и предлагается опробовать оба. Можно предполо- предположить, что в языке, на котором написан интерпретатор, или уже имеются конструкции либо и нет, или эти конструкции отсутствуют. 7.6. Правила, данные в этой главе для команд ЛИ Б и ТУП, таковы, что, когда встречается СТОП, машина останавливается, а результат находится в вершине стека. Если дать новые правила для команды СТОП, то можно было бы продолжать вычисления с состояниями, оставшимися в регистре во- возобновления, так чтобы получить все результаты, возможные для недетерми- недетерминированной программы. Дайте такие правила для команды СТОП в предпо- предположении, что существует способ сохранить все полученные результаты.
Упражнения 211 7.7. Интерпретация, которая дана программам, использующим операции либо и нет, такова, что при каждом вычислении либо делается такой выбор, который по возможности не приводит к вычислению нет. Можно считать, что выражение е, вычисление которого требуег такого выбора, имеет множество значений, и именно тех значений, которые не приводят к нет. Например, зна- значением выражения 1 либо 2 либо 3 либо 4 может быть элемент множества {1, 2, 3, 4}. Пусть далее мы расширили наш строго функциональный язык так, что конструкция все е является правиль- правильным выражением, а его значением является множество всех возможных в ука- указанном смысле значений. Дайте соответствующее расширение инструменталь- инструментального Лиспа, приведите правила компиляции для расширения, а также рас- расширьте соответствующим образом SECD-машину, чтобы точно определить эту концепцию. Что является значением выражения все перест(Ь) где функция перест(Ь) определена в начале разд. 7.1? 7.8. Чтобы суметь получить результат недетерминированной программы, где некоторые выборы могут привести к незавершению программы, требуется реализовать понятие коллектива машин. Примером функциональной про- программы с проблемой завершения является следующее определение факториала: фак(п)^(фак(п—1)Хл) либо (если п=0 то 1 иначе нет) В этой программе существует последовательность выборов, которая приво- приводит к факториалу, все другие выборы приводят либо к нет, либо к незаверше- незавершению. Чтобы реализовать понятие коллектива машин, необходимо только по- построить список SECD-машин. Каждая машина списка продвигается на одну команду вперед по очереди. Команда ЛИ Б приводит к расширению списка на одну машину, ТУП приводит к сокращению списка на одну машину. Оп- Определите SECD-машину, которая работает таким образом. 7.9. Постройте функцию оргперест (а) с тем же самым определением, что и функция с тем же названием, приведенная в разд. 7.1, но которая не ис- использует операции либо и нет, а явно программирует возвраты. 7.10. Постройте функцию всеперест(а), которая генерирует множество всех перестановок элементов списка а. Сравните эту функцию с недетермини- недетерминированной программой, которая выдает произвольную перестановку. 7.11. Постройте функцию слоги(&), которая берет как аргумент список букв wy представляющий слово в английском языке, и выдает в качестве ре- результата произвольную слоговую структуру этого слова по следующему правилу. Если с представляет последовательность согласных букв, a v — последовательность гласных, то в слоговой структуре приемлемы только слоги вида cv, uc, cvc.
Глава 8 ВЫЧИСЛЕНИЯ С ЗАДЕРЖКОЙ —ФУНКЦИОНАЛЬНЫЙ ПОДХОД К ПАРАЛЛЕЛИЗМУ Эта глава посвящается небольшому расширению нашего функционального языка, которое помогает выявить параллель- параллельную природу, присущую таким языкам. Мы расширяем язык, допуская явную задержку вычисления выражений 1], Выраже- Выражения, задержанные таким образом, выдают значения, которые должны быть явно возобновлены, чтобы завершить их вычис- вычисление. Как мы увидим, это средство приводит к такому способу функциональных вычислений, когда каждая функция вычис- вычисляется некоторое время и затем вычисление задерживается, в то время как вычисляется другая функция. В конечном счете каждая функция периодически возобновляется, так что вычис- вычисления развиваются путем квазипараллельного прослаивания тел функций. Фактически расширение языка, введенное в этой главе, является функциональным эквивалентом понятия сопро- сопрограммы. Как и в случае недетерминистских примитивов, мы придадим точный смысл нашему расширению путем расшире- расширения операционного определения SECD-машины. Вычисления с задержкой позволяют описывать функцио- функциональные программы, оперирующие с бесконечными структурами, в частности с бесконечными списками, в то время как до сих пор можно было обращаться только с конечной их частью. Большинство примеров в этой главе служат иллюстрацией этого и призваны показать, что часто бывает проще действовать с бесконечными структурами, чем с соответствующими конеч- конечными. Задерживая почти каждое выражение в программе и запуская вычисления периодическим возобновлением недовы- численного результата, мы создаем способ интерпретации функ- функциональных языков, который называется замедленные вычисле- вычисления. Мы покажем, как просто решаются многие трудные про- проблемы программирования, если использовать замедленные вы- вычисления. В частности, будет показано, что замедленные вычис- вычисления дают более общую и более приемлемую интерпретацию ]) 4}тъ забегая вперед, подскажем читателю, что значением задержанного выражения является некоторое, вообще говоря, другое выражение, которое и подвергается впоследствии возобновленному вычислению.— Прим. ред.
8.1. Вычисления с задержкой 213 конструкции нустьрек. Наконец, мы рассмотрим некоторые интересные вычислительные задачи, в частности задачу генери- генерирования последовательности всех простых чисел. При этом мы введем понятие сети процессов, которое ясно показывает соот- соотношение между вычислениями с задержкой и параллельными процессами. ?.1. Вычисления с задержкой Рассмотрим простую программу (8.1). сумцел(п) s= сумма(це/1между(\, п)) сумма(х) = если равно(х, НИЛ) то 0 иначе саг(х) +сумма (cdr(x)) (8.1) целмежду(т, п) = если т>п то НИЛ иначе соп8(т,целмежду(т-{-1, п)) Вызов сумцел(п) вычисляет 1+2+. . .+я, сумму первых п членов натурального ряда, используя при этом вспомогатель- вспомогательную функцию для конструирования списка целых чисел и сум- суммируя этот список. Естественным представляется следующее поведение этой программы: сначала конструируется полный список A,2, . . ., п) и затем к нему применяется функция сумма. Однако можно предположить и другой способ вычислений, когда функция целмежду (т, п) конструирует только часть промежуточного списка чисел, которая и используется функцией сумма (х). Это возможно, так как функция целмежду (т, п) выдает список элементов точно в том порядке, в каком он тре- требуется для функции сумма(х). Вычисления могут выполняться в следующей последовательности. Начальный вызов сумцел(п) обусловливает вызов функции целмежду (I, п). В предположе- предположении п^\ функция целмежду может сконструировать cons(l, целмежду B f rij) в качестве своего результата. При задержке вычисления целмежду B, п) это значение может быть немедленно выдано как результат целмежду A, п). Оно берется как аргумент функции сумма(х), и, так как оно не есть НИЛ, оно использу- используется при вычислении саг(х)+сумма(саг(х)), что дает l+сумма (целмежду B, п)) Здесь требуется, чтобы задержанное вычисление целмеждуBУ п) было возобновлено, и, как мы уже видели ранее, при задержке вычисления целмежду C, п) это дает 1 +2+сумма (целмежду C, п)) и так процесс вычислений продолжается далее. Нет необходи- необходимости рассматривать промежуточный список как фактически целиком существующий на какой-либо стадии вычислений.
214 Гл. 8. Вычисления с задержкой Процесс вычислений с задержкой проще объяснить, если сде- сделать его явным, т. е. введя новые операторы в наш строго функ- функциональный язык, которые позволят явно задерживать и возоб- возобновлять вычисление выражений. Если е является правильным выражением, то задержать е есть тоже правильное выражение. Значением выражения задержать е является нечто вроде замы- замыкания, мы будем называть его рецептом. Он включает некото- некоторым образом недовычисленное выражение е и окружение, тре- требуемое для его вычисления. Если ё — правильное выражение, то возобновить е' тоже правильное. Значением ё должен быть рецепт, и в этом случае значением выражения возобновить ё является значение, полученное вычислением выражения, за- заключенного в этом рецепте. Таким образом, для всех правильных выражений е выражение возобновить (задержать е) всегда имеет то же самое значение, что и е. Рассмотрим простой пример: регулятор (а,Ь) = если а<0 то а иначе а+возобновить Ь Эта простая функция предполагает, что ее второй аргумент дол- должен быть задержан. Соответствующий вызов имеет, например, такой вид: регулятор (g(x), задержать /(*)) Такое использование операторов задержать и возобновить по- позволяет нам избежать вычисления f(x) в том случае, когда это не является необходимым, т. е. при g(x)<0. Так как нет воз- возможности избежать вычисления функции g(x), поскольку функ- функция регулятор (а, Ь) должна проверять ее значение, то если бы мы в вызове задержали первый аргумент, нужно было бы во- возобновлять все вхождения этого аргумента в определении функ- функции регулятор (а, Ь): регулятор(а, Ь) = если (возобновитьа) < 0 то (возобновить а) иначе (возобновить а) + (возобновить Ь) Тогда соответствующим вызовом был бы регулятор (задержать g(x), задержать /(*)) Эта запись наводит на мысль, что, хотя мы и смогли избежать ненужного вычисления /(х), в то же время нам пришлось дважды вычислять g(x) независимо от его знака. Однако, как мы сейчас объясним, это не так. При описании вычислений с задержкой введено понятие рецепта, и природа этого понятия требует дальнейших пояс- пояснений. Рецепт конструируется так, что значение задержать е представляет значение выражения е в том смысле, что значение, полученное при возобновлении рецепта, является как раз зна- значением е. Не существует способа, которым мы могли бы изменить
8.1. Вычисления с задержкой 215 значение, получаемое в результате возобновления рецепта. Таким образом, если рецепт возобновлен, можно заменить со- содержимое рецепта значением, полученным в результате его возобновления и сделать пометку на рецепте о вычислении. Последующие возобновления этого рецепта могут таким образом избежать перевычислений при обнаружении такой пометки. Отметим, что изменяется при этом только представление зна- значения рецепта. Объект, который начинает свое существование как рецепт, заключающий в себе невычисленное выражение е, заканчивает его как рецепт, включающий значение е. Но он все еще остается рецептом и должен быть возобновлен, чтобы выдать значение е. Рассмотрим, как можно использовать операторы задержать и возобновить для эффективного вычисления функции сумцел(п), которое избегает конструирования полного промежуточного списка п целых чисел. Представим этот промежуточный список структурой, в ко- которой саг есть очередное число, a cdr — рецепт. Тогда модифи- модифицируем определение функции целмежду (т> п) следующим об- образом: целмежду (т, я) = если т > п то НИЛ иначе cons(m, задержать целмежду(т + 1, п)) Теперь нужно соответственно модифицировать функцию сумма (*), чтобы учесть тот факт, что х теперь не простой список, а список, у которого вычисление cdr задерживается. Это делается просто: сумма(х) 2Sесли равно(х,НИЛ) то 0 иначе саг(х)-\-сумма(возобновнть cdr(x)) Определение функции сумцел(п) не требует модификации. Рас- Рассмотрим, как протекает процесс вычисления: функция сумма (х) при вычислении выражения возобновить cdr(x) возобновляет вычисление функции целмежду(гп+1, /г), которая снова выдает объект, cdr которого задержан. В результате промежуточный список чисел строится поэлементно, по требованию функции сумма (х). Этот эффект становится еще яснее, если рассмотреть вызов первые (ky целмежду A, п)) где функция первые (k, x) имеет следующее определение: первые (kf х)=если /г = 0 то НИЛ иначе cons(car(x)y nepebie(k—l,возобновить cdr(x))) Эта функция выдает список k первых членов х, и тогда первые (k, целмеждуA, п)) есть список A, 2, . . ., k) при k^n. Однако часть списка, начиная с k+\ и т. д., никогда не конструируется.
216 Гл. 8. Вычисления с задержкой Всякий раз, как вызывается функция целмежду(т, я), она выдает список, cdr которого задерживается. Таким образом, первый вызов целмеждуAу п) дает нам cons(l, 6), где O есть рецепт, представляющий целмеждуB, /г). Теперь вычисляем функцию первыеAг, cons(l, б)), которая при k>0 возобновляет б. Результат возобновления б есть consB, б'), где б' есть "рецепт, представляющий целмеждуC, л). Вычислениепервые(к, cons(\y б)) теперь продолжается включением вызова первые(k—1, consB, б')). Теперь б' должен быть возобновлен, и мы видим, что вы- вычисления продолжаются по очереди то в функции первые, то в целмеждуу расширяя промежуточную структуру данных, спи- список A, 2, . . ., л), лишь постольку, поскольку это требуется. В следующем разделе мы увидим, что это аналог вычислений по сопрограммам. Тот факт, что при задерживании cdr списка можно избежать вычисления этого списка целиком, означает, что мы можем фак- фактически обрабатывать потенциально бесконечные списки, если в действительности нам требуется только некоторая конечная часть такого списка. Рассмотрим определение функции целот (т) = cons (m, задержать целот (т+1)) Результатом этой функции является список целых чисел, на- начиная с т и далее, т. е. бесконечный список. Было бы бессмыс- бессмысленно вычислять сумма {целот A)), так как вычисления не смогли бы закончиться. Однако выражение первые(k, целот(\)) оп- определено совершенно правильно, это список A, 2, . . ., k). Тот факт, что результат целот(I) есть потенциально бесконеч- бесконечный список, не мешает делу, так как список от k+\ и далее никогда не вычисляется. Из последнего раздела этой главы станет очевидным, что использование потенциально бесконечных списков является мощной и прозрачной техникой для написания многих типов программ. Но прежде рассмотрим следующую программу для вычисления списка первых k сумм натурального ряда, т. е. списка, членами которого являются 1, 1 + 2, 1+2+3, . . ., 1 + +2+. . .+*: первсуммыф) = первыеAг, суммы@,целот{\))) суммы(ау x)^~cons(a-\-car(x)yзадержать суммы(а + саг(х)у возобновить cdr(x))) Функции целот (т) и первые(k, x) те же, что и прежде. Отметим, что функция суммы (а, х) требует списка х, у которого cdr за- задержан. Выдает эта функция список в такой же форме. Аргу- Аргумент а используется для накапливания суммы элементов х. Эти суммы@, целот(\)) образуют бесконечный список A, 3, 6, 10, 15, . . .). Интересной особенностью этой программы явля-
8.2. Операторы задержать и возобновить 217 ется способ, которым определены процессы генерирования последовательности натуральных чисел и частичных сумм на- натурального ряда — здесь не рассматривался вопрос о том, закончится ли этот процесс. Завершение всей программы в це- целом полностью определяется поведением функции первые (k, х). Эта программа является более простой по сравнению с соот- соответствующей программой, которая оперирует с конечными списками. Однако в заключительном разделе этой главы мы познакомимся с еще более простой версией. 8.2. Интерпретация операторов задержать и возобновить Прежде всего отметим, что, кроме соображений эффективно- эффективности, нет других причин расширять наш строго функциональный язык включением операторов задержать и возобновить. О та- такой возможности упоминалось в конце гл. 6, где обсуждалось понятие функции без параметров. Превращение выражения е в функцию без параметров X ( )е дает эффект задержки вычисле- вычисления этого выражения. Прежде чем использовать его значение, следует сделать вызов функции с соответствующим пустым спис- списком фактических параметров. Таким образом, из программы, ис- использующей операторы задержать и возобновить, можно вы- вывести эквивалентную программу, использующую функции без параметров. Делается это простой заменой задержать е —* Х( ) е возобновить е—>е( ) Эта замена для функций первые (k, x) и целот(т) из предыду- предыдущего раздела дает первые (ky x) s= если k = 0 то НИЛ иначе cons(car{x), первые (/?-— 1, (cdr(x))( ))) целот(т) = cons(my i ( ) целот(т + 1)) Вызовы этих функций не изменяются. Таким образом, функция первые(к, целот(т)) будет выдавать список, элементами кото- которого являются числа, m, m+\t. . ., m+k—1. Мы видим, что функция целот(т) выдает cons, где саг есть число, a cdr — функ- функция без параметров. Функция первые(к, х) требует как раз та- такую конструкцию в качестве второго аргумента и осуществляет простой доступ к cdr при помощи вызова (cdr(x))(). В этой про- простой программе каждая функция без параметров вызывается только однажды. Однако если список, получаемый из целот (m)t требовался бы дважды, как, например, в конструкции {пусть х=целот (т) cons (первые(к, х),первыеAу х))}
218 Гл. 8. Вычисления с задержкой то эта реализация, использующая функции без параметров, за- заставила бы вычислять этот список дважды. Но это не то, что тре- требуется от операторов задержать и возобновить. Тем не менее изучение функций без параметров непосредст- непосредственно привело нас к удовлетворительной реализации этих опе- операторов. Прежде всего отметим, что функции без параметров в форме %( )е всегда при вызове дают один и тот же результат сог- согласно соответствующим правилам связывания. Таким образом, если значение определено, т. е. когда вычисление е не является ни ошибочным, ни бесконечным, то таким же хорошим представ- представлением функции является хранение значения е. Итак, если вы- вычисления требуют в первый раз вызвать А,( )е, нельзя обойтись без этого, но, благополучно сделав этот вызов, можно теперь заменить все содержимое замыкания полученным значением. Дру- Другими словами, значение выражения задержать сможет быть пред- представлено замыканием, которое первоначально содержит выраже- выражение е, но после однократного воздействия оператора возобновить это замыкание заменяется уже самим значением е. Для того чтобы уточнить этот метод вычислений, включим конструкции задержать и возобновить в инструментальный Лисп и затем расширим SECD-машину, чтобы придать этим понятиям точный смысл. В соответствии с правилами языка будем обозна- обозначать соответствующие правильные выражения через (ЗАДЕРЖ ё) и (В030БН ё). Для компиляции этих выражений потребуется ввести три новые команды SECD-машины. Эти команды по на- названиям и реализации будут очень похожи на команды реализа- реализации функций. Это операции ВДВ (ввод выражения), ПРИМБП (применение функции без параметров) и ЗАМЕНА (возврат и замена). Они генерируются операторами ЗАДЕРЖ и В030БН следующим образом: (ЗАДЕРЖ е)*п = (ВДВ е*п\(ЗАМЕНА)) (В030БН е)*п = е*п\(ПРИМБП) Здесь мы видим, что (ЗА ДЕРЖ е) компилируется в управляющий список в форме (ВДВ с), где с есть список управления, получен- полученный при компилировании е, и заключительная команда с есть ЗАМЕНА. Кодом для (В030БН ё) является просто список ко- команд для е, завершаемый командой ПРИМБП. Чтобы завершить определения, нужно дать правила перехода состояний для новых операций SECD-машины. Сделаем это, вве- введя новый тип структуры. Так как задержанное выражение может быть представлено замыканием, содержание которого изменяется под воздействием оператора возобновить, то необходимо поме- помечать это замыкание, а именно помечать, вычислялось ли уже его содержимое. Напомним, что в гл. 6 замыкание представлялось парой (се), где с был списком управления, скомпилированным
8.2. Операторы задержать и возобновить 2Г9 для тела функции, не — окружение, содержащее значения гло- глобальных переменных. Здесь мы будем использовать объект по- подобной структуры. Этот объект назовем рецептом. Рецепт мо- может обозначаться через [Л (се)] в этом случае (се) есть замыкание и Л — флаг, указывающий на то, что рецепт еще не возобновлялся (не вычислялся). В про- противном случае рецепт обозначается через [Их] где х есть значение, а # — флаг, указывающий, что рецепт уже возобновлен (вычислен). Квадратные скобки помогают визу- визуально различать правила перехода. Можно себе представить ре- рецепт просто как cons, car и cdr которого должны обновляться в результате применения оператора возобновить. Теперь приведем правила перехода. Ссылки на соответствую- соответствующие правила для ВДФ, ПРИМ и ВОЗВР объясняют в основном структуру этих правил (см. также упр. 8.6). Прежде всего пра- правило для ВДВ выписывается непосредственно: se (ВДВ cc')d-+(W(ce)].s) e с' d Здесь мы видим, что команда ВДВ с просто строит рецепт [Л (се)] и помещает его в вершину стека. Вычисления продолжают- продолжаются по оставшейся программе с', а вычисление с задерживается. Далее рассмотрим правила перехода для операции ПРИ МБП. Она рассчитана на рецепт в вершине стека. Рецепт может быть вычислен или нет. Оба случая рассмотрим отдельно. (IHxls) e (nPHMBn.c)d->(x.s) e с d Если рецепт вычислен, просто помещаем его значение в вершину стека. Однако, если рецепт не возобновлен, мы должны начать с его вычисления. (№(ce)].s) е'(ПРИМБПх')а^ НИЛ е с (W(c.e)\s) e' c'.d) Это несколько сложнее. Здесь в буфер d помещается весь стек вместе с рецептом, а также окружение ё и остаток управляю- управляющей строки с'. Рецепт и стеки сохраняются потому, что они при- пригодятся в процессе возобновления рецепта. Это произойдет, ког- когда мы достигнем заключительной команды ЗАМЕНА управляю- управляющего списка с, который заключен в рецепте. Соответствующее правило перехода имеет вид (х) е (ЗАМЕНА) (W(c.e)].s)e* c'.d)-+(x.s)ef c'd и [Л(с.е)]-+[И х]
220 Гл. 8. Вычисления с вадержкой Само правило очевидно. Оно просто указывает, что когда вы- вычисление с закончено, его значение х выдается в вызывающее ок- окружение и вычисления возобновляются в этом окружении в точ- точности так, как если бы использовалась команда ВОЗВР. Однако наше правило имеет побочный эффект обновления содержания рецепта, так что флагом теперь является значение //, указываю- указывающее, что рецепт вычислен и что хранится теперь значение х, а не замыкание (се). Ясно, что нет семантической необходимости реализовать это обновление на том же месте, но из соображений эффективности это желательно. Желательно далее, чтобы каж- каждое вхождение рецепта заменялось таким же образом. Обычно можно добиться, чтобы рецепт сам никогда не копировался, а копировались бы только указатели на него; аналогично и сим- символическое выражение ]Л(с.е)] в этом правиле перехода состоя- состояний употребляется для всех вхождений (на практике единствен- единственного) рецепта. Таким образом, обновление действует на всю последовательность обращений к рецепту. Представим на момент, как расширенная SECD-машина вы- вычисляет программу для функции первые(к, целот (т)), которая рассматривалась в предыдущем разделе. Первой существенной частью этой программы является выражение в определении функ- функции целот(т), которое порождает список с задержанным cdr. Это выражение cons(m, задержать целот(т+1)) Другой существенной частью является выражение в функции nepebie(ky х)у в котором инициируется вычисление задержанно- задержанного cdr. Это выражение возобновить cdr(x) В момент вычисления возобновить cdr(x) мы связываем х со значением cons(my задержать целот (т+1)). Согласно нашему Рис. 8.1. определению вычисления оператора задержать, на SECD-маши- не это значение имеет структуру, показанную на рис. 8.1. Здесь первая запись есть обычный cons типа запись, а вторая — ре- рецепт. Далее конструкция возобновить cdr(x) создает программу, которая помещает это значение х в стек, берет его cdr (который является рецептом) и затем выполняет операцию ПРИМБП. Это обусловливает вычисление рецепта, поскольку флаг Л ука-
8.2. Операторы задержать и возобновить 22Л зывает, что это не было сделано. Так как рецепт является выра- выражением в форме целот(т+\), то результатом его вычисления будет список той же структуры, что и х, как это показано на рис. 8.2. Далее программа, созданная оператором задержать, закан- заканчивается операцией ЗАМЕНА, и это приводит к тому, что в Рис. 8.2. рецепте, который является cdr вышеуказанного х (он помечен меткой а), обновляются его поля, чтобы указать, что вычисле- вычисление уже имело место, и список для х теперь имеет вид, изобра- изображенный на рис. 8.3. Рис. 8.3. Таким образом, последующие вычисления выражения возоб- возобновить cdr(x) (которые отсутствуют в нашей простой программе) не требовали бы вычисления рецепта, а пропускали бы этот этап. В практической реализации есть возможность избежать этих промежуточных записей, но это здесь не рассматривается (см. упр. 8.7). Можно взглянуть на поведение программы, которая исполь- использует вычисления с задержкой, под другим углом зрения, и это довольно поучительно. Для функции первые(к,целот(т)) про- программа скорее напоминает сопрограммы, чем подпрограммы. То есть программа для целот(т) выполняется только частично и затем приостанавливается. Затем начинается выполнение про- программы для nepebie(ky x)} и после кратких вычислений она сама приостанавливается с возобновлением программы для целот(т). Эта последняя в свою очередь приостанавливается, и снова вы- выполнение программы для первые(к, х) возобновляется с того места, где она остановилась. Таким образом, вычисления по об- общей программе состоят в том, что они выполняются поочередно, то по одной, то по другой из этих сопрограмм; некоторое время выполняется первая сопрограмма, затем вторая, затем снова пер-
222 Гл. 8. Вычисления с задержкой вая и т. д. Это непривычный порядок выполнения программы, построенной для функции. Программа для функции без опера- операторов задержать и возобновить является в точности тем, что обычно называют подпрограммой. В нее входят в начале, выхо- выходят из нее в конце, и прерывается она за это время только для вызова других подпрограмм, вложенных обычным способом. Именно из-за этого явления обычной вложенности так трудно оперировать с конечными частями бесконечных структур. Без оператора задержать программа для функции целот(т) после входа в нее никогда бы не закончилась и не приостановилась. Сопрограммы являются очень простой формой квазипарал- квазипараллелизма. Мы увидим, почему их можно так трактовать, если пред- представим себе следующую интерпретацию операторов задержать и возобновить. Когда во время вычисления функции встречается выражение задержать, мы считаем, что это означает не задержку вычислений, а формирование отдельного независимого параллель- параллельного вычисления. То есть выражение е как бы вычисляется па- параллельно с тем выражением, в которое оно включено. Рецепт, ко- который выдается в качестве промежуточного результата задер- задержать еу помечается флагом Л, чтобы указать, что это выражение находится в процессе вычисления. Процесс, в который включен оператор задержать е, проверяет флаг, когда требуется доступ к этому рецепту, путем использования оператора возобновить. Если флаг равен Л, процесс ожидает, пока он заменится на И. Таким образом, можно рассматривать каждый рецепт с флагом Л как процесс, находящийся в стадии вычисления. Могут быть несколько процессов, ожидающих его завершения. Если имеет- имеется бесконечное число процессоров, то можно выполнять каждый рецепт по мере его возникновения на новом отдельном процес- процессоре. Если процессоров конечное число, следует организовать мультипрограммный режим вычисления. Подробности оставля- оставляем читателю в качестве упр. 8.9. 8.3. Замедленные вычисления В Алголе имеются два различных механизма управления па- параметрами процедуры. Это так называемые вызов по имени и вы- вызов по значению. Механизм, который используется в нашем стро- строго функциональном языке, есть вызов по значению. То есть каж- каждый аргумент функции вычисляется прежде, чем вычисляется тело функции. Вычисление тела функции происходит со значе- значениями фактических параметров, связанных с формальными име- именами параметров. Рассмотрим теперь механизм вызова по име- имени в строго функциональном языке. В Алголе параметр, вызван- вызванный по имени, не вычисляется до тех пор, пока он не встретится при вычислении тела функции. Кроме того, всякий раз, как в те-
8.3. Замедленные вычисления 223 ле функции встречается имя формального параметра, перевычис- перевычисляется значение соответствующего фактического параметра. Можно моделировать этот механизм в нашем строго функцио- функциональном языке, задерживая фактический параметр, который мы хотим вызвать по имени, и возобновляя соответствующий фор- формальный параметр в теле функции. Рассмотрим, например, функ- функцию /(а, &)=если а>0 то а иначе Ь и ее вызов f(g(x), h(x)). Можно избежать вычисления h(x) при g(x)>0, используя вызов по имени для параметра Ь. Для этого определение функции должно иметь вид /(а, Ь)=если а>0 то а иначе возобновить Ь а соответствующий вызов имеет вид f(?(x), задержать А (л:)) Итак, мы видим, что в нашем строго функциональном языке вы- вызов по имени (моделированный здесь) может быть полезным меха- механизмом. Другой пример был приведен в разд. 6.5: /1 (а, ?>, с)Е=если а то Ъ иначе с Здесь, для того чтобы сделать вызов /1 (а, Ь, с) семантически эк- эквивалентным оператору если а то b иначе с, следует вызвать Ъ и с по имени, чтобы избежать вычисления одного из них, когда выбрано другое (см. также упр. 6.8). Однако вызов по имени имеет недостаток, устраняемый таким моделированием. Недоста- Недостаток этот заключается в том, что при вызове по имени параметр, встречающийся несколько раз, должен перевычисляться при каждом вхождении. Так, например, в функции /(а, б^ЕЕЕесли а>0 то а иначе b+b если b вызывается по имени, то в вызове f(g(x)> h(x)) вычисление А(#) делается дважды. Однако при нашем моделировании вы- вызова по имени такого перевычисления не происходит благодаря обновлению рецепта. Рассмотрим, например, функцию /(а, Ь)=если а>0 то а иначе (возобновить Ь) + (возобно- (возобновить Ь) и соответствующий вызов f{g(x)> задержать А (л:)) Когда встречается первое вхождение выражения возобновить ft, требуется провести вычисление h(x)y но при анализе второго вхождения обнаружится, что h(x) уже вычислено, и мы восполь-
Гл. 8. Вычисления с задержкой зуемся его значением. Эта форма управления параметрами назы- называется вызовом по необходимости (call-by-need) и объединяет преимущества как вызова по значению, так и вызова по имени. Она обладает достоинствами вызова по имени в том смысле, что фактические параметры, которые в действительности не исполь- используются, не вычисляются. Достоинством вызова по значению яв- является то, что требуемые параметры вычисляются только од- однажды. Параметр, вызванный по необходимости, вычисляется только в том случае, когда его значение действительно исполь- используется. Можно заметить, что способ использования cons в разд. 8.1, когда задерживается его cdr, равносилен вызову второго аргу- аргумента по имени. Но есть небольшая разница. В нашем описании вызова по необходимости предполагалось, что функция либо вы- вычисляет вызванный по необходимости параметр, либо пропускает его. Функция cons не делает ни того, ни другого. Она выдает значение, в котором заключен параметр, все еще оставшийся не- вычисленным. В вычислительном отношении полезны обе эти возможности — как cons с задержанным cdr, так и механизм вы- вызова по необходимости. То есть имеются случаи, когда хотят за- задержать параметры определенной пользователем функции, и имеются случаи, когда хотят задержать параметры cons. Те- Теперь не составляет труда определить вариант нашего строго функционального языка, в котором все параметры вызываются по необходимости. И равным образом возможно определить ва- вариант, в котором задерживаются один или оба аргумента функ- функции cons и соответствующие результаты саг и (или) cdr автомати- автоматически возобновляются. В этих вариантах задержка и возобнов- возобновление будут неявными. Теперь перейдем к рассмотрению вари- варианта, в котором объединены обе эти возможности. То, что одна только задержка аргументов cons является не- недостаточной, иллюстрирует следующая программа: треуг(п) = соединить(целмежду A,п), треуг (я + 1)) соединить(ху у) = если равно(ху НИЛ) то у иначе cons(car (х), задержать соединить(ъозобно- вить cdr (x), у)) Функция треуг(п) выдает список (с задержанным cdr), состав- составленный из таких подсписков: A 2 ... п) A 2 ... п + 1) A 2 ... п + 2)... К сожалению, программа с механизмом вызова по значению для функции соединить(х, у) работает бесконечно. Причина кроется в определении функции треуг(п), где из-за вызова по значению
8.3. Замедленные вычисления 225 второго аргумента соединить функция треуг снова вызывается прежде, чем кончится соединить. Это приводит к бесконечной рекурсии. Легко видеть, что задержка второго параметра функ- функции соединить(х, у) решает эту проблему. Процессы, в которых задерживаются как аргументы опреде- определяемых пользователем функций, так и аргументы функции cons, называются замедленными вычислениями. Теперь рассмот- рассмотрим вопрос о том, когда следует применять соответствующие возобновления. Мы принимаем решение возобновлять аргумен- аргументы примитивных функций (таких как car, cdr, + и т. д.). Реше- Решение обосновано тем, что это последняя возможность, дальше ко- которой вычисления не могут быть задержаны. Например, нельзя продолжать сложение, если аргумент операции + задержан. На самом деле необходимо возобновлять каждый аргумент прими- примитивной функции повторно, чтобы гарантировать вычисление зна- значения аргумента. Для этой цели мы введем оператор повтор (повторно возобновить) с тем свойством, что для всякого пра- правильного выражения е выражение повтор е тоже правильное и его значение не является невычисленным рецептом. Другими сло- словами, если ввести предикат рецепт (х), истинный, только если аргумент является невычисленным рецептом, то оператор пов- повтор удовлетворяет уравнению повтор е=если рецепт (е) то повтор (возобновить е) иначе е Это равносильно тому, что выражение повтор е имеет то же са- самое значение, что и первое из значений, не являющееся невычис- невычисленным рецептом в ряду е, возобновить е, возобновить (возобно- (возобновить е) и т. д. Здесь видно, что можно применять оператор пов- повтор к любому выражению, в частности, когда его значение не задержано, и получать значение, которое является вычисленным рецептом. Теперь можно дать точное определение вычислениям, которые называют замедленными. В строго функциональной программе, обычно без явных вхождений операторов задержать и возобно- возобновить, сделаем следующие изменения, проходящие через всю про- программу: 1) задержать все аргументы определенных пользователем функций; 2) задержать все аргументы функции cons; 3) задержать все определения в блоках пусть и пустьрек; 4) повторно возобновить все аргументы примитивных функ- функций (отличных от cons); 5) повторно возобновить все проверки условий в условных выражениях; 8 № 929
226 Гл. 8. Вычисления с задержкой 6) повторно возобновить все функции в выражениях «приме- «применить функцию». Эти правила можно записать как ряд преобразований про- программы (8.2), где все е, elt. . .,'eft считаются правильными выра- выражениями и стрелка означает «заменить на». * (ei>- • • >ek) —> (повтор е) (задержать elt..., задержать е^) cons (elt e2) —> cons (задержать еъ задержать е2) саг(е) —> саг (повтор е)... если <?i то е2 иначе ен—>если повтор ех то *2 иначе в3 {пусть х1 = е1 \ 7 {пусть хх = задержать ех (8.2) и Jtft = ?ft | * | и x/f = задержать е^ е} ) { е) (аналогично для пустьрек) В конкретной реализации замедленных вычислений необхо- необходимо обеспечить, чтобы функции выборки (саг и cdr), используе- используемые в программе для печати конечного результата, повторно возобновляли их аргументы, или сделать что-либо эквивалент- эквивалентное этому. Решение о задержке определений в блоках пусть и пустьрек основано на обсуждавшейся ранее связи между этими блоками и применением функций. Нам не хотелось бы потерять эту связь. Мы увидим, однако, что это решение особенно удачно в случае блока пустьрек. В дополнение к повторному возобновлению ар- аргументов примитивных функций необходимо будет также пов- повторно возобновлять проверку условия в условных выражениях и функции в вызове функции, так как их значения нужны преж- прежде, чем начнется процесс вычисления. То, что оператор повтор необходим, видно из приведенных выше преобразований (8.2), из которых следует, что параметр, который просто передается из одной функции в подчиненные ей другие функции, может за- задерживаться дважды (или более). Наконец, отметим, что теперь задерживаются оба аргумента функции cons. Это позволит нам обращаться с потенциально бесконечными деревьями, частный случай которых — бесконечные списки — был рассмотрен ра- ранее в качестве примера. Легко видеть, что правила (8.2) приведут к конструированию многих рецептов в преобразованной программе. Если написать программу, которая оперирует с бесконечными структурами (их конечной частью), но не содержит операторов задержать и во- возобновить в явном виде, то преобразованная программа будет обладать всеми необходимыми задержками и возобновлениями. Рассмотрим, например, программу, которая позволит нам вы- вычислить первые (п, целот(\)), используя следующие определения
8.3. Замедленные вычисления 227 функций: первыеAг, х) s= если paeno(k9 0) то НИЛ иначе cons(car(x),nepebie(k — 1, cdr(x))) целот(т) = соп$(т,целот(т + 1)) Предположим, что эта программа преобразуется по приведен- приведенным выше правилам. Если бы мы написали ее, как принято, ис- используя блок пустьрек для введения определений функций, мы должны были бы повторно возобновлять все введенные пользо- пользователем функции в их вызовах. Мы опускаем подобный уровень детализации, оставляя это в качестве упражнения (упр. 8.5). Функция целот(т) выдает cons двух рецептов. Функция первые (&, х) выдает либо НИЛ, либо тоже cons двух рецептов. Таким образом, главный вызов первые(пуцелот(\)) в предположении п>0 выдает cons двух невычисленных рецептов. Отметим, что функция целот(\) не была вызвана. На этом можно было бы за- закончить, если не требуется проверить (например, напечатать) значение первые(пуцелот(Х)). В предположении, что требуется саг этого значения, нужно возобновить вычисление целотA), поскольку мы имеем cons двух невычисленных рецептов. Повтор- Повторное возобновление саг приведет к значению 1. Cdr, который далее приведет к значению 2, еще не затронут. В следующем разделе мы применим этот метод написания программ без явных операто- операторов задержать и возобновить, полагаясь на преобразование их к форме, в которой они могут быть вычислены. Мы будем назы- называть процесс выполнения преобразований, приводящих к замед- замедленным вычислениям, тоже «замедленные вычисления» и надеем- надеемся, что это не вызовет путаницы. Прежде, однако, рассмотрим простую программу пустьрек х = cons( 1, х) car(cdr(x)) По обычным правилам вычислений это не корректное выражение. Однако при замедленных вычислениях оно имеет вполне приемле- приемлемый смысл. Список х является бесконечным списком, составлен- составленным из единиц. Тогда очевидно, что car(cdr(x)) есть единица. Полезно рассмотреть вычисление этого выражения во всех дета- деталях, так как оно иллюстрирует многие моменты. Правила пре- преобразования приводят к следующему эквивалентному выраже- выражению: пустьрек х = задержать со^задержать 1, задержать х) шг(повтор cdr(noBTOp х)) Процесс вычисления организуется следующим образом. Как зна- значение, связанное с х, строится рецепт (рис. 8.4). Затем вычисле-
228 Гл. 8. Вычисления с задержкой ние шг(повтор саг(поътор х)) обусловливает возобновление х. Так получаем cons двух рецептов (рис. 8.5). Значением повтор х является запись cons, и тогда возобнов- возобновляется cdr этой записи. Cdr является рецептом с замыканием, Рис. 8.5. х, не конструируя при этом никаких дополнительных записей для представления х. Отметим также, что два оператора задержать в рекурсивном определении х оставили нас с двумя рецептами в его представлении. Это пример необходимости повторных возоб- возобновлений. Можно сделать вывод, что замедленные вычисления дают простую интерпретацию блоков пустьрек, где могут быть выражены и вычислены рекурсивные уравнения, определяющие бесконечные структуры данных. Это предмет следующего раз- раздела. В заключение рассмотрим более практическое применение за- замедленных вычислений к обычным конечным структурам. Зада- Задача (см. упр. 2.15) сравнения двух бинарных деревьев с целью выяснить, не обладают ли они при обходе слева направо одинако- одинаковым набором листьев, не представляется естественной для функ- функционального программирования, использующего соответствую- соответствующие рекурсивные функции, в том смысле, что программа, запи- Рис. 8.4. представляющим х. Возобновление этого рецепта иллюстрирует рис. 8.6. Значением повтор Ыг(повтор х) является та же самая запись cons. Тогда значением саг (повтор а/г(повтор х)) является рецепт, который содержит замыкание для 1. Это и есть требуемое зна- значение. Отметим, что можно и дальше получать элементы списка
8.3. Замедленные вычисления 229 санная на таком языке, необычайно искажена и неэффективна. Проблема возникает из-за того, что два бинарных дерева могут иметь совершенно различную форму. На рис. 8.7, например, де- деревья A), B) и D) одинаковы, а C) и E) различны. Рис. 8.6. Простейшей программой, которая может быть написана для этой задачи, является программа, использующая функцию на- Рис. 8.7. 6op(f), строящая списки листьев этих деревьев и сравнивающая затем эти списки. Программа приводится в (8.3). При обычном выполнении эта программа конструирует два списка и затем сравнивает их. Если у них различны уже первые
228 Гл. 8. Вычисления с задержкой ние шг(повтор саг(повтор х)) обусловливает возобновление х. Так получаем cons двух рецептов (рис. 8.5). Значением повтор х является запись cons, и тогда возобнов- возобновляется cdr этой записи. Cdr является рецептом с замыканием, Рис. 8.4. представляющим х. Возобновление этого рецепта иллюстрирует рис. 8.6. Значением повтор а*г(повтор х) является та же самая запись cons. Тогда значением саг (повтор саг(поътор х)) является рецепт, который содержит замыкание для 1. Это и есть требуемое зна- значение. Отметим, что можно и дальше получать элементы списка Рис. 8.5. х, не конструируя при этом никаких дополнительных записей для представления х. Отметим также, что два оператора задержать в рекурсивном определении х оставили нас с двумя рецептами в его представлении. Это пример необходимости повторных возоб- возобновлений. Можно сделать вывод, что замедленные вычисления дают простую интерпретацию блоков пустьрек, где могут быть выражены и вычислены рекурсивные уравнения, определяющие бесконечные структуры данных. Это предмет следующего раз- раздела. В заключение рассмотрим более практическое применение за- замедленных вычислений к обычным конечным структурам. Зада- Задача (см. упр. 2.15) сравнения двух бинарных деревьев с целью выяснить, не обладают ли они при обходе слева направо одинако- одинаковым набором листьев, не представляется естественной для функ- функционального программирования, использующего соответствую- соответствующие рекурсивные функции, в том смысле, что программа, запи-
8.3. Замедленные вычисления 229 санная на таком языке, необычайно искажена и неэффективна. Проблема возникает из-за того, что два бинарных дерева могут иметь совершенно различную форму. На рис. 8.7, например, де- деревья A), B) и D) одинаковы, а C) и E) различны. Рис. 8.6. Простейшей программой, которая может быть написана для этой задачи, является программа, использующая функцию на- Рис. 8.7. 6op(f)9 строящая списки листьев этих деревьев и сравнивающая затем эти списки. Программа приводится в (8.3). При обычном выполнении эта программа конструирует два списка и затем сравнивает их. Если у них различны уже первые
230 Гл. 8. Вычисления с задержкой тождкрон(п, /2)s= равспис(набор(И), na6op(t2)) наборA) = если amoM(t) то cons(t, НИЛ) иначе соединить(набор(саг(()), na6op(cdr(t))) coeduHumb(xt у) s= если равно(х, НИЛ) то у иначе cons(car(x), соединить(сс1г(х), у)) (8.3) равспис(х, у) s= если равно(х) НИЛ) то равно(у, НИЛ) иначе если равно(у, НИЛ) то «/7 иначе если равно(саг(х), саг(у)) то paecnuc{cdr(x)t cdr(y)) иначе «/7 элементы, как в деревьях A) и E) на рис. 8.7, то большая часть работы пропадает впустую. Однако при замедленных вычисле- вычислениях процесс контролируется функцией равспис(х, у), которая требует построения только префиксной части этих списков, до тех пор, пока не встретится пара различных элементов. Конеч- Конечно, если списки окажутся одинаковыми, это не спасает положе- положения (на самом деле все эти операторы задержки и возобновления даже увеличат время вычислений). Однако если одно из деревьев окажется очень большим и результатом сравнения будет ложь, то вычисления с задержкой позволят избежать ненужной работы по анализу этого огромного дерева. 8.4. Сети связных процессов В этом разделе мы рассмотрим использование тех свойств за- замедленных вычислений, которые позволяют оперировать с бес- бесконечными структурами и просто интерпретировать взаимно ре- рекурсивные уравнения, определяемые в конструкции пустьрек. Все, что здесь будет сделано, может быть выполнено с явным ис- использованием операторов задержать и возобновить. Читателю рекомендуется самому удостовериться в этом, поскольку подоб- подобные упражнения в высшей степени способствуют пониманию того, как на самом деле протекают эти замедленные вычисления. Однако уравнения, которые будут построены для каждого при- примера этого раздела, понятны и сами по себе, без того чтобы точно знать, как именно они вычисляются. Поэтому мы не будем изла- излагать каждый пример с явными задержками и возобновлениями. В действительности, хотя каждый пример и записывается в виде функциональной программы, мы будем ее трактовать как множество процессов, связанных потоком значений. Эти процессы можно представлять себе параллельными, получающи- получающими данные из своих входных потоков и отправляющими резуль- результаты в свои выходные потоки. Синхронизация этой деятельности не составляет проблемы, но нужна уверенность, что процесс в целом развивается поступательно.
8.4. Сети связных процессов 231 Первый пример связан с построением списка всех целых чи- чисел. Предположим, имеется определение функции плюс 1 (х) =2 cons(car(x) +1, плюс 1 (cdr(x))) которая по бесконечному списку х выдает тоже бесконечный спи- список, полученный из х добавлением 1 ко всем его элементам. Бу- Будем ссылаться на бесконечные списки так, как будто бы они це- целиком построены, хотя на самом деле это не так, и мы хорошо Рис. 8.8. Рис. 8.9. знакомы с техникой задержки недоступных частей бесконечной структуры. Список всех целых чисел теперь определяется выра- выражением целые гдерек целые=^соп$(\,плюс\{целые)) То есть список целых начинается с единицы, за которой следуют результаты применения функции плюс\ к самому списку. Это довольно странный способ определения целых. Определение це- лот(\) представляется более естественным. Однако мы проиллю- проиллюстрируем это определение способом, который, возможно, сделает его более очевидным. На рис. 8.8 определение функции плюс\(х) становится тем, что мы называем процессом. Входящие стрелки обозначают ее аргументы, а выходящие—результат. Треугольни- Треугольником обозначена операция cons с двумя ее аргументами и одним результатом (рис. 8.9). Разветвление стрелки, помеченной словом целые на рис. 8.8, иллюстрирует тот факт, что это значение используется дважды, как выход всей сети и как вход в процесс плюс\. Связь между этой сетью и определением целых очевидна. Можно представить себе сеть, вычисляющей список всех це- целых двумя различными способами. Сеть может управляться либо запросами на ее выход, либо готовностью ее входов. Будем ссылаться на эти два способа, как на метод запросов и метод готовности соответственно. Метод запросов отвечает замедлен- замедленным вычислениям. Если нам требуется первое значение из спис- списка целых, то мы можем получить его, поскольку это значение есть 1, не затрагивая процесса плюс!. Когда нам требуется вто- второе значение из списка целых, это заставляет плюсХ запросить первое значение из этого списка, которое уже вычислено. Поэто- Поэтому цепь запросов закончена, и мы получаем второе значение. Ясно, что запрос на п-е значение списка обусловит запрос на
232 Гл. 8. Вычисления с задержкой (п—1)-е, если оно не было ранее вычислено, и т. д. С другой стороны, можно рассматривать вышеприведенную сеть как про- процесс, работающий независимо, получающий целые со своей соб- собственной скоростью. Первое значение доступно и поэтому выво- выводится, а также отправляется назад. Это делает доступным второе значение, с ним поступают так же, как и с первым, и т. д. Оба Рис. 8.10. способа вычислений требуют, чтобы мы рассматривали значения как бы образующими очереди на вершинах сети; на разветвле- разветвлениях стрелок будем предполагать, что все значения направляют- направляются по двум путям. Слияние стрелок не рассматривается. Достоин- Достоинства этого понятия сети проявятся в том случае, когда мы приме- применим другой способ рассуждений: сначала нарисуем сеть, а затем выведем соответствующую функциональную программу. Попытаемся сделать это на примере, с которого мы начали главу. Чтобы получить список частичных сумм натурального ряда, потребуется следующая функция: сумспис{ху y)=cons(car(x)+car(y), сумспис(саг(х), cdr(y))) Теперь можно построить сеть, которая вырабатывает список суммы всех частичных сумм, где n-й элемент списка есть S^(i). Заметим, что су мспис(целыеусоп8@,суммы)) выдает как раз нуж- нужный нам список суммы. Соответствующая сеть изображена на рис. 8.10. [В этом примере исправлена неточность у автора.— Ред.) Вход целые получается в ранее рассмотренной цепи. Выход суммы возвращается и добавляется элемент за элементом к спис- списку целых. Для получения правильного результата на входе в функцию сумспис операция cons добавляет в начало списка нулевой элемент. На выходе сети получается список A, 3, 6, 10, . . .). Эквивалентная функциональная программа имеет вид суммы гдерек суммы = сумспис(целыв,соп8($,суммы)) Комбинируя эти две сети, получаем новую сеть, изображен- изображенную на рис. 8.11. Функциональная программа для этой сети имеет вид (8.4).
8.4. Сети связных процессов 233 суммы гдерек суммы = сумспис(целые, cons@, суммы)) и целые = cons(\, плюс\(целые)) (8.4) При построении сетей следует обращать внимание на то, чтобы соответствующая программа действительно могла начать вычис- вычисления. Рис. 8.11. Несколько более сложным примером является сеть, приве- приведенная на рис. 8.12, которая получает последовательность чисел Фибоначчи. Логическим обоснованием этой цепи является то, что последовательность Фибоначчи, начатая с первого элемента, сложенная с такой же последовательностью, начинающейся со второго элемента, дает нам все ту же последовательность Фибо- Рис. 8.12. наччи, но начинающуюся с третьего элемента. Эти три последо- последовательности в цепи обозначены через/ь /2, /з. Имея сгенерирован- сгенерированную последовательность /3, мы можем получить из нее последо- последовательности ft и /2, просто добавив два начальных члена (оба 1). То есть числа Фибоначчи определяются соотношениями
234 Га, 8. Вычисления с задержкой а последовательности /ь /2 и /3 — соотношениями /j = Х1у X2i Х%у . . . = Хц /2 /2==Л'2» *3» *4» •••==#2> /8 / 3 ==г -^з» -^4 » "^5 > • ' # Программа для чисел Фибоначчи в функциональной форме может быть выведена из сети на рис. 8.12: /х гдерек f1=^cons(l9f2) и f2 = cons(l,f3) и fs = cyMcnuc(fu f2) Это — замечательная программа, и стоит приложить усилия и явно выписать все операторы задержать и возобновить, чтобы понять, как именно вычисления с задержкой обеспечивают тре- требуемый эффект. Следующая проблема, которую мы рассмотрим, предложена Хэммингом (Дейкстра, 1976). Задача заключается в построении последовательности, которая обладает следующими тремя свой- свойствами: 1. Значение 1 принадлежит последовательности. 2. Если х является элементом последовательности, то 2хх, Зхх и 5хл: также принадлежат ей. 3. Последовательность не содержит других значений, кроме тех, которые определены свойствами 1 и 2. Далее будем порождать элементы этой последовательности в возрастающем порядке. Чтобы сделать это, используем процесс, включающий функцию соед(х> у), которая соединяет две воз- возрастающие последовательности чисел х и у, опуская повторе- повторения. соед(ху у) = если равно(саг (х), саг (у)) то coed(cdr(x), у) иначе если саг (х) < саг (у) то cons(car(x)y coed(cdr(xI у)) иначе cons(car (у), соед(х> cdr (у))) Наш метод состоит в том, что, порождая последовательность х9 удовлетворяющую приведенным выше условиям A)—C), мы умножаем ее элементы на 2, 3 и 5 и соединяем (без повторе- повторений) полученные так последовательности. При повторении цик- цикла эта объединенная последовательность используется в ка- качестве исходной. Сеть, которую мы получили, изображена на рис. 8.13. Здесь процесс, помеченный знаком х2, включает функцию умн2 (х) «г cons(car (х) х 2, умн2 (cdr (x)))
8.4. Сети связных процессов 235 Аналогично определяются процессы хЗ и х5. Последователь- Последовательность х умножается соответственно на 2, на 3 и на 5, чтобы об- образовать последовательности х2, х3 и хъ> которые затем после- последовательно соединяются попарно и образуют последователь- последовательности х23 и х23ь. Последовательность х23ъ содержит все элементы Рис. 8.13. х, кроме-1, поэтому в процессе нужна операция cons. Это пояс- пояснение позволяет нам дать следующее функциональное описание программы (см. 8.5). Доказательство того факта, что эта программа хорошо опре- определена в том смысле, что приводит к верному результату, остав- оставляется в качестве упражнения читателю (упр. 8.19). Заметим, что эта программа может быть переписана в виде х гдерек x=cons(l, соед(соед(умн2(х), умнЗ(х)), умн5(х))) Наконец, построим сеть, которая реализует решето Эрато- сфена для нахождения простых чисел. Это не так просто, по- поскольку сеть содержит соответствующим образом вложенные копии самой себя. Другие генераторы простых чисел проще по структуре и вынесены в упражнения. Этот пример включен в текст из-за его чрезвычайной элегантности. Оригинальная функ- функциональная программа, на которой базируется наша сеть, была придумана П. Кварендоном. Решето Эратосфена обычно опи- описывается следующим образом. Составим список целых чисел, начиная с 2. Будем повторять следующий процесс пометки чи- чисел этого списка. 1. Первое непомеченное число списка является простым, назовем его р.
236 Гл. 8. Вычисления с задержкой 2. Пометим первое неотмеченное число в списке и, начиная с него, каждое р-е число списка независимо от того, помечено оно или нет (так мы пометим все кратные р числа). 3. Возвращаемся к п. 1. Разумеется, обычно список чисел, построенный к началу процесса, является конечным. Мы не будем обращать внимания на это ограничение. В процесс может быть включена следующая функция, реализующая п. 2: фильтр(р, у) s если саг(у) ост /? = 0 то филыпр(р, cdr(y)) иначе cons(car(y)y филыпр(р, cdr(y))) Теперь нужно построить сеть, которая по списку, определен- определенному в п. 1, порождает список всех простых чисел. То есть мы хотим построить сеть, реализующую процесс, изображенный на рис. 8.14. Рис. 8.14. Рис. 8.15. Если мы обозначим процесс получения саг и cdr некоторого списка треугольником, как это показано на рис. 8.15, то сеть, реализующая правила 1, 2 и 3, показана на рис. 8.16. Рис. 8.16. Заметим, что вход х расщепляется, чтобы получить два входа для функции фильтр (ру у). Результат фильтрации затем обра- обрабатывается новой копией всего процесса решето. Полученный таким образом список простых пополняется простым числом р. Приведенная выше сеть и представляет в целом решето. Для наглядности изобразим это на рис. 8.17. Теперь, имея в качестве начального входа в решето список целых, начиная с 2, можно составить для этой сети следующую функциональную программу:
8.4. Сети связных процессов 237 простые гдерек простые = решето(целот2) и целот2^саг(целые) и решето = Х(х) соп8(саг(х),решето(фильтр(саг(х),сс1г(х)))) Верификацию этой программы также оставляем читателю. В этой главе мы попытались показать, что функциональный язык является особенно естественной формой выражения некото- некоторых типов параллелизма. Особенно важным является отсутст- Рис. 8.17. вие предписания жесткого порядка вычисления подвыражений. Если вычисление выражений обладает побочным эффектом, параллельная интерпретация выражений становится гораздо более трудной. Замедленные вычисления позволяют реализо- реализовать вычисления, управляемые запросами, без изменения струк- структуры интерпретируемой программы, хотя это расширяет класс программ, которым можно теперь придать удовлетворительный смысл. Использование явных задержек и возобновлений оста- остается, однако, мощным средством, при помощи которого можно экспериментировать с различными формами квазипаралле- квазипараллелизма. Упражнения 8.1. Бесконечные списки можно обрабатывать разными способами. Вместо того, чтобы задерживать cdr, можно задерживать сам список. Это значит, что его нужно возобновить для проверки его саг. Таким образом список есть либо НИЛ у либо задержанный cons. Перепишите функции целот(т) и пер- первые (k, х) в этой форме, используя явные задержки и возобновления. 8.2. Определите функцию посл(а), которая строит бесконечную последо- последовательность (*i, x2, . . .), определенную следующим образом: 8.3. Определите функцию срезка (х, Ь)> которая выдает конечный список, полученный и* бесконечного списка х отсечением всех его элементов следую-
238 Гл. 8. Вычисления с задержкой щих за элементом, равным Ь. Так срезка (поелA7), 1) строит список A7 26 13 20 10 5 8 4 2 1) 8.4. Покажите, что программа, построенная для выражения задержать (возобновить е) дает тот же результат, что и программа для е, 8.5. Рассмотрите программу для задержать(возобновить е) Покажите, что вообще эта программа не эквивалентна по действию программе для е. 8.6. Постройте программу, возникающую при замене задержать е на X ( ) е, а возобновить е на е( ). Покажите, что действие этой программы такое же, как и программы с задержать и возобновить. 8.7. Реализация рецептов, которая была нами рассмотрена, обладает тем свойством, что при возобновлении списка с задержанным edr он представ- представляется списком с дополнительным cons и записью типа рецепт. Рассмотрите, как можно реализовать понятие рецепта, чтобы избежать этого и представ- представлять однажды возобновленный список как нормальный. 8.8. Анализируя программу в коде SECD-машины, покажите, что выраже- выражение {пустьрек x = cons(ltзадержать х) первые (k, x)} будет правильным в контексте, где первые (k, x) и k имеют соответствующим образом определенные значения. В частности, первые (k, x) есть функция, которую обычно определяли в этой главе. (Указание: изобразите структуру данных, представляющих *.) 8.9. Разработайте мультипрограммный способ вычисления рецептов, в котором очередь рецептов, требующих вычисления, подчиняется дисцип- дисциплине «первым пришел — первым ушел». Необходимо также упорядочить оче- очередь процессов, ожидающих завершения рецептов. Существуют ли программы, которые имеют разную интерпретацию при обычной однопрограммной реали- реализации и только что описанной мультипрограммной? 8.10. Рассмотрите следующий способ вычисления. Задержите все аргу- аргументы определенных пользователем функций, аргументы операции cons и все определения (правила A)—C) замедленных вычислений). Повторно возобно- возобновите все переменные и результаты операций саг и саг. 8.11. Покажите, что оператор условного возобновления, удовлетворяю- удовлетворяющий соотношению ув= если рецепт (е) то возобновить е иначе е не может быть использован вместо оператора повторного возобновления повтор повсюду в правилах преобразования для замедленных вычислений. 8.12. Рассмотрите процедуру поиска с приоритетом по ширине, описан- описанную в разд. 3.2. Используйте вычисления с задержкой, чтобы избежать проб- проблемы вычисления значения вершин, непроверяемых при поиске. 8.13. Разработайте интерпретатор с инструментального Лиспа, который вычисляет программу по правилам замедленных вычислений. Сам интерпре- интерпретатор может быть либо замедленным, либо нет. 8.14. Постройте сеть, которая генерирует бесконечный список единиц, каждый элемент которого есть число единица. 8.16. Постройте сеть, которая генерирует бесконечный список целых поэлементным сложением списка целых и списка единиц. 8.16. Постройте сеть для функции целот(т) (см. разд. 8.1) и сравните ее с сетью для функции целые,
Упражнения 239 8.17. Перестройте сеть для функции суммы, данную в разд. 8.4, исполь- используя вместо cons функцию минус\(х), где минусХ (x)^cons(car(x)—\, минус\ (cdr(x))) Покажите, что в этом случае вычисление сумм не является правильно опре- определенным. 8.18. Перепишите функциональную программу для чисел Фибоначчи, включив вместо замедленных вычислений явные задержки и возобновления. 8.19. Покажите, что программа для проблемы Дейкстры—Хзмминга действительно работает. (Указание: покажите, что при любом п запрос п-го выхода правильно определен.) 8.20. Переопределите функцию фильтр (р, х) в решете Эратосфена так, чтобы вместо использования операции ост просто пропускать каждый р-й элемент х. 8.21. Альтернативная программа для простых чисел может быть написана при использовании сети, изображенной на рис. 8.18. Рис. 8.18. Процесс бычкрат(целотЗ,простые) вычеркивает из списка целотЗ числа, кратные элементам списка простые. Список целотЪ содержит все целые числа, начиная с 3. 8.22. Другой генератор простых чисел может базироваться на том факте, что сеть, подобная сети в проблеме Дейкстры — Хэмминга, будет строить все составные (непростые) числа, имея множителями простые числа. Процесс, строящий простые числа вычеркиванием составных, может быть использован как источник простых множителей. Это трудное упражнение. В частности, чтобы обеспечить поступательное движение результатов, потребуется некото- некоторая модификация процесса соед.
Глава 9 ФУНКЦИИ ВЫСШИХ ПОРЯДКОВ Во второй главе уже были определены функции высших порядков. Это такие функции, которые могут иметь другие функции либо в качестве своих аргументов, либо в качестве результатов. В этой главе мы продемонстрируем, каким чрез- чрезвычайно мощным средством программирования являются эти функции. В частности, будет показано, что некоторые виды вы- вычислений очень естественно описываются функциями высших порядков. Для ряда программ определение соответствующих функций высших порядков далеко не просто. Однако тот факт, что построенная программа в результате этого часто является более короткой и ее общая структура легче для понимания, обычно с лихвой возмещает затраченные усилия. При обращении с функциями высших порядков возникает принципиальная трудность, о которой уже упоминалось в пер- первой главе. Она состоит в том, что по виду функции нельзя су- судить о типах ее аргументов. Таким образом, необходимо явно указать типы аргументов, и для этого разрабатывается некото- некоторая неформальная система обозначений. Тип аргумента для функций высших порядков описывается «выражением типа». Важнейшим свойством этих выражений является то, что когда описываемый аргумент сам является функцией, то его выраже- выражение типа в свою очередь содержит описание типа аргументов этой функции. Когда же мы имеем функции, аргументы которых са- сами являются функциями высших порядков, а это действительно бывает, тогда необходимость явного описания типов аргументов даже возрастает. Первым примером, который выбран для иллюстрации воз- возможностей функционального программирования, является простой распознаватель языка. Выбор соответствующих функ- функций высших порядков приводит к тому, что распознаватель за- задается почти в обычной форме синтаксических продукций. Та- Таким образом, можно сказать, что распознаватель задан выраже- выражением в естественной для него форме. Аналогично в нашем втором примере мы вводим выражения, естественно описывающие не- некоторый класс геометрических фигур. Выбранные фигуры мож- можно назвать «кирпичными стенками», и мы вводим в соответству- щих функциях высших порядков выражения, которые непосред-
9.1, Типы функций 241 ственно представляют геометрическое размещение «кирпичиков». Вычисление этих выражений приводит к таким структурам дан- данных, по которым графопостроитель непосредственно может на- нарисовать соответствующую фигуру. 9.1. Типы функций Обозначим через N множество всех целых чисел — положи- положительных, нуль и отрицательных. О функции /(*), заданной оп- определением f{x)^x+\ говорят, что она имеет тип N -+ N. Это означает, что тип ее ар- аргумента есть число и таков же тип ее результата. Обычно эта информация о типе записывается в виде Тип аргумента находится слева от стрелки, тип результата — справа. Функция #(*, у), определенная ниже, имеет тип N X N-+N. g(xt y)=x+yt g:NxN-+N Здесь типы аргументов функции g(x9 у) перечислены по поряд- порядку и отделены друг от друга знаком X. Вообще функция f(Xif. . ., хп) имеет тип XiX...xX„->- Y, если ее аргумент xt имеет тип Хи х2 имеет тип Х2 и т. д., а ее результат имеет тип У. Перейдем к несколько более интересным функциям. Рассмотрим функцию, которая берет в качестве аргументов число и функцию и выдает в качестве результата число: h(x9 /)=*+/(*) Тип аргумента х есть N, и тип аргумента / есть N -+ N. Резуль- Результат имеет тип N. Итак, мы можем представить описание типа функции h в виде h: NX(N-+ N)-> N Теперь рассмотрим функцию дважды (f)^X(x)f(f(x)) с описанием типа дважды: (N -> N) -+ (N -> ЛГ) обоснованным следующим образом. Так как х имеет тип N uf име- имеет тип N -> N, то f(x) имеет тип N. Далее, f(f(x)) имеет тип N, и поэтому М*)/(/л;)) имеет тип N ->• N. Таким образом, функция дважды отображает аргумент типа N -> N в результат того же типа N -> N.
242 Fa. 9. Функции высших порядков До сих пор при рассуждениях о функциях мы допускали не- некоторые вариации в их наименовании, например в случае функ- функции добавить {х, у)=... мы иногда ссылались на нее как на «функцию добавить (х, у)», а иногда как на «функцию добавить». Мы не можем себе позволить этой вольности далее. С этого момента «функция дважды^)» и «функция дважды» суть две разные вещи. На самом деле они имеют совсем разные типы: дваждыф: N —> N дважды: (N-» N)-+{N-+N) Мы были в состоянии избежать недоразумения, пока результа- результатом функции было просто значение данных, а не функция. Так, говоря «функция добавить(х, у)», мы не имеем в виду результат добавить(х, у), поскольку этот последний есть список, а не функция. Однако с этого момента мы будем более строги отно- относительно именования функций, и когда мы ссылаемся, например, на функцию дважды^), то мы имеем в виду результат примене- применения дважды к /, а не просто функцию дважды. Имеется еще одна тонкость относительно функции дважды. Она безразлична к тому, что ее аргумент применяется к числам. И было бы правильнее написать, что тип функции дважды имеет вид дважды: (X -> X) -+ (X -+ X) при любом X. Чтобы проиллюстрировать это и показать воз- возможности функций высших порядков, рассмотрим вызов дваж- ды(дважды). Предположим, что мы обозначили тип аргумента как (У-*К)-> (У->К), тогда ограничения на определение типа функции дважды удовлетворены. Этот аргумент имеет вид Х-+ X, если мы возьмем X = Y ->¦ У. Тогда вызов (дважды(дважды)) (g) определен правильно. Действительно, исследуя определение, мы видим, что это то же самое, что и дважды(дважды (g)) и очевидно, что это совпадает с четырежды (g), где четырежды{ц)=к (x)g (g (g (g (x)))) Заметим, что четырежды имеет тот же тип, что и дважды. Важ- Важна гибкость, которая имеет место при использовании функций, образующих функции. Если бы мы определили дважды как функцию двух аргументов, дважды([, х) ==/(/(*)), мы не могли бы
9.1. Типы функций 243 сделать вызов дважды(дважды). Оказывается, что исходная фор- форма определения функции дважды дает большие возможности, по крайней мере она допускает определение вида четырежды=дважды(дважды) Это первое из упрощений, которые позволяют сделать в выра- выражениях функции высших порядков. Обозначим через Список(Х) тип, значение которого есть ли- либо НИЛ, либо список значений типа X. Тогда мы можем описать типы основных примитивов Лиспа: cons: X х Список(Х) —> Список(Х) саг: Список(Х) —> X cdn Список(Х) —* Список(Х) Часто тип аргументов функции будет задан как более широкая область, нежели та, для которой функция действительно опре- определена. То есть мы будем рассматривать нашу функцию как определенную не для всех значений, которые могут быть взяты как аргументы по спецификации типа. В приведенных выше оп- определениях функции саг и cdr определены только для тех аргу- аргументов типа Список(Х), значение которых не есть НИЛ. Функ- Функции, которые не определены для некоторых аргументов, удов- удовлетворяющих ограничениям на тип, называются частичными. С нашим пониманием функций высших порядков вполне согла- согласуется их определение как частичных функций. Функция высшего порядка отобр, с которой мы встречались в гл. 2, имеет определение отобр(х, f) гз если равно(х, НИЛ) то НИЛ иначе cons(f(car(x)), omo6p(cdr(x), /)) Тип этой функции следующий: отобр : Список(Х) X (X -+ У) -> Список (У) То есть исходными данными для функции отобр являются список Х-ъ и функция из X в У, а выдает она список У-в. Мы можем проверить, что определение отобр корректно с точки зрения ти- типов. Для этого распишем типы подвыражений в cons (см. 9.1). х:Список(Х) f:X—+Y саг(х):Х cdr(x):CnucoK(X) (9.1) f(car(x));Y отобр (cdr(x)J): Список (Y) cons(f(car(x)) ,omo6p(cdr(x),/)): Список(У)
244 Гл. 9. Фукции высших порядков Здесь мы начали с параметров х и / и затем расписали типы всех подвыражений. Мы предполагаем, что в одном из подвыра- подвыражений функция отобр имеет как раз тот тип, который мы пытаем- пытаемся проверить для всего выражения, однако надеемся, что мы не попали в порочный круг. Но более формальное исследование этого процесса выходит за рамки данной книги. Переопределим функцию отобр так, чтобы она имела тип отобр: (X -V Y) -> (Список (X) -> Список (Y)) Другими словами, эта новая функция имеет только один аргу- аргумент — функцию типа X -> Y. Ее результатом является функ- функция типа Список (Х)-^ Список(Y). Требуемое определение имеет вид omo6p(f) = % (х) если равно(ху НИЛ) то НИЛ иначе cons(f(car(x))9(omo6p(f))(cdr(x))) или, может быть, лучше omo6p(f)z=g гдерек g^=X(x) если равно(х, НИЛ) то НИЛ иначе cons(f(car(x))> g(cdr(x))) Эта версия отобр, как мы покажем, является очень мощной функ- функцией. Пусть мы имеем / типа X -*• У, тогда отобр (/) имеет тип Список (X) -> Список (Y). Аналогичные рассуждения приво- приводят нас к заключению, что функция отобр (отобр (f)) имеет тип Список (Список (X)) -> Список (Список (Y)) Таким образом, функция отобр (отобр (f)) имеет своим аргу- аргументом двухуровневый список и выдает такой же. Если мы рас- рассмотрим, что именно она делает, то увидим, что она строит по аргументу х: Список (Список(Х)) конгруэнтный список у: Спи- Список (Список (Y))y в котором каждый элемент второго уровня из у получается как результат применения / к соответствующему элементу из х. Например, если /=Я(и)и+1 и мы имеем х«(A 2 3) D 5) F 7 8)) то (отобр(отобр(f)))(x)= (B 3 4) E 6) G 8 9)). Ясно, что вложение отобр может быть произвольно глубоким. Далее рассмотрим функцию отобр (отобр). Так как мы имеем отобр : (X -+ Y) -> (Список (X) -> Список (Y)) мы можем заключить, что отобр (отобр) : Список (X -> Y) -> Список (Список (X) -* Список (Y))
9.1. Типы функций 245 Другими словами, отобр(отобр) требует в качестве аргумента список функций, каждая из которых имеет тип X-+Y. Она выдает как результат список функций типа Список (X) -> Список (У). Соответствующий пример неизбежно потребует детального рассмотрения. Сначала определим функцию каждаяA1) = К (х) если равно(х, НИЛ) то НИЛ иначе cons((car(fl)) (х),(каждая(саг([1))) (х)) Эта функция имеет аргументом список функций. Ее резуль- результатом является функция, которая, будучи применена к х, по- порождает список, конгруэнтный исходному списку функций, и каждый элемент этого результирующего списка получается при- применением соответствующей функции к х. Таким образом, тип функции каждая таков: каждая: Список (X-+Y)-> (X -»- Список (Y)) Как пример аргумента для функции каждая рассмотрим список функций добавки, определенный следующим образом: do6aeKu=cons (К (v)v+1 ,cons (Мф+2,со/г$(?с(Ф+3,##«#))) Таким образом, список добавки состоит из трех функций, кото- которые соответственно увеличивают свой аргумент на 1, 2 и 3. Те- Теперь мы легко можем проверить правильность утверждения (каждая (добавки)) @)=A 2 3) В частности, мы можем проверить тип методом табулирования: добавки: Список(М —> N) каждая(добавки): N —>- Список (N) (каждая(добавки))@): Список (N) Теперь вернемся к рассмотрению функции отобр(отобр). Мы можем убедиться, что (каждая((отобр(отобр))(добавки)))@ 1 2)=(A 2 3) B 3 4) C 4 5)) Мы лучше поймем это выражение, если рассмотрим типы его подвыражений: добавкш CnucoK(N—+N) отобр(отобр)\ Список(Х —* Y) —> Список (Список(Х) -+ Список (Y)) (отобр(отобр)) (добавки): Список(Список(Ы) —+ Список(М)) каждая((отобр(отобр))(добавки)): Список(Ы) —> Список (CnucoK(N)) Из этого мы заключаем, что структуры аргумента и результата в нашем выражении имеют правильный тип. Более тщательный
246 Гл. 9, Функции высших порядков анализ убедит читателя в том, что выписанный результат дей- действительно тот, который может быть получен вычислением. Читатель может предположить, что если он сможет широко применять функции с возможностями отобр, то программы ста- станут не только очень короткими, но и редко будут использовать концепцию переменной. Действительно, этого можно достиг- достигнуть, по крайней мере в специальных областях применений, и недавно Бэкусом были приведены на этот счет очень убедитель- убедительные аргументы. Бэкус вводит некоторые мощные функции; те функции, которые мы здесь рассматривали, являются простыми эквивалентами функций, базирующихся на его более общих формах. В основе метода Бэкуса лежат результаты комбина- комбинаторной логики, где изучаются функции, называемые комбинато- комбинаторами. Эти результаты касаются того факта, что, задав только несколько элементарных комбинаторов, можно построить все другие функции без использования Х-выражений. Таким обра- образом, теоретически возможно вовсе избежать использования пере- переменных, хотя удобно иметь по крайней мере имена для функ- функций. Чтобы дать представление о программах такого рода, изу- изучим два из основных комбинаторов. Интересующемуся читателю рекомендуем не только работы Бэкуса, но и соответствующие книги по комбинаторной логике, тем более, что эта красивая теория может быть изучена даже независимо от остальных раз- разделов математической логики. Первый комбинатор, который мы рассмотрим, является эквивалентом функции комп, которая была введена в разд. 2.8. Там мы определили KOMn{fy g)^(x)f(g(x)) и отсюда, хотя мы и не имели возможности доказать это, следо- следовало, что комп: (Y -+ Z)x (X -+ Y) -v (X -> Z) для любых типов X, Y и Z. Эквивалентный комбинатор обозна- обозначают обычно через В. В имеет тип В : (Y-+Z)^ ((Х-У)- (X^Z)) и определением В служит соотношение B=k(f){X(g){X(x)f(g(x))}} Однако привычнее определять комбинаторы через тождества, которым они удовлетворяют. Для В имеем следующее тождест- тождество: ((B(f))(g))(x)=f(g(x)) В комбинаторной логике принимаются специальные меры, поз- позволяющие избежать лишних скобок, но здесь мы не будем ка-
9.1. Типы функций 247 саться этого вопроса. Из приведенного ясно, что можно опреде- определить дваясды(П**(Вф)ф но здесь мы не избежали использования переменной /. Эта проб- проблема решается при введении другого комбинатора, обычно обоз- обозначаемого через W, который удовлетворяет тождеству (W(f))(x)=(f(x))(x) Отсюда мы видим, что дважды может быть переопределена сле- следующим образом: дважды=№(В) просто из подстановки в тождество. Это является основой про- программирования без переменных, в нашем случае без Х-выраже- ний. Поучительно вывести тип функции дважды из исследова- исследования этого выражения, но это оставляется в качестве упражнения любознательному читателю. Наконец, в нашем экскурсе в область различных типов функ- функций высших порядков мы приходим к понятию продолжения. Здесь мы просто приведем пример полезности этого понятия и обсудим его общую форму, используя наше понятие типа. В сле- следующей главе будет приведен более убедительный пример, де- демонстрирующий возможности этого средства. Продолжение яв- является функцией, которая используется как параметр некоторой другой функции, а эта вторая функция передает свой результат продолжению. Вообще все, что должно быть сделано с результа- результатом этой второй функции, связано с продолжением. Полезность такого способа программирования заключается в том, что функ- функция, имеющая продолжение параметром, может, если нужно, не вызывать продолжение, отсекая часть или даже весь остаток программы. Рассмотрим функцию факториал, которая без продолжения определяется обычно следующим образом: фак(п)= если я=0 то 1 иначе пх фак (п—1) Теперь включим в определение продолжение факт(п, с) ^ если дг = 0 то с(\) иначе факт(п — 1, X (z)c(n x г)) Продолжением является параметр с. Сначала заметим, что фак (&)=факт (?Д(г)г). To есть функ- функция факт может вычислять те же самые значения, что и фак, если тождественную функцию принять за исходное продолже- продолжение. Мы должны убедиться, что факт действительно передает п\ продолжению с. Это верно при я=0. Если предположить, что
248 Гл. 9. Функции высших порядков это верно, когда первый параметр имеет значение п—1, то мы видим, что (п—1)! передается функции k(z)c(nXz)\ отсюда дей- действительно п! передается с. Вообще если мы имеем функцию типа X -»- Y и добавляем к ней продолжение, то получаем функ- функцию типа X X (Y ->- Z) -> Z для некоторого Z. Другими словами, тип результата продолжения определяет тип результата расши- расширенной функции. Чтобы проиллюстрировать этот факт, рассмот- рассмотрим вызов факт (/г, X(z) cons(z, z)), результатом которого будет па- пара (л!, п\). Теперь продемонстрируем использование продолжения для отсечения части программы. Сначала изменим определение факт, включив проверку неотрицательности аргумента: факт(п, с) = если п < 0 то сопз(ОТРИЦу п) иначе если п = 0 то с(\) иначе факт(п — 1, А, (г) с (п х г)) Важно отметить, что если при вызове факт обнаружится, что п<0, то результатом будет cons {ОТРИЦ, п) и продолжение не вызывается. Отсюда, если сделан, например, вызов фактAг, tk(z)cons(z дел m, z ост т)) и при этом ?^0, то факт действительно вычисляет факториал и передает его в продолжение, где вычисляется k\ дел т и k\ ост т. Однако при k<0 результатом будет (ОТРИЦ. ft), и он не пе- передается последующим арифметическим операциям дел и ост. Ясно, что если эти последующие вычисления были бы длиннее, то с тем большим основанием можно было бы сказать, что вычисле- вычисления были сокращены за счет отсутствия вызова продолжения. Программирование с продолжениями в больших программах яв- является далеко не прямолинейным, кроме тех случаев, когда применяются очень простые схемы, такие, например, где каждая функция имеет продолжение. В действительности мы не даем здесь рекомендации широко использовать программирование с продолжением, это средство было изобретено скорее, чтобы дать формальное математическое обоснование семантики таких кон- концепций языков программирования, как goto, а не как сред- средство программирования. Как программисты, мы должны пони- понимать, что программирование с такими функциями, как вторая версия факт, будет нелегким делом. Мы начинаем ясно осознавать это уже тогда, когда пытаемся специфицировать тип этой функ- функции. Результат факт(п, с) имеет тип либо Z, когда с: N~> Z, либо cons двух атомов. Оперирование с такими гибридными объек- объектами не делает программирование очень простым. Тем не менее продолжения являются важной концепцией, и их полезность мож- можно оценить, внимательно разобрав упр. 9.8.
9.2. Описание синтаксиса языка 249 9.2. Описание синтаксиса языка Перейдем к первому из наших двух примеров применения функций высших порядков. Обратимся к задаче построения рас- распознавателя для простого контекстно-свободного языка. Кон- Контекстно-свободным является язык, синтаксис которого может быть описан множеством правил, обычно называемых продук- продукциями. В нашем случае это следующие правила: <сгруппа> :: = С| С <сгруппа> (vepynnay :: = V \ V <vepynna> <слог> :: = <сгруппа> <угруппа> \ <мгруппа> <сгруппау \ <сгруппа> (vepynnay <сгруппа> В этом множестве продукций (грамматике) терминальными символами являются С и У. Грамматика определяет язык строк, состоящих из символов С и V. Имеются три типа конструкций: (сгруппау есть последовательность одного или более символов С, ^группа} — аналогичная конструкция из V, <слог> имеет три формы — последовательность символов С, завершаемая последовательностью V, затем симметричный случай, где С и V меняются местами, и, наконец, более сложная структура — группа С, группа V, группа С. Грамматика предназначается для описания общих форм слогов в английских словах. Здесь С служит для обозначения согласных, а V — гласных букв. Мы могли бы сделать С и V нетерминальными и перечислить для них соответственно 21 и 5 возможностей. Но мы не станем этого делать, так как это очевидно и неоправданно усложнило бы наш пример. Если в приведенной выше грамматике мы считаем С и V терминальными символами, то следующие строки будут пра- правильными экземплярами конструкции <слог>: CV, VC, CVC, CCV, VCC, VVC, CVVC, CCVVC Разумеется, существует бесконечно много таких возможностей. Предположим, что строки, предназначенные для распознава- распознавания, представляются списками элементов С и V. Пусть опре- определены функции глас и согл, такие что если х есть список атомов, то глас(х) есть ист только в том случае, когда список х состоит из единственного атома V, в противном случае глас(х) есть ложь. Аналогично согл (х) есть ист только в том случае, когда спи- список х состоит из единственного атома С, в противном случае значением согл (х) является ложь. Ясно, что мы без труда могли бы переопределить эти функции так, чтобы они выделяли 21 соглас- согласную и 5 гласных букв английского алфавита. Если мы обозна- обозначим через А тип атома и через L тип логичесского значения, ко-.
250 Гл. 9. Функции высших порядков торое есть либо И (ист), либо Л (ложь), то имеем глас: Список(А) —* L согя: Список(А) —> L Теперь мы хотим определить функцию типа Список(А) -> L, которая по данному списку, представляющему строку, полу- получала бы значение Я, если эта строка имеет одну из форм, раз- разрешенных нашей грамматикой, и значение Л в остальных слу- случаях. Чтобы сделать это, мы определим сначала некоторые функ- функции высших порядков, которые позволят нам получать преди- предикаты (функции типа X -> L) из предикатов. Рассмотрим сначала функцию или(р, q)^k(x) если р(х) то И иначе q(x) Если мы предполагаем, что р и q имеют тип X -> L, то типом этой функции будет или: (X->I)X(X-*L)-* (X-+L) Таким образом, или{р, q) также имеет тип X ->¦ L. Фактически или(р, q), будучи примененной к аргументу х : X, получает зна- значение ист только в том случае, когда это значение принимает либо /?, либо q. Например, или(согл, глас) является предикатом типа Список (А) -> L, принимающим значение ист, только если аргументом является список из одного атома С или V. Следую- Следующая нужная нам функция менее простая. Она должна опреде- определять по списку х, можно ли его разделить на две части так, чтобы первая из них удовлетворяла предикату р и вторая — q. На- Назовем эту функцию части и решим, что она имеет тот же тип, что и или. Рассмотрим, как можно расщепить список х на два списка у и z так, чтобы соединить (у, z)=x. Возможны два слу- случая: х=НИЛ и хфНИЛ. В первом случае существует только одна возможность: случай 1: х=НИЛ, у=НИЛ, г=НИЛ Во втором случае будем рассматривать две возможности: случай 2: хфНИЛ подслучай(а) у = НИЛ, z = x подслучай(Ь) Пусть соединить{у1У z1) = cdr(x) тогда y = cons(car{x), г/х), z-=-zx Подслучай (а) предполагает, что список х расщепляется на у и z простым способом, посредством выбора у=НИЛ. Подслу- Подслучай (Ь) более богат возможностями. Здесь мы расщепляем cdr(x) на части ух и zx и затем строим у из саг(х) и уи а в качестве z берем гх.
9.2. Описание синтаксиса языка 251 Можно экономнее записать определение функции части, если воспользоваться логической связкой и: ех и е2 принимает значе- значение ист только тогда, когда и еи и е2 оба имеют значение ист; ех и е2 есть ложь, если ех есть ложь либо ех есть ист, а е2 есть ложь. То есть мы имеем следующее тождество: ех и е2=если ех то е2 иначе Л Теперь определим функцию части: части(р, q) = X (х) если равно(ху НИЛ) то р(НИЛ) и д(НИЛ) иначе если р(НИЛ) и ^(х) то Я иначе (части(Цу)р(соп8(саг(х), у)), q)) (cdr{x)) Это определение требует некоторых объяснений. Три предложе- предложения в нем связаны соответственно со случаями 1, 2(а), 2(Ь). Если х есть НИЛ, то оба подсписка тоже НИЛ, и отсюда мы вычисляем р(НИЛ) и q(HИЛ). Когда хфНИЛ, мы проверяем, не удовлетворяет ли предикатам р и q тривиальное разбиение, и вычисляем р(НИЛ) и q(x). Если удовлетворяет, то вычисле- вычисление функции заканчивается и выдается значение ист. Если нет, то мы должны попытаться расщепить cdr(x). Фактически мы используем то соображение, что если х может быть разбит на два нетривиальных подсписка, которые удовлетворяют р и q соответственно, это означает, что cdr(x) может быть расщеплен таким образом, что удовлетворяются X(y)p(cons(car(x), у)) и q соответственно. Первый из этих двух предикатов просто берет первый подсписок списка cdr(x) и, ссылаясь на него как на у, присоединяет к нему саг(х), прежде чем применить р. Читатель может проверить, если захочет, что это определение вытекает из того, что мы приняли за тип функции части. Поясним теперь, как может применяться функция части. Рассмотрим выражение части (согл, глас). Этот вызов корректен по типу, а результат имеет тип Список (А) ->- L.Фактически этот предикат имеет значение ист, только когда применяется к списку, состоящему в точности из двух элементов, первый из которых есть С, а второй — У. Теперь рассмотрим части (согл, или(согл, глас)) Это снова функция типа Список (А) -+ L. Этот предикат исти- истинен только для списков из двух элементов, первый из которых есть С, а второй — С или V. Имея эти мощные функции, мы можем теперь определить распознаватель для нашего простого языка, введенного в начале раздела. Необходимые определения суть
252 Гл. 9. Функции высших порядков сгруппа = или(соглучасти(согл,сгруппа)) vepynna ss или(глас,части(глас,игруппа)) слог == или(части(сгруппа, vepynna), или(частиA*группа, сгруппа), части (сгруппа, части(ргруппа, сгруппа)))) Соответствие типов легко проверяется. Требует некоторого ис- исследования тот факт, что эти функции правильно определяют распознаватель. Предикат сгруппа истинен, когда его аргумент удовлетворяет предикату согл или является списком, разбивае- разбиваемым на два таких, один из которых удовлетворяет предикату согл, другой — сгруппа. То есть это в точности та категория фраз, на которую мы ссылаемся как на (сгруппау. Аналогично <vepynna> является распознавателем фраз категории <угруппа>. Наконец, слог истинен, когда применяется к списку х> имею- имеющему одну из трех структур, определенных в последнем син- синтаксическом правиле. Важно отметить, что определения этих функций представ- представляют собой точную транслитерациию синтаксических правил. Возможность написать наши определения в такой форме, без уточнения деталей более низкого уровня, была достигнута бла- благодаря удачному выбору функций высшего порядка или и части. Распознаватель такого сорта может быть не очень эффективным и может иметь некоторые теоретические ограничения (он может произвольным образом устранять неоднозначности в неодно- неоднозначном языке). Но он обладает тем важным свойством, что если наши определения действительно правильно реализуют эквива- эквивалентные концепции, используемые при написании продукций, то такой распознаватель будет правильным автоматически Программа не может более наглядным образом реализовать правила синтаксиса. Она по существу является синтаксическим правилом. Это наиболее убедительная демонстрация того, как функции высших порядков представляют выполняемые програм- программы в виде естественных выражений. 9.3. Описание структуры фигур Обратимся теперь к такого рода приложениям, какими мы нигде на протяжении этой книги не занимались. Мы покажем, как соответствующие функции высших порядков могут быть оп- определены так, что построенные из них выражения как будто спе- специально предназначены для описания геометрических фигур. Эти функции будут определены таким образом, что их вычисле- вычисление приводит к конструированию фигур. Класс фигур, которые могут изучаться таким образом, неограничен, но в этой главе мы сосредоточим внимание на фигурах, которые можно было
9,3. Описание структуры фигур 253 бы назвать «черепичными». В основном примере мы будем ис- использовать фигуры, составленные из упорядоченных кирпичи- кирпичиков, подобные кирпичным стенкам. Беглый взгляд на рисунки, иллюстрирующие эту главу, познакомит читателя с особеннос- особенностями этих фигур. Сначала необходимо припомнить некоторые свойства геометрического понятия вектора. Нам понадобятся только элементарные понятия сложения и вычитания векторов Рис. 9.1. Рис. 9.2. и умножения вектора на число. Мы включим эти средства в наш чисто функциональный язык, отметив, что они легко могут быть реализованы в терминах уже существующих в этом языке средств, но такой реализацией непосредственно заниматься не будем. В геометрии векторы используются для представления пере- перемещений в пространстве. Мы будем рассматривать только дву- двумерное пространство, т. е. плоскость. Будем обозначать векторы жирными латинскими буквами а, Ь, с и т.д. и изображать их на рисунке направленными стрелками (рис. 9.1). Значением век- вектора а является перемещение точки из конца стрелки к ее острию. Вообще двумерный вектор а представляется парой чисел <ах, ау>, представляющих компоненты перемещения параллельно оси х и у соответственно (рис. 9.2). Для такого представления мы можем определить операции сложения и вычитания векторов следующим образом: а = <ах, ау> а + b = <ах + Ьх, ау + Ьуу b = <bx, by> — а = <— аХУ —ауу Сложение векторов удовлетворяет известному правилу па- параллелограмма. На рис. 9.3. оно представлено в наглядной форме, к тому же особенно удобной для наших приложений. Перемещение, представляющее а+Ь, получается выпол- выполнением перемещения а и затем перемещения b или в обратном порядке. Читатель легко может проверить это, начертив проек- проекции векторов параллелограмма относительно осей х и у. Вектор с минусом дает вектор, равный по величине исходному и обрат- обратный по направлению (рис. 9.4). Мы определяем вычитание векторов соотношением a—b=a+(—b)
254 Гл. 9. Функции высших порядков Вектор не фиксирован в пространстве, он просто имеет на- направление и величину. Мы рассматриваем 0=<0, 0> как спе- специальный случай вектора с нулевой величиной. То, что он не Рис. 9.3. Рис. 9.4. имеет направления, не вызывает проблем. Ясно, что а+0=а Рассмотрим очень простую фигуру, изображенную на рис. 9.5. Все четыре параллелограмма конгруэнтны. Мы можем Рис. 9.5. перечислить перемещения из точки 0 в некоторые точки нашей фигуры, как это сделано в табл. 9.1. Таблица 9.1
9.3. Описание структуры фигур 255 Последнее необходимое нам полезное свойство вектора сос- состоит в том, что вектор можно умножать на число. Это умноже- умножение определяется соотношением ka=<kax, kayy Так, например, перемещение из О в / на рис. 9.5. может быть пе- переписано как а+2Ь+2с. Доказанным свойством векторов яв- является то, что мы можем манипулировать с ними алгебраически, Рис. 9.6. используя операции, которые мы определили, и применяя те же самые правила, что и для чисел. Теперь рассмотрим, как именно мы собираемся описывать фигуры, чтобы их можно было по этому описанию изобразить каким-либо механическим инструментом, таким как графопостро- графопостроитель. Мы выбираем метод построения структур данных, кото- которые содержат детали каждой линии фигуры. Предполагаем, что фигуры строятся из отрезков прямых линий. Сначала рассмот- рассмотрим, как можно определить такой отрезок. Мы должны описать его положение относительно начала, его величину и направление. Это можно сделать двумя векторами (рис. 9.6). Отрезок AB опи- описывается двумя векторами а и Ь. Вектор а определяет положение точки А относительно О, и вектор b — положение точки В отно- относительно А. Структура данных, которую мы будем использовать для представления отрезка AB, записывается так: отрезок (а, Ь) Лучше представлять себе функцию отрезок аналогичной cons. Она строит запись, которая включает векторы а и b и которая распознается как запись такого типа. Таким образом, при ана- анализе структуры данных, представляющей фигуру, мы можем оп- определить, когда нам встречается подструктура, представляющая отрезок. Далее, для представления фигуры, состоящей более чем из одного отрезка, мы используем другую, подобную cons функцию, которую назовем фигура, и такую, что если р и q суть представления фигур, то фигура (р, q) есть структура данных, полученная включением всех отрезков из р и всех отрезков из q.
256 Гл. 9. Функции высших порядков Если мы обозначим через V тип значения вектора и через Р тип структуры данных, представляющей фигуру, то имеем сле- следующие типы наших функций: отрезок] V xV —+ Р фигура: Р х Р —* Р Правило параллелограмма может быть описано конструи- конструированием структуры фигура(отрезок@,а), фигура(отрезок@, b), фигура(ртрезок) (а, Ь), фигура(отрезок(Ъ^,отрезок{Ъуъ + Ъ))))) которая представляет фигуру, использованную нами для иллю- иллюстрации этого правила, левый нижний угол этой фигуры рас- расположен в начале координат (рис. 9.3). Чтобы проиллюстри- проиллюстрировать, как именно эти функции используются для построе- построения фигур, рассмотрим функцию, которую назовем лесенка. Когда мы используем переменные, принимающие векторные значения, мы обозначаем их как обычные переменные через а, Ьу с,. . ., резервируя жирные буквы a, b, с,. . . для векторных значений. лесенка: N xV xV —+ Р лесенка(п,а,Ь) = если п = 1 то отрезок(а,Ь) иначе фигура(отрезок(п х ауЬ),лесенка(п — 1, а, Ь)) Эта функция, если ее вызвать с аргументами лесенкаG,а,Ь), построит фигуру, изображенную на рис. 9.7. Здесь мы для наглядности справа от фигуры изобразили век- векторы, на которых она базируется. Определение функции лесенка совершенно аналогично многим другим функциям, которые ис- использовались ранее, и читатель сразу увидит, что вызов лесенка G,а,Ь) породит фигуру, включающую отрезки отрезок Gа, Ь), отрезокFа,Ь),. . ., отрезок(а,Ь) которые и являются как раз ступеньками лестницы, изображен- изображенной на рис. 9.7. Если мы введем функции, которые позволят нам вычислять проекции вектора на оси координат, мы легко определим прямоу- прямоугольники. Определим хпроек и упроек следующим образом: хпроек(г)=<ах, 0>, упроек (а) = <0, а^> Обе функции имеют тип V -* V. Мы можем теперь описать пря- прямоугольник, задав положение одной из его вершин и величину и направление диагонали. Наши прямоугольники всегда имеют стороны, параллельные осям:
9.3. Описание структуры фигур 257 прямоуг(&, Ь) = фигура(фигура(отрезок(&> хпроек(Ъ)), отрезок(ь, упроек(Ъ))), фигура(отрезок(& + Ъ,—хпроек(Ъ), отрезок^ + Ь,— упроек(Ъ)))) Читатель может легко убедиться, что фигура, порожденная вы- вызовом прямоуг(&9 Ь), изображена на рис. 9.8. Здесь а задает пе- Рис. 9.7. Рис. 9.8. ремещение точки А относительно О и b — точки В относитель- относительно А. Теперь приступим к описанию функций высших порядков, которые будут задавать наши черепичные фигуры. Отметим, что функции отрезок и прямоуг имеют тип VX V-> P. Мы бу- будем называть функции этого типа базовыми. Нас интересуют та- такие функции, которые базовые функции переводят опять в ба- базовые. Сначала рассмотрим функцию обе(р, q)=X(af b) фигура (р (а, Ь), q(a, b)) которая имеет тип обе: (VxV->P)X(VxV->P)-> (VxV-*P) Другими словами, функция обе порождает базовую функцию по двум базовым. Она определяет базовую функцию обе(р, q), которая совмещает обе фигуры, порожденные функциями р w q, и размещает их на одном и том же месте. Так, базовая функция обе {прямоуг, отрезок) порождает прямоугольник с диагональю внутри его. То есть вызов (обе(прямоуг, отрезок)) (а, Ь) поро- порождает фигуру, изображенную на рис. 9.9. 9 № 929
258 Гл. 9. Функции высших порядков Вообще мы будем ссылаться на базовые функции без упоми- упоминания векторов, к которым они применяются. Все, что эти век- векторы могут сделать, это изменить расположение фигуры и ее величину, но это сейчас безразлично для нас. Так, мы говорим, что базовая функция обе (прямоуг, отрезок) порождает прямо- Рис. 9.9. угольник с диагональю. Если мы обозначим тип базовой функ- функции через S, то функции прямоуг, отрезок и обе (прямоуг, отре- отрезок) все имеют тип S, а функция обе — тип SxS -> S. Теперь определим функцию типа NxS-*- S, которая позво- позволяет нам перемещать фигуры вдоль оси: хсдвиг(к, р)вЯ(а9 Ь)р (а+кХхпроек(Ь), Ь) Из этого определения видно, что хсдвигф, р) порождает ту же самую фигуру, что и функция /?, но на k единиц правее, где еди- единицей является проекция вектора b на ось х. Так, например, функция обе (прямоуг, хсдвигC, прямоуг)) порождает прямо- Рис. 9.10. угольники, изображенные на рис. 9.10, при этом "в проме- промежутке могут разместиться ровно два таких же прямоугольника. Две подобные этой функции усдвиг и асдвиг позволяют нам перемещать фигуры вдоль оси у и вдоль диагонали соответст- соответственно. Приведем определения этих функций: ycdeue(k,p) = X (a, b)p(a + k х упроекф), Ь) dcdeu2(k9p)z=X(a, b)p(a + k x b, b) Используя эти определения, можно получить другие функции, позволяющие нам повторять фигуры произвольное число раз.
9.3. Описание структуры фигур 259 Например, следующие две функции имеют тип NXS-+S: строка(п,р) = если п=\ то р иначе обе(р, хсдвигA, строка(п—\,р))) столбец(п,р)^=-если п=1 то р иначе обе(р, усдвиг(\, столбец(п—\у р))) Эти функции представляют фигуры, полученные n-кратным пов- повторением фигуры, которая получается применением функции р. Повторение происходит в направлении осей х и у соответственно. Рис. 9.11.. Рис. 9.12. Отсюда строкаF, прямоуг) дает нам рис. 9.11, столбец C, пря- прямоуг) — рис. 9.12, а столбецC, строка(&, прямоуг)) дает нам картину, изображенную на рис. 9.13, и это уже почти настоя- настоящая кирпичная стенка, только без смещения кирпичей. Интерес- Интересно, что туже самую картину (рис. 9.13) дает и строкаF, стол- столбец C, прямоуг)), только предыдущее выражение разбивало фи- Рис. 9.13. гуру на 3 уровня по вертикали, а последнее — на 6 частей по горизонтали. Чтобы сместить кирпичи в стенке, введем следующую функ- функцию, которая напоминает хсдвиг, но совершает дробные смеще- смещения: хдроб(к, р)~к(а, b)p(a-\-xnpoeK(b)lk, b) Так, например, фигуры, обозначенные через прямоуг и обе (пря- (прямоуг, xdpo6Ct прямоуг)), изображены на рис. 9.14. Теперь для описания кирпичной стенки мы хотим чередовать слои строка (п, прямоуг) и хдробB, строка (л, прямоуг)). Oft- 9*
260 Гл. 9. Функции высших порядков ределим функцию, подобную функции столбец, но с чередую- чередующимися элементами по вертикали: верт(п, р, q) = если л = 1 то р иначе обе(р,усдвиг(\,верт(п—\, q, p))) Отметим, что две базовые функции, параметры р и q, чере- чередуются при рекурсивных вызовах функции верш. Кирпичная Рис. 9.14. стенка, изображенная на рис. 9.15, задается следующим выра- выражением: верт F ,строка {5,прямоуг),хдроб B,строка E .прямоуг))) Оно описывает 6 чередующихся слоев, которые состоят либо из б прямоугольников, либо из тех же прямоугольников, сдвину- Y/7//A Рис. 9.15. тых вправо на половину размера. Соответственно описание строка E, вертF9 прямоуг, хдробB, прямоуг))) задает нам строку из 5 столбцов, изображенных на рис. 9.15. Каждый столбец имеет в высоту 6 прямоугольников. Каждая из этих функциональных форм является кратким описанием фи- фигуры, которую она обозначает. Читателю рекомендуется убе- убедиться самому, что при вычислении они дают действительно фигуру, изображенную на рис. 9.15,—кирпичную стенку. Прежде чем расстаться с красивой темой, рассмотрим еще одну фигуру. Предположим, что имеется следующая функция, снова типа NXS-+S: KacKad(ntp) = tcm n=l то р иначе обе(р, усдвиг( 1 ,хдробB,каскад{п — 1,/?))))
9.3, Описание структуры фигур 261 Беглого взгляда достаточно, чтобы увидеть, что эта функция базируется на функциях строка и столбец. Исходная фигура смещается наверх, как и в функции столбец, но, кроме того, смещается и по горизонтали, на половину размера в направле- направлении х. Так, например, выражение каскад (Ъ, прямоуг) имеет форму, изображенную на рис. 9.16. Рис. 9.16. Рис. 9.17. Снова мы можем определить фигуру в виде кирпичной стен- стенки, используя строка (п, каскад (т, прямоуг)) или эквива- эквивалентную конструкцию каскад(т, строка (п, прямоуг)). Но в этом случае у стенки скошены два угла. Затем мы определим функцию даже более высокого уровня. Она будет иметь тип Nx(NxS->S)xS^S Второй параметр здесь того же типа, что и функции строка, столбец и каскад. Функция хцепь (п, s, p) строит фигуру, сос- Рис. 9.18. тавленную из s(i, р), где i принимает все значения от 1 до п. хцепь{п,8,р) = если л=1 то s(ltp) иначе обе(8(п,р),хцепь(п— 1, s, хсдвиг(р))) Согласно этому определению, выражение хцепьE, столбец, прямоуг) описывает фигуру, изображенную на рис. 9.17, а вы- выражение хцепь(Ьу каскад, прямоуг) — фигуру на рис. 9.18. Ясно, что аналогично можно определить функции уцепь и ацепь, и тог- тогда может быть описан еще целый ряд фигур, составленных из кирпичиков.
262 Гл. 9. Функции высших порядков Упражнения 9.1. Функция комп {увел (\),ост B)) была определена в разд. 2.8. Определите ее тип. 9.2. Функция редукция(х,gta) s= если равно(х,НИЛ) то а иначе g(car(x) ,редукция(саг (x)yg,a)) была определена в разд. 2.8. Определите ее тип. 9.3. Обозначим через Дерево (X) тип объекта, который либо имеет тип X, либо есть cons двух объектов типа Дерево (X). Переопределите типы функций car, cdr и cons. Затем определите тип функции наборах) ass если атом(х) то cons(x,HHJI) иначе соединить (набор (саг (х)), набор (cdr (x))) а также в предположении соединить: Список(Х)Х Список(Х)-+Список(Х) протабулируйте типы всех подвыражений. 9.4. Обозначим через Задерж(Х) тип объекта, который является рецептом, а его значение при возобновлении имеет тип X. Имеем задержать: X —> Задерж(Х) возобновить :3адер ж (X) —> X Теперь определим тип функции соединиться) зз если равно(х, НИЛ) то у иначе cons(car(x), задержать (соединить (возобновить cdr,(x)yy))) Необходимо будет ввести тип Задержсписок (X) и переопределить car, cdr, cons для этого типа. 9.5. Каков тип функции комп (отобр, отобр)? Определения комп и отобр, берем как в тексте: компA,8)шшЦх) f(g(x)) omo6p(f) за К(х) если равно(х, НИЛ) то НИЛ иначе cons(f(car(x))y(omo6p(f))(cdr(x))) 9.6. При указанном выше определении функции отобр каков будет тип функции отобр (отобр (отобр))} Как можно использовать такую функцию? 9.7. Использовав определение в тексте комбинатора В, определите тип В (отобр). 9.8. Возможно переделывать недетерминированные программы, описан- описанные в гл. 7, в детерминированные, использовав продолжения следующим об- образом. Каждой функции в программе даются два продолжения: одно берется в случае нормальной работы, другое — в аварийной ситуации. Когда встре- встречается нет, вызывается аварийное продолжение (оно не имеет параметров), в противном случае нормальным образом используется обычное продолжение. Введите соответствующие правила преобразования недетерминированных программ в детерминированные и преобразуйте по ним программу для огра- ограниченных перестановок, приведенную в разд. 7.1. 9.9. Определите функцию и того же типа, что и функция или, и такую что и(р, а) есть истина только в том случае, когда список, к которому она при- применяется, удовлетворяет как /?, так и q. Для какой проблемы синтаксического анализа была бы полезна эта функция?
Упражнения 263 9.10. Определите распознаватель для грамматики простого арифметиче- арифметического выражения <терм>:: =<первичное> | (терму—<первичное> <первичное>:: = / | <терм> где / обозначает некоторый идентификатор, Рис. 9.19. 9.11. Переопределите функции столбец, строка и каскад так, чтобы они имели тип не NXS-+S, a N-+(S-+S). Таким образом, например, вызов стол* бецF, прямоуг) заменится на (столбец F)) (прямоуг). 9.12. Определите функцию цепь так, чтобы, например, цепь {п,столбец ,хсдвиг,р)=хцепь (я, столбец,р) где функции столбец, хцепь и хсдвиг определены как в тексте. 9.13. Опишите фигуру, изображенную на рис. 9.19, использовав функции упр. 9.12 или иным способом.
Глава 10 ЯЗЫКИ ПРОГРАММИРОВАНИЯ И МЕТОДЫ ПРОГРАММИРОВАНИЯ Вернемся к обсуждению взаимосвязи между современными языками программирования и строго функциональным языком, которое было начато в гл. 1. Там было высказано предположе- предположение, что традиционные языки программирования так велики и громоздки потому, что они должны удовлетворять двум противо- противоречащим друг другу требованиям. С одной стороны, языки должны с приемлемой эффективностью использовать современ- современные машины, с другой стороны, они должны возможно яснее выражать программируемые алгоритмы, чтобы облегчить про- проверку последних. Строго функциональный язык, который изу- изучается в книге, будучи очень простым, демонстрирует тем не менее большую степень выразительности по сравнению с тради- традиционными языками с присваиванием. Многие из функциональ- функциональных программ могут достаточно эффективно вычисляться на современных машинах, но, разумеется, не так эффективно, как соответствующие программы с присваиванием. В этой главе мы рассмотрим, в какой степени это является просто свойством выб- выбранных специальных областей структур данных, и покажем, что возможен выбор других областей. Прежде всего, однако, рассмотрим способы, с помощью кото- которых можно более ясно представлять функциональные програм- программы в языках, несколько более детализированных по сравнению с тем, который изложен в книге. В частности, обратимся к проб- проблеме функций с составным результатом, так как здесь имеется важное и элегантное решение. Кратко будут рассмотрены дру- другие возможные области данных для функционального програм- программирования и показано, что при соответствующем выборе облас- областей функциональные программы можно рассматривать как опи- описание встроенных компонент машины. После этого мы сможем перейти к обсуждению важного вопроса эффективности. В сле- следующей главе будет изложена конкретная реализация функцио- функционального языка, которая является основой нашего утверждения о том, что некоторые функциональные программы могут эффек- эффективно выполняться на современных вычислительных машинах. Тем не менее мы должны признать преобладающее влияние сов- современных машин на языки и методы программирования, и в
10,1. О ясности выражения 2б§ особенности влияние оператора присваивания на структуру наших программ. Мы заканчиваем эту главу утверждением, что новая архитектура машин может изменить соотношение, которое сегодня сложилось в пользу оператора присваивания, и что раз- развитие в этом направлении чрезвычайно желательно. 10.1. О ясности выражения В главах 7—9 были приведены многочисленные примеры эффективных функциональных программ, естественным образом описывающих некоторые вычисления. Эта ясность выражения в программах имеет величайшую важность, помогая установить правильность нашей программы. Доказательство правильности может быть построено только для программ с ясной структурой, так как в противном случае оно будет слишком непонятным. Одним из фундаментальных свойств языка программирования, которое делает возможным ясно описывать вычисления на этом языке, является простота семантики его конструкций. Боль- Большим преимуществом нашего строго функционального языка яв- является то, что в нем имеется всего несколько основных понятий и каждое имеет относительно простую семантику. В частности, семантика нашего языка понимается полностью в терминах значений, которыми обладают выражения, а вовсе не в терминах действий в порядке их вычисления. Однако с практической точ- точки зрения было бы справедливым заключить, что наш строго функциональный язык чересчур элементарен и некоторые очень простые расширения значительно повысили бы его возможности ясного выражения некоторых классов вычислений. Мы должны тщательно различать чисто синтаксические расширения и рас- расширения, которые требуют пересмотра семантики языка. Пос- Последние требуют большой осторожности, если их включение в язык затрудняет понимание уже отлаженных программ. Чисто синтаксические изменения, которые также требуют внимания, не могут, однако, противоречить нашему пониманию уже гото- готовых программ. То, что чисто синтаксические расширения могут обладать большими возможностями повышения ясности выраже- выражения, иллюстрируется тем расширением, которое мы сейчас рас- рассмотрим. Расширим наш строго функциональный язык, позволив функ- функциям выдавать составные результаты. Это делается в два эта- этапа следующим образом. Сначала будем считать правильным вы- выражением список правильных выражений, заключенный в уг- угловые скобки. Так, выражение <еь е2,. . ., ?*> правильное, если правильны еи е2,. - ., ek. Другое расширение состоит в том, что если прежде в языке разрешались в левой части определений (в блоках) только простые переменные, то теперь допускается
266 Гл. 10. Языки программирования и методы программирования список идентификаторов, тоже заключенный в угловые скобки. Способ применения этого комбинированного расширения ил- иллюстрируется следующим примером: {пусть f = %(x,y)<x ост у, х дел у> пусть <г, d> = f(w, v)...} Заметим, что результатом функции / является список значений и что структуру этого списка точно копирует список перемен- переменных, которому присваивается результат. Отсюда внутренний блок связывает г со значением и ост v, a d со значением и дел и. Ясно, что это чисто синтаксическое расширение, так как все, чего мы добились при помощи угловых скобок, мы могли полу- получить операцией cons: {пусть ! = к(х, y)cons(x ост у, cons(x дел у,НИЛ) пусть s = f (и, v) пусть r = car(s) и d = car(cdr(s))} Поскольку список переменных допускается в левой части опре- определений, он может входить и в список формальных параметров. Далее совершенно очевидным образом можно разрешить вложе- вложение списков переменных, но мы не будем здесь это рассматривать. Чтобы проиллюстрировать возросшую ясность выражения, которая была нами получена при этом расширении, рассмотрим следующее определение поразрядного сложения с переносом единицы. Пусть имеется функция, которая складывает три циф- цифры по модулю /л. слож\ (а, Ъ, с)=<(а+&+с ) дел т, (a+b+с) ост т> Здесь предполагается, что а и b — цифры, которые складывают- складываются при поразрядном сложении, ас — единица переноса. Ре- Результатом функции слож\ является пара значений, а именно цифра переноса из данного разряда и цифра суммы. Теперь мож- можно определить функцию сложп, которая берет в качестве аргумен- аргументов два списка цифр по модулю /л, причем оба списка одинаковой длины, и выдает список цифр суммы той же длины и конечную цифру переноса. Определение прямолинейно: сложп(х, у) = если cdr(x) = НИЛ то слож1(саг(х), car(y)t 0) иначе {пусть <а, ьу^слож n(cdr(x), cdr(y)) {пусть <со,а> = слож\ {car(x), car(y),ci) <со, cons (d, s) > }} Если списки х и у содержат только по одной цифре, то можно использовать слож\ с цифрой переноса в данный разряд, равной 0. В противном случае применяется сложп, чтобы сложить все
10.1. О ясности выражения 267 цифры, кроме старших, и получить цифру переноса ci и сумму s. Затем применяется слож\, чтобы сложить старшие цифры и цифру переноса в старший разряд ci и получить цифру переноса со из старшего разряда. Старшая цифра результата есть dt и поэтому результат сложения чисел х и у получается добавле- добавлением при помощи операции cons цифры d перед цифрами s. Можно, разумеется, написать это определение и без рас- расширения языка. Становится ли функция более ясной в этом рас- расширении? Рассмотрим другие способы написания программы. Избавление от угловых скобок и замену их операцией cons оставляем в качестве упражнения читателю, который сам дол- должен решить, какая версия программы понятнее. Другой аль- альтернативой является реализация сложп в виде двух отдельных функций, одна из которых вычисляет переносы, а другая — спи- список цифр суммы. Это сильный конкурент в смысле ясности выра- выражения. Снова предоставим читателю построение этой программы (см. упр. 10.3), но на этот раз, по мнению автора, теряется ясность выражения. Фактически это не тот же самый алгоритм. Вычис- Вычисление переноса вызывает перевычисление младших разрядов суммы, а вычисление суммы вызывает перевычисление перено- переносов в младших разрядах, и все это весьма неэффективно. В итоге параллельное вычисление этих двух частей результата пол- полностью отсутствует: фактически результирующая программа предполагает многократное перевычисление переносов. Это процесс сложения другого вида. Третий метод избежать расши- расширения связан с использованием понятия продолжения, которое было введено в гл. 9. Посмотрим это решение. Каждая из функций слож\ и сложп расширяется включением дополнительного параметра, ее продолжения. Тогда, например, вызов сложп (х, у, /) означает сложение списков х и у и приме- применение функции / к результату этого сложения. Продолжение, естественно, требует двух параметров, соответственно переноса и списка цифр суммы. Новое определение имеет вид слож\(а, b,c,f) = f((a + b + c) дел т,(а + Ь +с) ост т) сложп(х, у, f) а если cdr(x) = HMJl то слож\{саг(х), саг(у), 0, /) иначе сложп(саг(х)9 cdr(y), {k(cl, s) слож\(саг(х), car(y), ci, {X(co,d)f(co,cons(d,s))})}) Это определение имеет форму, тесно связанную синтаксически с формой, использующей угловые скобки. Отметим, что факти- фактические продолжения, замещающие вложенные вызовы сложХ и сложп, записаны как Я-выражения с формальными параметрами, которые имеют те же самые имена (и играют ту же роль), что
268 Гл. 10. Языки программирования и методы программирования и соответствующие локальные переменные в прежней программе. Остается ли эта программа такой же ясной, как и прежняя? И ее можно назвать сильным конкурентом из-за тесной синтак- синтаксической связи с прежней программой, однако использование продолжений делает ее, по мнению автора, менее привлекатель- привлекательной. Безотносительно к эффективности (во всяком случае, эти программы сравнимы по эффективности) следует заключить, что расширение функционального языка путем включения функций с составным результатом значительно усиливает возможности языка в смысле ясности выражения. Существует много способов переопределения языка, в част- частности путем введения альтернативного синтаксиса, который мо- может повышать или не повышать выразительность и ясность кон- конструкций. Например, совсем отличным от примененного здесь синтаксиса выглядит синтаксис уравнений. Это то, чем мы поль- пользовались, когда определяли впервые компилятор для инстру- инструментального Лиспа в гл. 6. Каждая часть условного функцио- функционального определения записывалась как отдельное уравнение. Так, например, мы писали в компиляторе (ПЛЮС а е2)*п=е1т\е2т\(ЛЛЮС) Это было сокращение для формы компил(соп8(ПЛЮС, cons(eu cons(e2>HHJI))),n,c) = компил(е1Уп, компил(е2,п, соп8(ПЛЮС, с))) В таком уравнении структуры вводятся слева от равенства толь- только затем, чтобы присвоить имена их частям (здесь это ех и е2). То же самое имя справа указывает на включение в уравнение со- соответствующей части. При написании больших функциональ- функциональных программ часто предпочитают подобный вид записи. При- Причина, по которой мы не придерживались этого в нашей книге, заключается в том, что трудно определить приемлемые формы ле- левой части без обращения к языку с известной интерпретацией. Уравнения в такой форме являются мощным средством вырази- выразительности, поскольку допускают экономное описание (части программы) и отражают тесную синтаксическую связь с оконча- окончательной, исполняемой формой программы. 10.2. Области данных Наше исследование функционального программирования су- существенно использует конкретную область данных, а именно списки или S-выражения, введенные в самом начале гл. 2. Од- Однако значительная доля того, что сделано, не зависит от этого выбора, и, чтобы подчеркнуть это, рассмотрим некоторые аль- альтернативные области, которые можно было бы использовать.
10.2. Области данных 269 Отметим также и те вопросы, где выбор этой области данных был существен или заметно облегчил их решение. Нам не так важен тот факт, что S-выражения допускают, в частности, сим- символьные данные в виде буквенно-цифровых атомов, нас больше беспокоит то, что они ограничиваются представлением деревьев атомов, которые, будучи весьма общими, тем не менее в ряде приложений не дают достаточно естественного представления. Не так важно, будут ли атомы символьными или числовыми, как то, что из них можно легко составлять структуры, естественные для задач, которые мы хотим решить. Можно перечислить некоторые альтернативные области дан- данных, если рассмотреть средства других языков программирова- программирования. Вместо списков можно использовать множества, или мас- массивы, или последовательности. Однако, прежде чем обсуждать влияние такого выбора данных на развиваемый нами язык, а также на возможные приложения, вспомним некоторые факты о функциях, которые мы изучали. Нам известно, что функция может быть аргументом или результатом другой функции. Мы подробно рассматривали включение функции в структуру дан- данных. Следующий шаг приводит нас к заключению, что можно использовать понятие функции как единственную структуру данных. Тогда каждая структура данных, с которой манипули- манипулирует наша программа, будет функцией. В гл. 3 мы видели, как множество (его частный случай) можно представить функцией. В качестве другого примера рассмотрим следующие определе- определения функций car\ cd/ и cons': car'(s)=ss(CAR) cdr'(s) = s(CDR) cons'(x, y)^X(z) если z = CAR то х иначе у Это функции, которые можно использовать вместо car, cdr и cons соответственно. S-выражение, которое строит функция cons', представляется функцией, которая требует в качестве аргумента CAR или CDR и которая выдает в качестве результата соответствующую часть представляющей ее структуры. Отсюда видно, что действительно можно, по крайней мере теоретически, работать без списковых структур с тем же успехом, как и с ними. Однако, если внимательнее взглянуть на это предположе- предположение, видно, что многие вопросы остаются без ответов. В част- частности, можно ли сами функции реализовать без использования понятия списка или чего-либо подобного ему? Наше изучение SECD-машины в значительной мере дало нам возможность выразить функции через понятия, более ориентированные на машину. Это было сделано, безусловно, во многом благодаря возможности строить структуры деревьев произвольной слож-
270 Гл. 10. Языки программирования и методы программирования ности. Списки управления включались в замыкания и окруже- окружение в замыкания, и, наконец, замыкания включались в окруже- окружение. Следовательно, если бы реализация функций базировалась на такой машине, соответствующая реализация списков с исполь- использованием указанной выше операции cons' не избежала бы исполь- использования cons, но это просто было бы скрыто внутри машины. Фак- Фактически из-за того, что функция, однажды построенная во время выполнения программы, может существовать произвольно долго (возможно, до конца программы, так как функция сама может быть результатом), мы несомненно должны признать тот факт, что проблема распределения памяти для функций по меньшей мере так же сложна, как и для списков. Она, разуме- разумеется, того же порядка сложности, поскольку было показано, как реализовать функции, используя списки. Отсюда следует, что использование функций как единственной области данных не дает ничего другого, кроме некоторой, причем весьма незна- незначительной, экономии языка. Эффективность cons и связанных с ней функций — это другой вопрос. Едва ли можно ожидать, чтобы она была такой же как у cons. В зависимости от области применения функционального язы- языка более подходящими могут быть другие области данных. Три из них, о которых упоминалось,— множества, массивы и после- последовательности — заслуживают краткого обсуждения. В при- примерах, которые время от времени рассматривались, часто ис- использовались множества с такими функциями как включить и элемент. Это только иллюстрирует тот факт, что списки мо- могут использоваться для представления множеств. Обратное тоже верно, но не так тривиально. Однако для тех приложений, где множества — это все, что требуется, непосредственное ис- использование операций алгебры множеств более подходит, чем использование списочных операций. Но даже в таких прило- приложениях можно время от времени использовать списки. Это может случиться, например, если предположить, что наш функциональ- функциональный язык может содержать функции с составными значениями, как это было описано в предыдущем разделе. Массивы как область данных на самом деле требуют больше- большего обсуждения, нежели мы можем себе позволить. Основными операциями над массивами, как и в случае списков, являются операции конструирования массивов из подмассивов и опера- операции выборки произвольной части массива. Существенное от- отличие заключается в том, что выборка задается индексами. В про- простейшем случае выборки 0-мерного массива (скаляра) из 1- мерного массива (вектора) индексом является целое значение. Обратно, 0-мерный и 1-мерный массивы могут быть скомбини- скомбинированы в один 1-мерный массив. Преимущество массивов двояко. Во-первых, существует много приложений, где естественно опи-
10.2. Области данных 271 сывать задачи в терминах индексации, и, во-вторых, современ- современные машины очень хорошо приспособлены для выполнения этих скрупулезных операций. Раздел 10.3 посвящается изучению взаимосвязи между современными машинами и программирова- программированием, и поэтому там мы обсудим этот вопрос подробнее. Поня- Понятие последовательности не очень отличается от понятия списка. Поэтому мы можем использовать его в качестве альтер- Рис. 10.1. нативы. Оставляем изучение этого вопроса в качестве упраж- упражнения читателю (упр. 10.5). Как совершенно другой тип области данных для функциональ- функциональных программ рассмотрим аппаратную, а не программную реа- Рис. 10.3. лизацию алгоритмов. В качестве примера выберем, естественно, сумматор из предыдущего раздела. Эта программа намного про- проще, чем обычный сумматор реальной машины, но она служит ил- иллюстрацией принципа. Напомним, что функция сложп(ху у) требует в качестве аргументов два списка одинаковой длины п. Будем рассматривать эти п цифр не как список, к которому по- последовательно применяется операция cdry а будем считать, что к нему подведены п смежных проводов. Будем обращаться с этой связкой как целым и изображать ее стрелкой, помечая размер связки (рис. 10.1). Сумматор представляется тогда прямоуголь- прямоугольником, вид которого изображен на рис. 10.2. Аналогично одно- одноразрядный сумматор имеет вид, изображенный на рис. 10.3. Напомним, что все цифры представляются по единому основа-
272 Гл. 10. Языки программирования и методы программирования нию т, но мы не оговариваем этого. Для реального оборудова- оборудования это основание обычно равно 2. Сумматор, описываемый функцией сложа (х, у)> можно те- теперь проиллюстрировать двумя диаграммами. Сначала рассмот- х Рис. 10.4. рим случай, когда х и у имеют единичный размер (рис. 10.4). Затем рассмотрим списки х и у длины 2 и более. В этом случае имеем вложенное вхождение функции сложи (рис. 10.5). С по- помощью этих двух диаграмм можно легко построить макет, на- Рис. 10.5. пример, четырехразрядного сумматора. Разумеется, этот проект не адекватен реальному, так как здесь игнорируется проблема синхронизации.
10.3. Преобладание присваивания 273 Использование в качестве области данных связки проводов весьма интересно. Значения данных, которые обычно находятся в списке и доступны в результате последовательного приме- применения операции cdr, становятся доступными отдельным прово- проводам, размещенным в пространстве. Таким образом, мы преобра- преобразуем, и это существенно, временную последовательность зна- значений в последовательность в пространстве. Не следует рас- рассматривать это просто как метод описания элементов аппарату- аппаратуры. Нужно рассматривать использование аппаратуры как аль- альтернативный способ реализации наших функциональных про- программ. Ясно, что функциональная программа, которая встра- встраивается в оборудование, должна быть достаточно важной, и ни одна из программ этой книги не получила бы такого права. Проект нового скоростного мультипликатора для перемноже- перемножения чисел с плавающей запятой мог бы, например, быть описан функциональной программой, и такая программа с успехом могла бы быть реализована аппаратным путем. 10.3. Преобладание присваивания Принципиальной характеристикой современных вычисли- вычислительных машин является то, что они вычисляют, храня значе- значения данных в ячейках памяти, и время от времени заменяют значения в этих ячейках, перезаписывая их. Это свойство ре- реальных машин отражено в конструкции присваивания, которая является фундаментальной для большинства языков програм- программирования. Имеется, однако, и другой аспект реальных машин, благодаря которому использование ими традиционных языков является намного более эффективным по сравнению с функцио- функциональными языками. Это понятие доступа к структуре данных путем индексирования. В этом разделе мы рассмотрим, почему именно функциональные языки не могут достигнуть эквивалент- эквивалентной эффективности на современных машинах. Можно по-разному интерпретировать это наблюдение. Во-первых, можно прийти к заключению, что если мы не готовы согласиться с некоторой по- потерей в эффективности вычислений, то должны отказаться от функционального программирования. Во-вторых, можно ре- решить, что лучшим способом является разработка методов пре- преобразования функциональных программ для выполнения их на современных машинах, так чтобы ясность и выразительность этих языков сочетались с достаточной эффективностью. В-третьих, можно сделать вывод, что нужно подождать, пока машины ста- станут быстрее, или нужно перестроить их, чтобы улучшить интер- интерпретацию на них функциональных программ. Каждое из этих заключений справедливо с определенной точки зрения, именно их мы теперь и рассмотрим подробнее.
274 Гл. 10. Языки программирования и методы программирования Операция присваивания простой переменной тесно перепле- переплетается с понятием связывания значения с именем, что использо- использовалось в нашем функциональном языке. В гл. 5 мы видели, как программа с такими присваиваниями преобразовывалась в эквивалентную функциональную программу. Результирующая функциональная программа имела характерную структуру, и мы видели, что функциональная программа с такими ограни- ограничениями на ее вид могла компилироваться в программу с прис- присваиваниями просто обращением преобразования. Поэтому мож- можно сделать вывод, что функциональные языки не исключают возможности эффективного использования современных машин, если не требовать полной общности этих языков. Но это рас- рассуждение затрагивает лишь часть проблемы. Присваивание простой переменной, характерное для операторов вида I := 1 г := г — х совершенно отличается от присваивания компоненте такой структуры как массив, которое характеризуется операторами вида a[i] := 1 a[i + l] := a[l]+\ В этом последнем случае изменяется только небольшая часть всей структуры, остальная часть структуры не изменяется, и это делает оператор присваивания преобладающим оператором в языках, которые используются на современных машинах. В строго функциональном языке, где вычисляются значения и отсутствует понятие вычисления посредством действия, с массивами обращаются только как с целыми массивами значе- значений. Основными операциями над массивами значений являются индексирование для получения значения компоненты и констру- конструирование нового значения массива из старого. Если а есть зна- значение массива, i — индекс их — значение, то пишем обновить (a, i, x) для значения массива, которое остается прежним, кроме t-й позиции, где теперь будет значение х. Значением выражения обновить (обновить {a, i, a[j]), /, a[i]) будет тогда значение массива, полученное из а заменой значе- значений t-ro и /-го элементов друг на друга. Этот вид выражения предполагает, что новое значение массива, так же как и проме- промежуточное значение обновить (а, i, а[/1), строится как независимо существующее значение, поскольку это выражение может ис- использоваться в окружении, где в дальнейшем может еще потре-
10.3. Преобладание присваивания 275 боваться доступ к а. Вообще определение корректной интерпре- интерпретации выражения обновить (a, i, х) как перезаписи значения а является очень трудной проблемой. В приведенном выше выра- выражении, например, вложенный вызов, порождающий промежу- промежуточное значение массива, не может быть так интерпретирован. То, что проблема особенно сложна, видно, если понять, что одно и то же значение массива может иметь больше одного имени. Работа со значениями массива, и в особенности операция их обновления, приводит к многократному копированию таких значений. Этого может избежать программист, использующий присваивание вида a[i]:=x, именно потому, что этот оператор фактически заканчивает работу с текущим значением а и впос- впоследствии потребуется только значение обновить (a, i, x). Зада- Зададимся вопросом, существует ли такое представление значений массива, при котором можно целиком или частично избежать копирования. Проблемы не возникает при обычной реализации cons (детально разобранной в гл. 12), так как подструктуры, кото- которые становятся компонентами cons, не копируются — только указатели на них хранятся в структуре, представляющей ре- результат cons. He хотелось бы, однако, реализовывать значение массивов таким способом, так как при этом теряется один из главных источников эффективности современных вычислительных машин — возможность индексирования. Если значение массива находится в подряд расположенных ячейках памяти, одновре- одновременно доступны все элементы массива, независимо от их пози- позиции. В линейных списках, хранящих указатели, время доступа к i-му элементу пропорционально t, его расстоянию от начала списка. Можно использовать для представления значений мас- массива бинарные деревья, при этом для массива из п элементов время доступа к элементу пропорционально \og2n и число cons, требуемых для копирования значений, тоже пропорционально l<5g2n. Тем не менее это все еще остается не сравнимым с едини- единицей времени доступа и нулевым объемом памяти для копии, которые требуются при элементарном обновлении на том же месте в операторе a[i]:=x. Функциональные языки не могут конкурировать по эффек- эффективности с традиционными языками на современных машинах, если это абсолютное требование. Однако программы, написанные с элементным присваиванием структурированных значений, остаются программами очень низкого уровня. Как следствие можно ожидать, что их понимание и обсуждение будет требовать длительных усилий, и поэтому их будет трудно правильно по- построить. Быстродействие современных машин сильно возрастает не в последнюю очередь из-за того, что возрастает размер маши- машины, которую можно позволить себе приобрести. В настоящее время в вычислительных центрах большую машину можно рас-
276 Гл. 10, Языки программирования и методы программирования пределить во времени между сотней пользователей. В ближай- ближайшем будущем благодаря успехам микроэлектроники машины эквивалентной мощности станут доступны просто индивидуаль- индивидуальным пользователям. Таким образом, даже не учитывая достиже- достижения в области проектирования машин, можно ожидать стократ- стократного повышения быстродействия наших машин и стократного увеличения объема памяти. Многие приложения, где в настоя- настоящее время функциональное программирование не может быть применено, станут доступными. Можно надеяться, что прогресс в области архитектуры вычислительных машин, в особенности архитектуры памяти, что в принципе обсуждалось в этой главе, приведет даже еще к более значительным достижениям, но этот вопрос остается за рамками нашей книги. Неужели нет надежды на то, что мы в конце концов сможем проектировать машины, приспособленные к языкам, которые представляются нам ес- естественными? Или мы всегда будем иметь дело только с языка- языками, непосредственно отражающими структуру машин, на кото- которых они выполняются? Упражнения 10.1. Определите функцию, которая берет в качестве аргументов пару чисел и выдает их минимум или их максимум. Используйте расширенный язык разд. 10.1. 10.2. Определите теперь функцию, которая берет в качестве аргументов три числа и выдает в качестве результата эти же три числа в порядке возрас- возрастания. 10.3. Определите поразрядный сумматор из разд. 10.1 через отдельные функции для вычисления переноса и суммы цифр. Подтвердите замечание о повторном вычислении переноса. 10.4. Перепишите сумматор в виде совокупности уравнений. В левых час- частях этих уравнений нужно отличать списки вида cons(dt НИЛ) от списков вида cons(d> s), где зфНИЛ. 10.5. Рассмотрите функциональный язык программирования, в котором основной областью данных являются не списки, а последовательности. Опе- Операциями над последовательностями будут функция соединить (si, s2), которая строит из двух последовательностей одну, а также функции левая (s) и пра- правая (s), которые выдают левую и правую порции произвольного разбиения по- последовательности s. Последовательность s длины 1 удовлетворяет предикату ednocA(s)y и в этом случае значением ее единственного члена будет элемент (s). Мы имеем соединить (левая (s), правая (s))=s но ни одно из следующих условий не является необходимым: левая(соединить($\, s2)) = sl правая(соединить($\, s2)) = s2 Н апишите программы, которые определяют длину последовательности и мак- си мальное значение в последовательности чисел. Будут ли эти программы более ясными, чем соответствующие программы, использующие списки?
Упражнения 277 10.6. Постройте функцию, которая выполняет последовательное пораз- поразрядное умножение двух чисел по основанию т. Нарисуйте соответствующий мультипликатор. 10.7. Определите функцию сорт(а> л), которая выдает значение массива, полученное сортировкой п элементов значения массива а. Используйте под- подходящий метод сортировки. 10.8. Определите a[i] и обновить(at it x) через car, cdr и cons, используя для представления а (a) линейный список, (b) бинарное дерево. Подтвердите оценки времени и объема памяти, приведенные в этом разделе для доступа и копирования относительно этих структур.
Глава 11. ИНСТРУМЕНТАРИЙ ФУНКЦИОНАЛЬНОГО ПРОГРАММИРОВАНИЯ Итак, мы завершили обсуждение функционального програм- программирования как техники построения программ. Мы также за- закончили рассмотрение семантики функциональных языков про- программирования. Остается решить только один важный вопрос. Это вопрос о возможности эффективной реализации строго функци- функционального языка на современных машинах. Поэтому настоящая и следующая главы посвящены полному описанию реализации варианта Лиспа, который в гл. 4 и 6 был использован в качест- качестве примера конкретного языка программирования. Описанная здесь реализация основывается на операционной семантике гл. 6. Это отнюдь не единственный метод реализации строго функционального языка и необязательно самый лучший. Од- Однако главным преимуществом этого метода является его тесная связь с обычными методами реализации языков высокого уровня, подобных Алголу, которые пополнены необходимыми средства- средствами оперирования с функциями, которые в качестве своего зна- значения выдают функции. Важность изучения этой главы даже для читателя, не свя- связанного с реализацией функциональных языков, трудно перео- переоценить. Детали полной реализации устанавливают важную связь между строго аппликативными вычислениями и более зна- знакомыми вычислениями на современных машинах. Это поможет читателю лучше понять некоторые использования функций, а также оценить возможности соответствующим образом органи- организованных современных машин. Как упоминалось во введении, эти две последние главы можно было бы прочесть после ознаком- ознакомления с гл. 2, хотя полное понимание материала этих глав мо- может быть достигнуто только после прочтения гл. 4 и б. Надеемся, что читатель будет время от времени заглядывать в эти главы, чтобы использовать для лучшего уяснения материала все то, что может дать конкретная реализация. Повторив описанный здесь процесс построения программ, читатель сам может построить свою собственную инструменталь- инструментальную Лисп-систему и выполнять с ее помощью программы либо в качестве упражнений, либо решая задачи, в которых он заинте- заинтересован. Как метод описания здесь выбран традиционный метод
//./. Пространство списков 279 структурирования программы сверху вниз, дополненный отлад- отладкой построенной программы снизу вверх. При описании проек- проекта будем использовать псевдокод, приспособленный для целей описания, который очень напоминает Алгол, но не имеет ком- компилятора. Если читатель пожелает действительно закодировать программу, он должен переписать этот псевдокод на свой кон- конкретный язык программирования. В зависимости от языка, выбранного для реализации, это будет простой или непростой задачей. Преимущество использования псевдокода перед реальным языком программирования при описании эскизного проекта системы заключается в том, что при этом мы избегаем описания деталей реальных языков, которые не имеют особого отношения к экскизу, и, что, вероятно, более важно, избегаем ситуаций, вызванных отсутствием некоторых существенных изобразитель- изобразительных средств. В псевдокоде не может быть недостатка в изобра- изобразительных средствах, так как он позволяет вводить любые сред- средства, которые окажутся нужными. Будем ссылаться на програм- программу, написанную на псевдокоде, как на абстрактную программу. Описание проекта разделяется на два уровня. На верхнем уровне предполагается наличие таких удобных средств, как область хранения записей, область хранения строк и механизм реализации рекурсивных процедур. Следующая глава посвя- посвящается нижнему уровню, где описываются такие высокоуров- высокоуровневые средства реализации языков, которые в этих языках отсутствуют. Предполагается, что в большинстве реализаций будут использоваться многие или все средства, описанные в следующей главе. Мы завершим следующую главу подробным описанием процесса реального построения системы, когда от- отдельные компоненты уже запрограммированы. Особое внимание уделяется процессу отладки, в частности представлен план пол- полной тестовой проверки при сдаче новой инструментальной Лисп- системы. Наконец, поскольку всякому реализатору несомненно захочется расширить и изменить систему, описывается процесс раскрутки, с помощью которого это может быть сделано. 11.1. Пространство списков В Лиспе существуют символьные выражения трех видов: символьные атомы, численные атомы и их композиция, т. е. результат операции cons. Поэтому структуры памяти, которые представляют эти значения символьных данных, также будут трех видов. Их можно реализовать как записи (или поля памяти) трех типов. Назовем эти типы записей соответственно символ, число и cons. Когда в абстрактной программе (т. е. в описании на псевдокоде) ссылаются на запись, это делается посредством
280 Гл. 11. Инструментарий функционального программирования указателя. Указатель есть не что иное, как значение, которое единственным образом определяет запись. Можно представить его себе как ссылку на запись. Это может быть машинный ад- адрес, или индекс в массиве записей, или, возможно, нечто более сложное. Если мы будем считать указателем индекс в массиве записей, то это будет вполне удовлетворительным и, возможно, простейшим его понятием. Запись типа символ будет содержать строку, являющуюся буквенно-числовым атомом, который представляется этой за- записью. Новая запись такого типа может быть размещена вы- вызовом функции конструирования символ, значением которой является указатель на вновь размещенную запись. При задан- заданном указателе р на запись типа символ мы можем использовать функцию выборки сзнан для доступа к строке, которую эта за- запись содержит. Так, например, в абстрактной программе можно написать указатель р\ р := символ; сзнач(р) := "ЛЯМБДА" что будет обозначать следующую последовательность действий. Объявляется переменная указатель, размещается новая запись типа символ (причем р указывает на нее), и, наконец, заполняет- заполняется поле строки этой новой записи, чтобы указать на строку Рис. 11.1 "ЛЯМБДА". Можно наглядно изобразить результат этой аб- абстрактной программы, как это сделано на рис. 11.1. Здесь за- запись обозначена прямоугольником, а указатель на нее — стрел- стрелкой. Значение строки, хранимое в записи, написано внутри прямоугольника, а тип записи дан сверху. То, что указатель р в настоящий момент времени указывает на запись, показано начертанием его имени рядом со стрелкой. Необычным здесь, возможно, является использование оператора р :=символ для распределения памяти. Удачной является идея представлять символ как функциональную процедуру, результатом которой является указатель. В точности те же идеи используются для записи типа число. Здесь функцией конструирования является число, а функцией выборки — цзнач (цифровое значение). Значение, хранящееся в записи типа число, является целым числом, и отсюда програм- программа, аналогичная приведенной выше, имеет вид р := число\ цзнач (р) : = 127
//./. Пространство списков 281 Функцию выборки можно использовать просто для восстановле- восстановления значения, хранимого в записи. Можно, например, написать i:= цзнач(р) чтобы получить целое значение из записи (предположительно) типа число, на которую указывает р. Различать типы записей можно при помощи дополнительных функций (предикатов). Предикат этосимвол может быть применен к указателю р, и он выдаст значение истина или ложь в зависимости от того, будет Рис. 11.2. ли иметь запись, на которую указывает /?, тип символ или нет. Аналогично предикат эточисло можно применить, чтобы опре- определить, является ли данная запись записью типа число. Для си- ситуации, изображенной на рис. 11.2, имеем этосимвол(р) = истина этосимвол(д) = ложь эточисло(р) = ложь эточисло{ц) = истина Теперь можно дать некоторое представление о том, как вы- выглядит абстрактная программа, введя следующую процедуру проц иниц{р)\ если этосимвол(р) то сзнач :=" " иначе если эточисло(р) то цзнач(р) = 0; Эту процедуру можно использовать, если потребуется иници- инициировать запись, присвоив ей стандартное значение в соответст- соответствии с ее типом. Отметим, что, используя в наших абстрактных программах такие понятия современного алголовского стиля как конструкция если ... то ... иначе и процедура, мы вместе с тем допускаем некоторые вольности и не описываем, например, тип переменных, используемых как параметры, так как это легко определяется из контекста. Вообще мы попытаемся сде- сделать наш псевдокод по возможности легко читаемым за счет разумного компромисса между длиной программы и избытком деталей, которые в ней содержатся. Рассмотрим теперь записи типа cons. Они вводятся для пред- представления составных значений, в частности результатов опера- операции CONS в инструментальном Лиспе. Таким образом, они со-
282 Гл. 11. Инструментарий функционального программирования держат два значения, оба указатели, которые соответственно представляют саг и cdr этого составного значения (рис. 11.3). Обозначим функцию конструирования записи типа cons, как и раньше, через cons, а соответствующие функции выборки через саг и cdr. Тогда программу р := cons\ саг(р) := символ; сзнач(саг(р)) := "ЛЯМБДА" cdr(p) := число\ цзнач(саг(р)) := 17 можно использовать для построения структуры, изображенной на рис. 11.4. которая как S-выражение имеет вид (ЛЯМБДАМ) Кроме функций конструирования и выборки имеется предикат iscons, который используется для определения типа записи, указанной его аргументом. Рис. 11.4. Полезно рассмотреть структуры, которые получаются для различных S-выражений. Структура для списка (А В С) показана на рис. 11.5. Соответствие между структурой и S-выражением видно более отчетливо, если выписать последнее в полной то- точечной форме, т. е. в виде (А. (В. (С.НИЛ))), из чего видно, что имеется взаимное соответствие между записями cons в структуре и точками в S-выражении. Рассмотрим S-выражение ((А.В). (А.В)). Оно одинаково правильно представляется обеими струк- структурами, изображенными на рис. 11.6. Какая из этих структур будет фактически построена в конкретном случае, зависит от того, как именно конструируется значение ((А.В).(А.В)), Пер-
//./. Пространство списков 283 вая структура может быть построена при вычислении выражения {ПУСТЬ (CONS X X) (X КОД(А.В))) а вторая — выражения (CONS (КОД(А.В)) (КОД(А.В))) Наличие двух полей, указывающих на одну и ту же запись (когда два указателя имеют одно и то же значение), приводит Рис. 11.5. к понятию разделенной (или коллективной) памяти. Это может представлять серьезные трудности, и нужно приложить некото- некоторые усилия, чтобы избежать их. Однако мы не отказываемся от разделения памяти, так как именно эта концепция может сде- сделать нашу реализацию достаточно эффективной не только в от- отношении использования памяти, но и времени, так как при этом можно избежать многочисленного копирования структур. В качестве иллюстрации тех трудностей, которые могут быть вызваны разделением памяти, рассмотрим оператор присваива- присваивания саг (саг (р)) : = q где р указывает на структуру, представляющую значение ((А.В). (А.В))У a q— на структуру, представляющую значение С. В соответствии с тем, разделена или нет память, действие это- этого присваивания будет состоять в изменении значения структуры, на которую указывает /?, либо на значение ((C.?).(C.?)), либо на значение ((С.В).(А.В)). Ясно, что в каждой конкретной си- ситуации будет либо то, либо другое. Чтобы избежать такой не- неоднозначности, нужно соблюдать осторожность при изменении содержимого записи, на которую могут быть ссылки из разных мест.
284 Гл. П. Инструментарий функционального программирования Чтобы впоследствии избежать громоздких диаграмм, будем опускать прямоугольник и наименование типа в изображении записей типа символ и число. Диаграмма для списка (А В С) при- примет тогда вид, указанный на рис. 11.7, где к символьным атомам ведут стрелки и опущены все избыточные наименования типов записей. Понятно, что эта диаграмма является лишь сокра- cons Рис. 11.6. щенной формой предыдущей диаграммы, и, когда на следующем уровне мы перейдем к реализации этих структур записей, сим- символьные и числовые атомы будут фактически представляться за- записями. Теперь можно в табл. 11.1 собрать вместе всю информацию о том, как будут обрабатываться структуры записей. В дополне- дополнение к функциям, указанным в табл. 11.1, предположим сущест- существование переменной-указателя, которая называется нил, а объявляется и вводится следующим образом: указатель нил\ нил := символ; сзнач(нил) := "НИЛ"\
11.2. Принципиальная схема вычислений 285 Таблица 11.1 Реализовать описанную здесь функцию входит в обязанности модуля второго уровня. Оставаясь на первом уровне описания, будем предполагать, что эта реализация имеется в нашем распо- распоряжении. Рис. 11.7. 11.2. Принципиальная схема вычислений В предыдущем разделе было описано, какой представляется SECD-машина пользователю. Как показано на рис. 11.8, для нее требуется два входа: функция на языке SECD-машины и Рис. 11.8. список аргументов, к которому нужно применить функцию. На выходе машина выдает результат этого применения. Оба входа и выход являются S-выражениями. Функция является единым S-выражением, состоящим главным образом из число- числовых кодов операций, а аргументы представляют собой последо- последовательность S-выражений. Результат является единым S-вы- S-выражением. Введя соответствующие функции для чтения и печати S-выражений, а также для применения функции к ее аргумен- аргументам, можно дать весьма простую абстрактную программу для
286 Гл. 11. Инструментарий функционального программирования главной операции управления вычислительным процессом: указатель фн,арг,рез\ ввод(фн)\ вводспис(арг)\ рез := выполнить(фн,арг)\ вывод(рез) Здесь предполагается, что функция ввод(фн) будет читать единое выражение на входе, строить его представление в прост- пространстве списков и делать переменную фн указателем этой струк- структуры. Аналогично функция вводспис будет читать на входе после- последовательность S-выражений, отделенных друг от друга призна- признаком конца файла или чем-то подобным этому, строить список этих выражений в пространстве списков и связывать указатель арг с этим списком. Функция выполнить реализует интерпрета- интерпретацию программы в коде SECD-машины и свяжет указатель рез с результатом. Последняя введенная операция, вывод (рез), является операцией печати S-выражения на построчно печатаю- печатающем устройстве. Таким образом, благодаря надлежащему структурированию мы разделили трудности, связанные с чтением S-выражений, о печатью их, а также с выполнением команд SECD-машины. Остальная часть, описанная на верхнем уровне, относится к ре- реализации этих средств. 11.3. Ввод S-выражений Прежде чем рассмотреть ввод S-выражений, которые свобод- свободно формировались нами на протяжении всей книги, необходимо провести небольшой синтаксический анализ. Методы заимство- заимствованы нами из практики построения современных компиляторов, например из работ Вирта, но от читателя не требуется, чтобы он имел в этом опыт. Нашей целью является чтение S-выраже- S-выражения и построение в пространстве списков структуры записей, которая его представляет. Формализуем описание синтаксиса S-выражений, которое у нас было: <5-выражение> ::= <атом> | «список S-выражений» <список S-выражений) ::= <5-выражение> | <5-выражение>.<5-выражение> | <5-выражение><список 5-выражений> Это описание определяет две категории фраз, <5-выражение> и <список 5-выражений>, и предполагает, что где-то определена фраза <атом>. Как правило, для написания фраз это описание используется следующим образом. Имеются два способа написа-
11.3. Ввод S-еыражений 287 ния <5-выражения>. Можно написать <атом> и можно написать <список S-выражений), заключив его в круглые скобки. Сущест- Существуют три способа написания <списка Б-выражений). Можно на- написать либо просто <5-выражение>, либо пару <5-выражений>, разделенных точкой, или написать последовательно ^-выраже- ^-выражение) и список <5-выражений>. Эта последняя альтернатива дает нам средство записывать последовательность ^-выраже- ^-выражений), образуя <список Б-выражений). При помощи дерева син- Рис. 11.9. таксического разбора (рис. 11.9) можно проследить, как <Б-выражение> (А В.С) строится по этим правилам. При анализе входной структуры, согласно этому синтаксису, для каждого <5-выражения>, распознаваемого на входе, мы хотим разместить запись соответствующего типа и заполнить ее поля необходимыми значениями. В случае <5-выражение> ::=<атом> размещается запись типа символ или число, а ее поле заполняет- заполняется фактическим значением атома, появившегося на входе. В слу- случае <5-выражение> ::= «список S-выражений» будет размещаться запись типа cons, а ее поля заполняются в соответствии с тем, какого рода будет этот <список S-выраже- ний>. Если окажется, что <список Б-выражений) :: = <5-выражение>
288 Гл. 11. Инструментарий функционального программирования то поле саг размещаемой записи заполняется значением указа- указателя на <5-выражение>, заключенное в скобки, а в поле cdr по- помещается указатель на нил. Иначе говоря, выражение (А) поро- породило бы, например, конструкцию, изображенную на рис. 11.10. Таким образом, для (<5-выражения>) вообще мы имеем струк- структуру, изображенную на рис. 11.11. Рис. 11.10. Рис. 11.11. Аналогично случаю <список 5-выражений>:: = <5-выражение>.<5-выражение> отвечает рис. 11.12, а случаю <список 5-выражений>:: = <5-выражение><список Э-выражений) соответствует рис. 11.13. Рис. 11.12. Оказывается, что эти правила построения <5-выражений> таковы, что можно построить анализатор, осуществляющий так называемый рекурсивный спуск. Это означает не что иное, как написание рекурсивной процедуры для каждой категории Рис. 11.13. фраз (здесь это <5-выражение> и <список S-выражений» с целью сканирования вводимого материала и распознавания соответствующих фраз. Возможность сделать это зависит от возможности предсказать, основываясь только на очередном символе ввода, какую из альтернатив следует выбрать. В пред- предположении, что вводимый материал будет представлен как
11.3. Ввод S-выражений 289 последовательность «лексем», характеризуемых соответствую- соответствующим образом, это можно легко сделать. Предположим, что у нас имеется (а в следующей главе мы ее зададим) процедура вводлекс (лексема, тип) которая при каждом вызове выдает очередную лексему вводи- вводимого материала вместе с указанием ее типа. Имеются четыре типа лексем: БУКВ т. е. символьный атом ЧИСЛО т. е. числовой атом ОГРАН т. е. скобки и другие знаки пунктуации КЦ специальная лексема, отмечающая конец вводимого материала Поскольку вводимый материал разбит на отдельные лексемы, пользователь функции вводлекс не обращает внимания на длину отдельных кусков, он имеет дело с лексемами в целом. Так, последовательные вызовы функции вводлекс (лексема, тип) при вводе списка (A BAY 73) привели бы к значениям, указанным в табл. 11.2. Таблица 11.2 Проще всего описать работу анализатора рекурсивного спуска, нарисовав синтаксические диаграммы для каждого вида фразы, которые перейдут в процедуры анализатора. На рис. 11.14 дается диаграмма для ^-выражения> (сокращенно выр). Здесь мы видим, что если требуется разобрать выр, то можно определить, какую из трех альтернатив выбрать, просто по- посмотрев на очередную лексему. Если это открывающая скобка, то берем первую, если это числовой тип, берем вторую, и если буквенный — третью. Аналогичная синтаксическая диаграмма для <списка Б-выражений) (сокращенно списвыр) приводится на рис. 11.15. На первый взгляд кажется, что здесь имеются трудности. 10 № 929
290 Гл. //. Инструментарий функционального программирования При разборе известно, что за конструкцией списвыр всегда следует закрывающая скобка. Поэтому, следуя рис. 11.15, после разбора выр можно определить, какой путь выбрать, в зависимости от того, будет ли очередная лексема точкой (первая альтернатива), закрывающей скобкой (третья альтернатива) или чем-то еще (вторая альтернатива). Рис. 11.14. Теперь запрограммируем анализатор. Сначала во фраг- фрагменте A1.1) введем переменные лексема и тип, которые будут содержать очередную (еще не разобранную) лексему и ее тип, Рис. 11.15. а также процедуру разбор, которая будет обновлять значения этих переменных. Здесь видно, что процедура разбор просто читает следующую лексему. Однако в конце файла она ставит закрывающую скобку. Если разбор вызывается снова после конца файла, то появится ряд лишних закрывающих скобок. Фрагмент A1.2) содержит главные программы анализатора. Это процедуры, которые выдают в качестве результата пара- параметр, указатель на структуру, представляющую разобранную фразу.
НА. Ввод лексем 291 Общим для этих процедур является то, что после выхода из них, когда они просканировали все лексемы разбираемой фра- фразы, первая лексема следующей фразы образует значения пере- переменных лексема и тип. Это обеспечивают вхождения вызова разбор. Как можно видеть, эта псевдопрограмма тесно связана с синтаксической диаграммой. Заметим, что в программе вводспис вызывается программа вводвыр (саг (е)), чтобы присвоить зна- значение полю саг той записи типа cons, которую только что разме- разместили. Возможно, яснее было бы написать вводвыр (р); саг(е) := р\ где р есть некоторая локальная (промежуточная) указательная переменная. Пояснения требует также операция цел, которая будет опи- ипна позже и которая используется для отображения числовой строки в числовое значение. Рассмотрим работу этого анализатора, когда вызывается вводвыр, а на входе находится (А В). Открывающая скобка порождает после ее разбора вызов процедуры вводспис из про- процедуры вводвыр. Это в свою очередь порождает немедленный вызов процедуры вводвыр для разбора А. В табл. 11.3 перечис- перечислены различные вызовы процедур и их результаты (вплоть до следующего вызова). Прослеживая последовательность дей- действий, указанных в таблице, видим, что f действительно прини- принимает значение, показанное на рис. 11.16. ю*
292 Гл. 11. Инструментарий функционального программирования Таблица 11.3 Наконец, необходимо отметить, что синтаксический анали- анализатор нужно инициировать вызовом процедуры разбор, чтобы Рис. 11.16. придать переменным лексема и тип их значения, взятые из первой вводимой лексемы. 11.4. Ввод лексем Синтаксическому анализатору требуется программа вводлекс {лексема, тип) которая собирает на входе каждую лексему, определяет ее тип и выдает эти значения как результат последовательных вызовов. Этот процесс называется лексическим анализом. Удобнее опи- описать процесс лексического анализа, использовав диаграмму состояний (рис. 11.17). Здесь состояния обозначены овалами, а переходы между ними — стрелками. Каждая стрелка поме- помечена тем типом знака, которым вызывается этот переход. Рас- Рассматриваются четыре типа: пробел, буква, цифра и еще один тип, называемый ограничителем. При сборке лексемы начинаем из исходного состояния и анализируем очередную вводимую литеру. Пока это будет про-
ПА, Ввод лексем 293 бел, остаемся в этом состоянии. Любой другой тип литеры за- заставит нас переключаться на одно из состояний, помеченных классом лексемы, которую эта литера предвещает. В этом со- состоянии остаемся до тех пор, пока поступают соответствующие литеры, и наконец переключаемся на завершающее состояние. Литера' которая вызывает этот заключительный переход, может быть первой литерой следующей лексемы и поэтому должна сохраняться до начала сборки этой лексемы. Нужно также предусмотреть при вводе признак конца файла. Чтобы избежать Рис. 11.17. многократной проверки этого признака, условимся, что ему будет всегда предшествовать пробел. Это значит, что признаки конца всегда встречаются в исходном состоянии. Предположим, имеется программа вводлит, которая при вызове помещает в переменные лит и кц очередную вводимую литеру и признак конца соответственно. Эти переменные опи- описаны как литерный лит\ лог кц; Принимая это во внимание, программируем вводлекс следу- следующим образом. проц вводлекс(лексема> тип); начало лексема: = пустаястрока; пока —\ кц и лит = пробел цикл вводлит; если кц то тип : = "КЦ" иначе если этоцифра(лит) или лит = "—п то начало тип : = "ЧИСЛО"; лексема: = лексема ~ лит\ вводлит; пока этоцифра(лит) цикл начало лексема: = лексема " лит; вводлит конец; конец A1.3)
294 Гл. 11. Инструментарий функционального программирования * иначе если этобуква(лит) то начало тип: = "СИМВОЛ"'; лексема : = лексема " лит; ввод лит; пока этобуква(лит) или этоцифра(лит) цикл начало лексема := лексема" лит; ввод лит конец конец иначе начало тыл :="ОГРАН": лексема : = лексема"лит; вводлит конец конец В псевдокоде предполагаются некоторые средства манипу- манипулирования со строками. Эти средства допускают как присваи- присваивание целых строк, так и пополнение строки отдельными лите- литерами посредством оператора лексема : = лексема ^ лит В конкретном языке программирования такие средства должны быть реализованы подходящим для него способом. Для псевдо- псевдокода отобраны такие средства, которые с незначительными усилиями могут быть реализованы в любом языке программи- программирования. Отметим, что в процедуре вводлекс переменная лит всегда содержит литеру, следующую за рассмотренной лек- лексемой. Таким образом, предполагается, что на вводлекс перемен- переменная лит уже имеет значение. Поэтому необходимо заполнить лит перед первым вызовом вводлекс посредством вызова вводлит. Составим программу вводлит, используя программу ввод- набор, которая при вызове выдает набор из, скажем, 80 литер. После вызова вводнабор (набор, дл, кц) массив набор содержит на позициях от 1 до дл эти литеры, а кц содержит ложь. Когда ввод исчерпан, в кц заносится истина, а набор содержит не меньше одного пробела. Чтобы реализовать вводлит, нужно помещать вводимый материал в буфер, по од- одному набору за один раз, и получать литеры из этого буфера. Объявим литерный массив буферA:80)\ цел буфкон; цел буфтек\ где буфкон используется для указания последней значащей литеры в частично заполненном буфере, а буфтек указывает очередную литеру, которую нужно выбрать. Первоначально в буфтек заносится единица. Таким образом, имеем процедуру вводлит A1.4).
I/.5. Вывод S-выражений 295 Очередная литера (последнего набора) выбирается из буфера, а указатель буфера продвигается вперед. Однако, прежде чем это сделать, нужно гарантировать, что буфер не пуст. Это де- делает условный вызов вводнабор, вызывая процедуру только в том случае, когда указатель продвинулся за край буфера. От- Отметим, что здесь предусматривается, чтобы процедура вводнабор не выдавала пустой буфер (т. е. буфкон=0). Пустой набор на входе должен быть представлен набором, состоящим не меньше чем из одного пробела. Программа вводнабор рассматривается как составная часть системы интерфейса. Иначе говоря, любая система, обеспечивающая инструментальный Лисп, долж- должна иметь в своем составе такую программу, соблюдающую ука- указанные здесь ограничения. 11.5. Вывод S-выражений Обратимся теперь к процессу печати S-выражений. Пусть задан указатель структуры записей в пространстве списков и требуется напечатать S-выражение, соответствующее этой струк- структуре. Так как допустимы несколько S-выражений благодаря эквивалентности точечных и бесточечных обозначений, например (А.(В.НИЛ)) и (А В) то мы выбираем для печати кратчайшую форму, т. е. выражение с минимальным числом точек. Для описания этого процесса объединим в одну диаграмму (рис. 11.18) две диаграммы, изображенные на рис. 11.14 и 11.15. выр Рис. 11.18.
296 Гл. 11. Инструментарий функционального программирования В целях получения кратчайшего вывода после точки будет печататься только один вид выражений — атом. Это решение заставляет нас возвращаться назад в точке, отмеченной пунктир- пунктирной стрелкой, каждый раз, когда очередным элементом в списке является запись типа cons. Предполагая заданной программу печлекс(лексема), которая будет печатать строку из параметра лексема (независимо от ее типа), можно составить программу процедуры печвыр (см. 11.5). Напомним, что процедура печвыр (е) имеет параметром указатель той структуры памяти, которая будет напечатана. проц печвыр (е)\ если этосимвол(е) то печлекс (сзнач(е)) иначе если эточисло(ё) то печлекс(строка(цзнач(е))) иначе начало указатель р\ печлекс("(")\ р := е\ пока iscons(p) цикл A1.5) начало печвыр (car (p))\ p:=cdr(p) конец; если этосимвол(р) и сзнач(р) —"НИЛ" то пусто иначе начало печлекс ("."); печвыр(р) конец; печлекс (У) конец Локальная переменная р используется, чтобы указывать последовательные элементы списка записей cons (рис. 11.19). Рекурсивно вызывается процедура печвыр, чтобы печатать саг Рис. 11.19. списка, на который указывает р, до тех пор пока р не укажет на атом. Если это символьный атом НИЛ, то ничего не делается; в противном случае печатаем точку и затем вызовем печвыр, чтобы напечатать значение записи, даже хотя мы и знаем, что это атом. Это несколько экономит повторение программы. От- Отметим, что в этой версии печвыр отсутствует формат вывода. В зависимости от того, как печлекс обрабатывает лексемы, можно ожидать, что длинные S-выражения могут быть неудобочитаемы при использовании этой программы. 11.6. Вывод лексем Предположим, что можно ссылаться на литеры строки так, как будто бы они представляют собой массив литер с индексами от 1 и далее. Кроме того, пусть функция длина используется для
11.6. Вывод лексем 297 определения числа значащих литер строки. Тогда программа печлекс просто выражается через программу печлит, как это показано в A1.6). проц печлекс (лексема); начало цел дл\ дл: = длина(лексема); A1.6) для 1 = 1 до дл цикл печлит(лексемаA))\ печлитС ") конец Сначала определяем, сколько литер нужно напечатать, затем печатаем их по порядку при помощи программы печлит. В конце печатаем пробел, чтобы гарантировать отделение соседних лек- лексем на выходе. Остается составить программу печлит, что мы сделаем через системную процедуру вывнабор (набор, дл) которая по данному литерному массиву набор, скажем из 80 литер, будет печатать литеры, находящиеся на позициях с но- номерами от 1 до дл. Как и при вводе, имеем буфер вывода литерный массив буф\ A : 80); цел буф\тек\ где буф\тек указывает последнюю литеру, помещенную в буфер буф\. Первоначально в этот указатель заносится 0. Теперь процедура печлит имеет вид A1.7). проц печлит(с)\ начало если буф1тек — 80 то включпеч; буф1тек: = буф 1 тек + 1; 6уф1(буф1тек): =с конец проц включпеч; A1.7) начало вывнабор(буф! ,буф1 тек); буф1тек: = 0 конец; Сначала в процедуре печлит (с) нужно проверить, есть ли место в буфере, прежде чем поместить туда новую литеру из с. Если места не окажется, вызывается включпеч, чтобы напечатать содержимое буфера и освободить его. Эти процедуры имеют две важные операционные характеристики. Во-первых, необходимо вызывать процедуру включпеч в конце всей печати, чтобы осво- освободить буфер. Во-вторых, эти программы совершенно произ- произвольно расщепляют атомы (единые многолитерные лексемы) в выводимом наборе. Оказывается, что написанные нами про-
298 Гл. 11. Инструментарий функционального программирования граммы ввода, хотя этот факт и не был ранее отмечен, будут правильно прочитывать такие расщепленные атомы. Очень важно, чтобы при реализации инструментального Лиспа система могла прочитать все, что она может написать, и это гораздо важнее, чем иметь хороший формат вывода. При изменении приведенных здесь описаний следует всегда иметь это в виду. 11.7. Программы перевода Для полноты изложения приведем здесь наброски программ для перевода числовой строки в целое число и обратно. Эти процедуры в предыдущих разделах назывались цел и строка. Перевод строки в целое показан в A1.8). цел проц цел{1); начало лог отр\ цел s; отр: = /A) = "—"; s:=0; A1.8) для i: = (если отр то 2 иначе 1) до длина(г) цикл s: = \Oxs + decHm(t(i))\ выдать (если отр то — s иначе s) конец; Локальная целая переменная s накапливает значение целого числа, которое вычисляется по формуле s := lOxs+d, где dt есть десятичный эквивалент i-й цифры. В программе этому соответствует функция десят, которая отображает число- числовую литеру в числовое значение. Ясно, что эта операция зависит от машины. Сначала проверяется знак числа (вводится только минус), и его наличие или отсутствие записывается в логиче- логической переменной отр. Затем проверяется поочередно каждая цифра, начиная со второй, если встретился минус. Наконец, выдается результат, если нужно — со знаком минус. Программа перевода в другую сторону несколько длиннее, но также прямолинейна. Опять вводим новую формальную операцию которая позволяет присоединять литеру с к строке / слева, и получим программу A1.9). строч проц строкаA)\ если « = 0 то выдать "О" иначе начало строч /; лог отр\ omp.—i < 0; i = модуль{1)\ t: = пустаястрока; пока i > 0 цикл начало A1.9)
11 Я. Цикл выполнения программы 299 и = цифраA ост 10)^/; i: = i дел 10 конец если отр то t: ="—""Ч; выдать / конец Сначала имеем дело со специальным случаем, целым значе- значением нуль, которое представляется строкой ". Это единствен- единственное значение, где требуется, чтобы ведущий нуль включался в представление строки. Затем, если переводимое число не нуль, независимо от его знака делаем его положительным. Делением числа на десять получается очередная цифра числа. Предпо- Предполагается, что i ост 10 дает остаток, a i дел 10 — частное. Функ- Функция цифра применяется, чтобы определить литеру, эквивалент- эквивалентную данной цифре, и затем эта цифра присоединяется слева к строке /, которая накапливает по порядку цифры. Очевидно, в результате последовательного деления на 10 число станет нулем. Наконец, если это необходимо, к представлению строки присоединяется минус. 11.8. Цикл выполнения программы В гл. 6 был описан язык SECD-машины. Для наглядности код каждой операции был представлен символьным атомом. На практике, однако, код операции представляется числовым атомом, что позволяет эффективнее выполнять эту операцию. Фактические числовые эквиваленты операций приводятся в табл. 11.4. Таблица 11.4 Таким образом, машинная программа, которая раньше была бы записана как (ВДК (А.В) ВВОД @.1) CONS) в SECD-машине имеет вид B (А.В) 1 @.1) 13)
300 Гл. 11. Инструментарий функционального программирования Команды машины разработаны так, что при обычном выпол- выполнении правильной машинной программы управляющее выраже- выражение всегда имеет вид (операция . . .) Иначе говоря, команда управления всегда является составной и ее саг есть числовой код операции. Отсюда, если переменная указывает на управление, то структура рабочего цикла имеет вид A1.10). цикл: выбор цзнач{саг(с)) из начало 1: ... 2: ... A1.10) 21: на концикл\ конец; на цикл; концикл: Таким образом, по значению в вершине списка управления оператор выбора переключает управление на одну из 21 отдель- отдельных ветвей. Каждая из ветвей реализует переход состояния машины в соответствии с машинной командой, которую она представляет, с учетом управления, и затем нормальный выход из оператора выбора вызывает возвращение к началу цикла. Только выполнение команды СТОП (код операции 21) может оборвать этот процесс. Четыре главных регистра SECD-машины объявляются как указательные переменные. Кроме того, потребуются еще че- четыре дополнительных регистра; три из них будут содержать постоянные значения, представляющие константы истина, ложь и нил, а четвертый регистр будет рабочим. Эти четыре новых регистра до сих пор не фигурировали в состоянии машины по следующей причине. По своей природе три регистра с констан- константами никогда не изменяются в результате выполнения операций. Поэтому указание в конце каждой команды, что в этих трех регистрах по-прежнему находятся истина, ложь и нил, является избыточным. Рабочий регистр будем считать неопределенным до начала и после выполнения каждой команды. Объявим ре- регистры SECD-машины следующим образом: указатель s, е, с, d, t, f, нил, w Напомним, что оператор, заставляющий SECD-машину вы- выполнять программу, называется выполнить и имеет два пара- параметра (см. разд. 11.2 и 6.5). Первым параметром является про- программа на языке SECD-машины, которая скомпилирована из
11.8. Цикл выполнения программы 301 выражения (на инструментальном Лиспе), имеющего своим значением функцию. Компилятор добавляет к этой программе две команды: команду ПРИМ и команду СТОП. Выполнение программы вызовет ввод в стек замыкания, которое затем будет применяться вплоть до остановки машины. Замыкание приме- применяется ко второму параметру процедуры выполнить, который является списком аргументов. Таким образом, мы имеем эскиз A1.11) для фазы выполнения. указ проц выполнить(фн, арг); начало /:= символ; сзнач({)="И"; f := символ; сзнач (/) = "Л"; комментарий нил был введен в разд. 11.1; (И-И) s := cons; car(s) :— арг; cdr(c) := нил; е := нил; с := фн; d := нил; цикл: ... концикл: выдать car(s) конец Напомним, что, когда выражение скомпилировано и про- программа выполнена, значение выражения находится в вершине стека. Это объясняет, почему обращение в конце цикла к опе- оператору выдать car (s) приведет как раз к выдаче результата про- процедуры выполнить. Остается только уточнить детали програм- программирования каждой команды. Чтобы сделать это, введем условие, которое поможет нам пост- построить интерпретацию каждой команды более единообразно. Это условие касается использования рабочего регистра и по- позволяет яснее записывать реализацию машинных команд без явного упоминания о рабочем регистре. Так, разрешается писать вг := cons(e2y e3) где еи е* и е3 суть выражения, значениями которых являются указатели. Это означает размещение новой записи типа cons, заполнение ее полей соответствующими указаниями на е2 и е8 и настройку указателя вновь построенной записи на ех. Часто данный процесс реализуется в виде ег := cons; car(ex) := е2; cdr(ei) := е8; Этого сделать нельзя в случае, когда е2 или еа ссылается на значение ег. Например, s := cons (нил,s) В этом случае используется последовательность w := cons; car(w) := e2; cdr(w) := ez; ex := w
302 Гл. 11. Инструментарий функционального программирования Рассмотрим, например, s := cons(cons(car(cdr(s))ye)ys) что вначале расширяется до w:=cons] car(w):=cons (car (cdr (s))y e)\ cdr(w):=s; s:=w и затем до w: = cons\ car(ifli): = cons\ car(car(w)) := car(cdr(s))\ cdr(car(w)) := e\ cdr(w) :=s; s:= w Это ясно показывает, как использование условия (о расшире- расширении сокращения) делает единообразным применение рабочего регистра. Это также убедительно показывает необходимость такого регистра, так как нельзя начать приведенную выше последовательность с конструкции s:=cons, не потеряв сущест- существенной информации, содержащейся в s. Аналогичное условие может использоваться в случае записей типа число и символ. Каждая машинная команда описывается правилом перехода s е с d-+ sf e' с' d ' и поэтому может быть реализована последовательными присваи- присваиваниями значений указательным переменным s, e, с и d. На- Например, правило перехода для команды ВДК имеет вид s е (ВДК х.с) d -> (x.s) e с d и может быть реализовано двумя присваиваниями s := cons(car(cdr(c)), s); с := cdr(cdr(c)) Отметим, что система произвольным образом реагирует на не- неправильные списки управления; никакой проверки коррект- корректности не предусматривается. Присваивание е или d в реализации этого перехода не является необходимым, так как эти регистры не изменяются Рассмотрим один полный цикл машины, когда эти два присваивания вставлены на соответствующее место рабочего исполнительного цикла. Оператор выбора проверяет саг (с) и определяет, что он равен 2 (код операции ВДК). Первое присваивание вставляет операнд команды ВДК в стек, второе присваивание оставляет в списке управления его часть, следую- следующую за операцией ВДК и ее операндом, и затем цикл повторя- повторяется для новой команды. Рассмотрим теперь команду ВВОД с переходом s е(ВВОД i с) d-> (взять (i, e).s) e с d
118. Цикл выполнения программы 303 Этот переход можно реализовать в виде A1.12). w \— е\ для t:=l до car{car(cdr(c)))uyKnw := cdr(w); w:=car(w)\ A1.12) для t:=l до cdr{car(cdr(c))) цикл w: = cdr(w); w :=car(w)\ s := cons(w, s); с := cdr(cdr(c)) Операнд команды ВВОД имеет форму F.л), где ft и л — це- целые. Это означает, что значение, которое должно быть введено, находится на п-и позиции b-ro подсписка списка е. Для того чтобы найти это значение, используется w: сначала берем Ь раз cdr от е, выбираем найденный подсписок, затем, взяв п раз саг в этом подсписке, выбираем наконец требуемое значение. Последние два присваивания аналогичны тем, которые рас- рассматривались для операции ВДК: это вталкивание значения в стек и продвижение по списку управления к следующей ко- команде. Будем рассматривать команды не по порядку их номеров, а группируя их в порядке возрастающей сложности. Следую- Следующими рассмотрим базовые операции. Правило перехода для CAR имеет вид ((a.b).s) e (CAR.e) d-> (as) e с d Это реализуется присваиваниями s := cons {car (car (s)), cdr(s))\ с := cdr (с) Реализация CDR почти идентична s := cons (cdr (car (s)), cdr(s))\ с := cdr (с) Рассмотрим операцию ATOM. Здесь нужно проверить значение в вершине стека и заменить его на значение истина или ложь в зависимости от того, является ли исходное значение атомом или нет. Это выполняется операторами: если 9tno4ucAo(tar(s)) или этосимвол(сагE)) то s:= cons(t, cdr(s)) иначе s: = cons(f, cdr(s)), c:= cdr (c) Бинарные операции реализуются почти так же, как и унар- унарные; нужно только соблюдать осторожность с порядком опе- операндов. Сначала рассмотрим CONS. Ее правило перехода имеет вид (ab.s) е (CONS.c) d -+ ((a.b).s) e с d
304 Гл. 11. Инструментарий функционального программирования и реализуется присваиваниями s:—cons(cons(car(s), car(cdr(s))), cdr(cdr(s)))\ с: = cdr(c) Читателю полезно детализировать эту запись способом, который изложен при описании использования рабочего регистра, чтобы определить, какие в точности действия здесь выполняются. Арифметические операции кроме требования другого порядка аргументов требуют размещения новой записи для хранения результата. Как уже объяснялось, это происходит потому, что возможно разделение памяти, и поэтому мы не можем себе по- позволить изменить значение существующей записи типа число. Команда МИНУС имеет правило перехода (a b.s) е (МИНУС.с) d-+(b — a.s) e с d и реализуется последовательностью s := соп8(число(цзнач(саг(саг(8))) — i{3Ha4(car(s))),cdr(cdr(s))); с: = cdr(c) Значение в вершине стека заменяется новой записью типа число, содержащей разность. Каждая из команд ПЛЮС, УМН, ДЕЛ и ОСТ реализуется так же, только вместо минуса берутся соответствующие арифметические операции. Следующими идут операции отношений. Сначала рассмотрим операцию МР (меньше или равно). Поскольку она действует только с арифметическими операндами, ее реализация напо- напоминает программы арифметических операторов. Имеем если i{3Ha4(car(cdr(s))) <^ цзнач(саг($)) то s\z=:cons(t,cdr(cdr(s))) иначе s:=*cons(f,cdr(cdr(s)))\ с i a cdr{c) Здесь значения в вершине стека мы заменили на одно из логи- логических значений И или Л. Команда РАВНО определена не только для числовых значений, но также для символьных и составных. Имеем реализацию A1.13). если ->тосимвол(саг(s)) и amocuMeoA(tar{cdr(s))) и CBHu4(car(s)) = сзнач(саг (cdr (s))) или 9mo4ucAo(car(s)) и 9mo4UCAo(car(cdr(s))) и цзнач(саг($)) = цзнач(саг(саг(8))) A1.13) то s := cons{ty cdr(cdf(s))) иначе s : = cons(ft cdr(cdr(s))); с-= cdr(c)\
11.8. Цикл выполнения программы 305 Сначала проверяется, имеют ли значения в вершине стека оди- одинаковый тип, и если это так, то выполняется соответствующее сравнение этих значений. Следующим рассмотрим правило перехода для команды ВДФ s е (ВДФ с'.с) d-+ ((c'.e).s) e с d Реализация имеет вид s := cons(cons(car(cdr(c))t e), s); c: = cdr(cdr(c)) Это просто. Команда ПРИМ имеет правило перехода ((c'.e')v.s) е (ПРИМ.с) d-+ HHJI(v.e') с'(s e cd) которое реализуется в виде d:=cons(cdr(cdr($)), cons(e, cons(cdr(c), d)))\ e : = cons(car(cdr(s)), cdr(car(s)))\ с :=car(car(s)); s := hua\ Команда ВОЗВР имеет правило перехода (а) е'(ВОЗВР) (s e cd) -^ (a.s) e с d и реализацию s : = cons(car(s)y car(d))\ е := car(cdr(d))\ с != car(cdr(cdr(d)))\ d\=cdr(cdr(cdr(d)))\ Правило перехода для ФОРМ есть просто s e (ФОРМ.с) d-+s (Q.e) с d При реализации «Q» специальных средств не вводится. Просто используется НИЛ как формальный вход в вычислительную обстановку. Имеем реализацию e:=cons(HUA, ё)\ с := cdr(c) Правило перехода для команды РЕК есть ((c'.e')v.s) (Q.e) (PEK.c)d^ НИЛ завершение(ef .v)cf (s e cd) Оно программируется в виде Поскольку команды ФОРМ и РЕК работают специально вместе, полезно посмотреть, как наши программы обрабатывают эту пару команд. Рассмотрим программу, которая получается
306 Гл. 11. Инструментарий функционального программирования и имеет вид (ФОРМ ВДК НМД ВДФ cg CONS ВДФ С/ CONS ВДФ се РЕК) Здесь с„ cf и се суть подсписки, построенные по Х-выражениям для g, /и тела блока соответственно. Мы не будем касаться их Рис. 11.20. содержания. Команда ФОРМ придает окружению структуру, показанную на рис. 11.20. Затем значения / и g вычисляются и образуют список в вер- вершине стека. Поскольку fug — функции, их значениями явля- Рис. 11.21. ются замыкания (рис 11.21). Следующим вводится в стек за- замыкание для се и затем выполняется команда РЕК. Это повле- повлечет за собой, помимо всего прочею, обновление вычислительной обстановки с формальной первой компонентой, как показано на рис. 11.22. Отметим, что построена циклическая списочная структура. Окружение содержит замыкания, которые в свою очередь со- содержат в себе окружение. Это в точности то, что понимается под взаимно рекурсивным определением.
11.8. Цикл выполнения программы 307 Остались две нереализованные команды: ВЫБ и ВОССТ. Правило перехода для ВЫБ имеет вид (x.s) е (ВЫБ си Сд .с) d-+ s e cx (cd) Запишем его реализацию d : = cons(cdr(cdr(c))), d)\ если c3Hu4(car(s)) = "И" то с: = car(cdr(cj) иначе с: = car(cdr(cdr(c)))\ s: = cdr(s) Наконец, команде ВОССТ соответствует переход s е (ВОССТ) (c.d)-+s e с d который реализуется просто: с := car(d)\ d := cdr(d) На этом заканчивается рассмотрение реализации команд. Каждый кусок псевдопрограммы, который реализует некоторую Рис. 11.22. команду, включается в рабочий цикл выполнения, описанный в начале раздела. Поскольку каждая команда настраивает список управления для следующей команды, которая должна выпол- выполняться, мы видим, что цикл выполнения ведет себя так, как и предполагалось. Этим завершается эскизное построение верх- верхнего уровня инструментальной Лисп-системы. Разбиение текста этого эскиза на управляемые модули и упорядочение их загр\зки оставлены читателю.
Глава 12. ОСНОВЫ МАШИННОГО ОБЕСПЕЧЕНИЯ ИНСТРУМЕНТАРИЯ ФУНКЦИОНАЛЬНОГО ПРОГРАММИРОВАНИЯ 12.1. Хранение списков Наибольшие трудности связаны с допускаемым нами спо- способом манипулирования со списками записей. Эти трудности состоят в том, что некоторые записи действительно пропадают Рис. 12.1. в том смысле, что доступ к ним становится невозможным. Чтобы понять, как это происходит, рассмотрим оператор c\=cdr(c) Если до выполнения оператора имела место ситуация, изобра- изображенная на рис. 12.1, а после выполнения — на рис. 12.2, то к Рис. 12.2. ячейке, помеченной а, нет больше доступа ни из структуры, исходящей из саг, ни из регистра с. Возможно (и даже очень вероятно), что другие регистры или другие доступные записи будут указывать на а, и тогда запись а не будет потеряна. Од- Однако вероятно также и то, что не осталось указателей на а, и тогда мы никогда не сможем вновь получить доступ к записи. Аналогично и некоторые другие записи, исходящие из саг, могут стать недоступными. Это не составило бы проблемы, если бы не два обстоятельства. Первое заключается в том, что име- имеется только конечный запас записей, и когда он исчерпан, вы- вычисления не могут продолжаться. Было бы по меньшей мере обидно, если бы вычисления должны были прерваться в то время,
12.1. Хранение списков 309 когда имеется много недоступных записей, которые можно за- заново использовать, если только знать, где они находятся. Вто- Второе обстоятельство, которое делает проблему несколько более трудной, заключается в том, что когда запись становится недо- недоступной, не просто определить, что это действительно так. В приведенном примере, когда операция сокращает число ссы- ссылок на а на единицу, мы не знаем, не сокращается ли это число до нуля. Если бы мы знали, мы бы могли вернуть запись а в общий фонд, из которого берут записи. Процесс определения того, какие ячейки становятся недо- недоступными, и возвращение их в общий фонд для перераспреде- перераспределения называется сборкой мусора. Существуют различные стратегии, которые можно использовать при сборке мусора: здесь будет описан сборщик типа пометить и собрать. В этом сборщике ничего не делается до тех пор, пока не будет исчерпан весь запас записей. Тогда прослеживаются все структуры записей и помечаются все доступные ячейки. Затем просматривается вся область памяти, отведенная под записи, и все непомеченные записи (к которым потеряли доступ) собираются и образуют общий фонд для перераспределения. Как правило, если такой сборщик при запасе, скажем, 10 000 записей каждый раз соби- собирает около 5000 записей, такую ситуацию следует считать удов- удовлетворительной. Разумеется, фактическая доля собираемых записей будет зависеть не только от размеров запаса, но также и от характера задачи. Здесь мы хотим только отметить, что некоторая частота вызовов сборщика мусора является вполне приемлемой. Мы вернемся к разработке сборщика после обсуж- обсуждения форматов памяти. Напомним, что требуется обеспечить три типа записей: число, символ и cons. В них размещаются соответственно целое значение, строка и два указателя. Есть много преимуществ, вытекающих из того, что все записи занимают одинаковый объем физической памяти. Главное из них состоит в том, что нам нужно содержать только один общий фонд нераспределенных записей; и когда запись извлекается, она может быть перераспределена, если это нужно, под любой тип. Есть много способов достичь этого. Можно сделать запись достаточно длинной, чтобы она могла содержать наибольшее значение (вероятно, это строка в символьном атоме), или можно наложить ограничения на мак- максимальный размер значений, которые могут размещаться в записях. Каждую из этих крайностей можно критиковать. В пер- первом случае потери памяти почти наверняка будут значитель- значительными, а во втором могут быть неприемлемыми некоторые ре- решения. Например, нас, конечно, не удовлетворяют символьные атомы не более чем из четырех литер или числовые атомы, аб- абсолютные величины которых меньше 128.
310 Гл. 12. Основы машинного обеспечения инструментария Имеются также некоторые преимущества в работе с едини- единицами памяти, естественными для конкретной машины, на которой реализуется система. Так, например, если арифметические операции определены для 32-разрядных целых чисел, то выбор целых значений большей или меньшей длины может потребовать выполнения гораздо большего числа машинных команд, чем если бы мы выбрали целые точно такого размера. Итак, при выборе структуры памяти следует идти на некоторый компромисс, и будет ли этот вариант наилучшим, зависит от многих факторов. Здесь будет описан вариант, который мы сочли особенно полезным, не предполагая, что он в каком-то смысле наилучший, и не рас- рассматривая других возможностей. Первое решение, которое мы прини- принимаем,— логическое. Чтобы различать тип записи, введем с ней два логических рис 123 значения (разряда); условимся, что первый разряд будет использоваться для того, чтобы отличать записи cons от записей атомов, а вто- второй — чтобы отличать числовые атомы от символьных. Таким образом, логически требуется реализовать структуры, изобра- изображенные на рис. 12.3. На разряды, связанные с каждой записью, будем ссылаться как на разряд этоатом и разряд эточисло соответственно. Следующим шагом будет решение о том, что часть записи без двух разрядов должна быть достаточно велика, чтобы содержать стандартное (для рассматриваемой машины) целое число. То есть поле цзнач должно быть обычным целым значением. Далее, если мы решили, что для списка достаточно 2к ячеек, то на поле указателя отводится k разрядов, и если целое число требует п разрядов, то мы выберем размер записи, достаточный, чтобы вмещать п или 2k разрядов. Например, для машины IBM 370 при п=32 можно выбирать размер записи, равный 32 разрядам, особенно удобный для этой машины и идеальный, если доста- достаточно ?^16. Это допускает до 64К ячеек списка. Поскольку минимальное число ячеек списка (при компиляции компилято- компилятором самого себя) около 6К и 10К считается хорошим рабочим размером, размер 64К представляется достаточным. Однако, если имеются в виду гораздо более громоздкие вычисления, это может оказать решающее влияние на макет списочной памяти. Позднее мы вернемся к списочным записям типа символ. Здесь достаточно отметить, что реальные строки будут храниться в отдельной области, а в записи — только указатели на них. Таким путем можно преодолеть трудности упаковки строк в
12.1. Хранение списков 311 записи. Можно зафиксировать принятые нами решения, описав память, отведенную для хранения списков, как совокупность массивов, что и сделано в A2.1). разряд массив этоатом(\\размеру, разряд массив эточисло(\: размер); целый массив цзнач(\:размер); A2.1) указат массив сагA:размер); указат массив cdr\\: размер); Указатель фактически является коротким целым числом со значениями из промежутка 0 ... размер. Значение указателя О используется специальным образом, что будет описано позднее. Однако мы отведем под указатель точно половину разрядов, за- занимаемых целым значением. Взаимное расположение массивов саг и cdr относительно массива цзнач показано на рис. 12.4. Иначе говоря, р-й элемент массива цзнач занимает такой же объем памяти, как и р-е элементы массивов саг и cdr, которые вместе образуют запись cons. Такое описание массивов позволяет относительно незави- независимо от машины разрабатывать сборщик мусора и интерфейс- интерфейсные программы для модуля хранения списков. В значительной степени этому способствует и подходящий выбор имен массивов. Сначала рассмотрим предикаты. Они просто реализуются, как это показано в A2.2). лог npou iscons{p)\ начало выдать itnoamoM(p) =0 и этоншло(р) = 0 конец; лог проц этосимвол(р); начало A2.2) выдать этоатом(р) = 1 и 9гпочисло{р)=^0 конец; лог проц эточисло(р); начало выдать этоатом{р) = 1 и эточисло(р) == 1 конец;
312 Гл. 12. Основы машинного обеспечения инструментария Так же тривиально при помощи обычного механизма индек- индексирования массивов реализуются селекторы цзнач, саг и cdr. Селектор сзнач требует пояснений. Мы намерены хранить строч- строчные значения символьных атомов в отдельной области. Отложим описание этого механизма (обычный прием структурирования программ), введя вместо этого процедуры, которые делают то, что нам требуется. Пусть имеется процедура цел проц память (s); . . .; которая размещает строку s в памяти и выдает целое значение, по которому мы можем восстановить строку, использовав про- процедуру строч проц вводA)\ . . .; Всюду, где на верхнем уровне проекта системы мы писали сзнач (р) := s или использовали эквивалентные конструкции, теперь будем писать цзнач (/?): = память (s) Аналогично s : = сзнач (р) и эквивалентные конструкции заменим на s := ввод (цзнач (р)) Таким образом, селектор цзнач используется как удобный спо- способ выражения того факта, что если запись типа символ, то она содержит целое, которое в некотором смысле идентифици- идентифицирует строку, являющуюся значением этой записи. Рис. 12.5. Теперь можно рассмотреть размещение записей. Поскольку каждая запись может быть отведена под любой из трех типов записей, будем считать все незанятые записи записями типа cons и соединим их в один список //, который назовем свободным списком (рис. 12.5). Связь между элементами свободного списка осуществляется посредством полей cdr\ специальное значение указателя О, помещаемое в конечное поле cdr, означает конец списка. Если свободный список не исчерпан, то можно занять ячейку из него, просто написав />:=//; ff:=cdr(ff)
12.2. Сборщик мусора 313 Очевидно, что свободный список когда-нибудь будет исчерпан, и мы получим //=0. Чтобы оградить себя от этого, вместо при- приведенной выше последовательности будем писать если // = 0 то сбормусора\ f:=ff; ff:~cdr{ff) Здесь, разумеется, не исключена возможность, что сборщик мусора не соберет ни одной ячейки, но мы предполагаем, что в таком случае сборщик сам закончит вычисления. Следует детализировать приведенную последовательность размещения записи для каждого из трех типов. Это делается непосредственно. Если в абстрактной программе было записано w : = cons то это следует заменить на если // = 0 то сбормусора\ w:=ff\ ff: = cdr№ этоатом (w): = 0; эточисло (w): = 0; Последовательность размещения записей типа символ и число будет той же самой, исключая значения, используемые для каждого типа разрядов. 12.2. Сборщик мусора В предыдущем разделе было описано, как могут исчезать записи, в том смысле, что на них нет ссылок ни из регистра, ни из других записей, и поэтому к ним нет доступа. В этом разделе будет описан сборщик мусора типа пометить и собрать, который используется для того, чтобы вернуть подобные записи в свободный список. Сначала рассмотрим этап, на котором по- помещают записи. В каждой записи необходимо отвести один разряд, в который будет занесена 1, когда эта запись будет по- помечена. Таким образом, логически имеем дополнительный массив разр массив мет A '.размер); Можно пометить структуру записей п с помощью алгоритма A2.3).
ЗИ Гл. 12. Основы машинного обеспечения инструментария Запись, на которую указывает /г, помечается занесением 1 в мет(п). Затем, если запись составная, рекурсивно вызывается процедура метка, чтобы пометить сначала car, а потом cdr за- записи. К сожалению, это не подходит для циклических списков, но положение просто исправить, если проверить метку, наличие 1 в которой говорит о том, что запись уже рассматривалась (см. 12.4). проц метка(п) если мет(п) = 0, то начало ме/п(п) := 1; если ипоатом(п) = 0 то A2.4) начало метка(саг(п))\ метка(саг(п))\ конец коней Следующим рассмотрим этап сборки. Здесь мы хотим после- последовательно просмотреть всю область, используемую для хра- хранения записей, и собрать все непомеченные записи. Это выпол- выполняет алгоритм A2.5). Он просто проверяет каждую запись и, если она не помечена, присоединяет ее к свободному списку, помещая ее в начале списка. проц сбор; начало для i := 1 до размер цикл если Mem(i)=0 то начало A2.5) «И0:=,/; конец конец Так как // = 0, то, когда вызывается сборщик мусора, этот алгоритм будет правильно строить свободный список, снова заканчивающийся нулем. Это объясняет, как записи возвраща- возвращаются в свободный список. Аналогичный процесс A2.6) исполь- используется для первоначального заполнения свободного списка. проц начпамять; начало // := 0; для i := 1 до размер цикл начало cdr(i) : = //; A2.6) конец конец
12.2. Сборщик мусора 315 Остается решить, за какими значениями нужно проследить на фазе пометки. Ясно, что записи, доступные из регистров SECD-машины, должны быть прослежены, и поэтому сборщик мусора должен иметь к ним доступ. Поскольку программы ввода таковы, что они вызываются только перед тем, как SECD-ма- шина начинает вычисления, естественно предположить, что они не вызовут переполнения памяти. Тогда необходимо только проследить за структурами, порождаемыми регистрами SECD- машины, что сделано в A2.7). проц сбормусора\ начало для i := 1 до размер цикл метA) := 0; MeniKa(s); метка(е)\ метка(с)\ метка(а)', A2.7) метка(ш)\ метка(()\ меткаA)\ метка(нил)\ сбор; если // = 0 то ошибка; конец Причина, по которой необходимо помечать только ячейки, доступные из восьми регистров SECD-машины, связана с рядом обстоятельств. Единственный момент, когда может быть вызван сборщик мусора,— это момент размещения записи. Новые записи размещаются только в двух случаях: когда вводятся S-выраже- ния и во время исполнительного цикла SECD-машины. S-выраже- ния вводятся только в самом начале исполнения интерпрети- интерпретирующей программы, и поскольку древовидные представления строятся для них постепенно в списочной памяти, никакого мусора не создается. Поэтому если переполнение памяти проис- происходит в этот момент, мы ничем не можем помочь и должны пре- прекратить вычисления. Если бы потребовалось расширить машину, чтобы уметь читать S-выражения во время выполнения програм- программы, то для этого достаточно было бы убедиться, что сборщик мусора был вызван до начала чтения. Построенный здесь сборщик мусора можно несколько оптими- оптимизировать, засылая нули в массив мет во время фазы сборки. Это избавило бы нас от необходимости заполнять массив в начале следующего этапа пометок, и тем самым мы бы сэкономили один просмотр памяти, отведенной под записи. В массив мет потребо- потребовалось бы заслать нули только один раз, в самом начале вычисле- вычислений, но это означало бы, что занимаемую им память нельзя использовать в других целях. Другое важное замечание состоит в том, что поскольку алгоритм пометки рекурсивен, он требует для своего выполнения стековой памяти. Если мы программируем на языке, который не обеспечивает рекурсии и поэтому должен реализовать ее своими средствами, то это значит, что мы должны иметь для этого достаточно памяти. Но вероятно, эта память будет не так велика по сравнению с самой списочной памятью.
316 Гл. 12. Основы машинного обеспечения инструментария 12.3. Хранение строк Мы смогли единообразно обрабатывать записи благодаря тому, что отделили строчную часть символьного атома и хранили ее в другом месте. В записи хранится целое значение, по которому может быть восстановлена строка. Описание фактического меха- механизма хранения и восстановления строки было отложено до настоящего раздела. Главный вопрос, который нужно решить,— как много потребуется места в памяти для реализации этого ме- механизма. Рассмотрим типичную программу на инструментальном Лиспе (например, компилятор). Имеется около 20 стандартных слов, закрепленных за Лиспом, около 10 имен функций и около 10 различных имен переменных (хотя можно было бы использо- использовать до 20). В любом случае это меньше 50 различных символь- символьных атомов. С другой стороны, имеется около 300 различных вхождений символьных атомов. Среднее число литер в символь- символьном атоме около 4. Мы должны решить, что лучше — экономить память, стараясь не дублировать строки в памяти, или эконо- экономить время, требуемое на поиск строки, и разрешить дубликаты. Из приведенного выше следует, что разрешение дубликатов по- потребовало бы в 6 раз больше памяти, но даже в этом случае тре- требования на память не очень велики. С другой стороны, малое число различных символьных атомов не вызовет больших затрат времени на поиск, и, кроме того, поскольку поиск производится только при размещении, а это имеет место только при вводе S-выражений, число обращений к операции поиска равно числу вхождений символьных атомов. Это разовые накладные расхо- расходы. Поэтому нам кажется, что выбор любого варианта будет удовлетворительным. Однако имеется несокрушимый аргумент в пользу запрещения дубликатов. Если равные строки кодируются одним и тем же це- целым, можно избежать проверки значений строки, когда нам нужно посмотреть, не равны ли две строки. Поскольку (как мы предполагали и в чем действительно убедимся) такие сравнения очень часты, здесь может быть большая экономия времени. Таким образом, мы приходим к решению не допускать дублиро- дублирования строк и создать такой алгоритм размещения, который исследует все предварительно размещенные строки, чтобы избе- избежать дублирования строк в памяти. Будем хранить строки в последовательных ячейках памяти в виде массива строк. Повсюду до сих пор предполагалось, что абстрактный тип данных строка представляется последователь- последовательностью литер; теперь наложим конечные, но приемлемые огра- ограничения на ее длину. Это в свою очередь повлечет за собой огра- ограничения на длину символьных атомов. Ограничения порядка 12 литер было бы достаточно, но даже ограничение в 32 литеры
12.3. Хранение строк 317 было бы приемлемо с точки зрения памяти. Если представля- представляется обременительным занимать так много памяти под строки, средняя длина которых составляет всего лишь 4 литеры, то их можно упаковать в массивы литер, затратив при этом чуть боль- больше времени на программирование. Объявим строковую память размера все: строк массив строкпамять(\\все)\ цел верш\ Строки будут размещаться в этом массиве на позициях от 1 до верш, таким образом, первоначально в верш засылается 0. Преж- Прежде чем разместить строку в массиве, нужно проверить, не хранит- хранится ли уже она там. Это делает абстрактная программа A2.8), которая засылает 1 в целую переменную i и затем проверяет все позиции от 1 до верш, чтобы определить, не хранится ли там та- такая же строка, что и /. цел проц память^)', начало цел i\ i := 1; пока i^ верш и строкпамятьA) ф t цикл t:=i+l; если i<~ верш то выдать i иначе начало A2.8) верш := верш-\-\; строкпамять(верш) := t; выдать верш конец конец Если поиск успешен, то позицию строки t в памяти опреде- определяет значение i> которое меньше или равно значению верш. Зна- Значение i выдается тогда как результат процедуры. Если же поиск неудачен, на что указывает тот факт, что i в конце принимает значение верш + 1, то строка t размещается на следующей сво- свободной позиции, а ее индекс выдается в качестве результата. Вероятно, стоило бы запрограммировать проверку случая, когда мы пытаемся разместить больше строк, чем это позволяет отведенная под строки память. Очень просто получить строку, если задан ее индекс; мы про- программируем это следующим образом: строк проц ввод(ь)\ начало выдать строкпамятьA)\ конец Для любой реализации языка или машины процесс копиро- копирования или расширения строки будет, вероятно, «побуквенной» операцией. Здесь мы не рассматриваем такой уровень детализа-
318 Гл. 12. Основы машинного обеспечения инструментария ции. Достаточно сказать, что используются два различных спо- способа кодирования строк в литерные массивы и каждый из них удовлетворяет разным ситуациям. Первый метод состоит просто в дополнении коротких строк пробелами справа. При втором ме- методе кодируется длина строки, и эта информация упаковывается на дополнительных литерных позициях. Второй вариант, веро- вероятно, лучше, если реализация языка или машины не предусмат- предусматривает операций над целыми строками и, по всей видимости, эф- эффективнее даже и тогда. Однако вопрос эффективности не должен нас чрезмерно беспокоить, поскольку, как мы убедились, срав- сравнение строк имеет место только во время ввода. 12.4. Построение и отладка инструментальной системы При написании программ ввода и вывода S-выражений в гл. 11 и сборщика мусора в этой главе мы свободно позволяли себе использовать рекурсивные процедуры. Весьма вероятно, что при реализации инструментальной системы мы должны будем переписать эти программы на языке реализации, который не обеспечивает рекурсии. Разумеется, читателю хорошо известно, как воспользоваться рекурсией при организации собственного стека. Фактически в этой книге мы имеем дело с некоторыми деталями процесса реализации рекурсивных функций. Однако, что касается алгоритмов сложности вводвыр, вводспис, выводвыр и метка, необходима большая доля осторожности при програм- программировании вручную операций со стеком. Читателю, который пла- планирует реализовать инструментальную систему, мы рекомендуем не начинать программирование процедур с нуля, а разработать ряд правил для переписывания этих процедур и последовательно применять эти правила. Что немаловажно, структура результи- результирующей программы будет прямо связана с приведенным здесь псевдокодом и, таким образом, соответствующие модификации результирующей программы будут более легкими. В целом задача сборки всех частей интерпретатора SECD- машины является не очень сложной задачей, но к ней следует подойти с осторожностью. В частности, каждую часть реализа- реализации следует тщательно отладить по отдельности. В настоящем разделе будет описано, как это сделать. Конечно, в каждом конкретном случае необходимо будет изменять некоторые детали, но можно придерживаться общей организации этого подхода. Начнем с замечания, что если мы просто запрограммируем интер- интерпретатор машины, затем введем компилятор из приложения 2 и простой кусок программы на Лиспе для его компиляции, то в выс- высшей степени вероятно, что вычисление не удастся. Важно здесь то, что в случае неудачи мы не будем знать, где искать ошибку. Она может быть в любой части интерпретатора, или в нашей ко-
12А. Построение и отладка инструментальной системы 319 пии компилятора, или даже в программе, которую мы пытаемся скомпилировать. Чтобы не попасть в такое неудачное положение, воспользуемся методологией отладки, предложенной для раз- разработки функциональных программ в гл. 3. Иначе говоря, будем строить интерпретатор методом снизу вверх, последовательно все больше и больше отлаживая его логику. Таким образом, всякий раз, когда какой-нибудь этап отладки окажется неудач- неудачным, объем непроверенной логики, участвующей в этой отладке, будет не очень велик и поэтому ошибку будет легко обнаружить. Очевидно, это довольно разумный подход. Продолжительность периода отладки в целом может быть ограничена контролируемы- контролируемыми сроками. Кроме того, при таком подходе у программиста постепенно возникает уверенность в том, что все проверено и ра- работает правильно. Подобное чувство уверенности не может воз- возникнуть при более беспорядочном подходе, когда мы, загружая все вместе, становимся свидетелями неудачи такого решения и после этого пытаемся найти ошибку. Можно начать с отладки ввода и вывода литер. Это собствен- собственно часть лексической фазы, но хорошо известно, что на этапе буферизации легко могут возникать ошибки. Нам следует напи- написать простую отладочную программу, которая передает литеры со входа на выход, и затем использовать ее, чтобы скопировать некоторые данные, которые будут использоваться при последую- последующих отладках. Это имеет то преимущество, что, выполняя потом отладку, можно быть уверенным, что программы ввода правильно вводят данные. В частности, нужно удостовериться, что эта отла- отладочная программа может справиться с копированием строк из 80 литер и вспомогательных нулевых строк. Следующим уровнем отладки является обработка ввода и вывода лексем. Здесь можно использовать программу A2.9), аналогичную приведенной выше. На этот раз, однако, мы будем копировать по одной лексеме. Начальная и завершающая фазы опущены, показана лишь основная часть программы. строк лексема, тип, в эдлекс(лексема, тип); пока тип Ф "КЦ" цикл начало вывод леке (лексема); A2.9) ввод ле к с (лексема, тип) конец; Было бы неплохо также провести отладку, при которой печа- печатался бы также тип лексемы, поскольку приведенная выше про- программа будет работать, даже если все члены будут (неправильно) распознаваться как БУКВ. И снова следует проверить это на данных, которые будут использоваться при последующих отлад- отладках.
320 Гл. 12. Основы машинного обеспечения инструментария Далее, приведенную программу можно модифицировать в двух отношениях. Во-первых, можно проверить программы пере- перевода путем включений вида если тип = "ЧИСЛО" то начало i :=цел(лексема)\ i: = I + 1; лексема: = строкаA) конец; Во-вторых, можно проверить модуль хранения строк, включив программу если тип = "БУКВ" то начало t:= память(лексема)\ лексема: = eeod(i) конец; Проверку модуля хранения строк можно продолжить, используя программы перевода для печати значения индекса. Поэтому хоро- хорошо бы сделать такую отладку. Следующим логическим пунктом отладки будет модуль хране- хранения списков. Здесь стоит написать несколько простых отладоч- отладочных программ, которые строят списки и манипулируют с ними. Пример такой программы дан в A2.10). / := нил\ для i := 1 до 10 цикл начало п := число; цзнач(п) := г, г := cons; car (г) := л; cdr(r) := /; /:=г; конец; A2.10) пока / Ф нил цикл начало п := car(l)\ i := цзнач(п)\ вывод лекс(строкаA)); l:=cdr(l) конец; Важно отладить сборщик мусора. По всей вероятности, лучше всего это сделать, добавив некоторые средства в сам сборщик. Важно убедиться в том, что собран весь мусор. Выполняя такие примеры как A2.11), где сборщик помечает списки / и нил, и согласуя М и N с конкретными размерами пространства спис- списков, можно создавать запасы мусора известного объема и прове- проверять, собран ли сборщиком именно такой объем. Вероятно, при отладке неплохо было бы использовать только небольшую часть памяти, отведенной под списки, и такую, чтобы облегчить эти подсчеты. Сборщику мусора не должна повредить замена этих значений на большие.
12.4. Построение и отладка инструментальной системы 321 Отладив сборщик мусора, желательно отладить нашу схему обработки рекурсии, даже если язык реализации обеспечивает рекурсию. Ошибки в схеме обработки рекурсии легче найти сей- сейчас, чем на следующем шаге, когда мы займемся синтаксическим анализом. Хорошим примером послужила бы рекурсивная функ- функция соединить, данная в гл. 2. Можно использовать программу типа следующей: построить список целых от 1 до 10; соединить его с самим собой; напечатать результаты; Теперь, когда мы переходим к отладке ввода и вывода S-выражеиий, мы должны быть уверены в модулях ввода и вывода лексем, размещения и перевода лексем, размещения списковых структур и в схеме обеспечения рекурсии. Если написать теперь очень простую тестовую программу указат р\ вводвыр(р); выводвыр(р)\ то можно последовательно проверить эти процедуры. Будем за- задавать этой программе постепенно усложняющиеся данные: ПРИМ символьный атом 127 числовой атом —127 отрицательный числовой атом (ПРИМ .127) точечная пара (ПРИМ) список (ПРИМ 127) более длинный список (ПРИМ (ВВОД 127)) и т. д. Этот список мы пополним данными, которые будут использовать- использоваться при последующих отладках, в частности, данными, приведен- приведенными в табл. 12.2. Теперь, когда мы наконец переходим к сборке интерпретатора машины и выполнению главной программы, неотлаженным остал- остался только сам процесс выполнения команд. Итак, выполняем 11 № 929
322 Гл. 12. Основы машинного обеспечения инструментария указат фну арг, рез\ вводвыр(фн)\ вводспис(арг)\ рез:= применить(фну арг)\ выводвыр(рез)) для последовательности машинных программ, подобранных так, чтобы проверить каждую команду независимо. Напомним, что функция выполнить помещает аргумент фн в регистр управления, а аргумент арг — в вершину стека. Зна- Значение, выданное в рез, является значением, которое находится в вершине стека после выполнения команды СТОП. Итак, если мы введем в программу данные (СТОП) (В С) или более точно (используя числовые коды команд) B1) (В С) то тем самым проверим, что команда СТОП функционирует, как мы и ожидали, т. е. что результатом является ((В С)). А теперь по- поступим смелее и предположим, что используются тестовые дан- данные (снова воспользуемся мнемоническими командами, хотя на практике заменим их числовыми) (ВДК А СТОП) {В С) Здесь мы ожидаем, что результатом будет Л. Подобным образом можно последовательно отладить все команды при помощи программ, приведенных в табл. 12.1. В этой последовательности каждый новый тест затрагивает только одну новую команду, и поэтому в случае неудачи в пер- первую очередь следует проверить, нет ли ошибки в кодировке этой команды. В случае команды ПРИМ тест не охватывает этой функ- функции целиком, поэтому ПРИМ и ВЫХОД объединены скобкой. Команда ФОРМ используется необычным способом (сдвигает окружение на одну позицию) для отладки старшего индекса в команде ВВОД. Фактически, если ввести еще несколько команд ФОРМу то можно проверить и большие значения этого индекса. Теперь следовало бы проверить выполнение SECD-машиной достаточно длинной последовательности команд, чтобы удосто- удостовериться, что каждая команда оставляет машину в правильном состоянии. Такую последовательность можно придумать очень просто. Можно (и даже желательно) подготовить приведенную выше последовательность с некоторой дополнительной инстру- инструментовкой. Например, можно выводить после каждой команды значения регистров s, е, с и d (используя программу выводвыр). Прежде чем приступить к исполнению компилятора, неплохо было бы скомпилировать вручную функцию соединить и прове-
12.4, Построение фн и отладка инструментальной арг системы рея 323 Таблица 12.1 отлаженная операция (СТОП) (ВДК А СТОП) (ВДК А АТОМ СТОП) (ВДК (А) АТОМ СТОП) (ВДК (A) CAR СТОП) (ВДК А ВДК В CONS СТОП) (ВДК А ВДК В РАВНО СТОП) (ВДК А ВДК А РАВНО СТОП) (ВДК 271 ВДК 127 ПЛЮС СТОП) (ВДК 271 ВДК 127 МИНУС СТОП) (ВДК 271 ВДК 127 УМН СТОП) (ВДК 271 ВДК 127 ДЕЛ СТОП) (ВДК 271 ВДК 127 ОСТ СТОП) (ВДК 271 ВДК 127 МР СТОП) (ВДК 127 ВДК 127 МР СТОП) (ВДК 127 ВДК 271 МР СТОП) (ВДК И ВЫБ (ВДК А СТОП) (ВДК В СТОП)) (ВДК Л ВЫБ (ВДК А СТОП) (ВДК В СТОП)) (ВДК И ВЫБ (ВДК А ВОССТ) (ВДК В ВОССТ) СТОП) (ВДК Л ВЫБ (ВДК А ВОССТ) (ВДК В ВОССТ) СТОП)' (ВДФ (ВДК А) СТОП) (ВДФ (ВДК А СТОП) ПРИМ) (ВДФ (ВДК А ВЫХОД) ПРИМ СТОП) (ВДФ (ВВОД @.0) ВЫХОД) ПРИМ СТОП) (ВДФ (ВВОД @.1) ВЫХОД) ПРИМ СТОП) (ВДФ (ФОРМ ВВОД A.0) ВЫХОД) ПРИМ СТОП) (ВДФ (ФОРМ ВВОД A.1) ВЫХОД) ПРИМ СТОП) (ФОРМ ВДФ (ВВОД @.0) СТОП) РЕК) (ВС) любой любой любой любой любой любой любой любой любой любой любой любой любой любой любой ((В С)) А И\ л} А (В.А) Л\ и] 398 144 34417 2 17 Л\ и\ И стоп ВДК АТОМ CAR CONS РАВНО ПЛЮС МИНУС УМН ДЕЛ ОСТ МР 21 2 12 10 13 14 15 16 17 18 19 20 любой любой любой ВЫБ любой (? С) (В С) (В С) (В С) (В С) (D Е) (ВС) (В С) (D Е) (В С) А (B Л)) А\ {В С) ) (D Е) (В С) (D Е)у (В С) ВОССТ ВДФ ПРИМ выход > ввод ФОРМ РЕК 9 3 4 5 1 6 7 11*
324 Гл. 12. Основы машинного обеспечения инструментария рить ее потому, что тогда оставшиеся ошибки будет легче опре- определить. Теперь настало время загрузить компилятор, который нужно скопировать из приложения 2. Разумеется, это следует делать с величайшей осторожностью. Разумно было бы использовать некоторые средства верификации данных. По крайней мере один человек мог бы вслух прочитать данные, полученные из машины, а другой сверил бы их с приложением. Компилятор вводится в ма- машину как программа, предназначенная для исполнения, а про- программа, которую нужно компилировать,— как аргумент, т. е. непосредственно вслед за компилятором. Компилятор требует выражения на инструментальном Лиспе, значением которого является функция, например {ЛЯМБДА (X) (CONS X X)). К программе, которая компилируется для такого выражения, добавляются затем команды (ПРИМ СТОП). Таблица 12.2
12.5. Раскрутка и оптимизация 325 Чтобы отладить компилятор, зададим сначала выражения, ко- которые не имеют своими значениями функции, так что компи- компилятор строит программы, которые не будут выполняться. Просто мы проверим эти программы, чтобы убедиться, что они такие, как и ожидалось (табл. 12.2). Естественно, что там, где в табл. 12.2 используются символь- символьные коды операций, в действительности будут числовые значе- значения. Снова мы постепенно проверяем отдельные части компилятора таким образом, чтобы при неудаче искать ошибку в ограниченной области. Ошибка в объектной форме компилятора, однако, ло- локализуется с трудом, и приведенные выше тесты только немного помогают в этом отношении. Однако выполнить тесты очень важ- важно, так как на следующем этапе мы должны быть уверены в том, что если скомпилированная программа будет ошибочна, то дело здесь в программе, а не в компиляторе. Итак, наконец, мы будем компилировать программы и испол- исполнять их. Снова следует начать с совсем простых примеров, как этот: (ЛЯМБДА (X) (CONS X X)) и в конечном счете перейти к некоторым из примеров, приведен- приведенных в этой книге. В следующем разделе мы сможем убедиться в том, как компилируется и исполняется функция соединить, и удостовериться в том, что компилятор компилирует самого себя. 12.5. Раскрутка и оптимизация Теперь, когда мы умеем компилировать компилятор, появи- появилась возможность расширять и язык, и связанную с ним машину. Процесс использования компилятора для компиляции расширяю- расширяющихся версий самого себя называется раскруткой. Чтобы избе- избежать множественности версий интерпретатора машины и компи- компилятора, мы всегда будем пытаться вернуться к состоянию равно- равновесия, когда объектная форма компилятора при исполнении ее на машине вырабатывает из исходной формы идентичную копию объектной формы. Тогда мы избавляемся от более ранних версий как машины, так и компилятора. В качестве примера рассмотрим необычный процесс изменения кодов операций в командах CAR и CDR. Обозначим процесс исполнения скомпилированной программы / на машине т с аргументами х, приводящий к результату у, через у = m (/, *)
326 Гл. 12, Основы машинного обеспечения инструментария Тогда в случае компиляции, если Со есть объектная форма ком- компилятора, a Ps — исходная форма некоторой программы, то Ро, объектная форма этой программы, представляется в виде Ро = т (Cot Cs) В частности, в состоянии равновесия будем иметь Со = т (Со, Cs) где Cs — исходная форма компилятора. В выбранном нами примере обозначим различные машины и программы следующим образом: т0 — начальная машина, где CAR = 10, CDR = 11; тг — новая машина, где CAR =11, CDR = 10; Cs0 — исходная форма начального компилятора, строит программы для т0; Со0 — объектная форма начального компилятора, Со0 = =то(Со0, Cso); Csi — исходная форма нового компилятора, строит программы для тх. Сначала компилируем Cst следующим образом: Cot = mQ(Co0i Csi) Далее, Cot есть (промежуточная) программа, которая будет ис- исполняться на машине т0. Итак, теперь мы снова компилируем Csx на этой машине на следующем уровне: Сох = т0 (Cot, Csi) На этот раз получается программа, которая будет исполняться на новой машине ти и, если теперь компилируем Csx в третий раз, мы снова достигнем состояния равновесия: Сох = mi(Cob Csx) Можно изобразить этот процесс в виде диаграммы, приведен- приведенной на рис. 12.6. Из этой диаграммы ясно, что если бы мы не из- изменяли машины, т. е. т0 = т1у то можно было бы получить Сох непосредственно, без промежуточного состояния. То есть мы бы просто имели ситуацию, изображенную на рис. 12.7. В обоих случаях, однако, нам могут потребоваться дополни- дополнительные шаги. То есть может оказаться, что первая версия Сох не идентична последующим. В худшем случае мы можем обнару- обнаружить, что Coi строит неправильные программы. Поэтому необхо- необходима дополнительная отладка (эти шаги на рисунках обозначены пунктиром) до тех пор, пока Сох не станет текстуально идентич- идентичным конечной версии. Необходимо также пропустить и другие тесты, а не только удостовериться, что компилятор компилирует
12.5. Раскрутка и оптимизация 327 самого себя. На самом деле нужно повторить все машинные тесты из предыдущего раздела. И это потому, что не все команды ма- машины (или не все части нового компилятора) участвуют в компи- компиляции компилятором самого себя. Рис. 12.6. Эта возможность раскрутки новой системы делает расширение и оптимизацию инструментальной системы сравнительно простым делом. Однако, прежде чем приступать к оптимизации, разумно сделать некоторые подсчеты. В частности, важно оценить те преимущества, которые мы сможем извлечь. Рис. 12.7. Важными и простыми сведениями, помогающими принять хорошие решения относительно оптимизации, являются данные о частоте использования каждой команды SECD-машины. Дан- Данные табл. 12.3 являются типичными для выполнения программ, написанных в коде SECD-машины. Из этих данных, даже без точных подсчетов времени на выполнение каждой команды, легко видеть, что мы, вероятно, больше выиграем от оптимизации ко- команды ВВОД, чем, скажем, команды РАВНО, хотя обе они имеют очень длинную интерпретацию. Читателю, который решит построить свою инструментальную Лисп-систему, несомненно захочется усовершенствовать приве- приведенную здесь очень простую реализацию, например включив
328 Гл. 12. Основы машинного обеспечения инструментария сложные примитивы, обсуждавшиеся в гл. 7 и 8. Однако важно иметь уверенность, что конкретное расширение достаточно уве- увеличивает возможности системы, чтобы имел смысл очень длин- длинный процесс раскрутки со всеми вытекающими из него необ- необходимыми проверками. Ясно, что для некоторых расширений, таких как вычисления с задержкой, это стоит сделать, но вообще это такой предмет, о котором читатель со всей осторожно- осторожностью должен составить себе мнение.
Приложение 1. ОТВЕТЫ К НЕКОТОРЫМ УПРАЖНЕНИЯМ Глава 2 2.1. произв(х) = если равно(х,НИЛ) то 1 иначе cor(x)Xnpou3e(cdr(x)) 2.2. уменьш(х) s= если равно(х>НИЛ) то ////Л иначе corts(car(x) — 1, уменыи(саг(х))) уменыи(х, я) = если равно{х,НИЛ) то ##Л иначе cons(car(x) — /г, уменыи(сйг{х), п)) 2.3. позиция(х, у) = если равно(х,НИЛ) то 1 иначе 1 + позиция(х, cdr(y)) 2.4. позиция\(х, у)г= если элементах, у) то позиция(х, у) иначе О 2.5. индекс^, у) = если (=1 то саг(у) иначе индекс^—1, cdr(y)) 2.6. набор(х) ss если равно{х,НИЛ) то ЯЯЛ иначе если атом(сш(х)) то co/is(car(A:), na6op(cdr(x))) иначе соединить{набор(саг(х)),набор(саг(х))) 2.11. отобрмнож(ху /) = если равно(х,НИЛ) то ЯЯЛ иначе включить(f(car(x)) ,отобрмно ж (cdr{x) /)) 2.12. редукция2(х, g, а) = если равно(х, НИЛ) то а иначе редукция2(саг(х), g, g(a, car(x))) 2.13. ((Л ?)(C D)) (((Л)).Д) (Л ? С D Я F) ((Л.Б) CD) 2.14. набор(х) ^ если атол<(л-) то cons(x, НИЛ) иначе соединить(набор(саг(х)),набор(саг(х))) 2.15. кронтожд(х, у) s== если а/лож (л:) то если атом{у) то равно(х, у) иначе Л иначе если атом(у) то Л иначе {пусть х = регулятор(х) и у = регулятор(у) если равно(саг(х), саг (у)) то кронтожд(саг(х), cdr ((/)) иначе Л} регуляторах) ss если ятои^са/^л:)) то дг иначе {пусть х' =регулятор(саг{х)) cons(car(x')} cons(cdr(x')y cdr(x)))} Глава 3 3.2. naumunocA(s) зз HaumunocA\(cons(sMИЛ)УНИЛ) найтипосл\(8у а) ^ если S — НИЛ то //?Г иначе если /?(car(S)) то cons(car(S)y а) иначе {пусть t = найтипосл\(след(сагC)), cons(car(S) a)) если t = HET то ttotfm«/2<%vd(cdr(S), a) иначе /} 3.4. Haumu(s)z=cpedu(cons{s, НИ1). НИЛ) cpedu{S, 0 s если 5 = ЯЯЛ то
330 Приложение 1 если t — НИЛ то НЕТ иначе среди (car{t)), cdr(t))\ иначе если p(car(S)) то car(S) иначе cpedu(cdr{S), cons(cAed(car(S))y /)) 3.6. птобр(х, f)z=omo6pf(x) гдерек отобр(х) э= если х — НИЛ то ////«/7 иначе cons(f(car(x)) yomo6pf(cdr(x))) 3.7. uhomhom(s)^ если amoM(s) то cons(cons(s,HMJI), cons(sMHJl)) иначе {пусть с = индмнож{саг(8)) и d — UHdMHOM{cdr(s)) cons(o6beduHeHue(po3HOcmb(car(cI cdr(d))y разносmb(car(d)t cdr(c))))y объединенш(саг(с)> cdr(d)))} 3.8. послед(х, у) = если у = НИЛ то ЯЯЛ иначе если саг(у)=х то объединение(префикс(саг(у)), послед(ху cdr(y))) иначе посл^(а;, crfr((/)) префикс(у) s= если у —НИЛ то ЯЯЛ иначе cons(car(y), НИЛ) 3.10. добавить(х, f)^%(y) если # = * то Я иначе / (#) 3.11. вб/ч(х, f) = X(y) если # = х то Л иначе / (#) 3.12. Пустое множество—это Х(гс) 0 добавить(х, f)^X(n) если элементах, /) то /(/г) иначе если я = 0 то /@)+1 иначе если л=1 то * иначе /(л—1) элемент(х, f)z== разбор(х, /, /@), 1) разбор(х, /, л, i) г если i > п то Л иначе если / @ = * то # иначе разборах, f л, i+1) Глава 4 4.7. Результатом (ЗНАЧ е) является значение переменной, имя которой вычисляется как значение е. Это позволяет вносить имена во внутренние конструкции, что, впрочем, не очень желательно из-за крайней сложности использования. 4.10. вычислить(е, пу v) = если атом(е) то ассоц(еу п, v) иначе если саг(е) — КОД то car(cdr(e)) иначе если саг(е) = И то если вычислшпь(саг(саг(е)), п, v) то ebi4UCAumb(car(cdr(cdr(e))), /г, v) иначе Л иначе если саг(е) — ИЛИ то если вычислить(саг(саг(ё)), п v) то И иначе ebi4ucAumb(car(cdr(cdr(e))), n, v) иначе если саг(е) = НЕ то если вычнслить(саг(саг(е)), п, v) то Л иначе И иначе если саг(е) — ВСЕ то {если вычистть(е, cons(x, /г), cons(H, у)) то вычислить(е, cons(x, n), cons{Jl, v)) иначе Л где e = car(cdr(cdr(e))) и х = саг(го(/-(е))} иначе если гаг(е)=СУЩ то {если вычислить(е, cons(x, n), cons(H, i1)) то # иначе вычислить(е, cons{x, п). сопэ(Л v)) где е — car(cdr(cdr{e))) и x = car(cofr(e))} иначе ошибка
Ответы к некоторым упражнениям 331 ассоц(е, п, v) as если е = саг(п) то car(v) иначе ассоц(е> cdr(n)> cdr(v)) (Замечание: проще структура «список имен/список значений».) Глава 5 5.2. ассоц(е> о)=&о(е) ваменить(о, х, y)^X(z) если г — х то у иначе а (г) 5.4. внач({е1 и е2}> п* v)sseaiH tt то t2 иначе Л где г1 = знач (((?!}, n, u) и t2 = 3Ha4 ({е2}, п, v) внач{{е1 ии е2}, /г, у)^а«ач(если /х то {е2} иначе {Л}, л, у) где гх^знач^}, п, v) Глава в 6.1. ((с' .e').s)e(riPHMO.c)d—+ НИЛ(НИЛ.е') cf(s e с А) 6.2. ((c'.e')y.s)e (ПРИМХ.с) d-^НИЛ ((y).e')c'(s e cd) Эквивалентное использование ПРИМ— это (ВДК И ИЛ ..¦ программа ввода аргументов ... CONS ... программа ввода замыкания ... ПРИМ) 6.4. (е)*/1 = е*л|ЯРЯМ) (е е1)*п = е1*п\е*п\ПРИМ\ 6.5. (И1 ег е2)*п = е1*п\(ВЫБ е2*п\(ВОССТ) (ВДК Л ВОССТ)) (ИЛИ1 ег е2)*п=^е1^п\(ВЫБ (ВДК И ВОССТ) е2 * n \ (ВОССТ)) 6.8. / (а, Ь, с) 555 если а то b ( ) иначе с ( ) f(amoM(x), М )*» М ) саг(х)) 6.9. факт(п) никогда не останавливается. Глава 7 7.2. разбиение(х)г&соп8(соп8(саг(х),НИЛ), cdr(x)) или если равно(саг(саг(х)),НИЛ) то нет иначе {пусть s = pa36ueHue(cdr(x)) cons(cons(car(x), car(s)), cdr(s))} depeeo(x) s== если х = НИ Л то нет иначе если саг(х) — НИЛ то m/-(jc) иначе {пусть s = pa36uenue(x) cons(depeeo(car(x)I depeeo(cdr(s)))} (Замечание: функция разбиение(х) образует пару подсписков, представ- представляющих произвольное разбиение списка х.) Глава 8 8.1. целот(т) ss задержать соп$(т,целот(т + 1)) nepebie(k, х) =5 если & = 0 то ИИ Л иначе corts(ozr(возобновить х)у первые (k— 1, «//"(возобновить х))) 8.20. фильтр(р, х) ss пропуск(\, р, *) пропуска, р, х) 5=з если / = /? то соп5(ПРОБЕЛ,пропуск(\, р, cdr(x))) иначе corts(ozr(*), пропуска + 1» Р, cdr(jt))) (Замечание, список простые теперь будет содержать ПРОБЕЛЫ.)
332 Приложение I Глава 9 9.4. cons: ХхЗадерж{Задержсписок(Х))—> Заде рже писок(Х) саг: Задержсписок(Х) —> X edr: Задержсписок(Х)—> Задерж(Задержсписск(Х)) соединить: Задержсписок(Х) хЗадержсписок(Х) —> Заде рже писок(Х) х: Задержсписок(Х) у: Задержсписок(Х) cdr(x): Задерж(Задержсписок(Х)) возобновить cdr(x): Задержсписок(Х) саг(х): X cons(car(x)y задержать(соедын«тб(возобновить cdr(x), у))): Задержсписок(Х) 9.5. комп. (К-— Z)X(X—>К)—>(*—>Z) отобр: (U —* V) —¦* (Cnucofc(U) —* Список(У)) комп(отобр, отобр): (U2—^y2)—>(Список(Список(и2))—^Список(Список(У2))) Замечание, возьмем Y = U1—> Уг Z —Список{иi) —> CnucoK{Vi) Y = CnucoK(U2) —*• Списск(У2) т. е. U1 = CnucoK{U2) У1 = Список(У2) 9.7. В: (Y —» Z) —> ((X —* К) — (X —- Z)) отоб/?: ([/ —+ V) —-> (CnucoK(U) —+ Список(У)) В(отобр): (X—+(U—+ У)) —* (X —> (CnucoK(U) —* Список(У))) 9.9. */(/?, ^)^Я(а:) если р(л:) то ^ (л:) иначе «77 может использоваться для проверки того, что выражение удовлетво- удовлетворяет одновременно и р, и q. Глава 10 10.1. порядок(х, #)==<еели xs^t/ то х иначе уу если х^г/ то t/ иначе *> или порядок (х, у) ^ если дс^г/ то <х, г/> иначе <t/, jc> 10.2 сорт 3(ау Ь, с) = {пусть <а, Ь> = порядок(а, Ь) {пусть <&, су = порядок (Ь, с) {пусть <а, by = порядок(а, Ь) <а, Ь, су}}} 10.4. сложп(соп$(а\, ЯЯЛ), consul, ЯЯЛ)) = слояс1(а1, 61, 0) сложп(соп8{а\, а), consul, 6)) = <сО, cOAis(d, s)> где <с0, о?> = слодае1(а1, 61, а) где <а, 8У = сложп(а, Ь) 10.5. длина($) = если e(?Aioc^(s) то 1 иначе длина(левая($)) + длина(правая(з)) MaKc(s) = если едпосл(з) то элемент^) иначе больший (макс(левая($)), макс(правая(з))) больший{х, уM5если *^# то // иначе jc
Приложение 2. КОМПИЛЯТОР ДЛЯ ИНСТРУМЕНТАЛЬНОГО ЛИСПА Приведенный на стр. 334—335 текст состоит из двух частей: первая из них — компилятор в исходной форме, вторая — компилятор в объектной форме. (Текст компилятора приведен на языке оригинала. Ниже дается таб- таблица соответствия имен. — Ред.) ТАБЛИЦА СООТВЕТСТВИЯ ИМЕН
(LETREC COMPILE * (COMPILE LAMBDA (E) (CCMP E (QUOTE NIL) (QUOTE (U ICGHP LAMBDA (E N C) (IF (ATOM E) (CONS (QUOTE 1) (CONS (LOCATION E N) O) (IF (EQ (CAR E) (QUOTE QUOTE))* (CONS (QUOTE 2) (CONS (CAR (CDR E)) C)) (IF (EQ (CAR E) (QUOTE ADD)) (COMP (CAR (CDR E)) N (COMP (CAR (CDR (CDR E))) N (CONS (QUOTE 15) C))> (IF (EQ (CAR E) (QUOTE SUB)) (COMP (CAR (CDR E)) N (COMP (CAR (CDR (CDR E))) N (CONS (QUOTE 16) C))) (IF (EQ (CAR E) (QUOTE MUD) (COMP (CAR (CDR E)) N (COMP (CAR (CDR (CDR ?))) N (CCNS (QUOTE 17) C))) (IF (EQ (CAR E) (QUOTE DIV)) (COMP (CAR (CDR E)) N (COMP (CAR (CDR (CDR E))) N (CCNS (QUOTE 18) C))) (IF (EQ (CAR E) (QUOTE REH)) (COMP (CAR (CDR E)) N (COMP (CAR (C&R (CDR E))) N (CONS (QUOTE 19) C))) (IF (EQ (CAR E) (QUOTE LEQ)) (COMP (CAR (CDR E)) N (COMP (CAR (CDR (CDR E))) N (CONS (QUOTE 20) C))) (IF (EQ (CAR E) (QUOTE EQ)) (COMP (CAR (CDR E)) N (COMP (CAR (CDR (CDR E))) N (CONS (QUOTE 14) C))) (IF (EQ (CAR E) (QUOTE CAR)) (COMP (CAR (CDR E)) N (CCNS (QUOTE 10) C)) (IF (EQ (CAR E) (QUOTE CDR)) (COMP (CAR (CDR E)) N (CONS (QUOTE 11) C)) (IF (EQ (CAR E) (QUOTE ATOM)) (CCMP (CAR (CDR'E)) N (CCNS (QUOTE 12) C)) (IF (EQ (CAR E) (QUOTE CONS)) (COMP (CAR (CDR (CDR E))) N (COMP (CAR (CDR E)) N (CONS (QUOTE 13) C))} (IF (EQ (CAR E) (QUOTE IF)) (LET (COMP (CAR (CDR ?)) N (CONS (QUOTE 8) (CONS THENPT (CONS ELSEPT C)))) (THENPT COMP (CAR (CDR (CDR E))) N (QUOTE (9))) (ELSEPT COMP (CAR (CDR (CDR (CDR ?)))) N (QUOTE (9))) > (IF (EQ (CAR E) (QUOTE LAMBDA)) (LET (CONS (QUOTE 3) (CCNS BODY C)) (BODY COMP (CAR (CDR (CDR E))) (CONS (CAR (CDR E)) N} (QUOTE E))) ) (IF (EQ (CAR E) (QUOTE LET)) (LET (LET (COMPLIS ARGS N (CONS (QUOTE 3) (CONS BODY (CONS (QUOTE 4) C)))) (BODY COMP (CAR (CDR E)) M (QUOTE E)))) (M CONS (VARS (CDR (CDR E))) N) (ARGS EXPRS (CDR (CDR E)))) (IF (EQ (CAR E) (QUOTE LETREC)) (LET (LET (CONS (QUOTE 6) (COMPLIS ARGS M (CONS (QUOTE 3) (CONS BODY (CONS (QUOTE 7) C))))) (BODY COMP (CAR (CDR E)) M (QUOTE E)))) (M CONS (VARS (CDR (CDR E))) N) (ARGS EXPRS (CDR (CDR E)))) (COMPLIS (CDR E) N (COMP (CAR E) N (CONS (QUOTE H) C)))))))))))))))) Ш)> (COMPLIS LAMBDA (E N C) (IF (EQ E (QUOTE NIL)) (CONS (QUOTE 2) (CONS (QUOTE NIL) C)) (COMPLIS (CDR E) N (COMP (CAR E) N (CONS (QUOTE 13) C))))) (LOCATION LAMBDA (E N) (LETREC (IF (MEMBER E (CAR N)) (CONS (QUOTE 0) (POSN E (CAR N))) (INCAR (LOCATION E (CDR N)))) (MEMBER LAMBDA (E N) (IF (EQ N (QUOTE NIL)) (QUOTE F) (IF (EQ E (CAR N)) (QUOTE T) (MEMBER E (CDR N))))) (POSN LAMBDA (E N) (IF (EQ E (CAR N)) (QUOTE 0) (ADD (QUOTE 1) (POSN E (CDR N))))) (INCAR LAMBDA (L) (CONS (ADD (QUOTE 1) (CAR L>) (CDR I))))) (VARS LAMBDA (D) (IF (EQ D (QUOTE NIL)) (QUOTE NIL) (CONS (CAR (CAR D)) (VARS (CDR D)))>) (EXPRS LAMBDA (D) (IF (EQ D (QUOTE NIL)) (QUOTE NIL) (CONS (CDS (CAR Z>)) UXPB5 (CPR V)))))
F 2 NIL 3 A @ . О) 2 NIL 14 8 B NIL 9) B NIL 1 @ . О) 11 13 1 A . (О . 0) 10 11 13 9) 5) 13 3 A @ . 0) 2 NIL 14 8 B NIL 9) B NIL 1 @ 13 A Ч 1 @ . 0) 10 10 13 9) 5) 13 3 (б 2 NIL 3 A @ . 0) 11 2 0) 10 15 135) 133 A @ . 0) 1 @ . 1) 10 148 B09)- B 1 2NIL1 5) Ч 1 . 0) 11 1 1 (О @ . 1) 3 33 (9) 11 13 1 @ . 0) 13 1 A . 1) 4 15 9) 5) 13 3 A @ . 1) 2 NIL 14 8 B F 9) A (О . 0) 1 @ . 1) 10 14 8 B Т 9) B NIL 1 @ . 1) 11 13 1 @ . 0) 13 1 A . 0) 4 9) 9) 5) 13 3 B NIL 1 A . 1) 10 13 1 A . 0) 13 1 @ . 0) 4 8 B NIL 1 A 1) 10 13 1 A 0) 13 1 B « 3) 2) 2 1) 4 2 0 13 9) B NIL 2 NIL I A . 1) 11 13 1 A 0) 13 1 (О 4 13 1 @ . 2) 4 9) 5) 7 5) 13 3 A @ . 0) 2 NIL 14 8 A (О NIL 13 2 2 13 9) B NIL 2 NIL 1 @ . 2) 2 13 13 13 1 @ . 1) 13 1 @ . 0) 10 13 1 A • 1) 4 13 1 @ , 1) 13 1 @ , 0) 11 13 1 A . 2) 4 9) 5) 13 3 A (О • 0) 12 8 A @ . 2) 2 NIL 1 @ . 1) 13 1 @ . О) 13 1 ( 1 . 3) 4 13 2 1 13 9) A @ . 0) 10 2 QUOTE 14 8 A @ . 2) 1 @ . 0) 11 10 13 2 2 13 9) A @ . 0) 10 2 ADD 14 8 B NIL 2 NIL 1 @ . 2) 2 15 13 13 1 @ . 1) 13 1 @ . 0) 11 11 10 13 1 A . 1) 4 13 1 @ . 1) 13 1 @ . 0) 11 10 13 1 A • 1) 4 9) A @ . 0) 10 2 SUB 14 8 B NIL 2 NIL 1 @ . 2) 2 16 13 13 1 @ . 1) 13 1 @ . 0) 11 11 10 13 1 A . 1) 4 13 1 @ . 1) 13 1 @ . 0) 11 10 13 1 С1 . 1) 4 9) A @ , 0) 10 2 MUL 14 8 B NIL 2 NIL 1 @ . 2) 2 17 13 13 1 @ . 1) 13 1 @ ¦ 0) 11 11 10 13 1 A (О . С) 11 10 13 1 A . 1) 4 9) О @ . 0) 10 2 DIV 2) 2 18 13 13 1 @ . 1) 13 1 @ . 0) 11 11 10 13 1 A 1) 13 1 @ . 0) 11 10 13 1 A . 1) 4 9) A @ . 0) 10 2 REM 1« 19 13 13 1 @ « 1) 13 1 @ . 0) 11 11 10 13 1 О . (О . 0) 11 10 13 1 A . 1) ^ 9) A @ . 0) 10 2 LEQ 14 8 2 20 13 13 1 @ . 1) 13 1 @ . 0) 11 11 10 13 1 A . 1) (О . 0) 11 10 13 1 A . 1) 4 9) A @ . 0) 10 2 EQ 14 8 B 2) 2 14 13 13 1 СО • 1) 13 1 @ . 0) 11 11 10 13 1 A . 1) 4 13 1 @ . 1) 13 1 @ . 0) И 10 13 1 A . 1) 4 9) A @ . 0) 10 2 CAR 14 8 B NIL 1 @ . 2) 2 10 13 13 1 @ . 1) 13 1 @ . С) 11 10 13 1 A . 1) 4 9) A @ • 0) 10 2 CDR 14 8 B NIL 1 @ . 2) 2 11 13 13 1 @ . 1) 13 1 @ . 0) 11 10 13 1 1) 4 13 1 @ . 1) 13 1 14 8 B NIL 2 NIL 1 (О , 1) 4 13 1 (О 8 B NIL 2 NIL 1 @ ¦ 2) 2 1) 4 13 1 @ . 1) 13 1 B NIL 2 NIL 1 @ . 2) ,4 13 1 @ . 1) 13 1 NIL 2 NIL 1 (О A ¦ 1) 4 9) A @ . 1 @ . 0) 11 10 13 1 2) 2 13 13 13 1 (О 0) 10 2 АТОМ 14 8 B NIL 1 @ . 2) 2 12 13 13 1 @ . 1) 13 A 1) 4 9) A @ . 0) 10 2 CONS 14 8 B NIL 2 NIL 1 (О 1) 13 1 @ . 0) 11 10 13 1 A . 1) 4 13 1 @ . 1) 13 1 (О (О 0) 11 11 10 13 1 A . 1) 4 9) A @ ¦ 0) 10 2 IF 14 8 B NIL 2 NIL 2 (9) 13 \ 1) 13 1 @ . 0) 11 11 11 10 13 1 A . 1) 4 13 2 NIL 2 (9) 13 1 @ . 1) 13 1 @ . 0) 11 1t 10 13 1 A 2) 1 @ . 1) 13 1 (О 1) 4 13 3 B NIL 1 A 13 2 8 13 13 1 О • 1) 13 1 A . С) 11 10 13 1 B 2 LAMBDA 14 8 B NIL 2 NIL 2 E) 13 1 @ , 1) 1 (О 11 П 10 13 1 A . 1) 4 13 3 A A . 2) 1 @ . 0) 10 2 LET 14 8 B NIL 2 NIL 1 @ . 0) 11 11 13 1 A (О . 0) 11 11 13 1 A . 4) 4 13 13 3 B NIL 2 NIL 2 E) 13 1 @ . 0) 13 1 A 0) 1) Ч 5) 4 9) A @ . 0) 10 . 0) 11 10 13 13 1 @ . 0) 3 2 3 13 5) 4 9) A @ . 0) . 5) 4 13 1 @ . 1) 2 NIL 1 0I110131B B . 1) 13 1 A 1) 4 13 3 B NIL 1 B NIL 2 NIL 1 1 (О 1) 13 1 C 0) 11 11 13 1 2) 4 5) 4 5) 4 A . 5) 4 13 1 2) 2 4 13 1 @ . 0) 13 2 3 13 13 1 9) A @ . 0) 10 2 LETREC 14 8 B @.1J NIL 1@.0I11113 A . 4) 4 13 13 3 B NIL 2 NIL 2 E) 13 1 @ . 0) 13 1 A ¦ 0) 11 10 13 1 B • 1) 4 13 3 B NIL 1 B . 2) 2 7 13 1 @ . 0) 13 2 3 13 13 1 A . 0) 13 1 A • 1) 13 1 C . 2) 4 2 6 13 5) 4 5) 4 9) B NIL 2 NIL 1 @ . 2) 2 4 13 13 1 @ . 1) 13 1 @ . 0) 10 13 1 A . 1) 4 13 1 @ . 1) 13 1 @ . 0) 11 13 1 A . 2) 4 9) 9) 9) 9) 9) 9) 9) 9) 9) 9) 9) 9) 9) 9) 9) 9) 9) 5) 13 3 B NIL 2 D 21) 13 2 ML 13 1 CO • 0) 13 1 A . D 1 5) 13 3 A @ . 0) 5) 7 4 21)
Приложение 3. БИБЛИОГРАФИЯ Читатель, движимый намерением дальше развивать идеи, изложенные в этой книге, может реализовать его, следуя одним из трех путей. Он может за- заняться дальнейшим изучением техники программирования со строгими функ- функциями или методов формальной спецификации семантики языков программиро- программирования или же исследованием тонкостей Лиспа и его применений. С этой целью предлагается краткий обзор литературы по этим направлениям, в основном тех книг и статей, которыми пользовался сам автор в своих исследованиях. Вместо того чтобы расположить ссылки в порядке значимости, что было бы весьма трудно по причине их тесной взаимосвязи, рассмотрим их в том порядке, в ко- котором затрагиваемые темы расположены в книге. Строго функциональный язык, введенный во второй главе и используемый во всем тексте книги, основывается на ISWIM-обозначениях Лэндина A966). Многие авторы внесли свой вклад в развитие стиля программирования, к ним относятся Баррон A968), Бердж A975), Берсталл и др. A971) и Фостер A967). В этой же связи можно порекомендовать сборник работ под редакцией Фокса A966). Программы поиска из третьей главы базируются на методе, описанном Нильсоном A971). Читатель, интересующийся более широкими применениями функционального программирования, может обратиться к литературе по искус- искусственному интеллекту и, в частности, к работам по языку Лисп, указанным ниже. Особенно интересное приложение описано Робинсоном A979), который создал программу на Лиспе для доказательства теорем, впрочем написанную скорее на Лиспе в целом, чем на чисто функциональном его подмножестве. Версия Лиспа, предложенная в четвертой главе, основана на первоначаль- первоначальном определении Лиспа, данном Маккарти A960). Эту работу можно настоя- настоятельно порекомендовать в качестве самостоятельного источника как основных идей Лиспа, так и описания Лисп-интерпретатора. Не менее важна и более тра- традиционная работа по Лиспу Маккарти и авторов A962), которая также описы- описывает еще один Лисп-интерпретатор. О Лиспе как о языке программирования написано много. Автор считает полезными работы Фридмана A974), Маурера A973), Шиклоши A976) и Вейсмана A968). Однако следует отметить, что Лисп в целом, описанный этими авторами, вовсе не является чисто функциональным языком. Построение интерпретатора в гл. 4, явно опирающееся на работу Мак- Маккарти A960), заимствует также идеи Лэндина A964) и Рейнольдса A972). Описание интерпретатора фрагмента Алгола в пятой главе основано на работе Лэндина A965), то же самое относится и к большей части содержания этой главы. Метод преобразования операторных схем к рекурсивным функциям принадлежит Маккарти A960), в то время как идея сопоставления функцио- функционального эквивалента каждой конструкции языка программирования лежит в основе математической семантики, разработанной в Оксфорде Скоттом и Стрей- чи. Интересующийся этими вопросами читатель может обратиться к работам Милна и Стрейчи A976), Скотта A977) и Стоя A977). Работа Скотта наиболее краткая и может быть использована как введение к работам того же автора в данной области, а книга Стоя представляет собой доступное введение. Двух- Двухтомник Милна и Стрейчи — не для робких, но его можно настоятельно реко- рекомендовать как источник идей, касающихся использования строгих функций для определения семантики языков программирования,
Библиография 337 SECD-машина в гл. 6 основывается на абстрактной машине, разработан- разработанной Лэндином A964), которая описана также у Берджа A975) и Вегнера A968). Другие методы реализации строго функционального языка, отличные от пря- прямой интерпретации, в книге не обсуждаются. Однако читателю можно реко- рекомендовать работы Берклинга A975) и Тернера A979). Решение задачи о ферзях, описанное в седьмой главе, опирается на реше- решение, данное Дейкстрой в книге Дала и др. A972). Понятия вычислений с за- задержкой и замедленных вычислений, описанные в восьмой главе, опираются на аналогичные идеи Берджа A975), Фридмана и Вайса A976) и Вюймена A973). В действительности истоки этих идей можно обнаружить у Лэндина A965). Проблема, которая в книге Дейкстры A976) приписывается Хэммингу, решена методом, основанным на работе Кана и Маккуина A977). Функции высших порядков как метод построения практических программ подробно рассматриваются Бэкусом A978). Изложение книги Берджа A975) также основано на использовании этих функций. Заинтересованный читатель вряд ли оставит без внимания красивую теорию комбинаторной логики (Хин- дли и др., 1972). Понятие продолжения, введенное в девятой главе и исполь- используемое в десятой, основано на идее Стрейчи и Водсворта A974), также рассмат- рассматриваемой Рейнольдсом A972). При реализации Лисп-инструмента используются обычные идеи структур- структурного программирования. Для интересующегося читателя можно указать кон- конкретные источники: Вирт A976) — техника компиляции, Маккарти A960) — детали S-выражений и списочных структур и сборка мусора, Кнут A968) — другие методы сборки мусора. Баррон (Barron D. W.) 1968 Recursive Techniques in Programming, McDonald, London. (Имеется перевод: Баррон Д. Рекурсивные методы в программировании. — М. : Мир, 1974.) Бердж (Burge W. Н.) 1975 Recursive Programming Techniques, Addison-Wesley, Reading (Mass.). Берклинг (Berkling K. G.) 1976 Reduction Languages for Reduction Machines, ISF-76-8, GMD, Bonn. Берсталл, Коллинз, Поплстоун (Burstall R. M., Collins J. S., Popplestone R. J.) 1971 Programming in POP-2, Edinburgh University Press, Edinburgh. Бэкус (Backus J.) 1978 Can programming be liberated from the von Neumann style? A functional style and its algebra of programs. Comm. ACM 21 (8), 613—641. Вегнер (Wegner P.) 1968 Programming Languages, Information Structures and Machine Organisa- Organisation, McGraw-Hill, New York. Вейсман (Weissman C.) 1968 Lisp 1.5 Primer, Dickenson, Encino (Calif.) Вирт (Wirth N.) 1976 Algorithms + Data Structures = Programs, Prentice-Hall, Englewood Cliffs (N. J.). Вюймен (Vuillemin J. E.) 1973 Proof Techniques for Recursive Programs, Memo AIM-318, STAN—CS—73—393, Stanford University. Дал, Дейкстра, Xoap (Dahl O.-J., Dijkstra E. W., Hoare С A. R.) 1972 Structured Programming, Academic Press, London. (Имеется перевод: Дал У., Дейкстра Э., Хоор К. Структурное программирование.— М. : Мир, 1975.)
338 Приложение 3 Дейкстра (Dijkstra E. W.) 1976 A Discipline of Programming, Prentice-Hall, Englewood Cliffs (N. J.) (Имеется перевод: Дейкстра Э. Дисциплина программирования.— М. : Мир, 1978.) Кан, Маккуин (Kahn G., McQueen D.) 1977 Coroutines and Networks of Parallel Processes. Information Processing 77, North-Holland, Amsterdam. Кнут (Knuth D. E.) 1968 The Art of Computer Programming: Vol. 1, Fundamental Algorithms, Ad- dison-Wesley, Reading (Mass.). (Имеется перевод: Кнут Д. Искусство про- программирования для ЭВМ. Т. I.— М.: Мир, 1976.) Лэндин (Landin P. J.) 1963 The Mechanical Evaluation of Expressions, Computer Journal, 6D), 308— 320. 1965 A Correspondence between Algol 60 and Church's Lambda Calculus, Comm. ACM, 8 C), 158—165. 1966 The Next 700 Programming Languagues, Comm. ACM, 9 C), 157—164. Маккарти (McCarty J.) 1960 Recursive Functions of Symbolic Expressions and Their Computation by Machine, Comm. ACM, 3 D), 184—195. Маккарти, Абрахаме, Эдварде, Харт, Левин (McCarty J., Abrahams P. W., Ed- Edwards D. J., Hart T. P., Levin M. I.) 1962 The Lisp 1.5 Programmer's Manual, MIT Press, Cambridge (Mass.). Манна (Manna Z.) 1974 Mathematical Theory of Computation, McGraw-Hill, New York. Maypep (Maurer W.D.) 1973 A Programmer's Introduction to Lisp, American Elsevier, Amsterdam. (Имеется перевод: Maypep У. Введение в программирование на языке ЛИСП. М.: Мир, 1976.) Милн, Стрейчи (Milne R. E., Strachey С.) 1976 A Theory of Programming Language Semantics, Chapman and Hall, Lon- London. Нильсон (Nilsson N. J.) 1971 Problem Solving Methods in Artificial Intelligence, McGraw-Hill, New York. (Имеется перевод: Нильсон Н. Искусственный интеллект.— М. : Мир, 1973.) Рейнолдс (Reynolds J. С.) 1972 Definitional Interpreters for Higher-Order Programming Languages, Proc. ACM Annual Conference. Робинсон (Robinson J. A.) 1979 Logic: Form and Function. The Mechanisation of Deductive Reasoning, Edinburgh University Press, Edinburgh. Скотт (Scott D. S.) 1977 Loqic and Programming Languages, Comm. ACM, 20 (9), 631—641. Стой (Stoy J. E.) 1977 Denotations] Semantics: The Scott — Strachey Approach to Program- Programming Language Theory, MIT Press, Cambridge (Mass.). Стрейчи, Водсворт (Strachey С, Wadsworth С. Р.) 1974 Continuations, A Mathematical Semantics for Handling Full Jumps, Technical Monograph PRG-11, Oxford University Computing Laboratory. Тернер (Turner D. A.) 1979 A New Implementation Technique for Applicative Languages, Software Practice and Experience, 9, 31—49.
Библиография 339 Фокс (Fox L., ed.) 1966 Advances in Programming and Non-numerical Computation, Pergamon Press, London. Фостер (Foster J. M.) 1967 List Processing, McDonald, London. (Имеется перевод: Фостер Дж. Об- Обработка списков.— М.: Мир, 1974.) Фридман (Friedman D. Р.) 1974 The Little Lisper, Science Research Associates, London. Фридман, Вайс (Friedman D. P., Wise D. S.) 1976 CONS Should Not Evaluate Its Arguments, in Automata, Languages and Programming, S. Michaelson and R, Milner (eds.), Edinburgh University Press, Edinburgh. Хиндли, Лерчер, Селдин (Hindley J. R., Lercher В., Seldin J. P.) 1972 Introduction to Combinatory Logic, Cambridge University Press, Camb- Cambridge. Шиклоши (Siklossy L.) 1976 Let's Talk Lisp, Prentice-Hall, Englewood Cliffs (N, J.)
АЛФАВИТНЫЙ УКАЗАТЕЛЬ Абсолютные единицы в размерностях 73 Абстрактная программа 279 — форма программ на Лиспе 100 Абстрактный синтаксис 132 Активная запись 157, 167 Анализ размерностей 68 — случаев для списков 33 S-выражений 61 Аппликативная структура языка 17 Аппликативный язык 18 Аргумент см. Параметр Ассоц (и, а) для ассоциативных спис- списков 70 Ассоц (х, п, v) в Алголе 135 — в Лисп-интерпретаторе 116 Ассоциативный список 70. См. так- также Список значений Атом 24, 31 АТОМ у интерпретация 118 — команда SECD-машины, правило перехода 172 реализация 303 — компиляция 181 — синтаксис 103 Базовые функции 257 Баррон 336 Бердж 337 Берклинг 337 Берсталл 336 Бесконечные структуры 216 Бинарное дерево как эквивалент S-выражения 65 Бросание монеты 199 Бэкус 246 ВВОД, команда SECD-машины, пра- правило перехода 173 реализация 303 ВДВ, команда SECD-машины, пра- правило перехода 218 ВДК, команда SECD-машины, пра- правило перехода 168 реализация 302 ВДФу команда SECD-машины, пра- правило перехода 174 реализация 305 Вегнер 337 Вейсман 336 Векторы 253 — правило параллелограмма 253 — сложение 253 Взаимная рекурсия 54, 113 Вирт 337 Включить (s, х), операция над мно- множествами 41 Вложенные правильные выражения 102 ВОЗВР (ВЫХОД), команда SECD- машины, правило перехода 176 — реализация 305 Возвраты, программирование с ними 204 — поиск с ними, явное программи- программирование 207 Возобновить 214 Возобновления регистр в SECD-ма- шине 202 ВОССТ, команда SECD-машины 173 — — — реализация 307 Вращения тетраэдра 79 Вталкивание (значение), операция в стеке 153 ВЫ Б, команда SECD-машины, пра- правило перехода 173 — реализация 307 Выбор (п), недетерминированный вы- выбор 198 Вызов по значению 165, 191, 222, 223 имени (наименованию) 165, 191, 222, 223 — — необходимости 224 — функции 15 Выполнить(е, п, и), функция в ин- интерпретаторе фрагмента Алгола 136
Алфавитный указатель 341 Выполнить (фи, арг), функция, ими- имитирующая работу SECD-машины 168, 190, 300 Выр(а) 124, 188 Выражение типа 240 Выражения, свойство чистого ре- результата 154 в Лиспе 177 Высших порядков функции 50, 240, 337 синтаксис 107 тип 241 Выталкивание, операция в стеке 153 Вычисление выражений в стеке 153 — методом запросов 231 — через действия, значений 20, 130, 265 Вычисления с задержкой 212, 337 Вычислить (е, п, v), в интерпретаторе с Лиспа 117 Вычитание, — 32 Вюймен 337 Евклида алгоритм 145 ЕСЛИ, интерпретация 118 — компиляция 182 — синтаксис 104 Завершение (х, у) 121, 176 Задача о восьми ферзях 205 Задерж(х), тип 262 Задержать 214 ЗАДЕРЖАТЬ, компиляция 218 Замедленные вычисления 222, 225, 227, 337 ЗАМЕНА, команда SECD-машины, правило перехода 219 Замыкание 112, 174 Записи для символа, числа и cons 280 Знач(е, п, v) в Алгол-выражениях 135 Значение, вызов по нему 165, 191, 222, 223 — вычисление 20, 130 Где блок 47, 88, 109 Гдерек блок 54, 88, 113 Геометрическое понятие вектора 253 Глобальная организация программы 279 — переменная 56, 87, 108 Готовности метод, способ вычислений 231 Граф состояний 81 Данные, области 273 — представление 73 — символьные 23 — структурирование 22 — структуры, включающие функ- функции 96 Дейкстра 234, 337 ДЕЛу команда SECD-машины, пра- правило перехода 171 реализация 304 — компиляция 181 — синтаксис 104 Деление, ч- или дел 32 Дерево (X), тип 262 Длина (х) 28, 33 Добавить (х, у) 38 Доступ к переменным 88 И для разделения определений 48 Избыточность точной записи 59 Ии, логическая связка 159 И, логическая связка 159, 251 И1, логическая связка 195 Императивная программа, функцио- функциональный эквивалент 138 — преобразование 141 Императивный язык 131 Индексирование памяти 273 Индексные пары в SECD-машине 172 Индивидуумы^ 64, 90 Инструментальный Лисп 99 Интерпретатор 101 — Алгола 134 — Лиспа 115 Интерпретация коллектива машин в случае недетерминизма 200, 204 Итеративная форма функции 152 Кан 337 Квазипараллелизм 212 Кварендон 235 Кирпичные стенки 260 Кнут 337 КОД, интерпретация 118 — синтаксис 103 Комбинатор 246 КомпA,ё) 56
342 Алфавитный указатель КомпилЦ) 166 Компилятор для Лиспа 177 Компиляторы и интерпретаторы 100 Композиция функций 16 Конкретная форма Лисп-программы 100 Константы в Лиспе, компиляция 180 Конструкторы 28 Контекст 108 — рекурсивный 114 Контекстно-свободный язык 249 Копирование массива значений 274 ЛИ Б, команда SECD-машины 202 Либо, недетерминированный выбор 197 ЛИБО, недетерминированный выбор 202 Лисп 336 — интерпретатор 115 — истинный и инструментальный 99 — компилятор 177 — конкретная форма программы 100 завершающий пример 107 Лисп-выражения, свойство чистого результата 177 Локальные определения 47, ПО — — функций 53 — переменные 86 Лэндин 17, 166, 168, 336, 337 ЛЯМБДА, интерпретация 119 — компиляция 183 — синтаксис 105 Маккарти 23, 115, 336, 337 Маккуин 337 Массивы 269, 270, 274 May pep 336 Машина SECD 168, 285 — — правила перехода 168 коллектив машин в случае недетерминизма 200, 204 Метки в операторной схеме 142 МИНУС, команда SECD-машины, правило перехода 171 — реализация 304 — компиляция 181 — синтаксис 104 Множество, представленное функцией 95 МР, команда SECD-машины, пра- правило перехода 171 — — — реализация 304 — компиляция 181 — синтаксис 104 Набор (t) 40 Набор (х) для списков 64 S-выражений 65 Накапливающие параметры 43, 64> 93, 151 Недетерминизм, использование про- продолжений 262 сообщества машин 200, 204 Недетерминированные программы 197 Недоступные записи 309 Незавершенное значение, используе- используемое в функции завершение 121 Некоммутативные операции в стеке 155 Неоднозначность синтаксическая 132 Неправильные программы 192 Нет, завершение вычислений 200 НЕТ, завершение вычислений 202 НИЛ 28 Нильсон 79, 82, 336 Область действия переменной в Лисп-программе 172 — значений функции 12 — определения функции 12 Обновить (a, i, x), операция над мас- массивами 274 Обратить {х) 37 — с использованием накопительных параметров 43 Объединение (и, v)у операция над множествами 43, 92 Окружение (вычислительная обста- обстановка, регистр SECD-машины 168 Оператор параллельного присваива- присваивания 163 — goto 146, 248 Операторные схемы 141 со многими переменными 144 Операции над множествами 40, 64, 92 Операционная семантика 195 Определение дополнительной функ- функции 37 — функции 14 ОСТ, команда SECD-машины, пра- правило перехода 171 реализация 304 — синтаксис 104 Остаток, ост 32 Отладка 77 — системы инструментального Лиспа 318 — снизу вверх 77, 232 — функциональных программ 72
Алфавитный указатель 343 Omo6p(x,f) 50, 97 — тип 243 Отображение 13 Отобрмнож (х, f) 65 Отрезок (а, Ь) 255 Память для строк 316 —разделение 283, 304 Параметр функции 14 накапливающий 43, 64, 93 Паскаль 18 Перезапись в памяти 273 Перем(а) 124, 188 Переменная в Лиспе 118, 180 — глобальная 56, 87, 108 — доступ к ней 87 — локальная 86 — область действия в Лисп-програм- Лисп-программе 172 — свободная 56, 108 Пересечение, операция над множест- множествами 64 Пересмотр семантики языка 265 ПЛЮС, команда SECD-машины правило перехода 170 реализация 304 — компиляция 180 — синтаксис 104 ПЛ/1 18 Повтор, повторное возобновление 225 Подстановочность 199, 209 Позиция (х, у) 63, 179 Поиск «сначала вглубь» 83 — — «вширь» 83 Пока, оператор цикла 132 Пометка использованной памяти 314 Последовательная отладка програм- программы 321 функции 77 Последовательности как область дан- данных 276 Последовательность операторов 132 — переходов 169 Построение программы для вызова функции 157 Постфиксная или обратная форма выражения 181 Правило параллелограмма для век- векторов 253 — перехода 300 Правильность программы 91, 252 Правильные выражения в Алголе 131 Лиспе 102, 107 вырожденные случаи 108 — операторы в Алголе 132 Предикаты 250, 281 — от S-выражений 31 Представление данных 73 — множества функций 95 — стека S-выражением 167 Преобразование императивных про- программ 141 Приведение выражений над размер- размерностями 75 ПРИМ, команда SECD-машины, пра- правило перехода 175 реализация 305 Применение функции 13, 15 Применить (/, х) 100, 125, 164 Примитивные функции от S-выраже- S-выражений 28 ПРИ МБП, команда SECD-машины, правило перехода 219 Присваивание, оператор 132, 274 Программа, как правильное выра- выражение 102 — функциональная 37 Программирование при помощи про- процедур 18 функций 12 Продолжение 241, 337 — в случае недетерминизма 267 — для составных результатов 262 Произв(х) 63 Произведение двух размерностей 75 Произвольный выбор 209 Простое связывание 56 Простой список 24 Пространство списков, размещение записей 312 Процедуры, программирование при помощи процедур 18 Процессы, сети процессов 230 Псевдокод 279 Пусто, пустой оператор 132 Пусти список 30 Пусть, блок 47, 109 Пусть, лямбда, эквивалентность конструкций 113, 121, 185 ПУСТЬ, интерпретация 120 — компиляция 185 — синтаксис 105 ПУ';ТЬРЕК, компиляция 186 — реализация 306 — синтаксис 105 Пустьрек 54 — в замедленных вычислениях 226 — вычисление 113 Равно 31
344 Алфавитный указатель РАВНО, команда SECD-машины, правило перехода 171 реализация 304 — компиляция 181 — синтаксис 104 Развертка тетраэдра 81 Разделение памяти 283, 304 Размер (S) Размерности, представление 69 Размещение записей в пространстве списков 312 Разность {х, у), операция над мно- множествами 64, 92 Раскрутка 325 Распределение переменных по пара- параметрам 156 Реальные машины 273 Реальный Лисп 99 Редукция {х, g, а) 51 Редукция2 (*, g, a) 65 Результат, элемент временного рас- расширения языка 146 Рейнольде 336 РЕК, использование в конструкции ПУСТЬРЕК 186 — команда SECD-машины, правило перехода 176 реализация 305 Рекурсивная функция 34 локальное определение 54 Рекурсивные процедуры 318 Рекурсивный спуск 288 Рекурсия взаимная 54, 113 Рецепт 214 — представление 219 Робинсон 336 Сборка мусора 309, 313 — свободной памяти 314 Сборщик мусора 313 Свободная переменная 56, 108 Свободный список 312, 314 Свойство чистого результата для выражений 154 Лиспа 177 Связанная переменная См. Параметр Связывание 108, 273 — простое 56 — статическое 56 — текущее 127 Селекторы 28 Семантика операционная 195 — определяемая интерпретатором 126 Сети взаимосвязанных процессов 230 Символ, запись для него 279 Символьное дифференцирование 49 Символьные данные 23 Символьный атом 24 Симмразность(х, у), операция над множествами 64 Синтаксис абстрактный 132 — языка 249 Синтаксическая неоднозначность 132 Синтаксические диаграммы 289, 295 Синтаксический анализ 286 Скотт 336 Слоги 249 Сложение, + 32 — векторов 253 Современные машины, функциональ- функциональное программирование для них 152 Соединить (х, у) 35 — инфиксная форма 178 Сокращения в точечной записи 59 Сопрограммы 222 Составной результат функции 47, 265 продолжение 267 Список 25 — значений в Алгол-интерпретаторе 134 — — — Лисп-интерпретаторе 116 Список имен 134 в интерпретаторе для Алгола 134 Лиспа 116 — простой 24 — пустой 30 — свободный 312 — функций 245 Список (х), тип 243 Списочная память 308 Статическое связывание 56 Стек в вычислении выражений 153 — как регистр SECD-машины 168 — операции над ним 153 — представление S-выражением 167 Стой 336 СТОП, команда SECD-машины 169 Стрейчи 336 Строго синтаксические расширения языка 265 — функциональный язык 18 Структура фигуры 252 — функциональной программы 87 Структурное программирование 337 Сум(х) 34 Сумматор 266 Текущее связывание 127 Тип функции 241
Алфавитный указатель 345 Тожд(х, у) 61 Точечная запись (выражений) 58 —- сокращенная форма 59, 282 Точка разрыва цикла 142, 146 ТУП, команда SECD-машины 202 Тэрнер 337 — параметр 14 — программирование при помощи 12 — рекурсивная 33 — с составным результатом 45, 265 — частично определенная 15, 243 — эквивалент оператора 138 переменной в Алголе 138 Указатель 280 Умн, X 32 УМН, команда SECD-машины, пра- правило перехода 171 — — — реализация 304 — компиляция 181 — синтаксис 104 Управление, регистр SECD-машины 168 — памятью для функций 270 Условный оператор 132 Фактические параметры 192 Фибоначчи последовательность 233 Фигура (р, q) 255 ФОРМ, использование в конструк- конструкции ПУСТЬРЕК 186 — команда SECD-машины, правило перехода 176 реализация 305 Формула, алгебраическое представле- представление 26 Фортран 18 Фостер 336 Фрагмент Алгола 130 правильные выражения 131 операторы 132 Фридман 336, 337 Функциональные программы 37 в виде уравнений 137, 268 на современных машинах 152 — эквиваленты императивных про- программ 138 Функция 12 — без параметров 194, 217 — всюду определенная 15 — вызов, интерпретация 119 компиляция 183 — — самый внешний 152 — вырабатывающая функцию 55 — высшего порядка 50, 240 — вычисление в стеке 157 — как значение 111, 269 — наименование 242 — область значений 12 — область определения 12 — определение 14, 29 Хранилище, регистр SECD-машины 168 Целые, список всех целых чисел 231 Цикл, точка разрыва 142, 146 Части (p,q) 250 Частично-определенная функция 15, 31, 243 Частное двух размерностей 76 Число, запись 280 Числовой атом 24 — — запись для числа 280 Числовые коды для команд SECD- машины 300 Шахматная доска, представление 27 Шиклоши 336 Эквивалентность вызова функции, за- заданной S-выражением, и блока пусть 113, 121, 185 Элемент (х, s), операция над мно- множествами 41, 93 Эратосфена решето 235 Эффективность программ 91 Язык, аппликативная структура 17 — аппликативный 18 — императивный 131 — контекстно-свободный 249 — пересмотр семантики 265 — синтаксис 249 — строго синтаксические расшире- расширения 265 функциональный 18 Ясность выражения 265 сааг (х) 188 cadr(x) 188 CAR, интерпретация 118 — команда SECD-машины, правило перехода 171
346 Алфавитный указатель реализация 303 — компиляция 181 — синтаксис 104 спг(х) 29 CDR, интерпретация 118 — команда SECD-машины, правило перехода 171 реализация 303 — компиляция 181 — синтаксис 104 cdr(x) 29 CONS, интерпретация 118 — команда SECD-машины, правило перехода 171 реализация 303 — компиляция 181 — синтаксис 104 cons 30 ISWIM-обозначения 336 S-выражение 23, 25, 61, 268 — предикаты 31 — примитивные функции 28 — синтаксис 286 — эквивалентное бинарное дерево 65
УВАЖАЕМЫЙ ЧИТАТЕЛЬ! Ваши замечания о содержании книги, ее оформлении, качестве пере- перевода и другие просим присылать по адресу 129820, Москва, И-110, ГСП, 1-й Рижский пер., д. 2, издательство «Мир».
Питер Хендерсон ФУНКЦИОНАЛЬНОЕ ПРОГРАММИРОВАНИЕ. ПРИМЕНЕНИЕ И РЕАЛИЗАЦИЯ Научный редактор Л. Н. Бабынина Мл научный редактор Н. С. Полякова Художник Н. Я Вовк Художественный редактор В. И. Шаповалов Технический редактор 3. И Резник Корректор Н. Н. Яковлева ИБ № 3467 Сдано в набор 1.11.82. Подписано к печати 2.03.83 Формат 6 0Х9 0/1в. Бумага типографская № 2 Гарнитура ла- латинская Печать высокая. Объем 11 бум. л. Уел печ. л. 22. Усл. кр. отт. 22,4 9. Уч.-изд. л. 19,08. Изд. № 1/2372. Тираж 15000 экз. Зак. 929. Цена 1 р. 4U к. ИЗДАТЕЛЬСТВО «МИР» Москва, 1-й Рижский пер., 2 Ордена Октябрьской Революции и ордена Трудового Красного Знамени Первая Образцовая типография имени А. А. Жданова Союзполиграфпрома при Государственном комитете СССР по делам издательств, полиграфии и книжной торговли Москва, М-54, Валовая, 28