Текст
                    Л.СТЕРЛИНГ, Э.ШАПИРО
ИСКУССТВО
ПРОГРАММИРОВАНИЯ
НА ЯЗЫКЕ
ПРОЛОГ
ИЗДАТЕЛЬСТВО <МИР>


ИСКУССТВО ПРОГРАММИРОВАНИЯ НА ЯЗЫКЕ ПРОЛОГ
THE ART OF PROLOG Advanced Programming Techniques Leon Sterling Case Western Reserve University Ehud Shapiro The Weizmann Institute of Science with a foreword by David H.D. Warren University of Manchester The MIT Press Cambridge, Massachusetts, London, England
Л.СТЕРЛИНГ Э. ШАПИРО ИСКУССТВО ПРОГРАММИРОВАНИЯ НА ЯЗЫКЕ ПРОЛОГ Перевод с английского канд. физ.-мат. наук С. Ф. СОПРУНОВА и канд. техн. наук Л. В. ШАБАНОВА . под редакцией д-ра техн. наук Ю. Г. ДАДАЕВА МОСКВА «МИР» 1990
ББК 32.973-01 С79 УДК 681.3 Стерлинг Л., Шапиро Э. С79 Искусство программирования на языке Пролог: Пер. с англ. -М.: Мир, 1990.-235 с, ил. ISBN 5-03-000406-8 В книге излагаются основы логического программирования. Дается описание языка Пролог. Обсуждаются ввод-вывод, приемы и средства организации интерактивных программ, вопросы недетерминированного программирования, применения структур данных, допускающих накопление данных, техника грамматического разбора, программирование метаинтерпретаторов. Изложение удачно иллюстрируется примерами программ. Рассматриваются некоторые приложения Пролога: программирование игр, создание экспертных систем и компилятора для языка высокого уровня. Для системных программистов и инженеров-математиков, разрабатывающих информационно-программное обеспечение ЭВМ. 2404010000-297 С 136-89, чЛ 41(01)90 ББК 32.973-01 Редакция литературы по информатике и робототехнике ISBN 5-03-000406-8 (русск.) ISBN 0-262-19250-0 (англ.) © 1986 by the Massachusetts Institute of Technology © перевод на русский язык, «Мир» 1990
Предисловие редактора перевода Программированию на языке Пролог, различным вариантам расширения Пролога и методам его реализации посвящено множество статей, докладов и отчетов. Издано несколько книг по этому широко известному языку, в том числе переводы на русский язык: У. Клоксин, К. Меллиш. Программирование на языке Пролог.-М.: Мир, 1987 и К. Кларк, Ф. Маккейб. Введение в логическое программирование на микро-Прологе.-М.: Радио и связь, 1987. Однако следует подчеркнуть, что предлагаемая вниманию читателей книга сильно отличается от указанных выше не только по содержанию, но и по методу описания языка Пролог. Авторы книги - известные специалисты, много сделавшие для развития и популяризации логического программирования. В частности, с именем Е. Шапиро связано создание языка Concurrent Prolog. Он является редактором серии по логическому программированию, которую открывает данная книга. Две новые книги этой серии (сборники статей по Concurrent Prolog под ред. Е. Шапиро) изданы MIT Press в 1988 г. Язык Пролог прошел длинный путь развития (почти 20 лет). Он продолжает весьма быстро распространяться. Появилось значительное число его реализаций, причем наиболее популярный вариант Turbo-Prolog доступен пользователям персональных компьютеров. Существует мнение, что Пролог пропагандируется и используется преимущественно специалистами, решающими задачи из области искусственного интеллекта. Однако, как показывает опыт использования различных Пролог-систем, язык Пролог может успешно применяться для разработки сложных программных систем традиционного назначения. Он нашел применение в системах автоматизированного проектирования, системах символьных вычислений и т. д. Говоря о генезисе Пролога, нельзя не отметить его тесную связь с задачей определения грамматик формальных языков и задачей синтаксического анализа текстов, написанных на этих языках. Обработка текста на естественном языке также является интересным и перспективным применением Пролога. В данной книге излагаются основы логического программирования, описываются «чистый» и «нечистый» Пролог, разнообразные, порой изящные и «фантастичные» методы и приемы составления логических программ. Большой интерес предстваляют третья и четвертая части книги, где рассматриваются более сложные средства и приемы программирования и приводятся развернутые примеры применения языка для решения практических задач (решатель нелинейных уравнений, экспертная система, компилятор и др.). Книга, несомненно, окажется полезной для программистов и специалистов по разработке Пролог-систем. Для многих из них особый интерес представляют главы о недетерминированном программировании, метапрограммировании, программировании второго порядка и глава о неполных структурах данных, в которой подробно описана техника использования разностных списков и даны убедительные обоснования эффективности их применения. Следует отметить, что в области логического программирования у нас терминология не установилась. В настоящее время в отечественной литературе перевод некоторых терминов (например, cut, negation as failure и др.) неоднозначен. Данная книга не является исключением, и внимательный читатель заметит, что коллектив переводчиков предлагает свой вариант перевода некоторых терминов. Что касается перевода имен предикатов переменных и других термов Пролога в Пролог-программах, то было принято компромиссное решение. Везде, где на наш взгляд требовалось облегчить понимание программы, термы давались в переводе на русский язык, в противном случае переводились только комментарии. Имена стандартных предикатов Пролога оставлены без перевода. Перевод книги выполнили С. Ф. Сопрунов (гл. 1-13), Л.В.Шабанов (гл. 14-23, приложения). Ю. Г. Дадаев
Посвящается Рут, Мириам, Михале и Дане Предисловие Эта книга возникла из аспирантских курсов по современному программированию на языке Пролог. За минувшие 15 лет с момента рождения Пролога появилось множество методов программирования. Естественно, что мы стремились познакомить читателей с теми из них, которые вызвали у нас интерес, разбудили фантазию и стимулировали нашу деятельность в этой области. Нужда в подобной книге очевидна. Пролог и логическое программирование вообще в последние годы получили широкую рекламу. Однако имеющиеся книги и отчеты обычно описывают лишь основные понятия языка, и поэтому людям, не являющимся специалистами по Прологу, какие-либо неэлементарные примеры применения Пролога остаются недоступными. На протяжении всей книги мы подчеркиваем различие между логическим программированием и программированием на Прологе. Логические программы можно постигать, основываясь на двух абстрактных, машинно-независимых понятиях: истине и логической выводимости. При некоторой интерпретации символов программы можно спросить, истинны ли аксиомы программы или выводимо ли данное логическое утверждение из программы. Ответы на такие вопросы не зависят от конкретного механизма исполнения. В отличие от этого Пролог является языком программирования, заимствовавшим основные конструкции из логики. Программы на Прологе имеют точное операционное значение: они являются наборами инструкций, выполняемых на некоторой машине, называемой Пролог-машиной. Хорошие программы на Прологе почти всегда можно читать как логические предложения, и это характеризует наследование Прологом некоторых абстрактных свойств логических программ. Однако более важно, что результат вычисления программы на Прологе логически следует из аксиом программы. Эффективное программирование на Прологе требует понимания теории логического программирования. Книга состоит из четырех частей: логического программирования, языка Пролог, современных методов программирования и приложения. Первая часть является самостоятельным введением в логическое программирование. Она состоит из пяти глав. В гл. 1 вводятся основные конструкции логических программ. Мы ведем изложение в терминах логической выводимости, что отличает наше введение в логическое программирование от общепринятого. Исходным в обычном изложении является понятие резолюции, на основе которого и возникло логическое программирование. Мы считаем, что наш подход более пригоден для обучения, поскольку использует интуитивно ясные понятия, легко воспринимаемые обучающимися. В гл. 2 и 3 первой части вводятся два основных метода логического программирования: программирование баз данных и рекурсивное программирование. В гл. 4 обсуждается вычислительная модель логического программирования и вводится понятие унификации. В гл. 5 приводятся без доказательства некоторые теоретические результаты. Для того чтобы при точном описании современных методов программирования можно было использовать материал первой части, мы вводим новые понятия и пересматриваем старые. В частности, обсуждаются понятие типа в логическом программировании и проблема остановки. Другими важными понятиями, не получившими еще конкретного развития, являются сложность и корректность. Вторая часть представляет собой введение в Пролог. В нее входят главы с 6 по 13. В гл. 6 описывается вычислительная модель Пролога и указываются ее отличия от модели логического программирования. Здесь же проводится сравнение Пролога с обычными языками программирования, подобными языку Паскаль. В гл. 7 обсуждается разница в способах построения логических программ и программ на Прологе. Приводятся примеры применения основных приемов программирования. Следующие пять глав описывают системные предикаты, необходимые для того, чтобы Пролог стал языком практического программирования. Мы разделили системные предикаты Пролога на четыре категории: предикаты, относящиеся к эффективной реализации арифметики; предикаты анализа структур; металогические предикаты, анализирующие процесс вычисления; внелогические предикаты, вызывающие побочные эффекты за пределами вычисли-
Предисловие 7 тельной модели логического программирования. Одна глава отведена для обсуждения отсечения (cut) - внелогического предиката Пролога, имеющего самую скверную репутацию. В этих главах приводятся основные приемы использования системных предикатов. В заключительной главе второй части даны разнообразные практические рекомендации. Основной частью книги является ч. III. В ней описаны получившие распространение среди специалистов современные методы программирования на Прологе. Эти методы иллюстрируются небольшими, но содержательными программами. Примеры выбираются среди типичных приложений, в которых полезно применять рассматриваемый метод. В шести главах рассматриваются недетерминированное программирование, неполные структуры данных, синтаксический анализ с использованием для DC-грамматик, программирование второго порядка, методы поиска и метаинтерпретаторы. Заключительная часть состоит из четырех глав, в которых показано, как на основе изложенного материала можно создавать прикладные программы. Общее желание программистов, начинающих работать с Прологом,-увидеть большие прикладные программы. Начинающие программисты умеют писать короткие изящные программы, но построение больших программ вызывает у них затруднения. В приложениях приводятся игровые программы, прототип экспертной системы для обработки кредитных запросов, программа символьного решения уравнений и компилятор. В процессе создания книги потребовалось пересмотреть исходные понятия и основные примеры, существовавшие в качестве фольклора логического программирования. Принятая нами схема изложения представляет собой новую структуру материала при обучении Прологу. Содержание данной книги успешно использовалось в нескольких курсах по логическому программированию и Прологу: в Израиле, США и Шотландии. Изложенного материала более чем достаточно для семестрового курса для аспирантов первого года обучения или студентов старших курсов. На его основе преподаватели могут свободно строить и специальные курсы. Мы рекомендуем распределить материал книги при 13-недельном курсе для старшекурсников и аспирантов следующим образом: 4 недели-логическое программирование, помогающее освоиться с декларативным стилем создания программ, 4 недели - основы программирования на Прологе, 3 недели - методы программирования и 2 недели - работа над приложениями. При изучении методов программирования следует уделить внимание понятию недетерминизма, неполным структурам данных, основным предикатам второго порядка и базовым метаинтерпретаторам. Время, отведенное на приложения, можно использовать и для изучения других разделов. В качестве областей приложения следует выделить методы поиска в задачах искусственного интеллекта, построение экспертных систем, создание компиляторов и программ синтаксического анализа, символьные вычисления и обработку на естественном языке. Порядок изложения может быть достаточно свободным. Сначала следует изложить материал ч. I. Материал частей III и IV можно излагать совместно с материалом ч. И. Это позволит показать учащимся, что большие программы, создаваемые с помощью сложных методов, строятся по тому же принципу, что и простые примеры. Оценка, которую мы выставляли студенту, на 50% основывалась на домашних заданиях, выполнявшихся на протяжении курса лекций, и на 50%-на самостоятельной разработке программной системы. Наш опыт показал, что в процессе самостоятельной разработки учащиеся способны решать серьезные программистские задачи. Примерами самостоятельных разработок могут служить прототипы экспертных систем, ассемблеры, игровые программы, программы, реализующие частичные вычисления и теоретико-графовые алгоритмы. Тем, кто изучает книгу самостоятельно, мы настоятельно рекомендуем тщательно ознакомиться с наиболее абстрактным материалом ч. I. Правильный стиль программирования на Прологе основан на декларативном рассмотрении логической структуры задачи. В то же время с теорией, изложенной в гл. 5, можно ознакомиться позже. Диапазон упражнений в книге весьма широк-от простых и полностью определенных заданий до сложных задач, требующих дальнейшей разработки. Большинство упражнений пригодно в качестве домашнего задания. Некоторые из наиболее сложных задач предлагались в качестве курсовых проектов. Тексты программ в книге написаны в основном на Edinburg-Прологе. При чтении различных курсов использовались различные версии Edinburg-Пролога, что не вызывало никаких осложнений. Все примеры можно выполнять и в системе Wisdom-Пролог, описанной в приложениях.
8 Предисловие Мы выражаем благодарность и признательность всем, кто непосредственно принимал участие в создании книги. Мы также благодарим, не называя поименно, всех, кто опосредованно участвовал в создании книги, влияя на наш подход к программированию на Прологе. Текст книги был улучшен благодаря советам Лоуренса Бьярда (Lawrens Byrd), Одед Малер (Oded Maler), Джека Минкера (Jack Minker), Ричарда О'Кифа (Richard O'Keef), Фернандо Перейры (Fernando Pereira) и нескольких анонимных рецензентов. Мы высоко ценим содействие студентов, присутствовавших на лекциях в процессе «отладки» книги. Один из авторов признателен учащимся Эдинбургского университета, Института науки Вейцмана, Тель-Авивского университета и Case Western Reserve University. Другой автор вел курсы в институте Вейцмана, Еврейском университете в Иерусалиме и краткие курсы в промышленности. Мы благодарны многим помогавшим при подготовке книги к печати. Особенно благодарны Саре Флигельман (Sarah Fliegelmann), изготовившей эскизы и фотокопии, хотя это часто и не входило в круг ее обязанностей. Без ее титанических усилий книга не могла бы появиться. Арвинд Бэнсел (Arvind Bansal) подготовил указатель и помог разобраться в ссылках на литературу. Иегуда Барбут (Yehuda Barbut) создал почти все иллюстрации. Макс Гольдберг (Max Goldberg) и Шмуэль Сафра (Shmuel Safra) подготовили приложения. Издательство MIT Press оказало помощь и содействие. Наконец, мы благодарны семье и друзьям за оказанную поддержку, без которой вообще ничего не могло быть сделано.
Введение Истоки логики связаны с исследованием научного мышления. Логика предоставляет точный язык для явного выражения целей, знаний и предположений. Логика дает основание, позволяющее выводить следствия из исходных положений. Логика позволяет исходя из знания об истинности или ложности некоторых утверждений сделать заключение об истинности или ложности других утверждений. Логика позволяет обосновывать непротиворечивость утверждений и проверять истинность приведенных доводов. Компьютеры сравнительно недавно появились в нашей интеллектуальной истории. Как и логика, они являются и объектом^ научного исследования, и мощным инструментом, успешно применяемым в различных областях науки. Как и в логике, компьютерное решение задачи невозможно без явной и точной формулировки целей и предположений. Однако если развитие логики определяется силой человеческого разума, то развитие компьютеров с самого начала было ограничено некоторыми технологическими и техническими рамками. Хотя компьютеры предназначены для использования людьми, возникающие при их создании трудности были столь значительны, что язык описания проблемы и инструкций для их решения на компьютере разрабатывался применительно к инженерным решениям, заложенным в конструкцию компьютера. Почти все современные компьютеры основаны на ранних, разработанных в 40-х годах идеях фон Неймана и его коллег. Машина фон Неймана содержит большую однородную, состоящую из ячеек память и процессор, снабженный локальной памятью, ячейки которой называются регистрами. Процессор может загружать данные из памяти в регистры, выполнять арифметические или логические операции над содержимым регистров и отсылать значения из регистров в память. Прдграмма машины фон Неймана представляет собой последовательность команд выполнения перечисленных операций вместе с дополнительным множеством команд управления, влияющих на выбор очередной команды, возможно, в зависимости от содержимого некоторого регистра. По мере преодоления технических проблем построения компьютеров накапливались проблемы, связанные с их использованием. Трудности сместились из области выполнения программ компьютером в область создания программ для компьютера. Начались поиски языков программирования, пригодных для человека. Начиная с языка, воспринимаемого компьютером (машинного языка), стали появляться более удобные формализмы и системы обозначений. Главным результатом этих усилий явилось создание языков, упрощающих работу человека, но продолжающих оставаться близкими к исходному машинному языку. И хотя степень абстракции языков возрастала, начиная с языка ассемблера и далее к Фортрану, Алголу, Паскалю и Аде, все они несут на себе печать машины с архитектурой фон Неймана. Образованному непредубежденному человеку, не знакомому с кухней проектирования компьютеров, машина фон Неймана кажется надуманным и причудливым устройством. Рассуждать в терминах ограниченного множества операций такой машины далеко не просто, и иногда такие рассуждения требуют предельных мыслительных усилий. Эти характерные особенности программирования на компьютерах фон Неймана приводят к разделению труда: есть люди, которые думают, как решить задачу, и разрабатывают соответствующие методы, а есть люди - кодировщики, которые пишут тексты программ, т. е. выполняют прозаическую и утомительную работу по переводу инструкций разработчиков в команды, воспринимаемые компьютером. И в логике, и в программировании требуется явное выражение знаний и методов в некотором подходящем формализме. Явная формулировка каких-либо сведений является утомительной работой. Однако формализация в логике часто является интеллектуально благодарной работой, поскольку при этом приходит большее понимание задачи. В отличие от этого формализация задачи и метода решения в виде набора инструкций машины фон Неймана редко приводит к подобному полезному эффекту. Мы полагаем, что программирование может и должно быть интеллектуально благодарной деятельностью; что хороший язык программирования является мощным инструмен-
10 Введение том абстракций-инструментом, пригодным для организации, выражения, экспериментирования и даже общения в процессе интеллектуальной деятельности; что взгляд на программирование как на «кодирование»-рутинный, интеллектуально тривиальный, но очень трудоемкий и утомительный заключительный этап решения задачи при использовании компьютеров,-возможно, и является глубинной причиной явления, называемого «кризисом программирования». Мы склонны думать, что программирование может и должно быть частью процесса собственно решения задачи; что рассуждения можно организовать в виде программ, так что следствия сложной системы предположений можно исследовать, «запустив» предположения; что умозрительное решение задачи должно непосредственно сопровождаться работающей программой, демонстрирующей правильность решения и выявляющей различные аспекты проблемы. Предложения в этом направлении делались под лозунгом «быстрый прототип». Компьютерам все еще далеко до полного достижения цели-стать равным партнером человека в его интеллектуальной деятельности. Однако мы считаем, что с исторической точки зрения использование логики в качестве подходящей ступени на этом длинном пути является естественным и плодотворным, поскольку именно логика сопровождает процесс мышления человека с момента зарождения интеллекта. Конечно, логика давно используется и при проектировании компьютеров, и при анализе компьютерных программ. Однако непосредственное использование логики в качестве языка программирования, называемого логическим программированием t возникло сравнительно недавно. Логическое программирование, так же как и родственное ему направление-функциональное программирование, радикально отклоняется от основного пути развития языков программирования. Логическое программирование строится не с помощью некоторой последовательности абстракций и преобразований, отталкивающейся от машинной архитектуры фон Неймана и присущего ей набора операций, а на основе абстрактной модели, которая никак не связана с каким-либо типом машинной модели. Логическое программирование базируется на убеждении, что не человека следует обучать мышлению в терминах операций компьютера (на некотором историческом этапе определенные ученые и инженеры считали подобный путь простым и эффективным), а компьютер должен выполнять инструкции, свойственные человеку. В своем предельном и чистом виде логическое программирование предполагает, что сами инструкции даже не задаются, а вместо этого явно, в виде логических аксиом, формулируются сведения о задаче и предположения, достаточные для ее решения. Такое множество аксиом является альтернативой обычной программе. Подобная программа может выполняться при постановке задачи, формализованной в виде логического утверждения, подлежащего доказательству,-такое утверждение называется целевым утверждением # цы_ полнение программы состоит в попытке решить задачу, т. е. доказать целевое утверждение, используя предположения, заданные в логической программе. Отличительной чертой фрагмента логики, используемого в логическом программировании, является то, что обычно целевые утверждения содержат кванторы существования- утверждается, что существуют некоторые объекты с заданными свойствами. Пример целевого утверждения —«существует такой список X, что упорядочение списка [3, 2, 1~\ приводит к списку X». В вычислениях используется конструктивный метод доказательства целевых утверждений: если доказательство прошло успешно, то в процессе доказательства находятся элементы, соответствующие неопределенным объектам, входящим в целевое утверждение. Такое соответствие и образует результат вычисления. В приведенном выше примере в предположении, что логическая программа содержит подходящую аксиоматику отношения sort, результатом вычислений должен быть ответ X = [/, 2, 3]. Описанные идеи можно сформулировать в виде двух метафорических равенств: программа = множество аксиом вычисление = конструктивный вывод целевого утверждения из программы Можно проследить происхождение идей, лежащих в основе этих равенств, вплоть до интуиционистской математики и теории доказательств, зародившихся в начале столетия. Они связаны с программой Гильберта, выдвинутой для обоснования всей совокупности математических знаний на логической основе и обеспечивающей математическое доказательство теории лишь на основе аксиом математической логики и теории множеств. Интересно отметить, что неудача этой программы, связанная с результатами Геделя и Тьюринга о
Введение 11 неполноте и неразрешимости, положила начало современной эре компьютеризации. В программировании первое практическое использование данного подхода основано на алгоритме унификации и принципе резолюции, опубликованных Робинсоном в 1965 г. Было предпринято несколько робких попыток использовать данный принцип в качестве основы вычислительной модели, но они не увенчались успехом. Создание логического программирования можно приписать Ковальскому и Колмероэ. Ковальский разработал процедурную интерпретацию хорновых дизъюнктов. Он показал, что аксиома А, если Bj и В2и ... и Вп, может расматриваться и выполняться в качестве процедуры рекурсивного языка программирования. В этом случае А является заголовком процедуры, а набор Я,-телом процедуры. В дополнение к декларативному пониманию: «А истинно, если истинны /?,», утверждение допускает и следующее понимание: «для решения (выполнения) А следует решить (выполнить) Вj и #2 и • • • и Вп}>- При таком понимании процедура доказательства хорнова дизъюнкта сводится к интерпретации языка и к алгоритму унификации. Этот алгоритм является сердцевиной процедуры доказательства методом резолюций, он обеспечивает основные операции с данными при изменении значений переменных, передачу параметров, выбор и создание данных. В то же самое время в начале 70-х гг. Колмероэ и его группа создали в университете Марсель-Экс специальную, написанную на Фортране программу, предназначенную для доказательства теорем. Эта программа использовалась при построении систем обработки текстов на естественном языке. Программа доказательства теорем, названная Прологом (от Programmation en Logique), включала в себя интерпретатор Ковальского. Позднее ван Эмден и Ковальский исследовали формальную семантику языка логических программ, показав, что его операционная семантика, теоретико-модельная семантика и семантика, основанная на понятии неподвижной точки, совпадают. Несмотря на обилие теоретических работ и волнующих идей, концепция логического программирования казалась нереалистичной. В этот период в результате исследований, проводившихся в США, были обнаружены серьезные недостатки «языков искусственного интеллекта следующего поколения», таких, как Микро-Плэннер и Коннивер, призванных заменить Лисп. Основные претензии к этим языкам: они безнадежно неэффективны и очень трудны в реализации. Помня о горьком опыте с языками высокого уровня, основанными на логике, не следует особенно удивляться, что американские специалисты, услышав о Прологе, решили: «то, чем взволнованы европейцы, мы, американцы, уже исследовали, опробовали и отвергли». В такой атмосфере появление компилятора с Пролога-10 стало почти фантастическим явлением. Эта эффективная реализация Пролога, разработанная в конце 70-х гг. Дэвидом Уорреном и его коллегами, полностью развеяла мифы о непрактичности логического программирования. Их компилятор, остающийся и поныне одной из лучших реализаций Пролога, показал на программах чисто списковой обработки эффективность, сравнимую с эффективностью лучших имевшихся в то время реализаций Лиспа. Более того, сам компилятор был почти полностью написан на Прологе, и это наводило на мысль, что сила логического программирования может принести выигрыш и в классических программистских задачах, а не только в изощренных проблемах искусственного интеллекта. Влияние этой реализации переоценить невозможно. Без нее не существовал бы тот богатый опыт, который и привел к появлению данной книги. Несмотря на перспективные идеи и практически пригодные реализации, большинство западных организаций, занимавшихся программированием и исследованиями в области искусственного интеллекта, сохраняли невежество. Это невежество внешне проявлялось в виде враждебности или, в лучшем случае, безразличия по отношению к логическому программированию. Число исследователей, активно занимавшихся в 1980 г. логическим программированием, составляло всего несколько десятков в США и около сотни во всем мире. Может быть, чуть позже логическое программирование все равно покинуло бы задворки программистских исследований, если бы не японский проект пятого поколения, который был объявлен в октябре 1981 г. Хотя исследовательская программа была сформулирована японцами несколько расплывчато, в соответствии с их традицией достигать компромисса почти любой ценой, важная роль логического программирования в следующем поколении вычислительных систем была объявлена громко и отчетливо.
12 Введение С этого момента произошел быстрый переход Пролога от юности к зрелости. Существует большое число коммерчески доступных реализаций Пролога для почти всех распространенных компьютеров. Имеется большое число книг по программированию на Прологе, предназначенных для читателей разного уровня и освещающих различные аспекты языка. Да и сам язык более или менее стабилизировался, появился стандарт de facto - семейство Edinburgh-Пролога. Зрелость языка означает, что он больше не является доопределяемой и уточняемой научной концепцией, а становится реальным объектом со всеми присущими ему пороками и добродетелями. Настало время признать, что хотя Пролог и не достиг высоких целей логического программирования, но тем не менее является мощным, продуктивным и практически пригодным формализмом программирования. Учитывая обычный жизненный цикл языков программирования, можно ожидать, что следующие несколько лет покажут, имеют ли указанные качества ценность лишь в учебных аудиториях или они окажутся полезными и в тех областях, где за решение задач платят деньги. Что же в настоящее время является объектом активных исследований в логическом программировании и в Прологе? Ответ на этот вопрос можно найти в научных журналах и трудах конференций: Logic Programming Journal, Journal of New Generation Computing, International Conference of Logic Programming, IEEE Symposium on Logic Programming и др. Ясно, что одной из наиболее интересных тем исследования является связь логического программирования и Пролога с параллелизмом. Грядущие параллельные компьютеры в сочетании с параллелизмом, который, как кажется, достижим в логическом программировании, породили множество попыток параллельной реализации Пролога и разработки новых параллельных языков программирования, основанных на вычислительной модели логического программирования. Однако это-тема другой книги.
Часть I Логические программы Логическая программа-это множество аксиом и правил, задающих отношения между объектами. Вычислением логической программы является вывод следствий из программы. Программа задает множество следствий, которое и представляет собой значение программы. Искусство логического программирования состоит в построении ясных и изящных программ с требуемым значением. Глава 1 Основные конструкции Основные конструкции логического программирования - термы и утверждения- заимствованы из логики. Имеются три основных вида утверждений: факты, правила и вопросы. Имеется единственная структура данных: логический терм. 1.1. Факты Простейшим видом утверждения является факт. Факты используются для констатации того, что выполнено некоторое отношение между объектами. Например: отец (авраам, исаак). Этот факт утверждает, что Авраам является отцом Исаака или что выполнено отношение отец между индивидами, названными авраам и исаак. Другое название для отношения -предикат . Имена индивидов называются атомами . Аналогично факт плюс (2,3,5) означает, что имеет место отношение «2 плюс 3 равно 5». Известно отношение плюс может быть задано с помощью множества фактов, определяющих таблицу сложения. Вот начальный фрагмент таблицы: плюс(0,0,0). плюс(0,1,1). плюс(0,2,2). плюс(0,3,3). плюс(1,0,1). плюс(1,1,2). плюс(1,2,3). плюс(1,3,4). Достаточно большой фрагмент этой таблицы, являющийся, кстати, допустимой логической программой, будет использоваться на протяжении этой главы в качестве определения отношения плюс. Синтаксические соглашения, используемые в данной книге, будут вводиться по мере надобности. Первым является соглашение об использовании строчных и
14 Часть I. Глава 1 прописных букв. То, что в фактах имена и предикатов, и атомов начинаются со строчных, а не с прописных букв, весьма существенно. Такие имена выделяются в основном тексте курсивом. отец(фарра, мужчина(фарра). авраам). отец(фарра,нахор). мужчина(авраам). отец(фарра,аран). мужчина(нахор). отец(авраам, мужчина(аран). исаак). отец(аран,лот) мужчина(исаак). отец(аран,милка). мужчина(лот). отец(аран,иска). женщина(сара). мать(сара,исаак). женщина(мил ка). женщина(иска). Программа 1.1. База данных библейской семьи. Конечное множество фактов образует программу. Это простейший вид логической программы. Множество фактов, с другой стороны, составляет описание ситуации. Такой подход лежит в основе программирования баз данных, рассматриваемого в следующей главе. Пример базы данных семейных отношений в Библии приведен в программе 1.1. Предикаты отец, мать, мужчина и женщина выражают очевидные отношения. 1.2. Вопросы Второй формой утверждения в логической программе является вопрос. Вопрос- это средство извлечения информации из логической программы. С помощью вопроса выясняется, выполнено ли некоторое отношение между объектами. Например, с помощью вопроса отец (авраам,исаак)? выясняется, верно ли, что выполнено отношение отец между объектами авраам и исаак? Исходя из фактов программы 1.1, ответ на этот вопрос -да. Синтаксически вопросы и факты выглядят одинаково, однако их можно различить по контексту. В тех случаях, когда возможна неоднозначность, конец факта будет обозначаться точкой, а конец вопроса - вопросительным знаком. Объект без точки и вопросительного знака мы называем целью, факт Р. утверждает, что цель Р является истинной. В вопросе Р? спрашивается, является ли истинной цель Р. Простой вопрос состоит из одной цели. Что касается программы, то поиск ответа на вопрос состоит в том, чтобы определить, является ли вопрос логическим следствием программы. В данном разделе мы постепенно определим понятие логического следствия. Логические следствия выводятся путем применения правил. Простейшее правило вывода -совпадение; из Р выводимо Р. Вопрос является логическим следствием тождественного с ним факта. Если используется программа, содержащая факты, подобно программе 1.1, то процедура поиска ответа на простой вопрос выполняется непосредственно. В программе ищется факт, предполагаемый в вопросе. Если факт, тождественный вопросу, найден, то ответ -да. Ответ нет дается в том случае, когда факт, тождественный вопросу, не найден, поскольку данный факт не является логическим следствием программы. Такой ответ
Основные конструкции 15 не подвергает сомнению истинность вопроса, он просто показывает, что нам не удалось доказать истинность вопроса с помощью программы. На оба вопроса- женщина(авраам)? и плюс(1,0,2)? программа 1.1 ответит нет. 1.3. Логические переменные, подстановки и примеры Логические переменные служат для обозначения неопределенных объектов и соответственно этому используются. Рассмотрим их применение в вопросах. Предположим, мы хотим выяснить, чьим отцом был авраам. Для этого можно задавать вопросы: отец (авраам,лот)?, отец (авраам,милка)?..., отец (авраам,исаак)?,... до тех пор, пока не будет получен ответ да. С помощью переменной реализуется более удобный способ поиска ответа. Вопрос формулируется в виде отец (авраам, X)?. Ответом на вопрос является X = исаак. Переменные в данном случае служат Оля представления совокупности вопросов. Как будет объяснено в дальнейшем, с помощью вопроса, содержащего переменную, выясняется, имеется ли такое значение переменной, при котором вопрос будет логическим следствием программы. Использование переменных в логических программах отличается от использования переменных в традиционных языках программирования. В логических программах переменная обозначает неопределенный, но единственный объект, а не некоторую область памяти. Введя переменные, мы можем определить единственную структуру данных в логических программах -термы. Определение термов индуктивно. Константы и переменные являются термами. Кроме того, термами являются составные термы, или структуры. Составной терм содержит функтор (называемый главным функтором терма) и последовательность из одного или более аргументов, являющихся термами. Функтор задается своим именем, которое суть атом, и своей арностью, или числом аргументов. Синтаксически составные термы имеют вид f(tPt2,...,t„), где/-имя «-арного функтора, а ^-аргументы. Примеры составных термов: s(0), горячий (молоко), имя (джон, доу), list (a, list (Ъ,nil) ), foo (X) и tree (tree (nil,3, nil), 5,R). Вопросы, цели и вообще термы, в которые не входят переменные, называются основными. Термы, содержащие переменные, называются неосновными. Так, foo (a,b)-основной терм, a bar(X) -нет. • Определение: Подстановкой называется конечное (возможно, пустое) множество пар вида Xt = tt, где ^-переменная и Г,-терм; Xt ф Х} при i Ф jn Xt не входит в tj при любых i и j. • Примером подстановки, состоящей из одной пары, может служить {X = исаак}. Подстановки могут применяться к термам. Результат применения подстановки 0 к терму А, обозначается AQ, есть терм, полученный заменой каждого вхождения переменной X в А на t для каждой пары вида X = t из 0. Результат применения {X = исаак} к терму отец (авраам,X) есть терм отец (авраам,исаак). • Определение: А называется примером В, если найдется такая подстановка 0, что А = BQ. • По определению цель отец (авраам,исаак) является примером терма отец (авраам,Х). Аналогично мать(сара,исаак) -пример терма мать (X,Y) при подстановке {X = сара, Y = исаак}.
16 Часть I. Глава 1 1.4. Экзистенциальные вопросы В логических терминах переменные в вопросах связаны квантором существования; это означает на интуитивном уровне, что вопрос отец(авраам,Х)? следует читать: «Существует ли такое X, что авраам является отцом ХЪ>. В общем случае вопрос Р(Т1Т2,...,Тп)?, содержащий переменные Xv X2,..., Хк, означает следующее: «Существуют ли такие Xv X2,..., Хк, что p(TvT2,...,Tn)b>. Для удобства квантор существования обычно не пишется. Введем еще одно правило вывода -Рооощение . ПрИ лю5ой подстановке 0 экзистенциальный вопрос Р логически следует из примера F0. Из факта отец (авраам, исаак) следует существование такого X, что истинно отец(авраам,Х), а именно X = исаак. Процедура поиска ответа на экзистенциальный неосновной вопрос при использовании программы, состоящей из фактов, сводится к поиску факта, являющегося примером вопроса. Ответом, цлиРешением , будет такой пример. Ответ нет дается, если подходящего факта нет в программе. Поиск ответа на неосновной вопрос представляет собой вычисление, выходом которого является пример вопроса. Иногда мы изображаем этот пример с помощью подстановки, применение которой к вопросу и дает решение. Экзистенциальный вопрос в общем случае может иметь несколько решений. Из программы 1.1 ясно, что Аран-отец троих детей. Следовательно, вопрос отец (аран,Х)? имеет решения {X = лот},{X = милка},{X = иска}. Другим вопросом, обладающим множеством решений, является плюс(Х,У,4)?, в котором ищутся числа, дающие в сумме 4. Решениями, например, будут {X = О, Y = 4} и {X = 1, У= 3}. Обратите внимание, что различным переменным X и У могут соответствовать различные объекты. Интересным вариантом последнего вопроса является (плюсХ,Х,4)?, в котором требуется, чтобы два числа, дающие в сумме 4, совпадали. Имеется единственный ответ- {X = 2}. 1.5. Универсальные факты Переменные также полезны и в фактах. Предположим, что все библейские персонажи любят гранаты. Вместо того чтобы включать соответствующий факт для каждого индивида: любит (авраам, гранаты). любит (сара, гранаты). все это можно выразить универсальным фактом любит(X,гранаты). В данном случае переменные позволяют выразить совокупность многих фактов. Факт умно- жить (0,Х,0) объединяет все факты, утверждающие, что 0, умноженный на любое число, дает 0. Переменные в фактах неявно связаны квантором общности; это на интуитивном уровне означает, что факт любит (X, гранаты) утверждает, что для всех X справедливо: X любит гранаты. В общем случае факт р(ТрТ2,...,ТП) следует понимать так, что при любых значениях переменных X р..., Хк, где Xt - переменные, входящие в факт, p(TpT2,...,Tn) выполнено. Естественно, из факта с квантором общности можно вывести любой пример факта. Например, из любых (X,гранаты) следует любит (авраам, гранаты).
Основные конструкции 17 Это-третье правило вывода, называемое конкретизацией; из утверждения Р с квантором общности выводится пример Р0 при любой подстановке 0. Как и в случае вопроса, можно добиться, чтобы два неопределенных объекта, обозначенных переменными, совпадали-для этого нужно использовать имя одной и той же переменной. Факт плюс(0,Х,Х) означает, что О является левой единицей по сложению. Это следует понимать так, что при всех значениях X, 0 плюс X равно X. Аналогичное использование переменных возникает при переводе фразы «каждый любит себя» в факт любит (Х,Х). Поиск ответа на основной вопрос при использовании факта с квантором общности происходит непосредственно. Ищется факт, для которого вопрос является примером. Например, ответом на вопрос плюс (0,2,2) на основе факта плюс(0,Х,Х) будет да. Поиск ответа на неосновной вопрос при использовании неосновных фактов требует нового понятия: общий пример двух термов. • Определение: С называется общим примером термов А и В, если С есть пример А и С есть пример В. Иными словами, если найдутся такие подстановки 0Х и 02, что С = AQ i синтаксически совпадает с BQ 2. • Например, цели плюс(0,3,У) и плюс(0,Х,Х) имеют общий пример плюс (0,3,3). Применения подстановки {У = 3} к плюс(0,3,У) и подстановки {X = 3} к плюс (О,X, X) приводят к плюс (0,3,3). В общем случае при поиске ответа на вопрос с использованием факта ищется общий пример вопроса и факта. Если общий пример существует, то он и будет ответом. В противном случае ответ-нет. Ответ на экзистенциальный вопрос на основе универсального факта с использованием общего примера требует двух логических выводов. Факт выводится из примера с помощью правила конкретизации, а пример выводится из вопроса с помощью правила обобщения. 1.6. Конъюнктивные вопросы и общие переменные Важным обобщением обсуждавшихся до сих пор вопросов являются конъюнктивные вопросы . Конъюнктивные вопросы-это конъюнкция целей, поставленная в виде вопроса, например: отец (фара,X),отец (X,Y)? или в общем виде: Qp...,Qn?. Простые вопросы-это частный случай конъюнктивных вопросов с единственной целью. Логически вопрос состоит в выводимости конъюнкции из программы. Мы всюду используем «,», для обозначения логического «и». Не следует путать запятую, отделяющую аргументы цели, с запятой, отделяющей цели и обозначающей конъюнкцию. В простейшем конъюнктивном вопросе все цели простые, например, отец (авраам^исаак),мужчина (лот)? Ясно, что ответ на этот вопрос, исходя из программы \А,-да, так как обе цели являются фактами программы. Вообще на вопрос Qi>--->Qn?> в котором каждое Qt-основная цель, ответом, исходя из программы Р, будет да, если каждое Q{ выводится из Р. Таким образом, конъюнкция основных вопросов не очень интересна. Конъюнктивные вопросы представляют интерес в тех случаях, когда имеются одна или болееобщие переменные , т. е. переменные, входящие в две различные цели вопроса. Примером служит вопрос отец (аран,Х),мужчина (X)? Областью переменной в конъюнктивном вопросе является вся конъюнкция. Таким образом, вопрос р (X),q(X)? означает: «Существует ли такое X, что р(Х) и q(X) одновременно?» Как и в простых вопросах, переменные в конъюнктивных вопросах неявно связаны квантором существования. Общие переменные используются для ограничения простых вопросов путем
18 Часть I. Глава 1 сужения области значения переменных. Мы уже рассматривали пример с вопросом плюс (X, X, 4) ?, в котором поиск чисел, дающих в сумме 4, был ограничен тем, что числа должны быть равными. Рассмотрим вопрос отец (аран, X), мужчина (X)? В этом случае решения вопроса отец(аран, X)? ограничены детьми мужского пола. Программа 1.1 показывает, что существует единственное решение {X = лот}. С другой стороны, данный вопрос можно рассматривать как ограничение решений вопроса мужчина(X)? множеством индивидов, отцом которых является Аран. Несколько иное использование общих переменных демонстрируется в вопросе отец (фарра,Х),отец (X, Y)?. С одной стороны, вопрос ограничивает сыновей Фарры теми, кто сам является отцом. С другой стороны, ищутся индивиды, чьи отцы были сыновьями Фарры. Имеется несколько решений, например: {X = авраам,У = исаак) и {X = аран, Y = лот). Конъюнктивный вопрос логически следует из программы Р, если все цели - следствия Р, причем общим переменным в разных целях сопоставлено одно и то же значение. Достаточное условие выводимости состоит в существовании основного примера вопроса, являющегося следствием Р. Из такого примера вопрос может быть получен с помощью обобщения. То, что мы ограничились основными примерами, несущественно, это ограничение будет снято в гл. 4 при обсуждении модели вычисления логических программ. Однако мы используем это ограничение в ближайших разделах для упрощения рассуждений. Процедура поиска решения конъюнктивного вопроса АрА2,...,Ап с помощью программы Р состоит в нахождении такой подстановки 0, что A fi и ... AnQ будут основными примерами фактов из Р. То, что одна и та же подстановка применяется ко всем целям, обеспечивает единое соответствие переменным в вопросе. Рассмотрим, например, вопрос отец (аран,Х)? мужчина (X)? при использовании программы 1.1. Применение подстановки {X = лот} к вопросу дает основной пример отец (аран,лот),мужчина (лот), который является следствием программы. 1.7. Правила Интересные конъюнктивные вопросы сами по себе определяют отношения. Вопрос отец(аран,Х),мужчина(Х)? выражает свойство быть сыном Арана. Вопрос отец(фарра,Х),отец(X,Y)? относится к внукам Фарры. Так мы приходим к третьему и самому важному виду утверждения в логическом программировании-к правилу, позволяющему определять новые отношения в терминах существующих отношений. Правила -это утверждения вида A^BVB2 Вп. где п ^ 0. А называется заголовком правила, а последовательность В(-П"'-10Л! правила. Как А, так и В( должны быть целями. Правила, факты и вопросы называются также хориовыми предложившими или просто предложгчич -.т% Заметим, что факт-это частный случай правила при п = 0. Факты называются также единичными предложениями.. Кроме того, имеется специальное название для правил, тело которых состоит из одной цели, т.е. для случая п = 7. Такие предложения называются итерационными нредложеничми. В правилах, как и в фактах, переменные связаны кванторами общности, область действия кванторов - все правило. Правило, выражающее свойство быть сыном: сын(Х,У) <- отец(У,Х), мужчина (X).
Основные конструкции 19 Аналогично можно определить свойство быть дочерью: дочь(Х,У) <- отец (Y,X), женщина (X). Правило для отношения быть дедушкой: дедушка (X,Z) <- отец(Х,У), OTeu(Y,Z). Возможны два способа интерпретации правил. При первом правила используются для построения новых и сложных вопросов в терминах простых вопросов. Вопрос сын(Х,аран)? в программе, содержащей приведенное выше правило для отношения сын, переводится в соответствии с этим правилом в вопрос отец (аран,Х),мужчина(Х)? и решается как и раньше. Новый вопрос, включающий отношение сын, строится из простых вопросов, содержащих отношения отец и мужчина. Такая интерпретация правил сводится к процедурному истолкованию правил. Процедурное истолкование правила для отношения дедушка состоит в следующем: «Для ответа на вопрос, является ли X дедушкой Y, ответьте на конъюнктивный вопрос, является ли X отцом Z и Z отцом У». Второй способ основан на интерпретации правил как логических аксиом. Обратная стрелка <- используется для обозначения логического следования. Правило для отношения сын понимается так: «Х-сын Y, если У-отец X и Х-мужчина». Здесь правила используются для построения новых или сложных отношений на основе других, более простых отношений. Предикат сын определяются через предикаты отец и мужчина. Соответствующее понимание правила известно как декларативное понимание. Декларативное понимание правила для отношения дедушка состоит в следующем: «Для всех X, У и Z, если Х-отец Z и Z отец Y, то Х-дедушка У». Хотя формально все переменные в предложении связаны квантором общности, мы будем иногда говорить о переменных, входящих лишь в тело, а не в заголовок предложения, как если бы они были связаны в теле квантором существования. Например, правило для отношения дедушка можно понимать так: «Для всех X и У;Х-дедушка Y, если найдется такое Z, что Х-отец Z и Z-отец У». Мы не приводим обоснования подобной словесной трансформации и рассматриваем ее как некоторое удобство чтения. В случаях когда это станет источником недоразумений, читатель может вернуться к формальному пониманию предложения с внешними кванторами общности по всем переменным. Для введения правил в наше рассмотрение логического вывода потребуется закон modus ponens. Modus ponens утверждает, что из В и А <- В следует А. • Определение: Обобщенный закон modus ponens утверждает, что из правила R= (A^BvB2,..,,Bn) и фактов «2 в. выводится А при условии, что А' <- ВГВ2 Вп является примером R. • Правила совпадения и конкретизации являются частными случаями обобщенного закона modus ponens.
20 Часть I. Глава 1 Теперь все готово для полного определения понятия логической программы и соответствующего понятия логического следствия. • Определение: Логическая программа - это конечное множество правил. • • Определение: Цель G с кванторами существования является логическим следствием программы Р, если в Р найдется такое предложение с основным примером А <- В1 Вп,п^0, что В Р..., Вп - логические следствия Р и Л-пример G. • Заметим, что цель G логически следует из программы Р тогда и только тогда, когда G может быть выведена из Р с помощью конечного числа применений обобщенного правила modus ponens. Рассмотрим вопрос сын(S,аран)? относительно программы 1.1, расширенной правилом для отношения сын. Применение подстановки {X = лот, Y = аран} к данному правилу приводит к примеру сын (лот,аран) <- отец (аран,лот), мужчина (лот). Обе цели в теле правила являются фактами программы 1.1. Таким образом, из обобщенного modus ponens следует ответ S = лот. Процедура поиска ответа на вопрос отражает определение логического следования. Угадываются основной пример цели и основной пример правила, далее рекурсивно ищется ответ на конъюнктивный вопрос, соответствующий телу этого правила. Доказательство цели А в программе Р сводится к выбору правила A j <- ВРВ2 Вп в Р и указанию такой подстановки 0, что А = А^и Bfi-основные цели при 1 ^ / ^ п. Далее рекурсивно доказывается каждая цель Bfi. В такой процедуре могут возникать сколь угодно длинные цепочки доказательств. В общем случае угадать нужный основной, пример и выбрать должное правило сложно. В гл. 4 мы объясним, как можно избавиться от угадывания примера. Правило, сформулированное для описания отношения сын, корректно, но не является полным описанием соответствующего понятия. Мы не сможем, например, заключить, что Исаак был сыном Сары. Не было указано, что ребенок является сыном как отца, так и матери. Можно добавить новое правило, описывающее данное понятие, а именно: сын(Х,У) <- мать (Y,X), мужчина (X). Аналогично определение отношения внук требует четырех правил, включающих оба случая-отец и мать: внук(Х,У) <r- oTeu(Y,Z), oTeu(Z,X). внук(ХД) <- отец (Y,Z), мать (Z,X). внук(Х,У) <- MaTb(Y,Z),OTeu(Z,X). внук(ХД) <- MaTb(Y,Z), MaTb(Z,X). Имеется лучший, более компактный способ задания этих правил. Нам нужно задать отношение родитель, подходящее для отца и для матери. Частично искусство логического программирования и состоит в определении промежуточных предикатов, дающих полную и изящную аксиоматизацию некоторого отношения. Правило, описывающее отношение родитель, строится непосредственно, исходя из того, что родитель является или отцом, или матерью. Логические программы могут включать альтернативные определения или, более формально,-дизъюнкцию, применяя альтернативные правила, как в случае отношения родитель: родитель (X,Y) <- отец(Х,У). родитель (X,Y) <- мать(Х,У). Правила для отношений сын и внук теперь запишутся так: сын(Х,У) <- родитель (Y,X), мужчина (X). внук(Х,У) <- родитель (Y,Z), родитель (Z, X).
Основные конструкции 21 Совокупность правил с одним и тем же предикатом в заголовке, такая, как пара правил для родителей, называется процедурой. Позже мы увидим, что при операционной интерпретации этих правил в Прологе подобные наборы правил действительно аналогичны процедурам или подпрограммам в традиционном программировании. 1.8. Простой абстрактный интерпретатор Операционная процедура поиска ответа на вопрос была неформально описана в предыдущих разделах. Углубляясь в детали, рассмотрим абстрактный интерпретатор логических программ. Поскольку обобщенное правило modus ponens ограничено применением к основным целям, интерпретатор отвечает лишь на основные вопросы. Абстрактный интерпретатор выполняет вычисления с ответами да/нет. Он получает программу Р и основной вопрос Q и дает ответ да, если Q выводимо из Р, и ответ нет в противном случае. Кроме того, если цель невыводима, интерпретатор может вообще не завершить работу. В этом случае не выдается никакого ответа. Рис. 1.1 демонстрирует работу интерпретатора. Вход: Основной вопрос Q и программа Р Результат: да, если найден вывод Q из программы Р, Алгоритм: нет в противном случае. Положить резольвенту равной Q. пока резольвента А 7 Ап не пуста начало выбрать цель Av 1 ^ / ^ п, и такой основной пример предложения А <- Blf В2 Вк, к ^ 0 в Р, что А = А{ (если такого предложения нет, то выйти из цикла); положить резольвенту равной Л At4, Bt Вк, Лш Ап конец если резольвента пуста, то результат - да, иначе результат - нет. Рис. 1.1. Абстрактный интерпретатор логических программ. На любой стадии вычислений существует текущая цель, называемая резольвентой. Протоколом интерпретатора называется последовательность резольвент, которые строятся в процессе вычисления, вместе с указанием сделанного выбора. Рассмотрим вопрос сын(лот,аран)? при использовании программы 1.2, являющейся подмножеством фактов программы 1.1, которое дополнено правилами, определяющими отношения сын и дочь. На рис. 1.2 приводится протокол поиска ответа. Протокол в неявном виде содержит вывод основного вопроса в программе. Удобнее изображать вывод в виде дерева вывода. Введем необходимые понятия. Вход: сын (лот, аран)? и программа 1.2 Резольвента не пуста Выбор: сын (лот, аран) (единственный выбор) Выбор: сын (лот, аран) <- отец (аран, лот), мужчина (лот). Новая резольвента: отец (аран, лот), мужчина (лот)? Резольвента не пуста Выбор: отец (аран, лот) Выбор: отец (аран, лот). Резольвента не пуста Выбор: мужчина (лот) Выбор: мужчина (лот). Новая резольвента пуста Результат: да Рис. 1.2. Протокол интерпретатора.
22 Часть I. Глава 1 отец(аврам, мужчина(исаак). исаак). отец(аран, мужчина(лот) лот) отец(аран, женщина(милка). милка). отец(аран, женщина(иска). иска). сын(Х,У) <- отец(У,Х),мужчина(Х). дочь(Х,У) <- отец(У,Х),женщина(Х). Программа 1.2. Отношения в библейской семье. • Определение: Основная редукция цели G с помощью программы Р состоит в замене цели G телом того примера правила из Р, чей заголовок совпадает с данной целью. • Позже мы распространим определение на общие (неосновные) редукции. Редукция является главным вычислительным шагом в логическом программировании. Она соответствует применению обобщенного правила modus ponens, а также одной итерации цикла «пока» в абстрактном интерпретаторе. Замененная при редукции цель называется снятой, появившиеся цели называются прои модными. Применим данные понятия к анализу протокола на рис. 1.2. В протоколе имеются три редукции. Первая редукция снимает цель сын (лот,аран) и строит две производные цели -отец (аран,лот) и мужчина (лот). Следующая редукция применяется к цели отец (аран,лот) и не дает производных целей. Третья редукция также не дает производных целей при снятии цели мужчина (лот). Дерево вывода состоит из вершин и ребер и изображает цели, снимаемые в процессе вычисления. Корень дерева вывода в случае простого вопроса совпадает с этим вопросом. Вершины дерева соответствуют целям, снимаемым в процессе вычисления. Направленное ребро, ведущее из одной вершины в другую, соответствует переходу от снимаемой цели к производной. Деревом вывода для конъюнктивного вопроса является просто совокупность деревьев вывода для отдельных целей в конъюнкции. Рис. 1.3 изображает дерево вывода для протокола, приведенного на рис. 1.2. сын (лот, аран) I \ отец (аран, лот) мужчина (лот) Рис. 1.3. Простое дерево вывода. В интерпретаторе имеются два неопределенных выбора: выбор цели из резольвенты для снятия и выбор предложения (и подходящего основного примера) для редукции. Эти два выбора имеют различную природу. Выбор цели для снятия произволен. В любой имеющейся резольвенте все цели должны быть сняты. Можно показать, что порядок снятия при построении доказательства не имеет значения. Иными словами, если существует доказательство цели, то оно является доказательством независимо от того, в каком порядке применяются редукции. В терминах дерева вывода это означает, что порядок ветвей несуществен.
Основные конструкции 23 Наоборот, выбор предложения и подходящего основного примера является решающим. В общем случае существует несколько вариантов предложений и бесконечно много основных примеров. Выбор производится недетерминированно. Понятие недетерминированного выбора применяется весьма плодотворно в определении многих моделей вычислений, например в конечных автоматах и машинах Тьюринга. Недетерминированный выбор состоит в неопределенном выборе из некоторого числа альтернатив, который предположительно производится методом «предвидения»; если только некоторые из альтернатив приводят к успешному вычислению (в нашем случае-к нахождению доказательства), то одна из них и будет выбрана. Формально понятие определяется следующим образом: вычисление, содержащее недетерминированный выбор по определению заканчивается успешно, если существует последовательность недетерминированных выборов, приводящая к успеху. Конечно, ни одна из реальных машин не может непосредственно реализовать это определение. Можно, однако, использовать полезные приближения к данному понятию, как это и сделано в Прологе; соответствующие объяснения приведены в гл. 6. Интерпретатор, представленный на рис. 1.1, может быть расширен так, чтобы он давал ответы и на неосновные экзистенциальные вопросы. Для этого следует ввести дополнительный шаг: угадывание основного примера вопроса. Этот шаг подобен тому, на котором интерпретатор угадывает основные примеры правил. Угадать правильный основной пример в общем случае непросто, так как это означало бы знание результата вычисления до его выполнения. Чтобы избавиться от ограничений, налагаемых основными примерами, и бремени угадывания, потребуются новые понятия. В гл. 4 мы покажем, как можно устранить угадывание основных примеров, и опишем более полную вычислительную модель логического программирования. Деревья вывода позволяют использовать важную меру сложности - число вершин в дереве. Она показывает, сколько шагов редукции выполнялось при вычислении. Мы используем эту меру как основу для сравнения различных программ в гл. 3. 1.9. Значение логической программы Откуда известно, что логическая программа вычисляет то, что мы хотим? Корректна она или некорректна? Для ответа на подобные вопросы мы должны определить, что же является значением логической программы. Дав такое определение, можно будет проверять, совпадает ли значение программы с предположительным значением. • Определение: Значением логической программы Р, обозначаемым М(Р), называется множество выводимых из Р основных единичных целей. • Из этого определения следует, что значением логической программы, построенной просто из основных фактов, такой, как программа 1.1, является сама программа. Другими словами, для простых программ программа «значит ровно то, что написано». Рассмотрим программу 1.1, расширенную двумя правилами определения отношения родитель. Что будет ее значением? Оно содержит кроме фактов об отцах и матерях, явно указанных в программе, также все факты вида родитель (X, Y) для каждой пары X и Y, такой, что факт отец (X, Y) или мать (X, Y) присутствуют в программе. Этот пример показывает, что значение программы в явном виде содержит все то, что программа утверждает неявно. Если предположить, что подразумеваемое значение программы также задано в виде множества основных единичных целей, то можно спросить, как соотносятся
24 Часть I. Глава 1 действительное и подразумеваемое значения программы. Мы можем проверить, все ли утверждения программы корректны или вычисляет ли программа все, что нам требуется. Неформально мы называем программу корректной относительно некоторого заданного значения М, если значение М (Р) программы Р является подмножеством М. Иными словами, корректная программа не вычисляет того, что не требуется. Программа полна относительно М, если М есть подмножество М(Р), т.е. полная программа вычисляет все, что задано. Следовательно, программа Р корректна и полна относительно заданного значения М, если М = М(Р). В тех случаях, когда из имен предикатов или констант интуитивно ясно, что является их значением, будем считать в данной книге, что подразумеваемое значение определяется в программе смыслом имен. Например, если в программе для отношения сын содержится только первая аксиома, которая ссылается на отношение отец, то программа неполна относительно интуитивно понимаемого значения отношения сын, так как цель сын(иссак,сара) невыводима. Если к программе добавить правило сын(Х,У) <- мать(Х,У), мужчина (Y). то получим программу, некорректную относительно подразумеваемого значения ввиду выводимости утверждения сын (сара, исаак). Исследование понятий корректности и полноты логических программ будет продолжено в гл. 5. Хотя понятие истинности пока не было полностью определено, мы будем называть основную цель истинной относительно подразумеваемого значения, если цель входит в данное значение, иложной в противном случае. В тех случаях, когда подразумеваемое значение определяется именами предикатов и констант, содержащихся в программе, цели, входящие в такие значения, будем называть просто истинными. 1.10. Резюме Завершим данный раздел обзором введенных конструкций и понятий, сопровождая их необходимыми определениями. Основная структура в логических программах - это терм. Терм есть константа, переменная или составной терм. Константы обозначают конкретные объекты, такие, как целые числа и атомы, в то время как переменная обозначает единственный, но неопределенный объект. Символом атома может служить любая последовательность букв, которая берется в кавычки, если ее можно спутать с другими символами (такими, как переменные или целые числа). Символы переменных отличаются начальной прописной буквой. Составной терм строится из функтора (называемого главным функтором терма) и последовательности из одного или более термов, называемых аргументами. Функтор задается своим именем, т.е. некоторым атомом, парностью, или числом аргументов. Константы рассматриваются как 0-арные функторы. Синтаксически составной терм записывается как f(tpt2,...,tn), где /-имя л-арного функтора, а ^-аргументы. Для «-арного функтора / используется запись f/n. Функторы с одинаковыми именами, но различными арностями различны. Терм является основным , если в нем не содержится переменных; в противном случае терм неосновной. Цели -это атомы или составные, в общем случае неосновные термы. Подстановка -это конечное (возможно, пустое) множество пар вида X = t, где X - переменная, t-терм, причем переменные в левых частях пар различны. Для
Основные конструкции 25 любой подстановки 0 = {X 2 = t р X 2= t2,...,Xn = t„} и терма S терм S0 обозначает результат одновременной замены каждого вхождения в S переменной Xt на tit l<i<n. Терм S0 называется примером терма S. Логическая программа- конечное число предложений. Предложением или правилом является замкнутое квантором общности утверждение вида A^BVB2 Bk, k^O, где А и Я,-цели. Декларативное понимание такого утверждения - «А следует из конъюнкции целей В(», процедурная интерпретация - «для ответа на вопрос А ответь на конъюнктивный вопрос В рВ2,...,Вк». А называется заголовком предложения, последовательность В(- телом предложения. При /с = 0 предложение называется фактом или единичным предложением и имеет запись А., декларативное понимание-«Л истинно», процедурная интерпретация - «цель А выполнена». При к = 1 предложение называется итерационным. Вопросом называется конъюнкция вида At Ап?п > О, где Л,-цели. Считается, что переменные в вопросе связаны квантором существования. Вычисление логической программы Р строит пример заданного вопроса, логически выводимый из Р. Цель G выводима из Р, если существует такой пример А цели G, что А+-В 1,.,Вп, п^ 0,- основной пример предложения в Р и каждое Bt выводимо из Р. Выводимость цели из тождественного факта рассматривается как особый случай. С помощью логической выводимости индуктивно определяется значение программы Р. Множество основных примеров фактов из Р принадлежит значению Р. Основная цель G входит в значение, если существует такой основной пример G <- Bv...,Bn правила из Р, что ВР...,ВП входят в значение Р. Таким образом, значение состоит из тех основных примеров, которые выводятся из программы. Подразумеваемое значение М программы Р также задается в виде множества основных единичных целей. Программа Р корректна относительно подразумеваемого значения М, если М(Р) образует подмножество М. Программа Р полна относительно М, если М - подмножество М(Р). Ясно, что программа корректна и полна относительно ее подразумеваемого значения (наиболее желательный случай), когда М = М(Р). Основная цель называется истинной относительно подразумеваемого значения, если она принадлежит этому значению. В противном случае цель называется ложной В этой главе определения логической выводимости и, следовательно, значения логической программы даны в синтаксических терминах. В гл. 5 приводятся другие способы описания значения логической программы и обсуждается их эквивалентность данному определению.
26 Часть I. Глава 2 Глава 2 Программирование баз данных Имеются два основных применения логических программ: построение логической базы данных и работа со структурами данных. В этой главе обсуждается программирование баз данных. Логическая база данных строится из множества фактов и правил. Мы покажем, как множество фактов может определять отношения, так же как в реляционных базах данных. Мы покажем, как правила могут определять сложные реляционные вопросы, также как в реляционной алгебре. Таким образом, функциональные зависимости, связанные с реляционными базами данных, могут выражаться логическими программами, составленными из фактов и правил весьма ограниченного вида. 2.1. Простые базы данных Начнем с повторного рассмотрения программы 1.1, библейской базы данных и ее расширения правилами, задающими семейные отношения. В самой базе данных имеются четыре основных предиката: отец/2, мать/2, мужчина/1 и женщина/1. Мы примем соглашение из теории баз данных и снабдим каждое отношение реляционной схемой* которая указывает роль каждой позиции в отношении (каждого аргумента в цели). Реляционными схемами для этих четырех предикатов будут соответственно отец (Отец,Ребенок), мать (Мать,Ребенок), мужчина (Человек) и женщина (Человек). Принятые мнемонические имена говорят сами за себя. Будем придерживаться типографского соглашения о курсивном выделении реляционных схем. В правилах переменным даются содержательные имена, в то время как в вопросах - имена X или Y. Многословные имена используются по- разному для переменных и предикатов. В случае переменной каждое слово начинается с прописной буквы, например: ПлемянницаИлиПлемянник; в случае имен функций и предикатов слова соединяются нижней чертой, например: конфликт_рас- писания. Новые отношения строятся из этих основных отношений с помощью определения подходящих правил. Соответствующими реляционными схемами для отношений, введенных в предыдущей главе, являются сын (Сын, Родитель), дочь (Дочь,Родитель), родитель (Родитель, Ребенок) и внук (Внук, Дед И ли Бабушка). С точки зрения логики несущественно, какие отношения определяются с помощью фактов, а какие-с помощью правил. Например, если соответствующая база данных содержит факты родитель, мужчина и женщина, то правила, определяющие отношения сын и дочь, остаются в силе. Для отношений, более не определяемых фактами, например отец и мать, следует ввести новые правила. Подходящими правилами будут отец(Папа, Ребенок) <- родитель (Папа, Ребенок), мужчина(Папа). мать (Мама, Ребенок) <- родитель (Мама, Ребенок), женщина (Мама). Интересные правила могут появиться при превращении отношений, неявно присутствующих в базе данных, в явные. Например, поскольку нам известны отец и мать ребенка, то нам известно, у каких пар был потомок, или известны, в терминах Библии, прародители. Это отношение не задано явно в базе данных, однако простое
Программирование баз данных 27 правило может выявить эту информацию. Реляционная схема -прародители (Мужчина, Женщина). прародители (Мужчина, Женщина) «- отец(Мужчина, Ребенок), мать (Женщина, Ребенок). Правило означает: «Мужчина и Женщина являются прародителями, если есть Ребенок, отцом которого является Мужчина, а матерью -Женщина». Другим примером извлечения информации из более простых сведений является отношение «быть детьми одних родителей» - братья и сестры. Дадим правило для брат (Брат, Дети Одних Родителей). брат (Брат, Дети ОднихРодителей) «- родитель (Родитель, Брат), родитель (Родитель, Дети ОднихРодителей), мужчина (Брат). Однако такое правило означает: «Брат является братом Детей ОднихРодителей, если Родитель является родителем Брата и Детей ОднихРодителей и Брат является мужчиной». Определение отношения брат приводит к некоторой проблеме. Вопрос брат (Х,Х)? выполняется для любого ребенка X мужского пола, что не согласуется с нашим пониманием отношения брат. Для того чтобы исключить подобные утверждения из значения программы, введем предикат Ф (Терм1,Терм2). Удобнее записывать этот предикат в обычном (инфиксном) виде. Таким образом, Терм! Ф Терм2 истинно, если Терм! и Терм2 различны. Ограничимся пока примером термов, являющихся константами. В принципе этот предикат может быть определен с помощью таблицы X Ф Y для каждой пары различных индивидов X и Y из интересующей области. Рис. 2.1 представляет собой такую таблицу для программы 1.1. авраам Ф исаак. авраам ф аран. авраам Ф лот. авраам Ф милка. авраам Ф иска. исаак Ф аран. исаак Ф лот. исаак Ф милка. исаак Ф иска, аран ф лот. аран ф милка. аран Ф иска, лот Ф милка. лот ф иска. милка ф иска. Рж*. 2.!. Определение неравенства. Новое правило для отношения брат: брат (Брат, ДетиОднихРодителей) «- родитель (Родитель, Брат), родитель (Родитель, ДетиОдних Родителей), мужчина (Брат), Брат Ф ДетиОдних Родителей. Чем больше отношений уже введено, тем проще определять новые сложные понятия. Программа 2.1 определяет отношения дядя (Дядя, ПлемянницаИлиПле- мянник), дети_одних_родителей (ДетиОдних Родителей 1, ДетиОдних Родителей2) и родственник (Родственник!, Родственник). Дополнительные примеры приведены в качестве упражнения в конце раздела. Другое отношение, неявно содержащееся в семейной базе данных,-является ли женщина матерью. Это отношение определяется с помощью отношений мать/2. Новой реляционной схемой является мать (Женщина), которая определяется правилом мать (Женщина)«- мать (Женщина, Ребенок). Это означает: «Женщина является матерью, если у нее есть Ребенок». Заметим, что
28 Часть I. Глава 2 дядя(Дядя,Субъект) «- брат(Дядя,Родитель), родитель(Родитель,Субъект). дети_одних_родителей(ДетиОднихРодителей 1 ,ДетиОднихРодителей2) <- родитель(Родитель, ДетиОднихРодителей 1), родитель(Родитель,ДетиОднихРодителей2), ДетиОднихРодителей 1 Ф ДетиОдних Родителей2. родственник(Родственник 1 ,Родственник2) <- родитель(Родитель 1 родственник 1), родитель(Родитель2,Родственник2), дети_одних_родителей(Родитель1,Родитель2). Программа 2.1.Определение семейных отношений. одно и то же предикатное имя мать используется для описания двух разных отношений мать. Предикат мать имеет в этих двух случаях различное число аргументов, т. е. различную арность. В общем случае одно и то же предикатное имя с различными арностями описывает различные отношения. Чтобы не доводить семейные отношения до кровосмешения, сменим примеры и обратимся к описанию простых логических схем. Схему можно рассматривать с двух точек зрения: как топологический слой физических элементов, обычно описываемый диаграммой, и в виде взаимодействия функциональных элементов. Оба подхода легко отобразить в логической программе. Совокупность фактов представлена в диаграмме схемы, а правила описывают функциональные элементы. База данных, приведенная в программе 2.2, представляет собой упрощенное описание логического вентиля «И», изображенного на рис. 2.2. Факты изображают связи между отдельными резисторами и транзисторами, входящими в схему. Реляционная схема резистора - resistor (Вывод 1, Вывод2), транзистора * * - transistor (Затвор, Исток, Сток). Программа демонстрирует стиль толкования логических программ, который будет поддерживаться на протяжении всей книги. Перед каждой содержательной процедурой приводятся реляционная схема процедуры и словесное описание отношения. Советуем следовать этому способу комментирования, так как он заостряет внимание на декларативном понимании любых программ, в том числе и программ на Прологе. Питание пг Y777777777 Рис. 2.2.Логическая схема. Канального транзистора. - Прим. ред.
Программирование баз данных 29 Правила, относящиеся к функциональным компонентам схемы, описывают работу отдельных групп резисторов и транзисторов. Схема описывает вентиль «И», в котором выходной сигнал является логическим «и» двух входных сигналов. Один из способов построения такого вентиля состоит в последовательном соединении вентиля «И-НЕ» и инвертора. Такой способ и использован в данной схеме. Реляционные схемы для этих трех компонентов -and_gate (Input1, Input2,Output), nand_gate(Inputl,Input2,Output) и invertorf Input,Output). Чтобы понять программу 2.2, давайте разберем правило для инвертора. Оно утверждает, что инвертор строится из транзистора с заземленным истоком и резистора, один вывод которого присоединен к источнику питания. Затвор транзистора является входом инвертора, а свободный вывод резистора следует соединить со стоком транзистора,-это соединение образует выход инвертора. Общие переменные используются для описания общих соединений. Рассмотрим вопрос and_gate(Inl,In2,Out)? относительно программы 2.2. Имеется решение {Inl = пЗ,1п2 = n5,Out = nl}. Это решение подтверждает, что факты действительно описывают вентиль «И», и указывает входы и выход вентиля. resistor(power,nl). resistor(power,n2). transistor(n2,ground,n 1). transistor(n3,n4,n2). transistor(n5,ground,n4). inverter (Input,Output) <- Output является инверсией Input. inverter(Input,Output) <- transistor(Input,ground,Output), resistor(power, Output). nand-gate (Inputl,Input2,Output) Output является логическим «И-НЕ» Inputl и Input2. nand gate(Inputl,Input2,Output) <- transistor(Input 1 ,X,Output), transistor(Input2,ground,X), resistor(power,Output). and-gate (Input J, Input2, Output) Output является логическим «И» Inputl и lnput2. and_gate(Inputl,Input2,Output) <- nand_.gate(Input 1, Input2,X), inverter(X,Output). Программа 2.2.Схема логического вентиля «И». -О 1 Г Hi О '///////////////////// Рис. 2.3. Вентиль ИЛИ.
30 Часть I. Глава 2 Упражнения к разд. 2.1 1. Измените правило для отношения брат, чтобы описать отношение сестра, правило для отношения дядя, чтобы описать отношение племянница, и правило для отношения де- ти_одних_родителей, чтобы ему удовлетворяли только родные братья и сестры, т.е. те, у которых общие и мать, и отец. 2. Используя предикат супруги (Жена,Муж), опишите отношения теща, шурин и зять. 3. Напишите логическую программу, подобную программе 2.2, описывающую логическую схему вентиля «ИЛИ». Схема изображена на рис. 2.3. Добавив инвертор, расширьте программу до вентиля «ИЛИ-НЕ». 2.2. Структурированные и абстрактные данные Недостаток программы 2.2, описывающей вентиль «И», состоит в том, что программа рассматривает схему как черный ящик. В ответе на вопрос «and_gate» не содержится никакой информации о структуре схемы, хотя эта структура неявно использовалась при поиске ответа. Правила утверждают, что схема реализует Вентиль «И», но структура вентиля задана лишь неявно. Мы устраним этот недостаток, добавив дополнительный аргумент к каждой цели в базе данных. Для единообразия этот аргумент будет стоять на первом месте. Основные факты просто получают идентификатор. На рис. 2.2 отметим слева направо: резисторы rl и г2, транзисторы tl, t2 и t3. Имена функциональных компонентов должны отражать их структуру. Инвертор строится из транзистора и резистора. Чтобы представить это, нам потребуются структурированные данные. Нужное средство дает составной терм inv(T,R), где Г и ^-соответствующие имена транзистора и резистора, образующих инвертор. Аналогично именем вентиля «И-НЕ» будет nand(Tl,T2,R), где 77,72 и R-имена двух транзисторов и резистора, образующих вентиль «И-НЕ». Наконец, имя вентиля «И» может быть построено на основе имен инвертора и вентиля «И-НЕ». Программа 2.3 представляет собой модифицированный текст, содержащий нужные имена. Вопрос and_gate(G,Inl,In2,Out) имеет решение {G = and(nand(t2,t3r2),inv(tl, rl) ), Inl = n3,In2 = n5,Out = nl}. Inl,In2 и Out имеют те же значения, что и ранее. Сложная структура G в точности отражает функциональную схему вентиля «И». Структурированные данные имеют важное значение в программировании вообще и в логическом программировании в частности. Они применяются для содержательной организации данных, которая позволяет сформулировать правила более абстрактно, без учета несущественных деталей. В результате можно добиться большей модульности программ, поскольку изменение представления данных не означает полного изменения программы. Продемонстрируем это на следующем примере. Рассмотрим два способа представления факта о курсе лекций по теории сложности, которую Дэвид Хэрел читает по понедельникам в корпусе им. Фейнберга, аудитория А: курс (сложность, понедельник, 9,11,дэвид, хэрел, фейнберг, а). и курс (сложность, время (понедельник, 9,11), лектор (дэвид, хэрел), место (фейнберг, а)). Первый факт описывает курс как отношение между восемью элементами: название курса, день недели, время начала, время окончания, имя лектора, фамилия лектора, корпус и аудитория. Второй факт рассматривает курс как отношение между четырьмя элементами: название, время, лектор и место-с дальнейшим уточнением. Время состоит из дня, времени начала и времени окончания; лектор имеет имя и
Программирование баз данных 31 resistor(R,Nodel,Node2) <- R-резистор с выводами Node! и Node2. resistor(rl,power,nl). • resistor(r2,power,n2). transistor ( T,Gate,Source,Drain) <- T- транзистор с затвором Gate, истоком Source и стоком Drain. transistor(t 1 ,n2,ground,n 1). transistor(t2,n3,n4,n2). transistor(t3,n5,ground,n4). inverter (I,Input,Output) <- /-инвертор, который обращает Input в Output. inverter(inv(T,R),Input,Output) <- transistor(T,Input,ground,Output), resistor(R,power,Output). nand-gate(Nrand,Input J,Input2,Output) <- Nand- вентиль «И-НЕ» с входами Input] и Input2 и выходом Output. nand _gate(nand(T 1 ,T2,R),Input 1 ,Input2,Output) <- transistor(T 1 ,Input 1 ,X,Output), transistor(T2,Input2,ground,X), resistor(R,power,Output). and-gate(Andjnputl,Input2,Output) <- And- вентиль «И» с входами Input 1 и Input2 и выходом Output. and_gate(and(N,I),Input 1 ,Input2,Output) <- nand _ gate(N,Inputl,Input2,X), inverter(I,X,Output). { Ip.v p.:mvi;i \3. База данных схемы с именами. фамилию, место определяется корпусом и аудиторией. Второй факт яснее отражает существующие отношения. Вариант отношения курс с четырьмя аргументами допускает более компактную запись правил за счет абстрагирования от деталей, несущественных при постановке вопросов. Программа 2.4 содержит некоторые примеры подобных правил. Правило для отношения занята использует предикат «меньше или равно», записанный в виде двоичного инфиксного оператора ^. Правила, не связанные с определенными значениями структурированного аргумента, не должны «знать», какова структура аргумента. Например, правила для отношений продолжительность и занятие представляют время явно в виде время (День, Начало, Окончание), так как в этих правилах требуются День, Начало или Окончание занятий. Напротив, в правиле для лектор эти детали не требуются, что приводит к большей модульности, так как можно изменять форму представления данных о времени занятий без изменения правил, не связанных со структурой этих данных. Мы не обладаем определенными правилами для решения вопроса об использовании структурированных данных. Без них можно пользоваться единообразным представлением, в котором все данные простые. Преимуществами структурированных данных являются модульность и компактность записи, в которой точнее отражается наше понимание ситуации. Можно сослаться на соответствующее обсуждение для традиционных языков программирования, в терминологии которых
32 Часть I. Глава 2 лектор(Лектор,Курс) <- курс(Курс,Время,Лектор,Место). продолжительность(Курс,ЧислоЧасов) <- курс(Курс,время(День,Начало,Окончание),Лектор,Место), плюс(Начало,ЧислоЧасов,Окончание). занятие(Лектор,День) <- курс(Курс,время(День,Начало,Окончание),Лектор,Место). занята(Аудитория,День,Время) курс(Курс,время(День,Начало,Окончание),Лектор, Аудитория), Начало ^ Время,Время ^ Окончание. Программа 2.4. Правила расписания. фактам соответствуют таблицы, а структурированным данным - записи с составными полями. Мы придаем важность оформлению программ, особенно при решении трудных задач, и хорошее структурирование данных может иметь свое значение. Некоторые правила программы 2.4 задают бинарные отношения (отношения между двумя объектами), используя одно, более сложное отношение. Все сведения о курсе лекций могут быть записаны в терминах бинарных отношений, например, в следующем виде: день (сложность, понедельник). время_начала (сложность, 9). время_окончания (сложность, 11). лектор (сложность, хэрел). корпус (сложность, фейнберг). аудитория (сложностью). Правила теперь следует записывать иначе, в духе предыдущего метода явной записи неявных взаимосвязей: занятие (Лектор, День) <- лектор (Курс, Лектор), день (Курс, День). Упражнения к разд. 2.2 1. Добавьте правила, определяющие отношения место (Курс,Корпус), занят (Лектор, Время) и не ^встречаются (Лектор 1,Лектор2). Факты о курсе лекций заданы, как выше. 2. Используя отношения из упражнения (1), определите отношение конфликт_расписа- ния ( Время, Место, Курс 1, Курс2). 3. Добавьте дополнительный аргумент в правила для семейных отношений, чтобы задать такие отношения, как отец (X,Y,nana(X,Y) ). (Указание: следуйте примеру базы данных для схемы.) 4. Постройте небольшую базу данных по своему вкусу. Используйте один предикат для описания информации и введите подходящие правила. 2.3. Рекурсивные правила До сих пор описывались правила, определяющие новые отношения в терминах существующих отношений. Важным расширением множества подобных правил являются рекурсивные определения, в которых отношения определяются в терминах этих же отношений. Один из способов перехода к рекурсивным правилам состоит в обобщении множества нерекурсивных правил. Рассмотрим правила, описывающие прародителей, прапрародителей и т.д.:
Программирование баз данных 33 прародитель (Предок, Потомок) «- родитель (Предок, Человек), родитель (Человек, Потомок), прапрародитель (Предок, Потомок) «- родитель (Предок, Человек), прародитель (Человек, Потомок), прапрапрародитель (Предок, Потомок) «- родитель (Предок, Человек) прапрародитель (Человек, Потомок). Можно заметить простую схему, которую мы опишем в правиле, задающем отношение предок (Предок, Потомок): предок (Предок, Потомок) «- родитель (Предок, Человек), предок (Человек, Потомок). Это правило представляет собой обобщение введенных ранее правил. Логическая программа для отношения предок требует также и нерекурсивного правила, выбор которого влияет на значение программы. Если использовать факт предок (Х,Х), приводящий к рефлексии отношения предок, то каждый человек будет своим собственным предком. Это не соответствует интуитивному пониманию слова предок. Программа 2.5 является логической программой, определяющей отношение предок, в ней родители считаются предками. предок(Предок,Потомок) <- Предок является предком субъекта Потомок. предок(Предок,Потомок) «- родитель(Предок,Потомок). предок(Предок,Потомок) «- родитель(Предок,Человек),предок(Человек,Потомок). Программа 2.5. Отношение предок. Отношение предок является транзитивным замыканием отношения родитель. В общем случае благодаря использованию рекурсивных правил в логической программе легко строится транзитивное замыкание любого отношения. Рассмотрим программу проверки связности в ориентированном графе. Ориентированный граф может быть задан в виде логической программы с помощью набора фактов. Факт edge (Nodel,Node2) входит в программу, если в графе существует ребро, ведущее из вершины Nodel в вершину Node2. На рис. 2.4 изображен некоторый граф, а программа 2.6 дает его представление в виде логической программы. а — b f i i i с -> d -> e g Рис. 2.4. Простой граф. edge(a,b). cdge(a,c). edge(b,d). cdge(c,d). edge(d,e). edge(f,g). Программа 2.6. Ориентированный граф. 2 1402
34 Часть I. Глава 2 Две вершины графа связны, если существует последовательность ребер, ведущая из первой вершины во вторую. Таким образом, отношение connected(Nodel,Node2), являющееся истинным, если вершины Nodel и Node2 связны, есть транзитивное замыкание отношения edge. Например, в графе на рис. 2.4 вершины а и е связы, а Ъ и /-нет. Программа 2.7 описывает требуемое отношение. Значение программы совпадает с множеством целей вида connected(X,Y), где X и У-связные вершины. Заметим, что ввиду выбора исходного факта connected является транзитивным рефлексивным отношением. connected(Nodel,Node2) <- вершина Nodel связна с вершиной Node2 в графе, заданном отношением edge/2. connected(Node,Node). connected(Nodel,Node2) <- edge(Node 1 ,Liifk), connected(Link,Node2). Программа 2.7.Транзитивное замыкание отношения edge. Упражнения к разд. 2.3 1. Башня из кубиков может быть описана совокупностью фактов вида на (Кубик 1,Кубик2), которые истинны, если Кубик 1 поставлен на Кубик2. Определите предикат выше ( Кубик 1,Кубик2), который истинен, если Кубик 1 расположен в башне выше, чем Кубик2. (Указание: выше является транзитивным замыканием на). 2.4. Логические программы и модель реляционной базы данных Логические программы можно рассматривать как мощное расширение модели реляционной базы данных. Дополнительные возможности возникают здесь за счет применения правил. Многие введенные понятия имеют содержательные аналоги в теории баз данных, верно и обратное. Основные операции реляционной алгебры легко выражаются в логическом программировании. Процедуры, построенные исключительно из фактов, соответствуют отношениям, при этом арность отношения совпадает с арностью процедуры. Пять основных операций определяют реляционную алгебру: объединение, симметрическая разность, декартово произведение, проекция и выборка. Покажем, как каждая из них выражается в логической программе. Операция объединения строит одно «-арное отношение из двух «-арных отношений г и s. Новое отношение, которое обозначим r_union_^, является объединением г и s. Это отношение непосредственно задается логической программой, состоящей из двух правил: r_union_s(X1,...,Xn)^r(X1,...,Xn). r_union_s(X1,...,Xn)^s(X1,...,Xn). Определение симметрической разности использует понятие отрицания. Введем предикат not. Интуитивно ясно, что цель not G истинна относительно программы Р, если G не является логическим следствием Р. Отрицание в логических программах рассматривается в гл. 5, там же обсуждаются недостатки интуитивного определения. В случае основных фактов, однако, определение корректно. Такой случай и имеет место при рассмотрении реляционных баз данных.
Программирование баз данных 35 Определяется «-арное отношение r_diff_s для «-арных отношений г и s\ r_diff_s(X1,...,Xn)^r(X1,...,Xn), not s(Xu...,Xn). r..diff_s(X1,...,Xn)^s(X1,...,Xn), not ^„.„XJ. Декартово произведение может быть определено одним правилом. Если r-w-ap- ное отношение, a s-«-арное, то (т + «)-арное отношение r-xs определяется так: r_x_s(X1,...,Xm + n)-r(X1,...,XJ,s(Xm+1,...,Xm + n). Проекция состоит в построении отношения, использующего лишь некоторые аргументы исходного отношения. Определение в любом конкретном случае не вызывает сложностей. Например, проекция г 13 оставляет первый и третий аргументы трехарного отношения, т.е. гЩХиХъ)^г{Хх,Х2,Хъ). Так же просто описывается любой конкретный случай выборки. Рассмотрим отношение, описывающее наборы, в которых третьи элементы больше вторых, и отношение, в котором первый элемент есть Смит или Джонс. В обоих случаях для иллюстрации используется трехарное отношение г. Первый пример состоит в построении отношения rl: гЦХ^Хз) w(X1?X2,X3),X2 > Х3. Второй пример состоит в построении отношения г2, использующего дизъюнктивное отношение смит_или_джонс\ г2(Х1,Х2,Х3) <- г(Х1,Х2,Х3Х смит или джонс(Х 1). смит_или_джонс (смит). смит_или_джонс(джонс). Некоторые производные операции реляционной алгебры непосредственно выражаются в конструкциях логического программирования. Мы опишем две из них-пересечение и объединение. Если г и s -некоторые «-арные отношения, то их пересечение, r_meet_s, тоже является «-арным отношением, задаваемым единственным правилом: r_meet_s(X1,...,Xn)^-r(X1,...,Xn),s(X1,...,Xn). Прямому объединению точно соответствует конъюнктивный вопрос с общими переменными. 2.5. Дополнительные сведения Читателей, стремящихся подробнее разобраться во взаимосвязи логического программирования и теории баз данных, мы отсылаем к многочисленным статьям, написанным на эту тему. Хорошим введением является обзор Гэлльера и др. (Gallaire et al., 1984). Имеются более ранние работы о связи логики и баз данных Гэлльера и Минкера (Gallaire, Minker, 1978). Интересная книга Ли относится к применению языка запросов баз данных в Прологе (Li, 1983). Наше обсуждение реляционных баз данных следовало Ульману (Ullman, 1982). Другое интересное исследование реляционных баз данных может быть найдено в работе Мейра (Maier,1983). В общем случае, как показал Ковальский (Kowalski, 1979), л-арное отношение может быть задано совокупностью из п + 1 бинарных отношений. В тех случаях, когда один из аргументов является ключом, таким, как название курса в примере разд. 2.2, достаточно п бинарных отношений. 2*
36 Часть I. Глава 3 Глава 3 Рекурсивное программирование В программах предыдущего раздела использовались конечные структуры данных. Сила же математики проявляется как раз на бесконечных или потенциально бесконечных структурах. Конечные примеры являются лишь частным случаем. Логические программы овладевают этой силой, используя рекурсивные типы данных. Логические термы можно классифицировать по типам. Тип -это множество (возможно, бесконечное) термов. Некоторые типы удобно определять унарными отношениями. Отношение р/7 определяет тип /?, совпадающий с множеством всех таких термов X, что выполнено р(Х). Например, использованные ранее предикаты мужчина/1 и женщина/1 задают типы мужчина и женщина. Более сложные типы могут быть заданы рекурсивными логическими программами. Такие типы называются рекурсивными типами. Типы, задаваемые унарными рекурсивными программами, называются простыми рекурсивными типами. Программа, задающая тип, называется определением типа. В этой главе мы рассмотрим логические программы, задающие отношения над объектами простых рекурсивных типов, таких, как целые числа, списки и бинарные деревья. Но будут показаны программы и с более сложными типами, такими, как полиномы. 3.1. Арифметика Простейший рекурсивный тип данных - натуральные числа-лежит в основании математики. Арифметика основана на натуральных числах. В данном разделе мы опишем логические программы, задающие арифметику, и хотя, как будет показано в дальнейшем, аналогичные программы на Прологе существенно отличаются от приведенных, полезность этого описания несомненна. Во-первых, обычно арифметические операции рассматриваются как функции, а не как отношения. Изучение примеров из столь знакомой области подчеркивает новый подход, необходимый при построении логических программ. Во-вторых, так будет проще обсуждать основные математические вопросы, такие, как корректность и полнота программ. Натуральные числа строятся с помощью двух объектов - константного символа О и унарной функции следования s. Таким образом, все натуральные числа рекурсивно представляются в виде 0, s(0), s(s(0)), s(s(s(0))).... Используем сокращение s"(0) для обозначения натурального числа и, т. е. для обозначения «-кратного применения функции следования к 0. Как и в предыдущей главе, каждый предикат снабжается реляционной схемой, в которой описывается подразумеваемое значение предиката. Напомним, что программа Р называется корректной относительно подразумеваемого значения М, если значение программы является подмножеством М. Программа Р полна, если М является подмножеством значения программы Р. Программа корректна и полна, если ее значение совпадает с М. Доказательство корректности устанавливает, что любое утверждение, выводимое из программы, является подразумеваемым значени-
Рекурсивное программирование 37 ем программы. Доказательство полноты устанавливает, что любое подразумеваемое значение выводимо из программы. В данном разделе приводятся два типичных доказательства корректности и полноты. Определение натуральных чисел в виде простого типа явно сформулировано в виде логической программы 3.1. Ее реляционная схема natural .number(X) <- X -натуральное число. natural ..number(O) natural_number(s(X) <- natural_number(X). Профамма З.1. Определение натуральных чисел. naturaLnumher (X) предполагает, что «X есть натуральное число». Программа состоит из одного единичного предложения и одного итерационного предложения (предложения, тело которого состоит из одной цели). Такая программа называется минимальной рекурсивной. naturaJ.number(sn(0)) plus(sn(0),sm(0),sn+m(0)) I I natural -number(sn-* (0)) plus(sn-* (0) ,sm (0) ,sn+m"* (0)) I I I I natural.number(s(0)) plus(s(0),sm(0),sm+1 (0)) I I naturaLnumber(O) plus(0,sm(0) ,sm(0)) I natural_number(sm (0)) Рис. З.1. Дерево вывода, подтверждающее полноту программ. • Утверждение. Программа 3.1 корректна и полна относительно множества целей naturaLnumher (sl (0) ) при / > 0. • Доказательство. (1) Полнота. Пусть «-натуральное число. Докажем, что цель naturaLnumher(п) выводима из программы. Для этого явно построим дерево вывода. Число п имеет вид 0 или sn(0). Дерево вывода цели naturaLnumher (0) тривиально. Дерево вывода цели naturaLnumher (s (.. .s(0)...)) содержит п редукций, использующих правило программы 3.1 и приводящих к факту natural_number(0), как это показано на левой части рис. 3.1. (2) Корректность. Пусть цель naturaLnumher (X) выводима из программы 3.1, количество редукций в выводе равно п. Индукцией по п докажем, что natural _numher (X) принадлежит подразумеваемому значению. Если п = 0, то цель должна выводиться из программы с помощью единичного предложения, следовательно, X = 0. Если п > 0, то цель должна иметь вид naturaLnumher (s (X1)), так как лишь такие цели выводимы с помощью правила программы, причем naturaLnumher (XI) выводимо с использованием п — 1 редукций. По предположению индукции
38 Часть I. Глава 3 naturaLnumber(X1) принадлежит подразумеваемому значению программы, т.е. XI = &(0) для некоторого к> О.ф Существует естественный порядок натуральных чисел. Программа 3.2 является логической программой, задающей отношение «меньше или равно», соответствующее этому порядку. X ^ У X и У-натуральные числа, такие, что X меньше или равно У О ^ X <- natural_number(X). s(X) ^ s(Y) «- X ^ Y. natural_number(X) <- См. программу 3.1. Программа 3.2. Отношение меньше или равно. Мы записываем это отношение с помощью бинарного инфиксного символа (опера- торс!) < в соответствии с привычной математической записью. Выражение 0 < X есть не что иное, как терм с функтором </2 и аргументами 0 и X, и синтаксически эквивалентно ^(0,Х). Реляционной схемой будет N^Nj. Подразумеваемое значение программы 3.2-все основные факты вида X < У, где X и У-натуральные числа и число X не больше числа У Упражнение (2) в конце раздела состоит в доказательстве корректности и полноты программы 3.2. Рекурсивное определение отношения < не является «вычислительно эффективным». Дерево вывода, доказывающее, что конкретное п меньше конкретного т, содержит т + 2 вершины. Обычно считается, что сравнение двух чисел выполняется за один шаг независимо от величины чисел. В действительности Пролог не определяет арифметические операции на основании приведенных здесь аксиом, а непосредственно использует встроенные арифметические операции компьютера. Сложение является базовой операцией, определяющей отношение между двумя натуральными числами и их суммой. В разд. 1.1 с помощью таблицы задавалось отношение плюс для всех необходимых натуральных чисел. Рекурсивная программа позволяет задавать это отношение изящнее и компактнее, так, как это сделано в программе 3.3. Подразумеваемое значение программы - множество фактов вида plus(X,Y,Z), где X, У и Z-натуральные числа и X + Y—Z. plus(XXZ) <- X, У и Z-натуральные числа, такие, что Z есть сумма X и У plus(0,X,X) <- natural-number(X). plus(s(X),Y,s(Z))«- plus(X,Y,Z). natural-number(X) <- См. программу 3.1. Программа 3.3. Сложение. • Утверждение. Программы 3.1 и 3.3 задают корректную и полную аксиоматизацию сложения относительно стандартного подразумеваемого значения предиката plus/3. • Доказательство. (1) Полнота. Пусть X, У и Z-натуральные числа, причем Z = X + У Построим дерево вывода цели plus(X,Y,Z). Если X равно 0, то У равно Z. Так как программа 3.1 -полная аксиоматизация натуральных чисел, то найдется дерево вывода для цели natural_number (Y), которое легко преобразуется в дерево вывода для цели plus(0,Y,Y). Пусть теперь X равно ?(0) для некоторого п. Если У равно sm(0), то Z равно sn+m(0). Дерево вывода в правой половине рис. 3.1 доказывает полноту программы.
Рекурсивное программирование 39 (2) Корректность. Пусть plus(X,Y,Z) принадлежит значению программы. Методом индукции по X, как и в доказательстве предыдущего утверждения, легко установить, что X + Y= Z. • Сложение обычно рассматривается как функция от двух аргументов, а не как трехместное отношение. В общем случае логическая программа, соответствующая функции от п аргументов, определяет (п + 7)-арное отношение. Вычисление значения функции происходит при постановке вопроса, в котором значения п аргументов заданы, а значение аргумента, соответствующего значению функции, не задано. Решение вопроса дает значение функции при заданном значении аргументов. Чтобы прояснить аналогию, дадим функциональное определение сложения, соответствующее логической программе. О + X = X. s(X) + Y = s(X + Y) Одно из преимуществ программы, задающей отношение, перед функциональной программой состоит в том, что программу для отношения можно использовать многими способами. Например, вопрос plus (s (0),s(0),s(s(0)))? состоит в проверке равенства 7 + 7 = 2. (Конечно, десятичная запись привычнее при работе с числами.) Как и в случае отношения ^, программа plus не эффективна. Дерево вывода в доказательстве того, что сумма пит равна п + т, содержит п + т + 2 вершины. Постановка вопроса plus(s(0),s(0),X)? (пример обычного использования программы) приводит к вычислению суммы 7 и 7. Однако столь же просто программа может быть использована и для вычитания. Для этого достаточно задать такой вопрос, как plus(s(0), X, s(s(s(0))))? Вычисленное значение X является разностью 3 и 7, т. е. 2. Аналогично вопрос с неопределенным первым аргументом и определенными вторым и третьим аргументами также сводится к вычитанию. Существенно новые возможности дают вопросы с многочисленными решениями. Рассмотрим вопрос plus(X, Y,s(s(s(0))))? Он означает: «Существуют ли числа X и У, дающие в сумме 3?» Другими словами, ищутся разбиения числа 3 на два числа X и Y Легко видеть, что имеется несколько решений. Вопросы с многочисленными решениями становятся интереснее, если ограничить свойства переменных в вопросе. Имеются два вида ограничений: использование дополнительных конъюнктивных членов в вопросе и уточнение переменных в вопросе. Мы рассматривали подобные примеры в случае баз данных. Упражнение (4) в конце раздела состоит, в частности, в определении предиката even(X), который истинен, если Х-четное число. Используя этот предикат, можно поставить вопрос plus(X,Y,n),even(X), even(Y)?, который сводится к разбиению числа п на два четных числа. Второй тип ограничений иллюстрируется примером вопроса plus(s(s(X)),s(s(Y)), n)?, в котором требуется, чтобы числа, дающие в сумме л, были строго больше 7. Почти все логические программы допускают многообразное использование. Рассмотрим, например, программу 3.2, задающую отношение ^. В вопросе s(0) <zs(s(0)) проверяется, верно ли, что 7 не больше 2. В вопросе X ^ s(s(0)) ищутся числа X, не большие 2. Можно даже с помощью вопроса X < У? вычислить пары чисел, одно из которых не больше другого. Программа 3.3, определяющая сложение, не единственна. Например, логическая программа plus(0,X,0) <- naturaLnumber(X). plus(X,s(X),s(Y)) <- plus(X,Y,Z). имеет в точности то же значение, что и программа 3.3. Наличие двух программ объясняется симметрией между первыми двумя аргументами. Доказательство корректности и полноты программы 3.3 преобразуется в доказательство для данной программы путем замены аргументов на симметричные. Значение программы для отношения plus не изменится, даже если объединить эти
40 Часть I. Глава 3 две программы. Это объединение, однако, приводит к неудобствам, так как теперь имеется несколько деревьев вывода одной и той же цели. Как для эффективности выполнения программы, так и для выразительности программы важно, чтобы аксиоматизация логической программы была минимальной. Мы назовем типовым условием использование предиката, определяющего тип. Типовым условием для натуральных чисел является любая цель вида natural_number (X). Обычно программы, подобные 3.2 и 3.3, упрощаются за счет отбрасывания тела правила naturaLnumber (X) в исходном правиле. В этом случае, однако, в значение программы попадут такие факты, как О^а и plus(0,a,a), при произвольной константе а. Типовые условия необходимы для корректности программ. Но для упрощения программы и сокращения дерева вывода обычно они опускаются. И мы в последующих примерах программ явные типовые условия будем опускать. Описанные основные программы используются для определения более сложных отношений. Типичный пример - определение умножения в виде повторяющегося сложения. Программа 3.4 задает требуемое отношение. Схема отношения times(X,Y,Z) означает: X, умноженное на У, равно Z. times(XXZ) <- X, У и Z-натуральные числа, такие, что Z равно произведению X и У times(0,X,0). times(s(X),Y,Z) <- times(X,Y,W), plus(W,Y,Z). plus(X,Y,Z) <- См. программу 3.3. Программа 3.4. Умножение как повторяющееся сложение. Возведение в степень определяется в виде повторяющегося умножения. Программа 3.5 для exp(N,X,Y) задает отношение XN = Y Эта программа совпадает с программой 3.4 для times(X,Y,Z), если в последней заменить times и plus соответственно на ехр и times. Исходные факты для возведения в степень -Х° = 1 для всех положительных X и 0N = 0 для положительных значений N. exp(N,X,Y)<- N,X и У-натуральные числа, такие, что У равно X, возведенному в степень N. exp(s(X),0,0). exp(0,s(X),s(0)). exp(s(N),X,Y) <- exp(N,X,Z), times(Z,X,Y). times(X,Y,Z) <- См. программу 3.4. Программа 3.5. Возведение в степень как повторяющееся умножение. Определение функции факториал использует определение умножения. Напомним, что N! = N- (N — ])-... -2-1. Предикат factorial(N,F ) сопоставляет числу N его факториал F. Аксиоматизация приведена в программе 3.6. Не все отношения, связанные с натуральными числами, определяются рекурсивно. Отношения можно также определять в духе программ гл. 2. Примером служит программа 3.7, определяющая минимум двух чисел через отношение minimum (N1, N2, Min).
Рекурсивное программирование 41 factorial! N,F) <- F равно факториалу числа N. factorial(0,s(0)). factorial(s(N),F) <- factorial(N,F 1) times(s(N),F 1 ,F). times(X,Y,Z)«- См. программу 3.4. Программа 3.6. Вычисление факториала. minimum (N1 ,N'2,Min) <- Min равен минимуму натуральных чисел N1 и N2. minimum(Nl,N2,Nl)«-Nl ^ N2. minimum(N 1,N2,N2) <- N2 ^ N 1. N1 ^ N2 <- См. программу 3.2. Программа 3.7. Минимум двух чисел. При построении программы, определяющей остаток от деления целых чисел, возникает любопытное явление - различные математические определения одного и того же понятия приводят к разным логическим программам. Программы 3.8а и 3.86 представляют собой два различных определения отношения mod(X,Y,Z), которое истинно, если Z является вычетом X по модулю У, т. е. Z является остатком от деления X на У. Программы используют отношение <, определенное в упражнении (1) в конце раздела. mod(X,Y,Z)<r- Z остаток от деления X на Y. mod(X,Y,Z) <- Z < Y, times(Y,Q,W), plus(W,Z,X). Программа 3.8а. Нерекурсивное определение остатка. mod(XXZ)*-. Z -остаток от деления X на У. mod(X,Y,X) <- X < Y. mod(X,Y,Z)<-plus(Xl,Y,X), mod (X1,Y,Z). Программа 3.86. Рекурсивное определение остатка. Программа 3.8а служит примером непосредственного перевода математического определения, являющегося логическим высказыванием, в логическую программу. Программа соответствует экзистенциальному определению целого остатка: «Z равно Xmod У, если Z меньше У и существует такое число Q, что X = Q- Y+ Z». Математические определения в общем случае легко преобразуются в логические программы. Можно рассмотреть связь программы 3.8а с конструктивной математикой. Несмотря на то что определение внешне выглядит как определение существования, оно в то же время конструктивно ввиду конструктивности отношений < ,plus и times. Например, число Q, требуемое в определении, будет явно вычислено с помощью отношения times в любом конкретном вычислении отношения mod. Программа 3.86 в отличие от программы 3.8а определена рекурсивно. Программа описывает алгоритм нахождения целого остатка, основанный на мно-
42 Часть I. Глава 3 гократном вычитании. Первое правило утверждает, что X mod У равно X, если X меньше Y. Второе правило утверждает, что X mod У совпадает с X — Ymod Y. Процесс вычисления при определении остатка состоит в последовательном вычитании У из X, пока не будет получена величина, меньшая У Ясно, что это вычисление приводит к правильному значению. Математическая функция XmodYae определена при У= 0. Ни программа 3.8а, ни программа 3.86 не включают цель mod(X,0,Z) в значение программы для любых X, Z. Проверка отношения « < » гарантирует это. Наша вычислительная модель дает возможность сравнить две программы вычисления mod. Выбрав конкретные числа X, У и Z, удовлетворяющие отношению mod, мы можем сравнить деревья вывода. В общем случае дерево вывода программы 3.86 будет меньше дерева вывода программы 3.8а. В этом смысле программа 3.86 «эффективнее». Отложим более строгое обсуждение эффективности до рассмотрения списков в программах на Прологе. Другим примером непосредственного перевода математического определения в логическую программу служит программа, определяющая функцию Аккермана. Функция Аккермана - простейший пример рекурсивной функции, не являющейся примитивно рекурсивной. Это функция от двух аргументов, определяемая тремя равенствами: ackermann(0,N) = N + 1. ackermann(M,0) = ackermann(M — 1,1). ackermann(M,N) = ackermann(M — l,ackermann(M,N — 1)). Программа 3.9 представляет собой запись функционального определения в виде логической программы. Предикат ackermann(M,N,A) означает, что А = аскег- mann(M,N). Третье правило содержит два вызова функции Аккермана, один-для вычисления второго аргумента. ackermannfX,Y,A) <- А равно значению функции Аккермана для натуральных чисел X и Y. ackermann(0,N,s(N)). ackermann(s(M),0,Val) <- ackermann(M,s(0),Val). ackermann(s(M),s(N),Val) <- ackermann(s(M),N,Val 1), ackermann(M,Val 1 ,Val,A). Программа З.9. Функция Аккермана. В функциональном определении эти два вызова выглядят очевиднее. Вообще, функциональная запись более естественна для чисто функциональных определений, как в случае функции Аккермана. Аналогичным примером является программа 3.8а. Запись выражения X = QY+ Z, являющегося утверждением о функциях, в виде логической программы выглядит несколько неуклюже. Заключительным примером данного раздела является алгоритм Евклида нахождения наибольшего общего делителя двух натуральных чисел, записанный в виде логической программы. Подобно программе 3.86, это - рекурсивная программа, не использующая рекурсивное задание целых чисел. Схема отношения - gcd(X, Y,Z), а подразумеваемое значение Z является наибольшим общим делителем (н.о.д.) двух натуральных чисел X и У В данной программе для задания отношения mod используется любая из двух программ-3.8а или 3.86. Первое правило программы 3.10 отражает логическую сущность алгоритма Евклида. Н.о.д. чисел X и У совпадает с н.о.д. У и Xmody Доказательство
Рекурсивное программирование 43 gcd(XXZ)^ Z является наибольшим общим делителем натуральных чисел X и Y. gcd(X, Y,Gcd)«- mod(X, Y,Z), gcd( Y,Z,Gcd). gcd(0,X,0) <- X > 0. Программа ЗЛО. Алгоритм Евклида. корректности программы 3.10 зависит от корректности предыдущего математического утверждения о наибольшем общем делителе. Доказательство корректности алгоритма Евклида также основано на этом математическом утверждении. Второе предложение программы 3.10 является исходным фактом. Следует указать, что X должно быть больше 0, чтобы исключить gcd (0,0,0) из значения программы. Н.о.д. пары 0 и 0 не определен. Упражнения к разд. 3.1 1. Изменив программу 3.2, опишите отношения <, > и ^. Рассмотрите различные использования этих программ. 2. Докажите, что программа 3.2 является корректной и полной аксиоматизацией отношения ^. 3. Докажите, что дерево вывода цели s"(0) ^ sm(0) при использовании программы 3.2 имеет т + 2 вершины. 4. Определите истинность предикатов even(X) и odd(X) в случаях, когда X четно и нечетно. (Указание: измените программу 3.1 для отношения naturaljnumber.) 5. Напишите логическую программу, задающую отношение fib (N, F) для определения iV-ro числа Фибоначчи F. 6. Предикат times может быть использован для деления без остатка с помощью вопроса вида times(s(s(0)),X,s(s(s(s(0)))))?, сводящегося к нахождению частного от деления 4 на 2. Вопрос times(s(s(0)),X,s(s(s(0))))? для нахождения 3/2 не имеет решения. Во многих приложениях требуется, чтобы целочисленное деление давало в качестве 3/2 результат 1. Напишите программу целочисленного деления. (Указание: используйте повторяющееся вычитание.) 7. Измените программу 3.10 для нахождения н.о.д. двух целых чисел так, чтобы непосредственно использовать повторяющееся вычитание, а не функцию mod. (Указание: программа многократно вычитает меньшее число из большего, пока два числа не станут равными.) 3.2. Списки Исходной операцией арифметики является унарная функция следования. Хотя при этом можно определять столь сложные рекурсивные функции, как функция Аккермана, использование унарной рекурсивной структуры является существенным ограничением. В данном разделе рассматривается бинарная структура -список. Первый аргумент списка - это элемент, второй аргумент рекурсивно определяется как остаток списка. Списковых структур достаточно для большинства вычислений - об этом свидетельствует успех языка программирования Lisp, в котором списки являются основной составной структурой данных. С помощью списков можно представлять любые сложные структуры, хотя в иных конкретных задачах удобнее пользоваться специальными структурами данных. При построении списков, как и при построении чисел, необходимо наличие константного символа, чтобы рекурсия не была бесконечной. Это «пустой список», называемый nil, будем обозначать его знаком [ ]. Нам также требуется функтор арности 2. Исторически общепринятый функтор для списков обозначается «•». Чтобы не перегружать обращение к символу «точка», будем использовать специальную запись. Терм .(X, Y) будет обозначаться [Х|У]. Компоненты терма имеют специальные названия: X называется головой, а Y-хвостом списка.
44 Часть I. Глава 3 Терм [X | У] соответствует в языке Lisp применению операции cons к паре X, Y. Соответствующими названиями для головы и хвоста списка будут саг и саг. Рис. 3.2 поясняет соотношение между различными способами записи списков. Первая колонка представляет запись списков с помощью функтора «•», именно так термы логических программ задают списки. Вторая колонка представляет запись с помощью квадратных скобок, эквивалентную функтору «точка». Третья колонка содержит выражения, являющиеся упрощением представлений второй колонки, при котором рекурсивная структура списка оказывается скрытой. В такой записи список представляется в виде последовательности элементов, ограниченной квадратными скобками и разделяемой запятыми. Пустой список, применяемый для завершения рекурсивного описания, явно не приводится. Отметим использование «cons-записи» в третьей колонке при описании списка, хвост которого-переменная. Формальный Cons-запись Запись с по- объект мощью элементов •W1) [а|[Ц [а] .(а,.(Ь,[])) [а|[Ь|[])) [а,Ь] .(а,.(Ь,(с,{ ]))) [а|[Ь|[с |[ ]]]] [а,Ь,с] •(а,Х) [а|Х] [а|Х] .(а,.(Ь,Х)) [а|[Ь|ХЦ [а,Ь|Х] Рис. 3.2. Эквивалентные записи списков. list(Xs) <- Xs- список. list([ ]). list([X | Xs]) <- list(Xs). Профамма 3.11. Определение списка. С помощью функтора «•» строятся более общие термы, чем списки. Программа 3.11 является точным описанием списка. Декларативная интерпретация: «Списком является либо пустой список, либо значение функции cons на паре, хвост которой-список». Эта программа подобна программе 3.1, задающей натуральные числа, и является определением простого типа для списков. Рис. 3.3 содержит дерево вывода цели list([a,b,c]). В дереве вывода неявно содержатся основные примеры правила программы 3.11, например, /^/([tfAc])*- list{[b,c]\ Мы приводим частный пример в явном виде, так как примеры списков в виде «С0и^-записи» могут сбить с толку. Список [a,b,c] является примером терма [Х| Xs] при подстановке {X = a,Xs = \b,c]}. Поскольку списки являются более богатой структурой данных, чем числа, с ними связан больший набор полезных отношений. Фундаментальной операцией является, возможно, определение вхождения отдельного элемента в список. Предикатом, задающим это отношение, будет member (Element, List). Программа 3.12 является рекурсивным описанием отношения member/2. Декларативное понимание программы 3.12 очевидно. X является элементом списка, если в соответствии с первым предложением Х-голова списка или в соответствии со вторым предложением X является элементом хвоста. Значение программы есть множество всех основных примеров цепи member (X,Xs), где Х-элемент списка Xs. Мы опускаем типовое условие в первом предложении. Первое предложение с условием запишется так: member(X,[X \ Xs]) <- list(Xs).
Рекурсивное программирование 45 list([a,b,c]) i list([b,cl) I list([c]) I list([]) Рис. 3.3. Дерево вывода при проверке списка. member( Element,List) <- Element является элементом списка List. member(X,[X | Xs]). member(X,[Y | Ys]) +- member(X,Ys). Программа 3.12. Принадлежность списку. Как будет показано в дальнейшем, эта программа имеет много интересных приложений. Основные применения состоят в проверке принадлежности элемента списку с помощью вопросов вида member(b,[a,b,cjp., в нахождении элементов списка с помощью вопросов вида member(X,[a,b,cW и списков, содержащих элемент, с помощью вопроса вида member(b,X)l. Последний вопрос выглядит странным, однако имеется ряд программ, основанных на таком использовании отношения member. Всюду, где это возможно, мы используем следующее соглашение о наименовании переменных в программах, использующих списки. Если символ X используется для обозначения головы списка, то А^-для обозначения хвоста списка. В общем случае имена во множественном числе используются для обозначения списка элементов, а имена в единственном числе-для отдельных элементов. Числовые суффиксы обозначают различные варианты списков. Реляционные схемы по-прежнему содержат мнемонические имена. Нашим следующим примером будет предикат sublist(Sub,List), служащий для определения, является ли список Sub подсписком списка List. Подсписок должен содержать последовательные элементы: так, \_Ь,с] является подсписком списка [a,b,c,d], а \_а,с] не является. Для упрощения определения отношения sublist удобно выделить два специальных случая подсписков. Введение содержательных отношений в виде вспомогательных предикатов является правильным подходом к построению логических программ. Двумя рассматриваемыми случаями будут начальный подсписок (префикс списка) и конечный подсписок (суффикс списка). Программы представляют и самостоятельный интерес. Предикат prefix (Prefix, List) истинен, если Prefix является начальным подсписком списка List, например, prefix([a,b],[a,b,cl) истинен. Предикатом, парным к prefix, будет предикат suffix (Suffix,List), утверждающий, что список Suffix является конечным подсписком списка List. Например, suffix{[b,c],\_a,b,c]) истинен. Оба предиката определены в программе 3.13. Для обеспечения корректности программ к исходным фактам надо добавить типовое условие list(Xs). Произвольный подсписок может быть определен в терминах префикса и суффикса, а именно как суффикс некоторого префикса или как префикс некоторого суффикса. Программа 3.14а задает логическое правило, согласно которому Xs
46 Часть I. Глава 3 prefix (Prefix,list) <- список Prefix есть префикс списка List. prefix([ ],Ys). prefix([X | Xs],[X | Ys]) <- prefix(Xs,Ys). sujfix(Suffix,list) <- список Suffix есть суффикс списка List. suffix(Xs,Xs). suffix(Xs,[Y | Ys]) «- suffix(Xs,Ys). Программа 3.13. Префикс и суффикс списка. sublist(Sub,list) <- Sub есть подсписок списка List. а: суффикс префикса sublist(Xs,Ys) «- prefix(Ps,Ys),suffix(Xs,Ps). b: префикс суффикса sublist(Xs,Ys) «- prefix(Xs,Ss), suffix(Ss,Ys). с: рекурсивное определение отношения sublist sublist(Xs,Ys) «- prefix(Xs,Ys). sublist(Xs,[Y | Ys]) <- sublist(Xs,Ys). d: суффикс префикса с использованием append sublist(Xs,AsXsBs) <- append(As,XsBs,AsXsBs),append (Xs,Bs,XsBs). e: префикс суффикса с использованием append sublist(Xs,AsXsBs) <- append(AsXs,Bs,AsXsBs), append(As,Xs,AsXs). Программа 3.14. Определение подсписка списка. является подсписком Ys, если существует такой префикс Ps списка Ys, что Xs-суффикс списка Ps. Программа 3.14b задает двойственное определение подсписка как префикса некоторого суффикса. Предикат prefix может быть также использован в качестве основы рекурсивного определения отношения sublist. Такое определение дается программой 3.14,с. Базовое правило гласит, что префикс списка является подсписком списка. В рекурсивном правиле записано, что подсписок хвоста списка является подсписком самого списка. Предикат member можно считать частным случаем предиката sublist, задав правило member(X,Xs) <- sublist([X],Xs). Основной операцией над списками является соединение двух списков для получения третьего. Эта операция задает отношение append(Xs,Ys,Zs) между двумя списками Xs, Ys и результатом их соединения, списком Zs. Программа 3.15, задающая append, имеет структуру, идентичную структуре программы 3.5 для отношения plus. На рис. 3.4 приводится дерево вывода цели appendi\a,b\\c,d\,\a,b,c,d§. Вид дерева наводит на мысль, что размер дерева линейно зависит от размера первого списка. В общем случае если список Xs содержит п элементов, то дерево вывода цели append(Xs,Ys,Zs) содержит п + 1 вершину.
Рекурсивное программирование 47 append(Xs,Ys, XsYs) <- XsUs- результат соединения списков Xs и Ys. append([], Ys, Ys). append([X | Xs],[X | Zs]) <- append(Xs,Ys,Zs). Программа 3.15. Соединение двух списков. append([a,b],[c,d],[a,b,c,d]) I append([b],[c,d],[b,c,d]) I append([ ],[c,d],[c,d]) Рис. З.4. Дерево вывода для соединения двух списков. Как и в случае отношения plus, существуют разнообразные использования отношения append. Основное использование состоит в соединении двух списков в один с помощью вопроса вида append([a,b,c]),[d,e]Xs)l, ответом на который будет Xs = [a,b,c,d,e]. Поиск ответа на вопрос вида append{Xs\c,d\, [a,b,c,d\V- сводится к нахождению разности Xs = [a,b~] между списками \_c,d] и [a,b,c,d]. Однако в отличие от отношения plus первые два аргумента отношения append не симметричны, и поэтому существуют два различных варианта нахождения разности между двумя списками. Процедурой, аналогичной разбиению натурального числа, является процедура расщепления списка. Например, при обработке вопроса append(As,Bs,[a,b,c,d])l ищутся такие списки As и Bs, что соединение списков As и Bs дает список [a,b,c,d\. Вопросы о расщеплении списка становятся более содержательными при частичном уточнении вида получаемых списков. Введенные выше предикаты member, sublist, prefix и suffix могут быть определены в терминах предиката append, если последний использовать для расщепления списков. Наиболее просто определяются предикаты prefix и suffix, в определении непосредственно указывается, какая из двух частей расщепления нас интересует: prefix (Xs, Ys)«- append (Xs, As, Ys). suffix (Xs, Ys)«- append (As, Xs, Ys). Отношение sublist определяется с использованием двух целей вида append. Существуют два различных варианта определения, они приведены в программах 3.14d и 3.14е. Эти две программы получены соответственно из программ 3.14а и 3.14Ь заменой целей вида prefix и suffix на их выражения в терминах предиката append. Отношение member может быть определено через отношение append следующим образом: member(X,Ys)«- append (As, [X | Xs], Ys). В определении сказано, что X является элементом списка Ys, если Ys может быть расщеплен на два списка, причем Х-голова второго списка. Аналогичное правило может быть приведено для отношения adjacent(X,Y,Zs), истинного, если X и У являются соседними элементами списка Zs: adjacent(X, Y,Zs) <- append(As,[X, Y | Ys],Zs).
48 Часть I. Глава 3 Другое отношение, легко задаваемое с помощью отношения append, состоит в определении последнего элемента списка. В правиле указывается, что второй аргумент предиката append должен иметь вид списка из одного элемента: last(X,Xs) <- append(As,[X],Xs). Многократное применение предиката append может быть использовано для определения отношения reverse (List, Tsil). Подразумеваемое значение предиката reverse состоит в том, что список Tsil содержит элементы списка List, расположенные в обратном порядке по сравнению с порядком в списке List. Примером цели, входящей в подразумеваемое значение программы, является reverse([a,b,c],[c,b,a]). Простейший вариант, приведенный в программе 3.16а, является логическим эквивалентом рекурсивного определения на естественном языке: рекурсивно обратить хвост списка и добавить затем первый элемент в конец обращенного хвоста. reverse (list, Tsil) «- список Tsil есть обращение списка list. а: «наивное» обращение списка reverse([ ],[ ]). reverse([X | Xs],Zs)<- reverse(Xs,Ys), append(Ys,[X],Zs). b: обращение с накоплением reverse(Xs,Ys) <- reverse(Xs,[ ],Ys). reverse([X | Xs],Acc,Ys) <- reverse(Xs,[X | Acc],Ys). reverse([ ],Ys,Ys). Программа 3.16. Обращение списка. Имеется иной способ определения предиката reverse (Xs, Ys,Zs), прямо не использующий обращений к предикату append. Определим вспомогательный предикат reverse (Xs, Ys,Zs), истинный, если Zs- результат соединения списка Ys с элементами обращенного списка Xs. Это определение приведено в программе 3.16Ь. Связь предикатов reverse/З и reverse/2 задана в первом правиле программы 3.16Ь. Программа 3.16Ь эффективнее программы 3.16а. Рассмотрим рис. 3.5, на котором приведено дерево вывода цели reverse{[a,b,c,d],[d,c,b,a~\) для обеих программ. В общем случае размер дерева вывода для программы 3.16а квадратично зависит от размера обращаемого списка, в то время как для программы 3.16Ь эта зависимость линейна. Преимущество программы 3.16Ь обусловлено лучшей структурой данных, представляющих последовательность элементов; мы обсудим это подробнее в гл. 7 и 15. Последняя программа этого раздела, программа 3.17, выражает отношение между числами и списками, основанное на рекурсивной структуре этих объектов. Предикат length (Xs,N) истинен, если длина списка Xs равна N, то есть список Xs содержит N элементов, где N-натуральное число. Например, length([a,b~],s{s{0))) означает, что список [а,Ь~] содержит два элемента, эта цель принадлежит значению программы. length(Xs,N) <- список Xs содержит N элементов. length([ ],0). length([X | Xs],s(N)) <- length(Xs,N). Программа 3 17. Определение длины списка.
Рекурсивное программирование 49 reverse([a,b,c,d],[d,c,b,a]) reverse([b,c,d],[d,c,b]) appended,с,b],[a],[d,с,b,a]) reverse([c,d],[d,c]) appended,с],[b],[d,c,b]) append([c,b],[a],[c,b,a]) / \ \ \ reverse([d],[d]) appended],[с],[d,c]) append([c],[b],[c,b]) append ([b], [a], [b, a]) X \ \ \ \ reverse([ ],[ ]) append([ ],[d],[d]) append([ ],[c],[c]) append([ ],[b],[b]) append([ ],[a],[a]) reverse([a,b,c,d],[d,c,b,a]) I reverse([a,b,c,d],[ ],[d,c,b,a]) I reverse([b,c,d],[a],[d,c,b,a]) I reverse([c,d],[b,a],[d,c,b,a]) I reverse( [d], [c,b,a], [d,c,b,a]) I reverse([ ],[d,c,b,a],[d,c,b,a]) Рис. З.5. Дерево вывода при обращении списка. Давайте рассмотрим различные возможности использования программы 3.17. Вопрос length([a,b],X)l сводится к вычислению длины списка [я,6], равной 2. В этом случае length рассматривается как функция от списка, имеющая функциональное определение length([ ])=.0. length ([X,Xs]) = sflength(Xs)). В вопросе length([a,b],s(s(0)))1 проверяется, имеет ли список [а,Ь~] длину 2. Вопрос length (Xs,s(s(0)))? порождает списки длины 2 с различными элементами. Упражнения к рам, 3,2 1. Следующие три правила задают вариант программы 3.14 для отношения sublist: subsequence([X | Xs],[X | Ys]) <- subsequence(Xs,Ys). subsequence(Xs,[Y | Ys]) <- subsequence(Xs,Ys). subsequence([ ],Ys). Объясните, почему значение этой программы отличается от значения программы 3.14. 2. Напишите рекурсивные программы для предикатов adjacent и last с теми же значениями, что и у предикатов, определенных ранее через append. 3. Напишите программу для отношения double (List, List List), в котором каждый элемент списка List удваивается в списке List List, например, double ([1,2,3], [1,1,2,2,3,3]) выполнено.
50 Часть I. Глава 3 4. Определите размер дерева вывода как функцию от размера входного списка для программ 3.16а и 3.16Ь, определяющих отношение reverse. 5. Определите отношение sum (List Of Integer,Sum), которое выполнено, если число Sum является суммой элементов списка List Of Integer: а) используя предикат plus/3; б) без использования каких-либо вспомогательных предикатов. (Замечание: достаточно трех аксиом.) 3.3. Построение рекурсивных программ До сих пор нами не давалось никаких пояснений, как же строятся примеры логических программ. Можно утверждать, что искусство построения логических программ постигается в процессе обучения и усвоения, во многом определяемом практикой. Для простых отношений наилучшая аксиоматизация обладает эстетическим изяществом, которое делает корректность их записи очевидной. Однако в процессе решения упражнений читатель мог заметить разницу между пониманием и построением изящных логических программ. В этом разделе приводятся дополнительные примеры программ, обрабатывающих списки. При их обсуждении, однако, основное внимание уделяется тому, как могут быть построены эти программы. Разъясняются два принципа: как совместить процедурное и декларативное понимание и как разрабатывать программы по нисходящей методологии. Мы приводили два типа понимания предложений: процедурное и декларативное. Как же они сочетаются при построении программ? На практике при программировании пользуются процедурным пониманием. Однако при рассмотрении проблемы истинности и определении значения программы применяется декларативное понимание. Один из способов совмещения этих двух типов понимания в логическом программировании состоит в процедурном построении программ с последующей интерпретацией результата как декларативного утверждения. Мы создаем программу, ориентируясь на предписанное использование программы, и далее выясняем, имеют ли различные использования программы декларативный смысл. Применим эти рассуждения к программе удаления элементов из списка. Первый и наиболее важный шаг состоит в определении подразумеваемого значения отношения. Для удаления элемента из списка требуются три аргумента: удаляемый элемент X, список L/, в который может входить X, и список L2, из которого удалены все вхождения элемента X. Подходящей реляционной схемой является delete (L1,X,L2). Естественное значение состоит из всех основных примеров, в которых список L2 получен из списка Ы удалением всех вхождений X. При построении программы проще всего рассматривать некоторое конкретное использование. Рассмотрим вопрос delete([a,b,c,b],b,X)l, типичный пример нахождения результата удаления элемента из списка. Ответом служит X = [а,с]. Программа будет рекурсивной по первому аргументу. Начнем с процедурного подхода. Рассмотрим рекурсивную часть. Обычный вид рекурсивного аргумента для списка есть [X | Xs]. Следует рассмотреть две возможности: X является удаляемым элементом и X отличен от удаляемого элемента. В первом случае искомым результатом будет рекурсивное удаление X из Xs. Подходящим правилом является delete([X | Xs],X, Ys) <- delete(Xs,X, Ys). Сменим подход на декларативный: «Удаление X из списка [X | Xs] приводит к Ys, если удаление X из Xs приводит к Ys». Условие совпадения головы списка и удаляемого элемента задано с помощью общей переменной в заголовке правила.
Рекурсивное программирование 51 Второй случай, в котором удаляемый элемент отличен от головы списка -X, подобен первому. Искомым результатом будет список, голова которого X, а хвост получен рекурсивным удалением элемента. Нужное правило: delete([X|Xs],Z,[X| Ys])<-X Ф Z,delete(Xs,Z,Ys). Декларативное понимание: «Удаление Z из списка \Х \ Xs~\ равно \Х \ Ys~\, если Z отлично от X и удаление Z из Xs приводит к Ys». В отличие от предыдущего правила условие неравенства головы списка и удаляемого элемента явно задано в теле правила. Исходный случай задается непосредственно. Из пустого списка не удаляется ничего, результатом будет также пустой список. Это выражается фактом deleted ],X,[ ]). Полная программа скомпонована в виде программы 3.18. delete(list,X,HasNoXs) <- список HasNoXs получен в результате удаления всех вхождений элемента X из списка list. delete([X | Xs],X,Ys) <- delete(Xs,X,Ys). delete([X | Xs],Z,[X | Ys]) <- X Ф Z, delete(Xs,Z,Ys). delete([ ],X,[ ]). Программа 3.1S. Удаление всех вхождений элемента из списка. Давайте взглянем на написанную программу и рассмотрим другие возможности формулировок. Опуская условие X = Z во втором правиле программы 3.18, получим иной вариант отношения delete. Значение этого варианта менее естественно, так как в этом случае может быть удалено любое количество вхождений. Например, все цели de/ete([a Дс,6] Д [я,с]), delete{[a,b,c],[a,c,b~]\ delete([a,b,c,b~],b,\_a,b,c] и delete(la,b,b],b, [a,b,c,b]) принадлежат значению этого варианта. И значение программы 3.18, и значение указанного выше варианта содержат примеры, в которых удаляемый элемент вообще не входит в исходный список, например, выполнено delete{[a],b,[a])- Существуют приложения, в которых это нежелательно. Программа 3.19 определяет отношение select(X,Ll,L2), в котором случай списка, не содержащего элемент X, рассматривается иным образом. Значением отношения select (X,L1,L2) является множество всех основных примеров, в которых список L2 получается из списка Ы удалением ровно одного вхождения X. select(X,HasXs,OneLessXs) <- список OneLessXs получен в результате удаления одного вхождения X из списка HasXs. select(X,[X | Xs],Xs). select(X,[Y | Ys],[Y | Zs]) <- select(X,Ys,Zs). Программа 3.19. Выделение элемента из списка. Эта программа является гибридом программы 3.12 для отношения member и программы 3.18 для отношения delete. Декларативное понимание программы: «Выделение элемента X из списка [X \ Xs~] приводит к Xs или выделение X из списка [Y\ Ys] приводит к [7|Zs], если выделение X из списка Ys приводит к Zs». Мы используем отношение select как вспомогательное отношение в приводимой далее «наивной» программе сортировки списков. Основное внимание при программировании следует уделять нисходящей методологии разработки, совмещаемой с пошаговым уточнением. Говоря нестрого, данная методология состоит в постановке общей задачи, разделении ее на подзадачи
52 Часть I. Глава 3 и решении отдельных частей. Программирование «сверху вниз»-один из наиболее естественных методов построения логических программ. Наше описание программ на протяжении всей книги будет в основном соответствовать этой методологии. Оставшаяся часть раздела посвящена описанию построения двух программ сортировки списков: сортировка перестановкой и быстрая сортировка. Внимание будет сосредоточено на применении нисходящей методологии. Логическая спецификация сортировки списка состоит в нахождении упорядоченной перестановки исходного списка. Эта спецификация может быть непосредственно записана в виде логической программы. Основная реляционная схема -sort (Xs, Ys), здесь >!?-список, содержащий элементы списка Xs, расположенные в порядке возрастания: sort(Xs, Ys) <- permutation (Xs, Ys), ordered (Ys). Цель верхнего уровня-сортировка-теперь разделена. Далее нам следует определить отношения permutation и ordered. Проверка того, что элементы списка расположены в возрастающем порядке может быть выражена двумя приведенными ниже предложениями. Факт утверждает, что список из одного элемента всегда упорядочен. Правило утверждает, что список упорядочен, если первый элемент списка не больше второго и остаток списка, начинающийся со второго элемента, упорядочен: ordered ([X]). ordered([X, Y | Ys]) <- X ^ Y, ordered([Y | Ys]). Программа для отношения permutation сложнее. Один из способов перестановки списка состоит в недетерминированном выделении того элемента, который будет первым в переставленном списке, и рекурсивной перестановке остатка списка. Мы выразим этот способ в виде логической программы для отношения permutation, использовав программу 3.19 для отношения select. Факт утверждает, что единственная перестановка пустого списка совпадает с ним самим: permutation (Xs,[Z | Zs]) <- select(Z,Xs,Ys), permutation (Ys,Zs). permutation([ ],[ ]). Другой процедурный подход к порождению перестановок списков состоит в рекурсивной перестановке хвоста списка и вставке головы списка в произвольное место. Этот подход тоже допускает прямую запись. Исходный факт совпадает с фактом предыдущей версии: permutation([X | Xs],Zs)«- permutation(Xs, Ys), insert (X,Ys,Zs). permutation([ ],[ ]). Предикат insert может быть определен в терминах программы 3.19. insert(X, Ys,Zs) <- select(X,Zs, Ys). Обе процедурные версии отношения permutation имеют ясную декларативную интерпретацию. Скомпонованная программа 3.20-это программа «наивной» сортировки (так мы назвали сортировку перестановкой). Она является примером подхода «порождения и проверки», который будет подробно обсужден в гл. 14. Проблема сортировки списков глубоко изучена. На практике сортировка перестановкой не является хорошим методом сортировки списков. Применение стратегии «разделяй и властвуй» приводит к намного лучшим алгоритмам. Идея состоит в
Рекурсивное программирование 53 sort(Xs,Ys)<- список Ys упорядоченная перестановка списка Xs. sort(Xs,Ys) <- permutation(Xs,Ys),ordered(Ys). permutation(Xs,[Z | Zs]) <- select(Z,Xs,Ys),permutation(Ys,Zs). permutation([ ],[ ]). ordered([X]). ordered([X,Y | Ys]) <- X ^ Y, ordered([Y | Ys]). Программа 3.20. Сортировка перестановкой. сортировке списка путем разделения его на две части, рекурсивной сортировке частей и последующем объединении частей для получения отсортированного списка. Методы разделения и объединения должны быть уточнены. Имеются два крайних случая. В первом разделение производится сложно, зато объединение-просто. Этот подход и принят в алгоритме быстрой сортировки. Ниже приведена соответствующая логическая программа быстрой сортировки. Во втором случае объединение производится сложно, зато разделение-просто. Этот подход используется в сортировке слиянием, которая приведена в качестве упражнения (4) в конце раздела, и в сортировке вставкой, представленной программой 3.21. sort(Xs,Ys)<- список Ys упорядоченная перестановка списка Xs. sort([X | Xs],Ys) <- sort(Xs,Zs), insert(X,Zs,Ys). sort([ ],[ ]). insert(X,[ ],[X]). insert(X,[Y | Ys],[Y | Zs]) <- X > Y, inscrt(X,Ys,Zs). insert(X,[Y | Ys],[X,Y | Ys]) <- X ^ Y. Программа 3.21. Сортировка вставкой. В сортировке вставкой один элемент (обычно первый) удаляется из списка. Остаток списка сортируется рекурсивно, затем элемент вставляется в список с сохранением порядка в списке. Идея быстрой сортировки состоит в разделении списка путем выбора произвольного элемента списка и дальнейшего разбиения списка на элементы, меньшие выбранного, и элементы, большие выбранного. Отсортированный список состоит из меньших элементов, за которыми следует выбранный элемент и далее - большие элементы. В описываемой программе в качестве основы разбиения берется первый элемент. Программа 3.22 определяет отношение sort с помощью алгоритма быстрой сортировки. Рекурсивное правило для sort означает: «Ys есть отсортированный вариант [А^АУ], если списки Littles и Bigs - результат разбиения списка Xs на элементы, меньшие X и большие X соответственно; Ls и Bs - результаты рекурсивной сортировки Littles и Bigs, и Ys есть результат соединения Ls и [X \ Bs]». Программа разбиения списка описывается непосредственно и подобна программе удаления элементов. Следует рассматривать два случая: голова текущего списка (1) меньше и (2) больше элемента, определяющего разбиение. Декларативное понимание первого предложения partition: «Разбиение списка с головой X и хвостом Xs в соответствии с элементом Удает списки \_Х \ Littles'] и Bigs, если X не больше У,
54 Часть I. 1 лава 3 sort(Xs,Ys) список Ys - упорядоченная перестановка Xs. quicksort([X | Xs],Ys) <- partition(Xs,X,Littles,Bigs), quicksort(Liltles,Ls), quicksort(Bigs,Bs), append(Ls,[X | Bs],Ys). quicksort([ ],[ ]). partition([X | Xs],Y,[X | Ls],Bs) <- X ^ Y, partition(Xs,Y,Ls,Bs). partition([X | Xs],Y,Ls,[X | Bs]) <- X > Y,partition(Xs,Y,Ls,Bs). partition([ ],Y,[ ],[ ]). Программа 3.22. Быстрая сортировка. и разбиение списка Xs в соответствии с У дает списки Littles и Bigs». Второе предложение для отношения partition понимается аналогично. Исходный факт: пустой список разбивается на два пустых списка. Упражнения к разд. 3.3 1. Напишите программу для предиката substitute(X,Y,L1,L2), где L2-результат подстановки У вместо всех вхождений X в L/, например, substitute(a,x,\_a,b,a,c],\_x,b,x,c]) истинно, a substitute(a,x,[_a,b,a,c],[a,b,x,c]) ложно. 2. Что является значением следующего варианта отношения select! select(X,[X|Xs],Xs). select(X,[Y | Ys],[Y | Zs]) <- X Ф Y, select(X,Ys,Zs). 3. Напишите программу для отношения no_doubles (LI,L2'), где L2-результат удаления всех повторных вхождений элементов в L/, например, no_doubles([a,b,c,b~],[_a,c,b~\) истинно. (Указание: используйте member). 4. Напишите программы для отношений even_permutation(Xs,Ys) и odd permutation (Xs, Ys) для определения четности (соответственно нечетности) перестановки Ys списка Xs. Например, even_permutation§_l,2,3~],[_2,3,l~\) и odd_permutation([\,2,y\,[_2,l,3~\) истинны. 5. Напишите программу сортировки слиянием. 3.4. Бинарные деревья Следующий рекурсивный тип данных, который мы рассмотрим,-бинарные деревья. Такие структуры имеют важное значение во многих алгоритмах. Бинарные деревья задаются с помощью тернарного функтора tree (Element, Left, Right), где Element-элемент, находящийся в вершине, a Left и Right-соответственно левое и правое поддерево. Пустое дерево изображается атомом void. Например, дерево а /\ Ь с может быть задано как tree (a, tree (b,void,void),tree (с,void,void)). Логические программы, работающие с бинарными деревьями, подобны программам, работающим со списками. Как и в случаях натуральных чисел и списков, начнем с типового определения бинарного дерева. Оно дается программой 3.23.
Рекурсивное программирование 55 binary-tree (Tree) <- Tree - бинарное дерево. binary _tree( void). binary_tree(tree(Element,Left,Right)) binary _ tree(Left),binary _ tree(Right). Программа 3.23. Определение бинарного дерева. Отметим, что программа-с двойной рекурсией, т.е. в теле рекурсивного правила имеются две цели с тем же предикатом, что и в заголовке правила. Этот эффект возникает благодаря двойной рекурсивной природе бинарных деревьев и может быть замечен и в остальных программах данного раздела. Давайте напишем некоторые программы обработки деревьев. Наш первый пример состоит в проверке, появляется ли некоторый элемент в дереве. Реляционная схема - tree_member (Element, Tree). Отношение выполнено, если элемент Element является одной из вершин дерева Tree. В программе 3.24 приведено определение. Декларативное понимание программы: «Х-элемент дерева, если X находится в вершине (согласно факту), или Х-элемент левого или правого поддерева (согласно двум рекурсивным правилам)». tree -member (Element, Tree) <- Element является элементом бинарного дерева Tree. tree_member(X,tree(X,Left,Right)). tree_member(X,tree(Y,Left,Right)) <- tree_member(X,Left). tree_member(X,tree(Y,Left,Right)) <- tree _ member(X,Right). Программа 3.24. Проверка принадлежности дереву. Две ветви бинарного дерева различны, но во многих приложениях это различие несущественно. Следовательно, возникает полезное понятие изоморфизма, которое определяет, являются ли два неупорядоченных дерева по существу одинаковыми. Два бинарных дерева 77 и Т2 изоморфны 9 если Т2 может быть получено из 77 изменением порядка ветвей в поддеревьях. На рис. 3.6 изображены три простых бинарных дерева. Первые два изоморфны, третье не изоморфно им. Изоморфизм является отношением эквивалентности с простым рекурсивным определением. Два пустых дерева изоморфны. В случае непустых деревьев два дерева изоморфны, если элементы в вершинах дерева совпадают, и/или оба левых поддерева и оба правых поддерева изоморфны, или левое поддерево одного дерева изоморфно правому поддереву другого и два других поддерева тоже изоморфны. <Х /0 0\ b с b с ас Рис. 3.6. Сравнение деревьев с точностью до изоморфизма. Программа 3.25 определяет предикат iso tree (Tree 1 ,Tree2), который истинен, если дерево Tree! изоморфно дереву Тгее2. Аргументы входят в предикат симметрично. Программы, относящиеся к бинарным деревьям, используют двойную рекурсию,
56 4acib I. Глава 3 isotree ( Tree I, Tree2) бинарные деревья Treel и Тгее2 изоморфны. isotree(void,void). isotree(tree(X,Left 1 ,Right 1 ),tree(X,Left2,Right2)) <- isotree(Left 1 ,Left2), isotree(Right 1 ,Right2). isotree(tree(X,Left 1,Right !)ytree(X,Left2,Right2))^- isotree(Leftl,Right2),isotree(Rightl,Left2). Ilpoipa.viMa 3.25. Определение изоморфизма деревьев. по одной на каждую ветвь дерева. Двойная рекурсия может проявляться двумя способами. В программе могут рассматриваться два отдельных случая, как в программе 3.24 для отношения tree member. В отличие от этого программа 3.12, проверяющая принадлежность элемента списку, содержит лишь один рекурсивный случай. При другом способе в теле рекурсивного правила содержатся два рекурсивных вызова, как в каждом рекурсивном правиле для isotree в программе^.25. Задание упражнения 3.3 (1) состоит в том, чтобы написать программу подстановки элементов в списки. Аналогичная программа может быть написана для подстановки элементов в бинарные деревья. Предикат substitute(X,Y,OldTree,NewTree) выполнен, если дерево New Tree получается из дерева О Id Tree заменой всех вхождений элемента X на У. Аксиоматизация отношения substitute/4 приведена в программе 3.26. substitute (X,Y,TreeX,TreeY) <- бинарное дерево TreeY результат замены всех вхождений X в бинарном дереве ТгееХ на Y substitutc(X,Y,void,void). substitute(X,Y,trce(X,Left,Right),tree(Y,Leftl,Rightl))^- substitute(X, Y,Left,Lcft 1), substitute(X,Y,Right,Rightl). substituted, Y,trec(Z,Left,Right), tree(Z,Leftl, Right 1)) <- X^Z, substitute(X,Y,Left,Lcftl), substitute(X,Y,Right,Rightl) lipoipa.vivia 3.26. Подстановка терма в дерево. Во многих приложениях, использующих деревья, требуется доступ к элементам, указанным в вершинах. Основной является идея обхода дерева в предписанном порядке. Имеются три возможности линейного упорядочения при обходе: сверху вшп, когда сначала идет значение в вершине, далее вершины левого поддерева, затем вершины правого поддерева; слева направо, когда сначала приводятся вершины левого поддерева, затем вершина дерева и далее вершины правого поддерева; и наконец, аппу вверх, когда значение в вершине приводится после вершин левого и правого поддерева. Определение каждого из этих трех обходов приведено в программе 3.27. Рекурсивная структура определений одинакова, единственное отличие состоит в порядке элементов, полученных применением целей вида append.
Рекурсивное программирование 57 pre ..order (Tree,Pre) <- Pre-обход бинарного дерева Tree сверху вниз. pre. order(tree(X,L,R),Xs) <- pre_order(L,Ls),pre_order(R,Rs),append([X | Ls],Rs,Xs). pre_order(void,[ ]). in_order(Tree,ln) <- In -обход бинарного дерева Tree слева направо. in_order(tree(X,L,R),Xs) <- in_order(L,Ls), in_order(R,Rs),append(Ls,[X | Rs],Xs). in._order(void,[ ]). post .order (Tree,Post) <- Post обход бинарного дерева Tree снизу вверх. post_order(tree(X,L,R),Xs) <- post. order(L,Ls), post_order(R,Rs), append(Rs,[X],Rsl), append(Ls,Rs 1 ,Xs). post _order(void,[ ]). Программа 3.27. Обходы бинарного дерева. Упражнения к разд. 3.4 1. Составьте программу subtree(S,T), определяющую, является ли S поддеревом Т. 2. Определите отношение sum_tree(TreeOJIntegers,Sum), выполненное, если число Sum равно сумме целых чисел, являющихся вершинами дерева Tree Of Integers. 3. Определите отношение ordered (Tree), выполненное, если дерево Tree является упорядоченным деревом целых чисел, т. е. число, стоящее в любой вершине дерева, больше любого элемента в левом поддереве и меньше любого элемента в правом поддереве. (Указание: Определите два вспомогательных отношения orderedjeft(X,Тгее) и ordered_right(X,Тгее), выполненных, если X меньше (больше) корня дерева Tree и дерево Tree упорядочено.) 4. Определите отношение treejnsert(X,Tree,TreeI), выполненное, если упорядоченное дерево Treel получается вставкой элемента X в упорядоченное дерево Tree. Если X уже присутствует в Tree, то Treel совпадает с Tree. (Замечание: Достаточно четырех аксиом.) 3.5. Работа с символьными выражениями До сих пор в данной главе рассматривались примеры логических программ, работающих с натуральными числами, списками и бинарными деревьями. Однако программирование имеет более широкую область применения. В этом разделе приводятся четыре примера рекурсивных программ - программа описания многочленов, программа символьного дифференцирования, программа решения задачи о ханойской башне и программа проверки выполнимости булевой формулы. Первый пример - программа, распознающая многочлены от переменной X. Многочлены определяются индуктивно. Как сама переменная X, так и любая константа являются многочленами от X. Сумма, разность и произведение многочленов от X являются многочленами от X. Многочленами также являются результаты возведения многочлена в целую степень и деления многочлена на ненулевую константу. Примером многочлена от х служит х2 — Ъх + 2. Данное утверждение следует из того, что это выражение есть сумма х2 — Зх и 2, принадлежность х2 — Ъх множеству многочленов устанавливается рекурсивно. Записав в требуемой форме приведенные
58 Час гь I. Глава 3 выше неформальные правила, мы получим логическую программу, распознающую многочлены. Программа 3.28 задает отношение polynomial (Expression^), истинное, если выражение Expression есть многочлен от переменной X. Приведем декларативное понимание двух правил программы. Факт polynomial (X,X) утверждает, что сам терм X есть многочлен. Правило polynomial (Term 1 + Term2,X) <- polynomial (Term 1, X), polynomial (Term2, X). утверждает, что сумма Terml + Term2 есть многочлен от X, если Terml и Тегт2- многочлены от X. Дополнительными соглашениями, принятыми в программе 3.28, являются использования одноместного предиката constant для распознавания констант и бинарного функтора «|» для обозначения возведения в степень. Терм X\Y обозначает XY. polynomial(Expression^) выражение Expression является многочленом от X. polynomial(X,X). polynomial(Term,X) <- constant(Term). polynomial(Terml + Term2,X)<- polunomial(Term 1 ,X), polunomial(Term2,X). polynomial(Terml — Term2, X)<- polynomial(Term 1 ,X), polynomial(Term2,X). polynomial(Term 1 * Term2, X) <- poly nomial(Term 1 ,X,) polynomial(Term2,X). polynomial(Term 1 /Term2,X) <- polynomial(Term 1,X), constant(Term2). polynomial(Term | N,X) <- natural_number(N),polynomial(Term,X). Программа 3.28. Распознавание многочленов. Следующий пример - программа нахождения производной. Реляционной схемой будет derivative ('Expression, X\DifjExpression). Подразумеваемое значение - выражение DiffExpression есть производная выражения Expression по переменной X. Как и в случае программы распознавания многочленов, логическая программа дифференцирования - просто набор соответствующих правил дифференцирования, записанный в корректном синтаксисе. Например, факт derivative(X,X,s(0)). означает, что производная X по X есть 1. Факт derivative(sin (X), X, cos (X)). означает, что производная sin(X) по X есть cos(X). Мы могли бы использовать и обычную математическую запись. Типичный набор функций и их производных описан в программе 3.29. derivative(Expression^.DiffExpression) DiffExpression -производная выражения Expression по X. derivative(X,X,s(0)). derivative^ | s(N),X,s(N) * X ] N). derivative(sin(X),X,cos(X)). derivative(cos(X),X, — sin(X)). derivative^ ] X,X,e | X). derivative(log(X),X,l/X). Программа 3.29. Правила дифференцирования..
Рекурсивное программирование 59 derivative^ + G,X,DF + DG) <- derivative(F,X,DF),derivative(G,X,DG). derivative^ - G,X,DF - DG) <- derivative(F,X,DF),derivative(G,X,DG). derivative(F*G,X,F*DG + DF*G) <- derivative(F,X,DF),derivative{G,X,DG). derivative(l/F,X, -DF/(F*F)) <- derivative(F,X,DF). derivative(F/G,X,(G*DF - F*DG)/(G*G)) <- derivative(F,X,DF),derivative(G,X,DG). Программа 3.29. (Продолжение). Сумма и произведение термов дифференцируются согласно соответствующим правилам для суммы и произведения. Правило дифференцирования суммы утверждает, что производная суммы есть сумма производных. Соответствующее предложение выглядит так: derivative^ + G,X,DF + DG) <- deri vative(F, X, DF), derivative(G, X, DG). Правило дифференцирования произведения чуть сложнее, но и здесь логическое предложение точно следует математическому утверждению: derivative(F*G,X,F*DG + DF*G) <- derivative^, X, DF), deri vati ve(G, X, DG). В программе 3.29 имеются также правила дифференцирования обратной величины и дифференцирования отношения. Правило дифференцирования сложной функции представляет несколько более тонкий случай. В правиле утверждается, что производная от f(g(x)) no x есть производная/У#(%)) по д(х), умноженная на производную д(х) по х. В данной- форме правило использует квантор по функциям и находится вне области логического программирования, рассматриваемой нами. Для каждой конкретной функции, однако, мы можем написать нужный вариант правила. Мы дадим в качестве примера правило дифференцирования XN и sin (X): derivative(UTs(N),X,s(N)*UTN*DU) <- derivative(U,X,DU). derivative(sin(U),X,cos(U)*DU) <- derivative(U,X,DU). Трудности формализации правила дифференцирования сложной функции объясняются нашим выбором представления термов. Обе программы-3.28 и 3.29- используют «естественное» математическое представление термов, при котором терм представляет сам себя. Такой терм, как sin(X), представляется с помощью унарного функтора sin. Если выбрать другое представление, например unary-term (sin,X), в котором имя структуры стало доступно, то проблема с правилом дифференцирования сложной функции будет снята. Правило может быть сформулировано следующим образом: derivative(unary_term(F, U), X, DF * DU) ^~ derivative(unary_term(F,U),U,DF),derivative(U,X,DU). Отметим, что при такой записи термов все правила программы 3.29 должны быть переформулированы в терминах нового способа представления, при этом выглядеть они будут менее естественно. Люди считают автоматическое упрощение выражений при дифференцировании
60 Часть I. Глава 3 само собой разумеющимся. В программе 3.29 упрощение отсутствует. Ответом на вопрос derivative(3*X + 2,X,D)? будет D = (3*1 + 0*Х) + 0. Мы могли бы упростить D до числа 3, но это не предусмотрено в логической программе. Следующий пример - задача о «ханойской башне» - обычный начальный пример применения рекурсии. Задача состоит в перемещении башни из п дисков с одного стержня на другой, с использованием вспомогательного диска. Перемещения осуществляются в соответствии с двумя правилами: за один раз можно перенести лишь один диск, и больший диск нельзя устанавливать на меньший. Имеется легенда, связанная с этой задачей. Где-то в окрестностях Ханоя, который во времена появления легенды был уединенным городком, спрятан монастырь. Монахи выполняют там работу, возложенную на них Богом при создании мира,-решают описанную выше задачу с 3 золотыми стержнями и 64 золотыми дисками. В момент завершения работы мир должен рассыпаться в прах. Поскольку оптимальное решение задачи с п дисками требует 2п — 1 перемещений, нам не следует терять сон из-за подобной перспективы. Величина числа 264 достаточна для сохранения спокойствия. Реляционная схема решения ydjxd4^-hanoi(N,A,B,C,Moves). Отношение истинно, если Moves - последовательность перемещений для переноса башни из п дисков со стержня А на стержень В с использованием промежуточного стержня С. Это является некоторым обобщением обычного решения, при котором последовательность перемещений не вычисляется, а, скорее, выполняется. Запись перемещений использует бинарный функтор to, применяемый в виде инфиксного оператора. Терм X toY означает, что верхний диск со стержня X переносится на стержень Y. Программа, решающая задачу,-программа 3.30. hanoi(N,A,B,C,Moves) <- Moves-последовательность перемещений при решении задачи о ханойской башне с N дисками и тремя стержнями--А, В и С. hanoi(s(0),A,B,C,[A to В]). hanoi(s(N),A,B,C,Moves) <- hanoi(N,A,QB,Msl), hanoi(N,C,B,A,Ms2), append(Msl,[A to В | Ms2],Moves). Программа 3.30. Ханойская башня. Декларативное понимание основы решения-рекурсивного правила программы 3.30, следующее: «Moves есть последовательность перемещений s(N) дисков со стержня А на стержень В с использованием промежуточного стержня С, если Msl - последовательность, решающая задачу перемещения N дисков с А на С с промежуточным диском B,Ms2- последовательность, решающая задачу перемещения N дисков с С на В с промежуточным диском А, и Moves есть результат присоединения \_AtoB\Ms2] к Msl». Рекурсия обрывается при перемещении одного диска. Более изящен, но менее очевиден базис рекурсии, состоящий в перемещении в отсутствие дисков. Соответствующий факт: hanoi(0,A,B,C,[ ]). Заключительный пример относится к булевым формулам. Булева формула есть терм, определяемый следующим образом: константы true и false - булевы формулы; если X и Y- булевы формулы, то XV УДЛ Уи ~ Х- булевы формулы, здесь V и Л - бинарные инфиксные операторы дизъюнкции и
Рекурсивное программирование 61 конъюнкции, а ~-унарный префиксный оператор отрицания. Булева формула F истинна, если: F = 'true' F = X Л Y, Хи Y истинны F = X V Y, одна из формул X или Y (или обе) истинны F = ~ X, X ложна. Булева формула F ложна, если F = 'false' F = X Л Y, одна из формул X или Y (или обе) ложнл F = X V Y, XhY ложны F = ~ X, X истинна. Программа 3.31 представляет собой логическую программу, определяющую истинность и ложность булевой формулы. Так как она может использоваться в случае булевой формулы с переменными, то эта программа важнее, чем кажется на первый взгляд. Булева формула с переменными аьтолпими 9 если существует истинный пример формулы. Она опровержима, если существует ложный пример. Эти отношения и вычисляются в программе. satisfiable( Formula) <- Существует истинный пример булевой формулы Formula. satisfiable(true). satisfiable(X AY)+- satisfiable(X), satisfiable(Y). satisfiable(X V Y)«- satisfiable(X). salisfiable(X V Y)«- satisfiable(Y). satisfiable( ~ X) <- invalid(X). invalid (Formula) <- Существует ложный пример булевой формулы Formula. invalid(false). invalid(X V Y)«- invalid(X), invalid(Y). invalid(XAY)^-invalid(X). invalid(XAY)<-invalid(Y). invalid(~ Y) <- satisfiable(Y). Программа 3.31. Выполнимость булевых формул. Упражнения к разд. 3.5 1. Напишите программу, определяющую, в нормальной ли форме задана арифметическая сумма, т.е. имеет ли она вид А + В, где А константа, а В -сумма в нормальной форме. 2. Напишите определение типа «булева формула». 3. Напишите программу, распознающую логические формулы в конъюнктивной нормальной форме, т.е. формулы, являющиеся конъюнкцией дизъюнкций литералов, где литерал- атомарная формула или ее отрицание. 4. Напишите программу, задающую отношение negation_inwards(Fl,F2), которое выполнено, если логическая формула F2 получается из логической формулы F1 внесением всех операторов отрицания внутрь конъюнкций и дизъюнкций. 5. Напишите программу приведения логической формулы к конъюнктивному нормальному виду, т.е. к конъюнкции дизъюнкций литералов.
62 Часть I. Глава 4 3.6. Дополнительные сведения Многие программы, приведенные в этой главе, широко известны в среде специалистов по логическому программированию, и сведения о происхождении этих программ утеряны. Некоторые программы имеются, например, в книге Клоксина и Мелиша (Clocksin, Mellish, 1984)1} и в сборнике неравноценных коротких программ на Прологе, «Как это решить на Прологе» Коэлхо и др. (Coelho et al., 1980). Классический справочник по бинарным деревьям и сортировке-труды Кнута (Knuth, 1968 и 1975). Большинство основных программ арифметики и обработки списков имеет простую структуру, что позволяет автоматически доказывать многие теоремы об их корректности. Сошлемся, например, на работы (Boyer, Moore, 1979) и (Sterling, Bundy, 1982). Функция Аккермана обсуждается в работе (Peter, 1967). Глава 4 Вычислительная модель логических программ Вычислительная модель, использовавшаяся в первых трех главах, обладает сильными ограничениями. Все цели, появляющиеся в процессе вывода, основные. Все примеры правил, применяемые в дереве вывода для доказательства целей, также основные. В описанном абстрактном интерпретаторе предполагается, что подстановка, дающая требуемый основной пример, может быть правильно угадана. На самом деле нужная подстановка может быть вычислена, а не угадана. В этой главе излагается полная вычислительная модель логических программ. В первом разделе представлен алгоритм унификации, устраняющий угадывание при определении примера терма. Во втором разделе описывается соответственно модифицированный абстрактный интерпретатор и приводятся примеры вычислений логических программ. 4Л. Унификация Основу вычислительной модели логических программ составляет алгоритм унификации. Унификация является основой автоматической дедукции и логического вывода в задачах искусственного интеллекта. Напомним необходимую для описания алгоритма терминологию из гл. 1 и введем нужные новые определения. Вспомним, что терм t есть общий пример двух термов гх и г2, если существуют такие подстановки 0i и 02, что t равно /i9i и fJ02- Терм s называется более общим, чем терм /, если /-пример s, но s не является примером г. Терм s называется Х) Клоксин У., Мелиш К. Программирование на языке Пролог: Пер. с англ. - М.: Мир, \9%1.-Прим. ред.
Вычислительная модель логических программ 63 алфавитным вариантом терма /, если /-пример s и s-пример t. Алфавитные варианты совпадают с точностью до переименования переменных, входящих в термы. Например, термы member(X,tree(Left,X,Right)) и member(Y,tree (Left,Y,Z)) являются алфавитными вариантами. Унификатором двух термов называется подстановка, которая делает термы одинаковыми. Если существует унификатор двух термов, то термы называются унифицируемыми. Существует тесная взаимосвязь между унификаторами и общими примерами. Любой унификатор определяет общий пример, и обратно, любой общий пример определяет унификатор. Например, append(\_l,2,3\[3,4],List) и append([X\Xs],Ys,[X\Zs]) унифицируемы. Унифицирующая подстановка {X = l,Xs = \_2,3~\,Ys = \_3,4~\, List = \_l,Zs~\}. Общий пример, задаваемый этой подстановкой,-это append(\_l,2,3],\_3,4],\_l \Zs]). Наибольшим оощим унификатором, или и.о.у, двух термов называется унификатор, соответствующий наиболее общему примеру. Если два терма унифицируемы, то существует единственный наибольший общий унификатор. Единственность определена с точностью до переименования переменных. Это эквивалентно тому, что два терма имеют единственный, с точностью до изоморфизма, наибольший общий пример. Алгоритм унификации находит наибольший общий унификатор двух термов, если такой существует. Если термы не унифицируемы, алгоритм сообщает об отказе. Описываемый алгоритм унификации основан на решении системы равенств. Входом алгоритма являются два терма - Т, и Т2. Результатом работы алгоритма является н. о. у. двух термов, если термы унифицируемы, или отказ, если термы не унифицируемы. Алгоритм использует стек для хранения равенств, подлежащих решению, и ячейку памяти 0 для построения выходной подстановки. В начале работы ячейка 0 пуста, в стек помещается равенство Тх = Т2. Алгоритм состоит из цикла, в теле которого из стека считывается и обрабатывается одно равенство. Цикл завершается, если в процессе работы появляется сообщение об отказе или стек оказывается пустым. Рассмотрим возможные действия при считывании равенства S = Т. Простейший случай, когда S и Т- одинаковые константы или переменные. Тогда равенство корректно, и ничего делать не надо. Вычисление продолжается, из стека считывается следующее равенство. Если S - переменная, а Г-терм, не содержащий вхождений S, то происходит следующее. В стеке отыскиваются все вхождения переменной S и каждое из них заменяется на Т. Аналогично все вхождения S в 0 заменяются на 7^ Затем подстановка S = Т добавляется к 0. Существенно, что S не входит в 7^ Проверка, соответствующая словам «не содержит вхождений», известна как проверка на вхождение. Если Г-переменная, S-терм, не содержащий Т, т. е. Т удовлетворяет проверке на вхождение относительно S, то выполняется аналогичная последовательность действий. Равенства добавляются в стек, если S и Т- составные термы с одним и тем же главным функтором одной и той же арности, например: f(Sl9...,S„) и f(Tl9...,Tn). Для унификации термов следует совместно унифицировать каждую пару аргументов, что достигается помещением в стек п равенств вида S, = 7]. Во всех остальных случаях выдается сообщение об отказе, и работа алгоритма завершается. Если стек пуст, то термы унифицируемы и унификатор содержится в 0. Полный алгоритм приведен на рис. 4.1. Фраза «не входящая в» подразумевает проверку на вхождение. Мы не доказываем корректность алгоритма и не анализируем его сложность.
64 Часть I. Глава 4 Вход: Два терма Tt и Т2, которые надо унифицировать. Результат: Подстановка Э-н.о. у. термов Г; и Т2 или отказ. Алгоритм: Начальное значение подстановки Э пусто, стек содержит равенство т, = т2, failure : = false, пока стек не пуст и не failure, выполнить, считать X = У из стека ветвление Х-переменная, не входящая в У: заменить все вхождения X в стеке и в 9 на У добавить X = Y к Э. У-переменная, не входящая в X: заменить все вхождения У в стеке и в 9 на X добавить У= X к 9. X и У-одинаковые константы или переменные: продолжать X естьДА^, ..., Хп) и Уесть f(Ylt ..., Уя) для некоторого функтора/и п > 1: поместить Х{ = Yt для всех / = 1, ..., п в стек в остальных случаях: failure : = true если failure, то результат - отказ; иначе результат 9. Рис. 4.1. Алгоритм унификации. Читателей, интересующихся этими вопросами, отсылаем к литературе в последнем разделе данной главы. Рассмотрим попытку унификации термов appendi\a,b~\\c,d\Ls) и append([X\Xs\ Ys, [X\Zs~]). Начальное состояние стека: append(la,bllc,d],Ls) = append&X \ Xs], Ys,[X \ Zs]). Эти два терма имеют один и тот же функтор - append одной арности 3, поэтому в стек добавляются три равенства, соответствующие подтермам двух термов: [я,6] = [X\Xsl [с4Л =Ys и Ls = [X\Xs\. Следующее равенство \_a,b~] = [X\Xs] считывается из стека. Эти два составных терма имеют один и тот же функтор «•» арности 2, поэтому в стек добавляются два равенства а = X и [6] = Xs. Далее считывается равенство а = X. Ситуация совпадает со второй ветвью алгоритма на рис. 4.1. Х-переменная, не входящая в константу а. Все вхождения X в стеке заменяются на а. Одно равенство при этом изменяется, а именно: Ls = [X\Zs] превращается в Ls = [a|Zv]. Равенство X = а добавляется к начально пустой подстановке, и работа алгоритма продолжается. Считывается следующее равенство: [6] = Xs. Вновь работает вторая ветвь алгоритма. Равенство Xs = [fr] добавляется к множеству подстановок, стек проверяется на вхождения Xs. Таких вхождений нет, считывается следующее равенство. Все та же вторая ветвь используется и для равенства \_c,d\ = Ys. Новая подстановка Ys = \_c,d\ добавляется к имеющимся, и считывается последнее равенство Ls = \a\Zc\ Здесь применима симметричная первая ветвь алгоритма. Ls не входит в [tf|Zs], так что равенство добавляется к унификатору и алгоритм успешно завершает работу. Унификатором является подстановка {X = a,Xs = \b~],Ys = [c,d], Ls = [tf|Zs]}. Общий пример, полученный с помощью унификатора, -аррепd (\_a,bW_c,d\, [a\Zs']). Заметим, что при данной унификации в процессе работы подстановки не изменялись. Проверка на вхождение необходима, чтобы предотвратить попытку унифицировать такие термы, как s(X) и X. Эти термы не имеют конечного общего примера.
Вычислительная модель логических программ 65 В большинстве реализаций языка Пролог проверка на вхождение из прагматических соображений опускается. Этот вопрос будет обсуждаться в разд. 6.1. При реализации алгоритма унификации в конкретных языках программирования явных подстановок в равенства, размещенные в стеке, и в унификатор избегают. Вместо этого логические переменные и другие термы представляются ячейками памяти с различными значениями, и связь с переменной обеспечивается ссылкой из ячейки памяти, представляющей логическую переменную, на ячейку памяти, содержащую представление терма, связанного с переменной. Таким образом, вместо заменить все вхождения X и У в стеке и в 9 добавить X = Y к подстановкам используется построить в X ссылку на У. Упражнения к разд. 4.1 1. Что является н.о.у. термов append{[fr],\_c.d\,L) и append(\_X\Xs],Ys,[X\Zs])l 2. Что является н.о. у. термов hanoi(s(N), A,B,C,Ms) и hanoi(s(s(0)),a,b,c,Xs)! 4.2. Абстрактный интерпретатор логических программ Мы пересмотрим абстрактный интерпретатор, описанный в разд. 1.8, в свете алгоритма унификации. В результате будет построена полная вычислительная модель логических программ. Все ранее введенные понятия, такие, как редукция цели и протокол вычисления, имеют соответствующие аналоги в полной модели. Неформально процесс вычисления логической программы может быть описан следующим образом. Он начинается с некоторого исходного (возможно, конъюнктивного) вопроса G и завершается одним из двух результатов: успешное завершение или отказ. В случае успешного завершения результатом должен быть доказанный пример G. Для данного вопроса может существовать несколько успешных вычислений, дающих различные результаты. Кроме того, могут иметь место бесконечные вычисления, с которыми мы не связываем никаких результатов. Вычисление развивается с помощью редукции целей. На каждом этапе имеется некоторая резольвента, т.е. конъюнкция целей, которые следует доказать. Выбираются такая цель в резольвенте и такое предложение в логической программе, что заголовок предложения унифицируем с целью. Вычисление продолжается с новой резольвентой, полученной из старой заменой выбранной цели на тело выбранного предложения и последующим применением наибольшего общего унификатора заголовка предложения и выбранной цели. Вычисление завершается, если резольвента пуста. В этом случае мы говорим, что цель решена программой. Для более формального описания вычислений введем несколько полезных понятий. Вычислением цели Q = Q0 программой Р называется, возможно бесконечная, последовательность троек <();,(/,., Cf>; здесь Q{-конъюнктивная цель, Gr цель, входящая в Qit С, предложение А <- BY,„.,Bk в Р с таким переименованием, что новые символы переменных не встречаются в Qjt 0 <у < /. Для всех / > 0 цель Qi + j является или результатом замены (7, на тело С,- в Q, и применения подстановки Qh наибольшего общего унификатора термов (7, и А{ (А{-заголовок С,), или константой true, если Gr единственная цель в Qt и тело С, пусто, или константой отказ, если Gt и заголовок С,- не унифицируемы. 3 1402
66 Часть I. Глапа 4 Цели Bfii называются производными от цели Gj и правила Су Цель Gj = BikQ, где Bik входит в тело предложения С£, называется порожденной G{ и С,. Цель Gt называется предшественником любой порожденной ею цели. Две цели с общим предшественником называются родственными иачми. Протоколом вычисления (^„^„С,) логической программы называется последовательность пар <G„0;), где д\\-подмножество н.о.у. 0,, полученного на 1-й редукции и ограниченного переменными из G. Опишем абстрактный интерпретатор логических программ. Он является модификацией интерпретатора основных целей (рис. 1.1). Ограничение, состоящее в использовании основных примеров предложений при выполнении редукций, теперь снято. Вместо этого алгоритм унификации, примененный к выбранной цели и заголовку выбранного предложения, выбирает корректную подстановку для применения к новой резольвенте. Следует позаботиться об избежании конфликта имен переменных в правилах. Дело в том, что переменные в предложениях локальны. Поэтому переменные с одними и теми же именами, входящие в различные предложения, на самом деле различны. Это различие обеспечивается переименованием переменных в предложении всякий раз, когда предложение выбрано для выполнения редукции. Среди новых имен не должно быть имен, ранее использованных в процессе вычисления. Пересмотренная версия интерпретатора приведена на рис. 4.2. Интерпретатор решает вопрос G с помощью программы Р. Результатом работы интерпретатора будет пример G, если найдено доказательство такого примера, или отказ, если в процессе вычисления возник отказ. Заметим, что интерпретатор может и отказаться закончить вычисления. Вход: Логическая программа Р цель G Результат: G0, если это пример G, выводимый из Р, или отказ, если возник отказ Алгоритм: Начальная резольвента равна входной цели G Пока резольвента непуста, выполнить Выбрать такую цель А из резольвенты и такое (переименованное) предложение А' <- В j, В2, ..., Вп, п ^ 0 из Р, что А и А' унифицируемы с н, о. у. Э (если нет таких цели и правила, то выйти из цикла). Удалить А и добавить В Jf В2, ••-, Вп к резольвенте. Применить Э к резольвенте и к G. Если резольвента пуста, то результат -G, иначе результат-отказ. Рис. 4.2. Абстрактный интерпретатор логических программ. Пример вопроса, для которого найдено доказательство, называется решением вопроса. Метод, в соответствии с которым добавляются и удаляются цели в резольвентах, называется методом расписания интерпретатора. В абстрактном интерпретаторе метод расписания не уточнен. Рассмотрим решение вопроса appendda^Wjc^^Ls)! относительно программы 3.15, задающей отношение append с использованием приведенного интерпретатора. Начальная резольвента - append([я,Ь~\, [с,d],Ls). Поскольку имеется единственная цель, то она и выбирается для редукции. Выбранным правилом будет appendix \ Xs], Ys,[X \ Zs\) <- append(Xs, Ys,Zs). Унификатор цели и заголовка - {X = a,Xs = [fr],Ys = [c,d~\,Ls = \_a\Zs~\}. Подробное описание нахождения унификатора приведено в предыдущем разделе. Новой резольвентой будет пример терма append (Xs, Ys,Zs) при применении унификатора, т. е.
Вычислительная модель логических программ 67 appendi\b\\c,d\,Zs). Эта цель выбирается на следующей итерации цикла. Выбирается то же предложение для отношения append, но во избежание конфликта переменные должны быть переименованы. Выберем такой вариант: append(\_Xl\Xsl~], Ysl[Xl\Zst\) <- append(Xsl,Ys,Zsl). Унификатор заголовка й тли-{XI = b,Xsl = [ ], Ysl = \_c,d],Zs = \b\Zsl~\}. Новая резольвента - append ([ ], \_c,d], Zsl). Далее выбирается факт append{[ \Zs2,Zs2)\ вновь производятся необходимые переименования переменных. На этот раз унификатор- {Zs2 = \_c,d],Zsl = [c,d]}. Новая резольвента пуста, и вычисление заканчивается. Для получения результата вычислений применим соответствующую часть вычисленного н. о. у. Первая унификация привела к изменению Ls на \_а \ Zs]. Во второй унификации Zs превратилось в \b\Zsl\ Далее Zsl стало lc,d]. Объединяя, получим, что Ls имеет значение [а\ \b\ \_c,d]~]\ или, упрощенно, \_a,b,c,d]. Вычисление может быть представлено протоколом. Протокол описанного выше вычисления append приведен на рис. 4.3. Для того чтобы протокол был понятнее, цели напечатаны с отступом относительно предшественника. Цель имеет глубину отступа d+ У, если глубина отступа предшественника - d. append'([a,b~\,[c,d],Zs) Zs = \_a\ ZsY\ append([b],[c,d],Zsl) Zs\ = lb \ Zs2~] append^ ~],[c,d\,Zs2) Zs2 = [c,d] true Output: Zs = \_a,b,c,d] Рис. 4.З. Протокол объединения двух списков. В качестве другого примера рассмотрим решение вопроса сын (S,аран) в программе 1.2. Вопрос редуцируется с использованием предложения сын(Х,У) «- <^отец(Х,У), мужчина(X). Наибольший общий унификатор-{X = S,Y = аран}. Применение подстановки дает новую резольвенту -отец(аран,Б), мужчина (S). Это - конъюнктивная цель. В качестве очередной цели можно выбрать одну из двух целей. Выбор цели отец (аран, S) приводит к следующему вычислению. Цель унифицируется с фактом отец (аран,лот), имеющимся в программе, и вычисление продолжается с заменой S на лот. Новая резольвента -мужчина (лот), она является фактом программы, и вычисление завершается. Протокол вычисления приведен в левой части рис. 4.4. сын (S,apan) сын (S,apan) отец(арап,S) S = лот мужчина(S) S = лот мужчина (лот) отец (аран,лот) true true Рис. 4.4. Различные протоколы, дающие одинаковое решение. Другая возможность получить ответ S = лот состоит в выборе цели мужчина (S) раньше цели отец(аран,$). Эта цель редуцируется фактом мужчина (лот) с заменой S на лот. Новая резольвента-отец (аран,лот), которая с помощью соответствующего факта сводится к пустой резольвенте. Протокол этого вычисления приведен в правой части рис. 4.4. Решение вопроса, полученное с помощью абстрактного интерпретатора, может содержать переменные. Рассмотрим вопрос member (a,Xs)l относительно программы 3.12, задающей отношение member. Его можно рассматривать как вопрос о том, какой список Xs содержит а в качестве элемента. Одно из решений, вычисленное з*
68 Часть I. Глава 4 абстрактным интерпретатором,-Xs = [д| Ys], т.е. список с головой а и произвольным хвостом. Решение, содержащее переменные, обозначает бесконечное множество решений-все их основные примеры. В интерпретаторе на рис. 4.2 имеются два выбора: выбор цели для редукции и выбор предложения для выполнения редукции. Оба выбора осуществляются в любой реализации вычислительной модели. Природа этих выборов принципиально различна. Выбор цели для редукции произволен; для успешного вычисления несущественно, какая цель выбрана. Если существует успешное вычисление при выборе данной цели, то существует успешное вычисление и при выборе любой другой цели. Два протокола на рис. 4.4 описывают успешные вычисления, различающиеся выбором цели на втором шаге вычисления. Выбор предложения для выполнения редукции является недетерминированным. Не каждый выбор приводит к успешному вычислению. Например, в обоих протоколах рис. 4.4 мы могли совершить неверный выбор. Если бы мы редуцировали цель отец(аран,$) с помощью факта отец (арап,иска), то не смогли бы редуцировать появившуюся цель мужчина (иска). Во втором вычислении, если бы мы редуцировали цель мужчина (S) с помощью факта мужчина (исаак), то цель отец(араи,исаак) уже не могла быть редуцирована. В некоторых вычислениях, например в вычислении, приведенном на рис. 4.3, на каждом шаге вычисления может быть использовано ровно одно предложение программы для редукции очередной цели. Такие вычисления называются детерминированными. Детерминированность вычисления означает, что нам не придется заниматься недетерминированным угадыванием. Альтернативные выборы, которые может сделать абстрактный интерпретатор при попытке доказать цель, неявно определяют дерево поиска, подробное описание такого дерева приведено в разд. 5.3. Если в дереве поиска существует путь, соответствующий доказательству цели, то интерпретатор «угадывает» этот путь. Однако могут быть построены и менее «умные», лишенные способности угадывать интерпретаторы, но обладающие теми же возможностями, что и абстрактный интерпретатор. Один из способов исследования дерева поиска состоит в поиске в ширину, т. е. в параллельном рассмотрении всех возможных выборов. Этот способ гарантирует, что если существует конечное доказательство цели (т.е. конечный успешный путь в дереве поиска), то оно будет найдено. Другой возможный способ исследования дерева поиска состоит в поиске в глубину. В отличие от поиска в ширину поиск в глубину не гарантирует нахождения доказательства, даже если оно существует. Это объясняется тем, что дерево поиска может содержать бесконечные пути, соответствующие потенциально бесконечным вычислениям недетерминированного интерпретатора. Поиск в глубину может попасть на бесконечный путь и никогда не обнаружить конечного успешного пути, даже если такой путь существует. Говоря формально, поиск в ширину определяет полную процедуру доказательства логических программ, а поиск в глубину- неполную процедуру. Несмотря на неполноту, в реализациях языка Пролог используется поиск в глубину, что объясняется практическими соображениями, рассматриваемыми в гл. 6. Мы приведем протокол более длительного вычисления вычисления программы 3.30 при решении задачи о ханойской башне с тремя дисками. Данное вычисление является детерминированным. Протокол вычисления приведен на рис. 4.5. Унификаторы к последним append-целям не приводятся. Их запись не вызовет никаких затруднений. Вычисление, подобное приведенному на рис. 4.5, можно сравнить с вычислениями в более традиционных языках программирования. При этом унификация
Вычислительная модель логических программ 69 hanoi(s(s(s(0))),a,b,c,Ms) hanoi(s(s(0)),a,c,b,Msl) hanoi(s(0),a,b,c,Msll) hanoi(0,a,c,b,Mslll) hanoi(0,c,b,a,Msll2) append([ ],[a to b],Msll) hanoi(s(0),b,c,a,Msl2) hanoi(0,b,a,c,Msl21) hanoi(0,a,c,b,Msl22) append([ ],[b to c],Msl2) append ([a to b],[a to c,b to c],Msl) append([ ],[a to c,b to c],Xs) hanoi(s(s(0)),c,b,a,Ms2) hanoi(s(0),c,a,b,Ms21) hanoi(0,c,b,a,Ms211) hanoi(0,b,a,c,Ms212) append([ ],[c to a],Ms21) hanoi(s(0),a,b,c,Ms22) hanoi(0,a,c,b,Ms221) hanoi(0,c,b,a,Ms222) append([ ],[ato b],Ms22) append([c to a],[c to b,a to b],Ms2) append([ ],[c to b,a to b],Ys) append([a to b,a to c,b to c],[a to b,c to a,c to b,a to b],Ms) append([a to c,b to c],[a to b,c to a,c to b,a to b],Xs2) append([b to c],[a to b,c to a,c to b,a to b],Xs3) append([ ],[a to b,c to a,c to b,a to b],Xs4) true Результат: Ms=[a to b,a to c,b to c,a to b,c to a,c to b,a to b] Рис. 4.5. Решение задачи о ханойской башне. Mslll = [ ] Msll2 = [] Msll = [atob] Msl21 = [] Msl22 = [] Msl2=[btoc] Msl=[atob|Xs] Xs=[a to c,b to c] Ms211 = [] Ms212=[] Ms21 = [c to a] Ms221 = [] Ms222=[ ] Ms22 = [ato b] Ms2=[c to a|Ys] Ys=[c to b,a to b] может быть соотнесена со многими механизмами таких языков-размещением записей, назначением и организацией доступа к полям записи, передачей параметров и т. д. Мы отложим обсуждение этого вопроса до введения вычислительной модели языка Пролог в гл. 6. Вычисление программы Р завершается, если G = true или G = отказ для некоторого п ^ 0. Такое вычисление является конечным вычислением длины п. Успешные вычисления соответствуют завершающимся вычислениям, оканчивающимся константой true. Безуспешные вычисления оканчиваются константой отказ. До сих пор приводились протоколы успешных вычислений. Рекурсивные программы допускают возможность бесконечных вычислений. Вопрос append(Xs,[c,ar\tYs\ решаемый с помощью определения отношения append, допускает любое число редукций с использованием правила для отношения append. В процессе редуцирования переменной Xs сопоставляется список произвольной длины. Это соответствует решениям вопроса, присоединяющим [c,d\ к произвольно длинному списку. Процесс вычисления показан на рис. 4.6. Все до сих пор рассмотренные протоколы имели одно важное общее свойство. Если две цели (7, и G} порождены одним предшественником и цель (7, появляется в
70 Часть I. Глава 4 append(Xs,[c,d],Ys) Xs=[X|Xsl], Ys=[X|Ysl] append(Xsl,[c,d],Ysl) Xsl=[Xl|Xs2], Ysl=[Xl|Ys2] append(Xs2,[c,d],Ys2) Xs2=[X2|Xs3], Ys2=[X2|Ys3] append(Xs3,[c,d],Ys3) Xs3=[X3|Xs4], Ys3=[X3|Ys4] Рис. 4.6. Незавершающееся вычисление. протоколе раньше цели Gp то все цели, порожденные Gi9 появятся в протоколе раньше цели Gj. Такой метод расписания упрощает реализацию протоколов, поскольку решение вопросов выполняется в глубину. Метод расписания определяет еще одно важное действие: заменяет переменные раньше, чем их значения потребуются в других частях вычисления. В зависимости от порядка выбора целей вычисление может оказаться детерминированным или недетерминированным . Рассмотрим протокол вычисления на рис. 4.5. Цель hanoi (s(s(s(0))),a,b,c, Ms) сводится к следующей конъюнкции: hanoi(s(s(0)),a,b,c,Msl), hanoi(s(s(0)),c,b,a, Ms2), append(Msl,Ms2,Ms). Если теперь выбрать цель append, то для ее редукции может быть неверно использован факт append. Применение редукции вначале к двум целям вида hanoi и ко всем порожденным ими целям приводит к цели append с правильными значениями для Msl и Ms2. Закончим этот раздел следующим замечанием. Вычисления описывались нами в виде последовательности редукций. Однако последовательный характер не присущ большинству вычислений. Наоборот, такие параллельные языки, как Параллельный Пролог, Parlog и GHC, создавались для того, чтобы использовать потенциальный параллелизм. Упражнения к разд. 4.2 1. Постройте протоколы решения цели sort{[3,l,2~],Xs)! при сортировке перестановкой (3.20), сортировке вставкой (3.21) и быстрой сортировке. 2. Постройте протокол решения цели derivative(3 *sin(x) — 4*cos(jc,D)) при использовании программы 3.29 для отношения derivative. 3. Потренируйтесь в разработке протоколов для своих программ. 4.3. Дополнительные сведения Унификация играет центральную роль при автоматическом поиске вывода и использовании логического вывода в искусственном интеллекте. Впервые унификация описана в основополагающей работе Робинсона (Robinson, 1965). Алгоритм унификации был объектом исследования во многих работах, например: (Martelli, Montanari, 1982), (Paterson, Wegman, 1978) и (Dwork et al, 1984). Типичное учебное изложение приводится в книгах (Bundy, 1983) и (Nilsson, 1980). Доказательство произвольности выбора редуцируемой цели в резолюции можно найти в работах (Apt, van Emden, 1982) или (Lloyd, 1984). В работе (Plaisted, 1984) предложен метод, позволяющий заменить проверку на вхождения на стадии выполнения программ на анализ программы в процессе компиляции.
Теория логических программ 71 Были предприняты попытки избавиться в унификации от избыточных проверок на вхождения в практических реализациях Пролога. Так, в работе (Colmerauer, 1982b) излагается теоретическая модель подобной унификации, допускающая вычисления с бесконечными термами. Новое использование унификации без проверки на вхождения приведено в работе (Eggert, Chow, 1983); при этом возникают изящные структуры, подобно рисункам Эшера уводящие в бесконечность. Глава 5 Теория логических программ Теория логических программ непрерывно развивается. В этой главе будут затронуты пять тем: семантика, корректность, сложность, деревья поиска и отрицание. Результаты приводятся без доказательств. 5.1. Семантика Семантика сопоставляет значение с программой. Обсуждение семантики позволит нам более формально описывать отношение, вычисляемое программой. В первой главе значение программы неформально определялось как множество основных примеров, выводимых из Р путем конечного числа применения обобщенного правила modus ponens. В данном разделе используется более формальный подход. ()пс;чп(иоииая семантика позволяет процедурно описывать значение программы. Операционное значение логической программы Р-это множество основных целей, являющихся примерами вопросов, которые программа Р решает с помощью абстрактного интерпретатора, приведенного на рис. 4.2. Это альтернативное определение ранее данному определению, определяющему значение в терминах логической выводимости. Лек шративна.ч семантика логических программ основана на стандартной теоретико-модельной семантике логики первого порядка. Для ее описания потребуется некоторая новая терминология. Пусть Р- логическая программа. Утшерсуум Эрбрана программы Р, обозначаемый U (Р)-зто множество всех основных термов, которые могут быть построены из констант и функциональных символов, входящих в Р. Пусть, например, Р-программа 3.1, определяющая натуральные числа: naturaljnumber (0). natural[„number (s (X) ) <- natural jnumber (X). В программе имеется один символ константы-0 и один унарный функциональный символ-^. Универсуум Эрбрана U(Р) данной программы есть {0,s(0),(s(sO)),...}. В общем случае универсуум Эрбрана бесконечен, если в программу входит хотя бы один функциональный символ. Если в программу не входят символы констант, то выбирается произвольным образом одна константа. Btrnte Эрбрана, обозначаемый В(Р), есть множество всех основных целей,
72 Часть I. Глава 5 которые можно построить с помощью предикатов программы Р и термов универ- суума Эрбрана. Если универсуум Эрбрана бесконечен, то бесконечен и базис Эрбрана. В нашем примере программа содержит один предикат -natural-number. Базис Эрбрана - {natural..number (0), natural jnumber (s (0)),...}. Интерпретация логической программы-это некоторое подмножество базиса Эрбрана. Интерпретация сопоставляет истинность и ложность элементам базиса Эрбрана. Цель, принадлежащая базису Эрбрана, является истинной относительно данной интерпретации, если цель входит в данную интерпретацию, в противном случае цель является ложной. Интерпретация / является моделью логической программы, если каждый основной пример А <- #lf..., Вп правила программы удовлетворяет следующему свойству: если Вх ,..,Вп принадлежат /, то и А принадлежит /. Интуитивно ясно, что моделями являются интерпретации, согласованные с декларативным пониманием предложений программы. В нашем примере цель natural_.number (0) должна входить в каждую модель; кроме того, если naturaljnumber \(Х) принадлежит модели, то и naturaljnumber (s (X)) принадлежит модели. Таким образом, любая модель программы 3.1 содержит весь базис Эрбрана. Легко заметить, что пересечение двух моделей логической программы Р также является моделью. Это свойство позволяет определить пересечение всех моделей. Модель, полученная пересечением всех моделей, называется минимальной моделью и обозначается М(Р). Минимальная модель и есть декларативное значение программы. Декларативное значение программы для naturai_number, т.е. ее минимальная модель, совпадает с полным базисом Эрбртгл-{natural...number (0),natural „number (s (0) ),natural_number (s (s (())) ),...}. Рассмотрим декларативное значение программы 3.15, определяющей отношение append: appendix \ Xs], Ys,[X|Zs]) <- append(Xs, Ys,Zs). append^ ],Ys,Ys). Универсуум Эрбрана- [ ], [[ ]], [[ ], [ ]], иными словами, все списки, которые можно построить, используя константу [ ]. Базис Эрбрана-все комбинации списков с предикатом append. Декларативное значение-все основные примеры цели append^ ],A\v,A\v), т.е. append^ ],[ ],[ ]), append{\_ ],[[ ]],[[ ]]),..., а кроме того, такие цели, как append([[ ]],[ ],[[ ]]), логически следующие ввиду применения (применений) правила. В данном случае значение будет лишь подмножеством базиса Эрбрана. Цель append([ ],[ ],[[ ]]), например, не принадлежит значению программы, хотя и принадлежит базису Эрбрана. Денотационная семантика устанавливает значения программам, основываясь на объединении программы с функцией, определенной в области вычисления программы. Значение программы определяется как наименьшая неподвижная точка функции, если такая точка существует. Областью вычислений логических программ являются интерпретации. Для заданной логической программы Р имеется естественная функция Тр, отображающая интерпретации в интерпретации и определенная следующим образом: Тр(1) = {А\А принадлежит В(Р), А <- В,, В2 Вп, п ^ #,-основной пример предложения в Р, В 7 Вп принадлежат /}. Данное отображение монотонно, так как если интерпретация / содержится в интерпретации J, то Тр(1) содержится в Tp(J).
Теория логических программ 73 Это отображение позволяет описать модели иным способом. Интерпретация / является моделью в том и только в том случае, если Тр(1) содержится в /. Данное отношение является не только монотонным, но и непрерывным (понятие непрерывности здесь не определяется). Эти два свойства гарантируют, что для каждой логической программы Р отображение Тр имеет наименьшую неподвижную точку, которая и является значением, определяемым денотационной семантикой программы Р. К счастью, все различные определения семантики в действительности описывают один и тот же объект. Показано, что операционная, денотационная и декларативная семантики совпадают. Это позволяет нам определить значение логической программы как минимальную модель программы. 5.2. Корректность программы Как было показано в разд. 5.1, каждая программа имеет вполне определенное значение. Это значение само по себе не может быть корректным или некорректным. Однако значение программы может совпадать или не совпадать с тем, к чему стремился программист. Таким образом, обсуждение корректности требует рассмотрения подразумеваемого значения программы. Наши предыдущие рассмотрения корректности и полноты всегда соотносились с некоторым подразумеваемым значением. Напомним определения гл. 1. Подразумеваемое значение программы Р-это некоторое множество основных целей. С помощью подразумеваемого значения мы выделяем те цели, для вычисления которых программа и была создана. Программа Р корректна относительно подразумеваемого значения М, если М(Р) содержится в М. Программа Р полна относительно подразумеваемого значения М, если М содержится в М(Р). Таким образом, программа корректна и полна относительно подразумеваемого значения, если эти два значения полностью совпадают. Другим важным вопросом, относящимся к логическим программам, является вопрос об остановке программы. Назовем областью любое множество целей (не обязательно основных), замкнутое относительно построения примеров. То есть для любой области D, если А входит в D и Л' -пример Л, то Л' тоже входит в D. Областью остановки программы Р называется такая область D, что каждое вычисление программы Р с каждой целью из D заканчивается. Обычно полезные программы должны иметь область остановки, охватывающую подразумеваемое значение. Однако поскольку вычислительная модель логических программ не зависит от порядка, в котором редуцируются цели в резольвенте, то большинство интересных логических программ имеют бессодержательную область остановки. Эта ситуация улучшится при рассмотрении языка Пролог. Ограничения, налагаемые на вычислительную модель Пролога, позволяют создавать нетривиальные программы с требуемыми областями остановки. Рассмотрим программу 3.1, определяющую натуральные числа. Программа останавливается для любой цели, принадлежащей базису Эрбрана. Однако в области {natural..number(X)} программа не останавливается. Это связано с возможностью незавершающегося вычисления, показанного на рис. 5.1. При анализе любой логической программы полезно найти области, в которых программа останавливается. Для рекурсивных программ это обычно сделать не просто. Нам нужно ввести описание рекурсивных типов данных, пригодное для исследования проблемы остановки. Напомним, что тип в соответствии с определением гл. 3-это множество термов. Тип называется полным, если данное множество замкнуто относительно построения
74 Часть I. Глава 5 natural-number (X) X=s(Xl) naturaLnumber(Xl) Xl=s(X2) natural .number(X2) X2=s(X3) Рис. 5.1. Незавершающееся вычисление. примеров. Каждому полному типу Тмы сопоставимт>т>.шыи пит IT, состоящий из термов, имеющих примеры, входящие в Т, и имеющих примеры, не входящие в Т. Продемонстрируем применение этих понятий на примере определения областей остановки рекурсивных программ гл. 3, использующих рекурсивные типы. Приведем примеры определений полного и неполного типов для натуральных чисел и списков. Константа 0 и любой терм вида sn(0) являются (полным) натуральным числом. Неполным натуральным числом является или переменная X, или терм вида sn(X). Программа 3.2, задающая отношение ^, останавливается в области, состоящей из целей, у которых первый или второй аргумент (или оба) являются полным натуральным числом. Список является по.тым, если любой пример данного списка удовлетворяет программе 3.11. Список неполный, если имеются примеры, удовлетворяющие программе 3.11, и примеры, не удовлетворяющие этой программе. Список [а,Ь,с], например, является полным списком (доказывается на рис. 3.3), в то время как переменная Х-неполный список. Два менее тривиальных примера: [а,X,с]-полный список, хотя и не основной, a [a,b\Xs]-неполный. Областью остановки программы, задающей append, является множество целей, у которых первый или третий аргумент (или оба) являются полными списками. Анализ областей других программ, обрабатывающих списки, проводится в разд. 7.2 при обсуждении проблемы остановки программ на Прологе. Упражнении к разд. 5.2 1. Опишите область, в которой останавливается программа 3.3, задающая отношение plus. 2. Опишите полные и неполные бинарные деревья. 5.3. Сложность Мы уже неформально анализировали сложность некоторых логических программ, например ^ и plus (программы 3.1 и 3.2) в разделе арифметики, append и два варианта отношения reverse (программы 3.15, 3.16а и 3.16Ь) в разделе списков. В этом разделе кратко дается более формальный подход к мерам сложности. Разнообразные использования логических программ слабо влияют на характер мер сложности. Поэтому вместо описания сложности в терминах длины входа при конкретном использовании программы мы обратимся к выводу целей из значения программы. Естественной мерой сложности логической программы является длина доказательств, порождаемых при выводе целей из значения логической программы. Начнем рассмотрение с введения нового определения - размера цели. /\т/<-/-; терма-это число символов в текстовой записи терма. Константы и переменные записываются с помощью одного символа и имеют размер 1. Размер составного терма на единицу больше суммы размеров аргументов терма. Например, список [Ь~] имеет размер 3, список [а,6] имеет размер 5, цель append§a,b\\c,d\X) имеет
Теория логических программ 75 размер 12. В общем случае список из п элементов имеет размер 2п + 1. Сложность длины вывода программы Р равна L(n), если любая цель G, принадлежащая значению программы и имеющая размер л, может быть выведена из программы Р с длиной вывода, не превосходящей L(n). Сложность длины вывода связана с обычными мерами сложности в теории алгоритмов. В случае последовательной реализации вычислительной модели эта сложность соответствует временной сложности. Программа 3.15, задающая отношение append, имеет линейную сложность длины вывода. Это показано в упражнении (1) в конце раздела. Применимость этой меры сложности к программам на Прологе, а не к логическим программам, зависит от использования алгоритма унификации без проверки на вхождение. Рассмотрим процесс вычисления простейшей программы соединения двух списков. Соединение двух списков, как показано на рис. 4.3, использует несколько унификаций целей вида append с заголовком правила ар- pend-append([X\Xs],\Ys,[X\Zs]). Потребуется не менее трех унификаций, сопоставляющих переменные со списками (возможно, неполными). Если в каждой унификации должна быть выполнена проверка на вхождение, то списки, соответствующие аргументам, будут найдены. Время этого процесса прямо пропорционально размеру входной цели. Однако если проверка на вхождение не производится, то время унификации ограничено константой. Общая сложность а/?/?еи*/-вычисления квадратично зависит от размера входных списков при использовании проверки на вхождение и линейно-без использования проверки. Введем другие полезные меры сложности, основанные на доказательстве. Пусть R-доказательство. Назовем глуоинои R самое глубокое использование цели в некоторой резолюции. Размер цели в R-максимальный размер редуцируемых в R целей. Логическая программа Р имеет сложность пспмсри ист G(n), если любая цель А, принадлежащая значению программы и имеющая размер л, может быть выведена из программы Р так, что размер цели в выводе не превысит G(n). Логическая программа Р имеет ,.ложность слуонны аыноОа D(n), если любая цель А, принадлежащая значению программы и имеющая размер я, может быть выведена из программы Р с глубиной вывода, не превосходящей D(n). Сложность размера цели связана с емкостью памяти. Сложность глубины вывода связана с объемом запоминаемой информации при последовательных реализациях и с емкостной и временной сложностью при параллельных реализациях. Упражнения к разд. 5.3 1. Покажите, что размер цели в значении программы для отношения append, описывающей соединение списка длины п и списка длины m в список длины п + т, равен 4п + 7т + 7. Покажите, что дерево вывода имеет т + 2 вершины. Выведите отсюда, что программа append имеет линейную сложность. Изменится ли сложность программы при добавлении типового условия? 2. Покажите, что программа 3.3, задающая отношение plus, имеет линейную сложность. 3. Исследуйте сложность других логических программ. 5.4. Деревья поиска В рассматривавшихся до сих пор вычислениях логических программ проблема недетерминизма всегда разрешалась корректным выбором. Например, в мерах сложности, основанных на дереве доказательства, предполагалось, что для прове-
76 Часть I. Глава 5 дения редукции из программы может быть выбрано нужное предложение. Другой способ вычислительного моделирования недетерминизма состоит в параллельном рассмотрении всех возможных редукций. В этом разделе мы рассмотрим деревья поиска - формализм, подходящий для исследования всевозможных путей вычисления. Дерево поиска цели G относительно программы Р определяется следующим образом. Корнем дерева является G. Вершины дерева образуют цели (возможно, конъюнктивные) с одной выделенной целью. Для каждого предложения, заголовок которого унифицируем с целью, выделенной в вершине /V, имеется ребро, ведущее из вершины N. Каждая ветвь дерева соответствует вычислению цели G относительно программы Р. Листья дерева называются успешными вершинами, если цель, соответствующая листу, пуста, и безуспешными вершинами, если, цель, выделенная в вершине, не может быть редуцирована. Успешные вершины соответствуют решениям корня дерева. В общем случае для одной и той же цели и одной и той же программы имеется много деревьев поиска. На рис. 5.2 приведены два дерева поиска при решении вопроса сын (S,аран)'? с помощью программы 1.2. Два дерева соответствуют двум выборам цели для редукции в резольвенте omeij(apan,S), мужчина(S). Деревья различны, но в обоих-единственная успешная ветвь, соответствующая решению вопроса,- S = лот. Соответствующие успешные ветви приведены в виде протоколов на рис. 4.4. сын (S, аран) сын (S, аран) \ \ отец (аран, S), мужчина (S) мужчина(S), отец (аран, 5) S=Jiom J S=ucnal Б=милкаУ 3=исаак I \S=Jiom мужчина (лот) мужчина (иска) мужчина (милка) отец(аран,исаак) отец(аран,лот) \ \ true true Рис. 5.2. Два дерева поиска. Мы придерживаемся некоторых соглашений при изображении деревьев поиска. Самая левая цель в вершине всегда выделена. Следовательно, в производных целях цели могут быть переставлены так, чтобы первой стояла цель, выбранная для редукции. На ребрах дерева записываются подстановки, применяемые к переменным самой левой цели. Эти подстановки вычисляются в процессе работы алгоритма унификации. В случае детерминированных вычислений деревья поиска тесно связаны с протоколами вычислений. Протоколы вычислений append-целей и /ш/ш/-целей, приведенные соответственно на рис. 4.3 и 4.5, могут быть легко преобразованы в деревья поиска. Это составляет содержание упражнения (1) в конце раздела. Дерево поиска содержит несколько успешных вершин, если вопрос имеет несколько успешных решений. На рис. 5.3 приведено дерево поиска для вопроса append(As,Bs,[a,b,c\) относительно программы 3.15, определяющей отношение append. Вопрос состоит в расщеплении списка [_а,Ь,с] на два списка. Совокупность меток на ребрах, ведущих в успешную вершину, задает решения для As и Bs. Например, самая левая ветвь дерева на рисунке соответствует решению {As = [a, b,c]9Bs = [ ]}. Число успешных вершин в различных деревьях поиска для данной цели и данной программы одинаково.
Теория логических программ 77 append(As,Bs,[a,b,c]) 1 As=[a|Asl]| "*^^^ As=[ ],Bs=[a,b,c] append(Asl,Bs»[b,c]) true \ Asl=[b|As2]T^--^4>^ Asl=[ ],Bs=[b,c] append(As2,Bs,[c]) ^ true \ As2=[c|As3Jr>^^^^^ As2=[ ],Bs=[c] append(As3,Bs»[ ]) irue iAs3=[],Bs=[]| true Рис. 5.З. Дерево поиска с многочисленными успешными вершинами. Дерево поиска может иметь бесконечные ветви, которые соответствуют незавершающимся вычислениям. Рассмотрим цель append(Xs,[c,d\yYs) относительно обычной программы, задающей отношение append. Дерево поиска приведено на рис. 5.4. Бесконечная ветвь соответствует незавершающемуся вычислению, показанному на рис. 4.6. append(Xs,[c,d],Ys). |Xs=[X|Xsl],Ys=[X|Ysl]| ^__^^ Xs=[ ],Ys=[c,d] append(Xsl,[c,d],Ysl) m^^true jXsl=[Xl|Xs2],Ysl=[Xl|Ys2]|^^^^Xsl=[],Ys=[c,d] append(Xs2,[c,d],Ys2) ^^ ^^^^true |Xs2=[X2|Xs3],Ys2=[X2|Ys3]|^_^^ Xs2=[ ],Ys=[c,d] append(Xs3),[c,d],Ys3) ""^ true Рис. 5.4. Дерево поиска с бесконечной ветвью. В терминах деревьев поиска можно определять меры сложности. В программах на Прологе применяется обход дерева в глубину. Поэтому меры сложности, основанные на размере дерева поиска, точнее отражают реальную сложность программ на Прологе, чем меры, основанные на сложности дерева вывода. Однако анализировать сложность дерева поиска гораздо труднее. Данный вопрос связан с серьезной проблемой. Дело в том, что взаимосвязь между деревьями поиска и деревьями вывода отражает взаимосвязь между детерминированными и недетерминированными вычислениями. Вопрос о совпадении классов сложности, определяемых в терминах деревьев вывода, с классами сложности, определяемыми в терминах деревьев поиска, является классическим вопросом "Р = NP'\ переформулированным в терминах логического программирования. Упражнения к разд. 5.4 1. Преобразуйте протоколы на рис. 4.3 и 4.5 в деревья поиска. 2. Постройте дерево поиска цели sort([2,4,l,5,3],Xs) при использовании сортировки перестановкой.
78 Часть I. Глава 5 5.5. Отрицание в логическом программировании Логическая программа является совокупностью правил и фактов, описывающих истинные утверждения. Ложные факты явно не указываются, они просто опускаются. В данном разделе мы опишем расширение вычислительной модели логического программирования, допускающее ограниченное использование отрицательной информации в программах. Определим отношение not G и опишем его значение. Данное отрицание является частным случаем отрицания в логике первого порядка. Отношение not трактует отрицание как отсутсигвие . Цель not G считается выводимой из программы Р, если G не выводима из Р. Опишем отрицание как отсутствие в терминах дерева поиска. Дерево поиска цели G относительно программы Р называется коп^чпе-ис jvcth'iiniNM, если в дереве нет успешных вершин и нет бесконечных ветвей. Копечно-осчуспситым множеством логической программы Р называется множество тех целей, для которых имеется конечно-безуспешное дерево поиска относительно программы Р. Цель not следует из программы Р при понимании отрицания как отсутствия, если G входит в конечно-безуспешное множество программы Р. Рассмотрим простой пример. Возьмем программу, состоящую из двух фактов: любит (авраам, гранаты). любит (исаак, гранаты). Цель not любит (сара, гранаты) следует из программы при понимании отрицания как отсутствия. Дерево поиска любит (сара, гранаты) имеет одну безуспешную вершину. Использование отрицания как отсутствия позволяет легко определять многие отношения. Например, отношение disjoint (Xs,Ys), означающее, что у двух списков Xs и Ys нет общих элементов, может быть декларативно определено следующим образом: disjoint(Xs, Ys) <- not(member(X,Xs),member(Y, Ys)); Это означает: «список Xs не пересекается со списком Ys, если никакой элемент Xs не является членом обоих списков Xs и Ж». Программа 5.1 является другим примером программы, использующей отрицание. В программе определяется отношение неженатый_студент(Человек), означающее, что Человек является неженатым студентом. Вопрос неженатый_сту- дент(Х)! имеет решение X = билл. Построение и корректной, и эффективной реализации отрицания как отсутствия сталкивается с серьезными трудностями. Большинство реализаций Пролога таково, что отрицание корректно в простых случаях, но приводит к логически неверным заключениям в более сложных случаях. Мы рассмотрим эти проблемы в разд. 11.2 в связи с написанной на Прологе версией программы 5.1. неженатый-студент (X) <- not женат (X), студент (X). студент (билл). женат (джо). llpoipavivui 5 1. Простая программа, использующая not. 5.6. Дополнительные сведения Классическая работа в области семантики логических программ - статья (van Emden, Kowalski, 1976). Существенное развитие идей в этой области содержится в работе (Apt, van
Теория логических npoipa.vivi 79 Emden, 1981). В частности, авторы показали, что число успешных вершин в дереве поиска инвариантно, и, следовательно, порядок выбора редуцируемых целей в резольвенте несуществен. В статье (Shapiro, 1984) меры сложности логических программ сравниваются со сложностью вычислений альтернирующих машин Тьюринга. Показано, что размер цели линейно связан с емкостью при альтернировании, произведение длины вывода на размер цели линейно связано с размером дерева при альтернировании и произведение глубины вывода на размер цели линейно связано с временем при альтернировании. Общепринятым в литературе названием для деревьев поиска являются SLD-деревья. Название SLD возникло в исследованиях по автоматическому доказательству теорем, предшествовавших появлению логического программирования. SLD-резолюция является одним из уточнений принципа резолюции, описанного в (Robinson, 1965). Вычисление логической программы можно рассматривать как последовательность шагов резолюции, точнее, SLD-pe- золюции,- обычно в литературе вычисление определяется именно таким образом. Аббревиатура SLD связана с выбором (Selecting) литералов посредством линейной (Linear) стратегии и просмотром набора возможных дедукций в глубину (Depth-first). Первое доказательство корректности и полноты SLD-резолюций, правда под именем LUSH-резолюций, дано Хиллом (Hill, 1974). Вопрос об отрицании привлекает большое внимание с момента возникновения логического программирования. Фундаментальная работа по семантике отрицания-как-отсутствия- статья (Clark, 1978). Исследования Кларка были продолжены в работе (Jaflar et al., 1983), в которой доказаны корректность и полнота правила. Концепция отрицания как отсутствия является частным случаем предположения о замкнутом мире в теории баз данных. По поводу дополнительных сведений мы отсылаем к работе (Reiter, 1978). Подробное обсуждение взаимосвязи между различными формулировками отрицания имеется в статье (Lloid, 1984), там же рассматриваются многие вопросы, обсуждавшиеся в этой главе.
Часть II Язык Пролог При реализации языка, основанного на вычислительной модели логического программирования, следует обратить внимание на три проблемы. Первая проблема связана с уточнением порядка выборов, оставшихся неопределенными в абстрактном интерпретаторе. Вторая проблема связана с увеличением выразительной силы исходной вычислительной модели логического программирования за счет добавления металогических и внелогических средств. И наконец, следует предусмотреть использование некоторых особенностей применяемого компьютера, таких, как быстрая арифметика, возможности ввода-вывода. В этой части книги вписывается, как данные проблемы решаются в языке Пролог, наиболее развитом языке, основанном на логическом программировании. Глава 6 Чистый Пролог Программа на чистом Прологе-это логическая программа, в которой задан порядок предложений и целей в теле каждого предложения. Абстрактный интерпретатор логических программ видоизменен так, чтобы использовать информацию о заданных порядках. В настоящей главе описываются отличия вычислительной модели программ на Прологе от модели логических программ; модель для Пролога сравнивается с более привычными языками программирования. Взаимосвязь логического программирования и языка Пролог напоминает взаимосвязь лямбда-исчисления и языка Лисп. Оба этих языка являются конкретной реализацией абстрактных вычислительных моделей. Логические программы, исполняемые с помощью вычислительной модели Пролога, называются программами на чистом Прологе- Чистый Пролог представляет собой приближенную реализацию вычислительной модели логического программирования на последовательной машине. Конечно, данная реализация не является единственно возможной. Она является, однако, одной из наилучших с практической точки зрения совмещения свойств абстрактного интерпретатора с возможностью эффективного выполнения. 6.1. Вычислительная модель Пролога При построении на основе абстрактного интерпретатора некоторог о интерпретатора для конкретного языка программирования необходимо принять два важных
Язык Пролог 81 решения. Во-первых, следует ограничить произвол в выборе редуцируемой цели в резольвенте, т.е. уточнить метод расписания. Во-вторых, нужно реализовать недетерминированный выбор предложения программы, используемого в редукции. Существуют разные языки программирования, использующие различные способы выбора. Грубо говоря, они делятся на два класса. Пролог и его расширения (например, Пролог-П, IC-Пролог и MU-Пролог) основаны на последовательном выполнении. Другие языки, такие, как Parlog, Параллельный Пролог и GHC, основаны на параллельном выполнении. Последовательные языки отличаются от параллельных методом реализации недетерминизма. Отличие Пролога от его версий состоит в методе выбора редуцируемой цели. Выполнение программ на Прологе заключается в работе абстрактного интерпретатора, при которой вместо произвольной цели выбирается самая левая цель, а недетерминированный выбор предложения заменяется последовательным поиском унифицируемого правила и механизмом возврата. Другими словами, в Прологе используется стековый метод расписания. В Прологе резольвента используется как стек -для редукции выбирается верхняя цель, производные цели помещаются в стек резольвенты. В дополнение к методу стека Пролог моделирует недетерминированный выбор редуцирующего правила с помощью последовательного поиска и механизма возврата. При попытке редуцировать цель выбирается первое предложение, заголовок которого унифицируем с данной целью. Если не существует правила, унифицируемого с выбранной целью, то вычисление восстанавливается на стадии последнего сделанного выбора и выбирается следующее унифицируемое предложение. Вычисление цели G относительно программы Р, написанной на Прологе, состоит в порождении всех решений цели G относительно программы Р. В терминах логического программирования вычисление цели G в Прологе является полным обходом в глубину конкретного дерева поиска цели (7, в котором всегда выбирается самая левая цель. Существует много версий языка Пролог. Они отличаются синтаксисом, некоторыми особенностями исполнения и удобством программирования. Мы в основном следуем версии Edinburgh-Пролог. Все наши программы пригодны и для выполнения в версии Wisdom-Пролог. Синтаксис в нашей версии совпадает с тем, который ранее использовался в логических программах. На самом деле многие логические программы будут правильно выполняться без какого-либо изменения записи. Протокол вычисления в Прологе является некоторым расширением протокола логической программы при использовании абстрактного интерпретатора, описанного в разд. 4.2. Мы еще раз рассмотрим вычисления, приведенные в гл. 4 и 5, чтобы отметить сходства и различия. Рассмотрим вопрос сын(Х, аранр. относительно программы 1.2, упрощенной библейской базы данных, повторно записанной в верхней части рис. 6.1. Вычисление приведено в основной части рис. 6.1. Оно соответствует обходу в глубину первого дерева поиска на рис. 5.2. Данное вычисление является расширением первого протокола на рис. 4.4, так как поиск производится во всем дереве поиска. Список обозначений, использовавшихся ранее в протоколах, следует расширить для описания отказов и возвратов. Символ / после цели означает, что цель недоказуема, т.е. в программе отсутствует предложение, заголовок которого унифицируем с целью. Встретив недоказуемую цель, вычисление переходит к цели, указанной механизмом возврата. Соответствующая цель уже появлялась в качестве предыдущей цели в протоколе на том же самом уровне и может быть идентифицирована именами переменной. В соответствии с правилами Edinburgh-Пролога символ «;» после решения означает, что вычисление продолжается для поиска
82 Часть II. Глава 6 отец(авраам,исаак). мужчина(исаак). отец(аран,лот). мужчина(лот). отец(аран,милка). женщина(милка). отец(аран, иска). женщина(иска). сын(Х,У) <- отец(У,Х), мужчина(Х). дочь(Х,У) <- отец(У,Х), женщина(Х). сын(Х,аран)? отец(аран,Х) мужчина(лот) true Результате = лот отец(аран,Х) мужчина(милка) f отец(аран,Х) мужчина(иска) f Нет (больше) точек выбора X = лот X = милка X = иска Рис. 6.1. Протокол простого вычисления в Прологе. других решений. Унификатор обозначается как и раньше. В некоторых версиях Пролога вычисления отличаются от описанных выше. Например, в одних версиях всегда даются все решения, в то время как в других после каждого решения ожидается реакция пользователя. append([X|Xs],Ys,[X|Zs]) <— append(Xs,Ys,Zs). append([ ],Ys,Ys). append(Xs,Ys,[a,b,c]) Xs=[a|Xsl] append(Xsl,Ys,[b,c]) Xsl=[b|Xs2] append(Xs2,Ys,[c]) Xs2=[c|Xs3] append(Xs3,Ys,[ ]) Xs3=[ ],Ys=[ ] true Результат: (Xs=[a,b,c],Ys=[ ]) append(Xs2,Ys,[c]) ' Xs2=[ ],Ye=[c] true Результат: (Xs=[a,b],Ys=[c] ) append(Xsl,Ys,[b,c]) ' Xsl=[ ],Ys=[b,c] true Результат: (Xs=[a],Ys=[b,c]) append(Xs,Ys,[a,b,c]) Xs=[ ],Ys=[a,b,c] true Результат: (Xs=[ ],Ys=[a,b,c]) Нет (больше) решений Рис. 6.2. Множество решений расщепления списка.
Язык Пролог 83 Протокол решения вопроса append([a,b~\,[c,d],Ls)! дающий ответ Ls = [a,b,c,d], в точности совпадает с протоколом на рис. 4.3. Рис. 4.5, содержащий протокол решения задачи о ханойской башне с 3 дисками, описывает также протокол вычисления программы Hanoi, если эту программу рассматривать как программу на Прологе, решающую вопрос Hanoi (s(s (s (0) )) ,a,b,c,Moves)!. Протокол детерминированных вычислений программы одинаков независимо от того, считаем ли мы данную программу логической или программой на Прологе, если только порядок целей сохраняется. Следующий пример состоит в поиске ответа на вопрос append(Xs, Ys,[a,b,c])! с использованием программы 3.15, определяющей отношение append. В этом случае имеется несколько решений. Дерево поиска для данной цели приводилось на рис. 5.3. На рис. 6.2 изображен протокол в Прологе. quicksort([X|Xs],Ys) «— paxtition(Xs,X,Littles,Bigs), qui cksort (Littles ,Ls), quicksort (Bigs ,Bs), append(Ls,[X|Bs],Ys). quicksort([ ],[]). paxtition([X|Xs],Y,[X|Ls],Bs) <- X < Y, partition(Xs,Y,Ls,Bs). partition([X|Xs],Y,Ls,[X|Bs]) «- X > Y, partition(Xs,Y,Ls,Bs). partition([ ],Y,[ ],[ ]). quicksort([2,l,3],Qs) partition([l,3],2,Ls,Bs) 1 < 2 partition([3],2,Lsl,Bs) 3< 2 f partition([3],2,Lsl,Bs) 3> 2 partition([ ],2,Lsl,Bsl) quicksort([l],Qsl) partition([ ],l,Ls2,Bs2) quicksort([ ],Qs2) quicksort ([ ],Qs3) appendfl ],[l],Qsl) quicksort([3],Qs4) partition([ ],3,Ls3,Bs3) quicksort([ ],Qs5) quicksort([ ],Qs6) append([ ],[3],Qs4) append([l],[2,3],Qs) append([ ],[2,3],Ys) true Результат: (Qs=[ 1,2,3]) Ls=[l|Lsl] Lsl = [3|Ls2] Bs=[3|Bsl] Lsl=[]=Bsl Ls2=[]=Bs2 Qs2=[ ] Qs3=[ ] Qsl=[l] Ls3=[]=Bs3 Qs5=[ ] Qs6=[] Qs4=[3] Qs=[l|Ys] Ys=[2,3] Рис. 6.З. Протокол вычисления quicksort.
84 Часть II. Глава 6 Протоколирование вычислений облегчает понимание процесса выполнения программ на Прологе. Приведем пример чуть большего размера - упорядочение списка с помощью программы быстрой сортировки (программа 3.22 выписана повторно). Вычисления программы quicksort по существу детерминированы и демонстрируют алгоритмическое поведение программы на Прологе. На рис. 6.3 приведен протокол решения вопроса quicksort ([2 fl,3~\,Xs)l Считается, что арифметическое сравнение- элементарная операция, а для отношения append используется стандартная программа. Введем различия между внешним и глубоким возвратами. Внешний возврат возникает при безуспешной унификации цели и предложения, в этом случае пробуется новое предложение. Глубокий возврат возникает при безуспешной унификации цели и последнего предложения процедуры, в этом случае происходит возврат к другой цели в дереве вычисления. Иногда удобно вводить проверочные предикаты, находящиеся в начале тела предложения, как часть унификации. Тогда возвраты, возникающие в результате безуспешности подобных унификаций, соответствуют внешним возвратам. Примером может служить рис. 6.3, содержащий выбор нового предложения при редукции цели partition([3~\,2,Lsl,Bs). Упражнения к разд. 6.1 1. Постройте протокол вычислений при решении вопроса дочъ(Х,аранр. относительно программы 1.2. 2. Постройте протокол вычислений при решении вопроса sort([3,2,l~\,Xs)l относительно программы 3.21. 3. Постройте протокол вычислений при решении вопроса sort([3,2,l~},Xs)l относительно программы 3.20. 6.2. Сравнение с традиционными языками программирования Язык программирования характеризуется присущими ему механизмами управления и обработки данных. Пролог как универсальный язык программирования можно рассматривать и с этих точек зрения. В этом разделе мы сравним средства управления последовательностью действий и обработку данных в Прологе и алголоподобных языках. При успешном выполнении вычисления средства управления программ на языке Пролог подобны средствам управления в обычных процедурных языках. Обращение к некоторой цели соответствует вызову процедуры, порядок целей в теле правила соответствует последовательности операторов. Точнее, правило A<r-BPB2 Вп можно рассматривать как определение процедуры: procedure A call Вг call В2 call Bn end. Рекурсивный вызов цели в Прологе в последовательности действий и в реализации подобен тому же вызову в обычных рекурсивных языках. Различие возникает
Язык Пролог 85 при реализации возврата. В обычных языках, если вычисление не может быть продолжено (например, все ветви в операторе case ложны), возникает ошибка выполнения. В Прологе вычисление просто возвращается к последнему выбору и делается попытка продолжить вычисления по новому пути. Структуры данных, которыми оперируют логические программы,-термы -соответствуют общим структурам записей в обычных языках программирования. Пролог использует очень гибкую систему организации структур данных. Подобно языку Лисп, Пролог является бестиповым языком, не содержащим объявления данных. Другие особенности использования структур данных в языке Пролог связаны с природой логических переменных. Логические переменные соотносятся с объектами, а не с ячейками памяти. Если переменной сопоставлен конкретный объект, то эта переменная уже никогда не может ссылаться на другой объект. Иными словами, логическое программирование не поддерживает механизм деструктивного присваивания, позволяющий изменять значение инициализированной переменной. В логическом программировании обработка данных полностью заключена в алгоритме унификации. В унификации реализованы: • однократное присваивание, • передача параметров, • размещение записей, • доступ к полям записей для одновременных чтения/записи. Рассмотрим протокол программы быстрой сортировки (рис. 6.3), обращая внимание на различные использования унификации. Унификация исходной цели quicksort([2J,3\Qs) с заголовком процедурного определения-quicksorH[X\Xs],Ys) демонстрирует несколько характерных использований унификации. Унификация списка [2./.J] и терма [X | Xs] обеспечивает обращение к записи в списке, а также выделение в этой записи двух полей -головы и хвоста. Унификация списка [/,.?] с термом Xs выполняет передачу параметра процедуре partition, используя общие переменные. Таким образом определяется первый аргумент процедуры partition. Аналогично унификация 2 и X приводит к передаче второго аргумента процедуре partition. Создание записей возникает при унификации цели partition^],3],2,Ls,Bs) с заголовком процедуры partition partitioni\X \ Ys],Z\X \ Lsl,Bsl]). При унификации переменной Ls сопоставляется терм \_l\Lsl~]. Точнее, в качестве Ls должен быть использован список, причем голове этого списка следует сопоставить значение У. Иными словами, при данной унификации происходит создание записи и назначение значений полям записи. Подобные аналогии могут подсказать, как эффективно реализовать Пролог на машине с архитектурой фон Неймана. Конечно, основная цель компиляции с Пролога состоит в преобразовании специальных случаев унификации в обычные операции работы с памятью. Традиционные языки, как правило, содержат различной степени сложности средства обработки ошибочных и исключительных ситуаций. Чистый Пролог не содержит механизма обработки ошибок и исключительных ситуаций, встроенного в описание языка. В отличие от традиционных языков ситуации, приводящие к ошибке (например, отсутствие нужной ветви в операторе case, деление на нуль), в чистом Прологе приводят к «отказу». Полный Пролог, описанный в дальнейших главах, содержит системные предикаты, такие, как арифметические предикаты, предикаты ввода/вывода, которые могут привести к ошибкам. Существующие реализации Пролога не имеют развитой системы обработки ошибочных ситуаций. Типичная реакция на ошибочную ситуацию состоит в печати сообщения об ошибке и переходе вычисления или в
86 Часть П. Глава 7 состояние «отказ», или в состояние «прерывание». Это краткое обсуждение различных способов обработки данных никак не может помочь ответить на наиболее интересный вопрос: как сравнивать программирование на Прологе с программированием на традиционных языках? По сути это неявная тема оставшейся части книги. 6.3. Дополнительные сведения Возникновение Пролога покрыто тайной. Известно только то, что два создателя языка-Роберт Ковальский, в то время работавший в Эдинбурге, и Алан Колмероэ из Марселя - разрабатывали в начале 70-х гг. сходные идеи и даже в течение одного лета работали вместе. В результате были сформулированы основные положения логического программирования и вычислительная модель (Kowalski, 1974), описан и реализован первый язык логического программирования-Пролог (Colmerauer et al., 1973). Использование логики в качестве основы практического языка программирования во многом обязано исследованиям эффективных методов реализации, начатым в работе (Warren, 1977). Транслятор Уоррена выделял специальные случаи унификации и преобразовывал их в эффективные последовательности обычных операций работы с памятью. Возникали версии языка Пролог, содержащие дополнительные средства управления, например IC-Пролог (Clark, Mc Cabe, 1979), однако было показано, что из-за большого увеличения времени работы программ их нельзя рассматривать в качестве альтернатив Прологу. Отдельные интересные версии языка будут упоминаться в соответствующих разделах книги. Другую ветвь языков логического программирования, косвенно возникшую из IC-Пролога, составляют параллельные логические языки. Сначала появился Реляционный Язык (Clark, Gregory, 1981), далее последовали: Параллельный Пролог (Shapiro, 1983b), Parlog (Clark, Gregory, 1984), GHC (Ueda, 1985) и несколько других версий. Литература к версиям, упоминаемым в тексте: Пролог-Н-(Уап Gengham, 1982), IC-Пролог-(Clark at al., 1982) и Ми-Пролог-(ЫшзЬ, 1985а). Благодаря работе (Kowalski, 1974) синтаксис Пролога основан на способе записи утверждений в логике. Исходный марсельский интерпретатор использовал терминологию позитивных и негативных формул, возникшую в теории резолюций. Правило А «- Bif...,B„ записывалось в виде + А — Bv.. — В„. Уоррен и его коллеги перенесли Marseille-Пролог на компьютер DEC-10, и это решение оказало большое влияние на дальнейшие работы. Многие системы придерживаются соглашений версии Пролог-10 (Warren et al., 1979), более известной под названием Edinburgh-Пролог. Основные характеристики этой версии описаны в широко распространенном введении в Пролог (Clocksin, Mellish, 1984). Edinburgh-Пролог излагается в нашей книге в основном в соответствии с описанием (Bowen et al., 1981). Недавняя статья (Coben, 1986) продолжает исследования связи языка Пролог с традиционными языками программирования. Глава 7 Программирование на чистом Прологе Основная цель логического программирования-создать возможность разработки программ на языке высокого уровня. В идеале программист должен записать аксиомы, определяющие требуемые отношения, полностью игнорируя, каким
Программирование на чистом Прологе 87 образом эти аксиомы будут использоваться в процессе выполнения. Имеющиеся языки логического программирования, и, в частности, Пролог, все еще далеки от этого идеала декларативного программирования. Нельзя игнорировать конкретный, четко определенный способ моделирования абстрактного интерпретатора в реализации каждого языка. Эффективное логическое программирование требует знания и использования этого способа. В настоящей главе обсуждаются возможности вычислительной модели Пролога для создания логических программ. Рассматриваются новые аспекты программирования. Программист должен не только уделять внимание корректной и полной аксиоматизации отношения, но и рассматривать его выполнение в соответствии с вычислительной моделью. 7.1. Порядок правил Два синтаксических понятия, несущественные в логических программах, важны при создании программ на Прологе. В каждой процедуре должен быть принят ;u>i>:-n)oK n/HMu.ii или порядок предложений. Кроме того, в теле каждого предложения должен быть определен порядок целей. Последствия этих решений могут оказаться колоссальными: эффективность выполнения программ на Прологе может изменяться в десятки раз. В крайних и тем не менее распространенных случаях корректные логические программы вообще не приведут к решению вследствие незавершающегося вычисления. Порядок правил определяет поряоок поиска решений. Изменение порядка правил в процедуре приводит к перестановке ветвей в любом дереве поиска цели, использующей данную процедуру. Обход дерева поиска производится в глубину. Поэтому перестановка ветвей дерева изменяет порядок обхода дерева поиска и порядок нахождения решений. Этот эффект очевиден при использовании фактов для нахождения ответов на экзистенциальный вопрос. Если использовать нашу библейскую базу данных для ответа на такой вопрос, как отец(Х,У)1, то изменение порядка фактов изменит порядок, в котором Пролог будет находить решения. Однако решение о том, как упорядочить факты, не имеет большого значения. Порядок, в котором находятся ответы на вопросы при работе рекурсивной программы, также определяется порядком предложений. Рассмотрим простую библейскую базу данных вместе с программой, определяющей отношение предок (программа 7.1). родитель (фарра, авраам). родитель (авраам, исаак). родитель(исаак,иаков). родитель (иаков, Вениамин). предок (X,Y) <- родитель (X,Y). предок (X, Y) <- родитель (X, Y), предок (Y,Z). Программа '1. Еще один семейный пример. При решении вопроса предок (фарра,Х)1 с использованием программы 7.1 ответы будут даны в следующем порядке: X = авраам, X = исаак, X = иаков и X = вениамин. Если переставить два правила, определяющие отношение предок, то решения будут появляться в другом порядке: X = вениамин, X = иаков, X = исаак и X = авраам. Изменение порядка правил, задающих отношение предок, меняет порядок поиска в неявно заданном генеалогическом дереве. При одном порядке Пролог перечисляет потомков в порядке порождения. При переставленных предложениях Пролог
88 Часть II. Глава 7 переходит к концу дерева и выдает решения в обратном порядке. Требуемый порядок решений определяется использованием программы, в соответствии с которым и выбирается порядок правил для отношения предок. Изменение порядка предложений в определении предиката member (программа 3.12) также приводит к изменению порядка поиска. В представленном виде программа просматривает список до тех пор, пока не будет найден нужный элемент. Если поменять порядок предложений на обратный, то программа будет всегда начинать поиск с конца списка. Это повлияет на порядок решений. Рассмотрим, например, вопрос member (X,[/,2,J])?. При обычном порядке правил порядок решений естествен: X = 1, X = 2, X = 3. Если правила переставлены, то порядок решений-X = 3, X = 2, X = 1. Порядок правил в программе 3.12 основан на интуиции и поэтому предпочтительнее. В тех случаях, когда дерево поиска данной цели содержит бесконечную ветвь, порядок правил может определять, будет ли вообще найдено хотя бы одно решение. Рассмотрим вопрос append (Xs,\_c,d],Ys)l. Из дерева поиска, приведенного на рис. 5.4, можно усмотреть, что решения найдены не будут. Если, однако, в программе, задающей отношение append, поставить факт перед правилом, то будет найдено бесконечное число пар Xs, Ys, удовлетворяющих вопросу. Не существует общих правил выбора порядка предложений в процедурах Пролога. Ясно, что стандартная для большинства традиционных языков проверка условия остановки перед продолжением итерации или рекурсии не имеет столь важного значения в Прологе. Это заметно в программе 3.15, задающей отношение append, равно как и в других программах, приведенных в книге. Дело в том, что применимость итерационного или рекурсивного правила проверяется с помощью унификации. Эта проверка производится явно и независимо от остальных предложений в процедуре. Порядок предложений в программах на общепринятом Прологе важнее, чем порядок предложений в программах на чистом Прологе. Одно из средств управления-отсечение, обсуждаемое в гл. 11, существенно зависит от порядка предложений. При использовании этой конструкции предложения теряют свою независимость и модульность и порядок предложений становится существенным. В данной книге мы следуем соглашению о том, что рекурсивные предложения предшествуют предложениям, которые будем называть базисными. Упражнения к разд. 7.1 1. Проверьте порядок нахождения решений вопроса предок(авраам,Хр. с помощью программы 7.1, а также с помощью варианта программы, полученного изменением порядка правил, задающих отношение предок. 2. В каком порядке появляются ответы на вопрос предок(Х, вениамин)'] при использовании программы 7.1? Что произойдет при перестановке правил? 7.2. Проблема завершения программ Используемый в Прологе принцип обхода дерева в глубину приводит к серьезным проблемам. Если дерево поиска цели относительно некоторой программы содержит бесконечную ветвь, то вычисление не завершится. Пролог может не найти решение цели, даже если существует конечное вычисление, решающее вопрос. Бесконечные вычисления появляются при использовании рекурсивных правил. Рассмотрим добавление отношения супруги!Мужчина,Женщина) к нашей базе данных семейных отношений. Один из фактов библейской истории -супруги (авраам,
Программирование на чистом Прологе 89 сара). Для пользователя, применяющего отношение супруги, несущественно, мужчина или женщина упоминается на первом месте, так как это отношение коммутативно. «Очевидный» способ задания коммутативности состоит в добавлении рекурсивного правила супруги(Х,У) <- супруги(ХХ). Если такое правило добавить к программе, то ни одно вычисление, использующее отношение супруги, никогда не закончится. В качестве примера на рис. 7.1 приведен протокол решения вопроса супруги (авраам,сара) ? супруги(Х,У) <- супруги(Х,У). супруги(авраам,сара). супруги(авраам,сара) супруги(сара,авраам) супруги(авраам,сара) супруги(сара,авраам) Рис. 7.1. Незавершающееся вычисление. Рекурсивные правила, в которых рекурсивная цель является первой целью в теле правила, называются левыми рекурсивными правилами. Аксиома для отношения супруги является примером такого правила. Левые рекурсивные правила в Прологе причиняют немало хлопот. В случае несоответствующих аргументов использование этих правил приводит к бесконечным вычислениям. Лучшее решение этой проблемы-отказаться от использования левой рекурсии. В отношении супруги левая рекурсия использовалась для выражения коммутативности. Коммутативные отношения лучше всего задавать иным способом, вводя новый предикат, снабженный правилом для каждой перестановки аргументов исходного отношения. В случае отношения супруги можно задать такой новый предикат: быть-супругами(Человек!,Человек2), введя два правила: быть_супругами(Х,У) <- супруги(Х,У). быть _супругами(Х,У)«- супруги(У,Х). В общем случае, к-сожалению, невозможно избавиться от всех появлений левой рекурсии. Все простые минимальные рекурсивные программы, приведенные в гл. 3, содержат левую рекурсию и могут привести к бесконечным вычислениям. Однако соответствующий анализ, использующий введенные в разд. 5.2 понятия области и полной структуры, позволяет определить вопросы, решение которых относительно рекурсивных программ приводит к результату. В качестве примера рассмотрим программу 3.15, соединяющую два списка. Программа, задающая отношение append, всюду завершается для множества целей, в которых первый или/и последний аргументы-полные списки. Вычисление любого append-Bonpocd, у которого первый аргумент-полный список, неизбежно завершится. То же самое справедливо для всех вопросов, у которых третий аргумент является полным списком. Вычисление программы завершается в том случае, когда первый или/и третий аргумент- основные термы, не являющиеся списками. Наконец, есть вопросы, не приводящие к завершению вычислений: это вопросы, у которых и первый, и третий аргументы-неполные списки. Условие остановки программы 3.12, задающей отношение member, также формулируется в терминах неполных списков. Решение вопроса не завершается, если второй аргумент - неполный список. Если второй аргумент вопроса-полный список, то вычисление остановится. Другой, не всегда замечаемый случай, который заведомо приводит к незавер-
90 Часть II. Глава 7 шающимся вычислениям,-это порочный круг в определении. Рассмотрим пару правил родитель(Х,Y) <- ребенок(У,Х). ребенок(У,Х) <- родитель(Х,У). Любое вычисление, использующее предикат родитель или ребенок (например, родитель (аран,лот)!), не завершится. Ввиду порочного круга дерево поиска обязательно содержит бесконечную ветвь. Упражнения к разд. 7.2 1. Рассмотрите проблему остановки программ, определяющих префикс и суффикс списков (программа 3.13). 2. Рассмотрите проблему остановки программы 3.14(c), задающей отношение sublist. 7.3. Порядок целей Порядок целей более существен, чем порядок предложений. Порядок целей имеет решающее значение при определении последовательности действий в программе на Прологе. В программах сортировки списков, например в программе 3.22 быстрой сортировки, порядок целей применяется для задания последовательности выполнения шагов алгоритма сортировки. Обсудим сначала порядок целей в случае программирования баз данных. Порядок целей может повлиять на последовательность решений. Рассмотрим вопрос дочь(X,аран)! относительно видоизмененной программы 1.2, в которой переставлены факты женщина(милка) и женщина(иска). Мы получим два решения в следующем порядке: X = милка, X = иска. Если теперь поменять порядок целей в правиле, задающем отношение дочь, т.е. дочь(ХУ) <- женщина(Х), omeu(Y,X), то порядок решений, даваемых той же самой базой данных, будет: X = иска, X = милка. Причина, по которой порядок целей в теле предложения влияет на порядок решений, отличается от причины, по которой порядок правил в процедуре влияет на порядок решений. Изменение порядка правил не изменяет дерево поиска, которое используется при решении данной цели. Просто обход дерева производится в ином порядке. Изменение порядка целей приводит к изменению дерева поиска. Порядок целей определяет дерево поиска. Порядок целей влияет на количество проверок, выполняемых программой при решении вопроса, так как порядок целей определяет дерево поиска для обхода. Рассмотрим два дерева поиска решения вопроса сын(X,аран)!, приведенные на рис. 5.2. Они соответствуют двум разным способам поиска решений. В первом случае выбираются все дети человека аран и проверяется, являются ли они мужчинами. Второй случай соответствует перестановке целей в теле правила, задающего отношение сын, т.е. правилу сын(Х,У) <- мужчина(Х), родитель(Y,X). Теперь при решении вопроса просматриваются все мужчины и проверяются, являются ли они детьми человека аран. Если в программе имеется много фактов, относящихся к предикату мужчина, то объем поиска будет велик. Для других вопросов, например для вопроса сын(сара, X)!, данный порядок будет предпочтительнее. Поскольку сара не является мужчиной, поиск завершится быстрее. В зависимости от того, как используется программа, тот или иной порядок целей является наилучшим. Рассмотрим определение отношения дедушка-или-бабушка. Имеются две возможности:
Программирование на чистом Прологе 91 дедушка, или _бабушка(Х,У) <- родитель(Х,У), родитель(У,7). дедушка, или _ бабу iiiKa(X,Z) <- родитель(У,7), родитель(Х,У). Если данное отношение использовать для поиска внуков, например, с помощью вопроса дедушка-или-бабушка (авраам,Хр., то первое правило ускоряет поиск. Если же ищутся чьи-то предки с помощью такого вопроса, как дедушка-или- бабушка(Х,исаак)1, то поиск ускоряется при использовании второго правила. Если эффективность имеет существенное значение, то можно посоветовать ввести два различных отношения- дедушка -или -бабушка и внук, которые пользователь будет применять по своему усмотрению. В зависимости от порядка целей (в отличие от порядка правил) вычисления могут быть конечными или бесконечными. Рассмотрим рекурсивное правило предок(Х,У) <- родитель(Х,7), предок(7,У). Если переставить цели в теле правила, то программа становится левой рекурсивной и все вычисления Пролога, использующие отношение предок, будут незавершающимися. Порядок целей играет важную роль и в рекурсивном предложении алгоритма быстрой сортировки (программа 3.22): sort([X | Xs], Ys) <- partition(Xs,X,Ls,Bs), sort(Ls,Lsl), sort(Bs,Bsl), append(Lsl,[X|Bsl],Ys). Список следует разбить на две меньшие части перед рекурсивной сортировкой частей. Если, например, поменять порядок целей partition и рекурсивной сортировки, то ни одно вычисление не завершится. Рассмотрим теперь программу обращения списка (программа 3.16): reverse([X | Xs),Zs) <- reverse(Xs,Ys), append(Ys,[X],Zs). reversed ],[ ]). Существен не порядок правил, а порядок целей. Программа будет завершающейся, если первый аргумент цели-полный список. Цели, у которых первый аргумент-неполный список, приводят к бесконечным вычислениям. Если в рекурсивном правиле цели переставить, то завершение вычислений будет определяться вторым аргументом. Решение цели reverse, у которой второй аргумент - полный список, приведет к завершению вычисления. Вычисление не завершится, если второй аргумент - неполный список. Менее очевидный пример возникает при рассмотрении отношения sublist, определенного как суффикс префикса, в терминах двух append-целей (программа 3.14(e)). Рассмотрим вопрос sublist([2,3~],\_1,2,3 AW относительно данной программы. Данная цель сводится к вопросу append(AXs,Bs,[l,2,3,4])9 append{As\2,3\AXsfl. Возникает конечное дерево поиска, и исходная цель успешно решается. Если в программе 3.14(e) цели переставить, то исходный вопрос сведется к вопросу append(As\2,3\AXs\ append(AXs,Bs,[l,2,3,4~\)7. Из-за первой цели возникнет незавершающееся вычисление, подобное приведенному на рис. 5.4. Для рекурсивных программ, содержащих проверки (такие, как арифметическое сравнение или неравенство констант), можно привести полезное эвристическое правило определения порядка целей. Правило состоит в том, что проверки следует выполнять как можно раньше. Примером является программа для отношения partition - фрагмент программы 3.22. Первое рекурсивное правило имеет вид partition([X | Xs],Y,[X | Ls],Bs) <- X ^ Y, partition(Xs,Y,Ls,Bs).
92 Часть II. Глава 7 Проверку X ^ Y следует выполнить до рекурсивного вызова. Это приводит к сокращению дерева поиска. При программировании на Прологе (возможно, в отличие от обычных жизненных ситуаций) наша цель состоит в наиболее быстром достижении неудачи. Неудачи сокращают дерево поиска и быстрее приводят нас к правильному решению. Упражнения к разд. 7.3 1. Рассмотрите порядок целей программы 3.14(d), определяющей подсписок списка как суффикс некоторого префикса. В чем достоинство выбранного в программе 3.14(d) порядка append- целей? 2. Рассмотрите порядок предложений, порядок целей и условия остановки программы, задающей отношение substitute [см. упражнение 3.3(1)]. 7.4. Избыточные решения При построении программ на Прологе следует обратить внимание на важную характеристику программы, не имеющую аналогов в логическом программировании,-отсутствие избыточных решений. Значением логической программы является множество выводимых из программы основных целей. Здесь несущественно, выводится ли цель единственным образом или существует несколько различных выводов; однако это существенно в Прологе, поскольку от этого зависит эффективность поиска решений. Каждый возможный вывод означает дополнительную ветвь в дереве поиска. Чем больше дерево поиска, тем дольше продолжается вычисление. В общем случае желательно сохранить размер дерева поиска по возможности минимальным. Наличие избыточных решений из-за возвратов может вызвать в предельном случае экспоненциальный рост времени работы программы. При решении конъюнкции п целей, каждая из которых имеет одно избыточное решение, в случае возвратов может быть порождено 2" решений, что приводит к изменению оценки времени работы программы-от полиномиальной (или даже линейной) к экспоненциальной. Одна из причин появления избыточных решений в программах на Прологе состоит в наличии различных правил, пригодных для одного и того же случая. Рассмотрим следующие два предложения, определяющие отношение minimum: minimum(X,Y,X) <- X ^ Y. minimum(X,Y,Y) <- Y ^ X. Вопрос minimum(2,2,Мр при использовании этих двух правил имеет единственный ответ М = 2, который будет найден дважды. Таким образом, одно решение избыточное. Аккуратное определение отношения позволяет избежать подобной ситуации. Второе предложение можно заменить на minimum(X,Y,Y) <- Y < X. Теперь в случае, когда два числа равны, применимо лишь первое правило. Такую же аккуратность следует соблюдать и при определении отношения partition как части программы быстрой сортировки (программы 3.22). Программист должен быть уверен, что в случае равенства сравниваемого числа и числа, определяющего разбиение, применяется лишь одно рекурсивное правило. Другая причина, приводящая к избыточным решениям, состоит в рассмотрении слишком большого числа специальных случаев. Иногда подобное рассмотрение
Программирование на чистом Прологе 93 мотивируется стремлением к эффективности. К программе 3.15, определяющей отношение append, можно добавить дополнительный факт, а именно append (Xs,\_ \Xs), что позволяет сократить рекурсивные вычисления, когда второй аргумент-пустой список. Для исключения избыточных решений каждое из остальных предложений программы должно применяться лишь к спискам с ненулевым вторым аргументом. Поясним это на примере построения программы 7.2 для отношения merge (Xs,Ys,Zs), которое истинно, если слияние списков Xs и Ys из упорядоченных по возрастанию целых чисел приводит к упорядоченному списку Zs. merge(Xs, Ys,Zs) <- упорядоченный список целых чисел Zs получен слиянием упорядоченных списков Xs и Ys. mergc([X | Xs],[Y | Ys], [X |Zs]) <- X < Y,merge(Xs,[Y | Ys],Zs). megre([X | Xs],[Y | Ys],[X,X | Zs]) <- X = Y,merge(Xs,Ys,Zs). merge([X|Xs],[Y|Ys],[Y|Zs])- X> Y,merge([X | Xs],Ys,Zs). merge([ ],[X | Xs],[X | Xs]). merge(Xs,[ ],Xs). Программа 7.2. Слияние упорядоченных списков. Имеются три самостоятельных рекурсивных предложения, покрывающие три возможных случая: голова первого списка меньше, равна или больше головы второго списка. Предикаты <, = и > будут обсуждаться в гл.8. Следует рассматривать два случая, соответствующие исчерпанию каждого из списков. Заметьте, что мы позаботились и о том, чтобы цель тегде([ ],[ ],[ ]) соответствовала лишь одному, последнему факту. Избыточные вычисления возникают при использовании отношения member для определения того, входит ли некоторый элемент в некоторый список и существует ли несколько вхождений проверяемого элемента в этот список. Например, дерево поиска цели member(a,\_a,b,a,c]) содержит две вершины, соответствующие решению. Избыточные решения в предыдущих программах устранялись с помощью тщательного изучения логики программы. К программе member этот способ не применим. Для изменения поведения программы необходимо создать модифицированную версию программы member. member_check(X,Xs) <- X является элементом списка Xs. member _check(X,[X| Xs]). member_check(X,[Y | Ys])<- X Ф Y,member, check (X,Ys). Программа 7.З. Проверка вхождения в список. Программа 7.3 определяет отношение member-check (X,Xs), которое проверяет, входит ли X в качестве элемента в список Xs. Программа получена из программы 3.12, задающей отношение member, добавлением проверки в рекурсивное правило. Значение программы не изменилось, но данная программа, рассматриваемая как программа на Прологе, работает иначе. Рис. 7.2, содержащий деревья поиска одной и той же цели этими двумя программами, демонстрирует различие последних. Левое дерево соответствует поиску цели member(а,[а,Ь,а,с~\) в прог-
94 Часть II. Глава 7 member(a, [a,b,a,c]) member.check(a, [a,b,a,c]) true member(a,[b,a,c]) true a^a, member_check(a,[b,a,c]) member(a,[a,c]) / ч true member(a,[c]) member (a, [ ]) Рис. 7.2. Различные деревья поиска. рамме 3.12. Отметим, что в дереве существуют две вершины, соответствующие решению. Правое дерево соответствует поиску цели member-check{a,[a,b,a,c~\) в программе 7.3, и в нем имеется лишь одна вершина, соответствующая решению. Мы ограничили использование программы 7.3 для поиска целей случаями, когда оба аргумента - основные термы. Это объясняется реализацией отношения ф в Прологе, обсуждаемой в разд. 11.3. 7.5. Рекурсивное программирование в чистом Прологе Списки являются структурой данных, чрезвычайно полезной во многих разработках на Прологе. В этом разделе повторно будет рассмотрено несколько логических программ разд. 3.2 и 3.3, связанных с обработкой списков. Мы поясним, почему был выбран тот или иной порядок целей и предложений, рассмотрим вопрос завершения этих программ. Кроме того, в этом разделе приводятся и некоторые новые программы. Мы опишем их свойства и обсудим, как создаются подобные программы. Программы 3.12 и 3.15, записанные для отношений member и append, являются корректными программами на Прологе. Так как они являются минимальными рекурсивными программами, то не возникает проблемы, связанной с порядком целей. Преимущества выбранного в программах порядка предложений уже обсуждались в данной главе. Вопрос о завершении программ рассматривался в разд. 7.2. Программа 3.19, задающая отношение select, аналогична программе, задающей отношение member. select(X,[X | Xs],Xs). select(X,[Y|Xs],[Y| Ys]) <- select(X,Xs,Ys). Анализ программы 3.19 похож на анализ программы 3.12. Порядок целей не рассматривается, так как программа-минимальная рекурсивная. Порядок предложений выбран таким, чтобы отражать естественный порядок решения вопросов типа select(X,[a,b,c],Xs), например {X = a,Xs = [b,c~\},{X = b, Xs = [я.с]},^ = с, Xs = [я,6]}. Первое решение получено в результате удаления первого элемента и т.д. Программа завершается всегда, кроме тех случаев, когда второй и третий аргументы являются неполными списками. Новая версия отношения select возникает при добавлении проверки X Ф Y в рекурсивное предложение. Как и раньше, мы предполагаем, что отношение ф определено лишь для основных аргументов. Эта версия описана в программе 7.4. задающей отношение select-first (X,Xs,Ys). Программы 3.12 и 7.2, задающие отношения member и member-check, имеют одинаковые значения. В отличие от этого
Программирование на чистом Пролою 95 select „first (X,Xs,Ys) <- список Ys получается из списка Xs удалением первого вхождения элемента X. selectJirst(X,[X|Xs],Xs). selest_first(X,[Y| Ys],[Y|Zs]) <- X ф Y,select_first(X,Ys,Zs). Пр01рлмма 7.4. Выбор первого вхождения элемента в список. значения программ 7.4 и 3.19 не совпадают. Цель select(a,\_atb,a,c],\_a,b,c~\) принадлежит значению программы select, а цель select-fir st(a,\_a,b,a,c],\_a,b,с]) не принадлежит значению программы select-first. Теперь мы рассмотрим программу 3.20, задающую отношение permutation. Порядок целей в данной программе, как и порядок целей в программе append, наиболее точно соответствует способу использования: permutation(Xs,[X | Ys]) <- select(X,Xs,Zs), permutation(Zs, Ys). permutation([ ],[ ]). Порядок целей и проблема завершения программы permutation тесно связаны между собой. Вычисление целей permutation, имеющей в качестве первого аргумента полный список, неизбежно остановится. Вопрос вызывает процедуру select с полным списком в качестве второго аргумента, которая завершается получением полного списка в качестве третьего аргумента. Таким образом, перед рекурсивным вызовом цели permutation будет построен полный список, соответствующий первому аргументу. Если же первый аргумент - неполный список, то вычисление не завершится, поскольку произойдет обращение к процедуре select, которое приведет к бесконечному вычислению. Если переставить цели в рекурсивном правиле программы permutation, при анализе вопроса о завершении решающим становится значение второго аргумента. Если это значение - неполный список, то вычисление не завершится. Полезным предикатом, использующим отношение ф, является nonmember(X,Ys), выполненный, если X не входит в список Ys в качестве элемента. Его декларативное определение очевидно: элемент не входит в список, если он отличается от головы списка и не входит в его хвост. Базисный факт - никакой элемент не входит в пустой список. Программа приведена в виде программы 7.5: nonmember (X,Xs) <- X не является элементом Xs nonmember(X,[Y| Ys]) <- X Ф Y, nonmember(X,Ys). nonmember(X,[ ]). Программа 7.5. Отсутствие вхождений в список. Так как в программе используется отношение ф, то применение предиката nonmember ограничено основными примерами. С интуитивной точки зрения это разумно. Существует сколь угодно много элементов, не входящих в данный список, и сколь угодно много списков, не содержащих данный элемент. Таким образом, поведение программы 7.5 при решении подобных вопросов не слишком существенно. Порядок предложений в программе nonmember соответствует соглашению о размещении рекурсивных предложений перед фактами. При определении порядка целей использовалось эвристическое правило, предписывающее ставить проверки перед рекурсивными целями.
96 Часть II. Глава 7 Мы рассмотрим построение двух программ, связанных с отношением «подмножество». Программа 7.6 определяет предикат, основанный на отношении member в программе 3.12; программа 7.7 определяет предикат, основанный на отношении select, в программе 3.19. В обеих программах рассматривается вхождение элементов первого списка во второй список. members (Xs, Ys) <- каждый элемент списка Xs является элементом списка Ys. members([X|Xs],Ys) <- member(X,Ys), members(Xs,Ys). members([ ],Ys). Программа 7.6. Определение подмножества. selects (Xs,Ys) <- список Xs является подмножеством списка Ys. selects([X|Xs],Ys) <- select(X,Ys,Ysl), selects(Xs,Ys1). selects([ ],Ys). select(X,Ys,Zs) <- См. программу 3.19. Программа 7.7. Определение подмножества. Программа 7.6, определяющая предикат member s(Xs,Ys), игнорирует неоднократные вхождения элементов в списки. Например, members([/?,/?], \_a,b,c]) принадлежит значению программы. В первом списке имеются два вхождения элемента Ь, а во втором-только одно. Программа 7.6 имеет ограниченную область завершения. Если или первый, или второй аргумент цели members - неполный список, то программа не завершится. Второй аргумент должен быть полным списком ввиду вызова процедуры member, a первый аргумент должен быть полным списком, так как он обеспечивает управление рекурсией. Поиск решения вопроса members(Xs,[l ,2,3~\), в котором спрашивается о всех подмножествах заданного множества, не завершится. Так как в Xs допускаются многократные вхождения одного и того же элемента, то существует бесконечное число решений и, следовательно, вычисление не закончится. Оба ограничения устранены в программе 7.7. Пересмотренное отношение называется selects(Xs,Ys). Цели, принадлежащие значению программы 7.7, содержат не больше вхождений элемента в первый список, чем во второй. Благодаря этому свойству программа 7.7 завершается всякий раз, когда второй аргумент является полным списком. Такие вопросы, как selects{Xs,[a,b,c]), имеют в качестве решений все подмножества данного множества. Рассмотрим теперь иной пример: пословный перевод списка английских слов в список французских слов. Отношение задается в виде translate (Words,Mots), где Words-список английских слов, a Mots-список соответствующих французских слов. Программа 7.8 выполняет перевод. В ней предполагается наличие словаря, содержащего пары соответствующих английских и французских слов; реляционная схема словаря -diet (Word,Mot). Перевод очень наивный, не учитывающий число, род, спряжение и тому подобное. Область применения программы состоит из вопросов вида translate{[the,dog,classes,the,cat],X)'}, решением является X = (\_le, chien,classes,le,chaf\). Эта программа может быть использована разными способами. Можно переводить английские предложения на французский, французские-на английский, или можно проверить, являются ли два предложения правильным переводом одного в другое. Программа 7.8 является типичной программой, выполняющей отображение, т. е. преобразующей один список в другой путем применения некоторой функции к
Программирование на чистом Прологе 97 translate ( Words, Mots) <- Mots-список французских слов, являющихся переводом списка английских слов Words. translate ([Word | Words], [Mot | Mots]) <- dict(Word, Mot), translate(Words, Mots), translate^ ],[ ]). diet (the, le). diet (dog, chien). diet (classes, classe). diet (cat, chat). Программа 7.8. Пословный перевод. каждому элементу списка. Порядок правил ставит на первое место рекурсивные правила, порядок целей обеспечивает сначала вызов процедуры diet, чтобы избежать левой рекурсии. Закончим данный раздел обсуждением использования структур данных в программах на Прологе. Использование структур данных в Прологе несколько отличается от их использования в обычных языках программирования. Вместо того чтобы вводить глобальную структуру, все части которой доступны, программист описывает логические взаимосвязи между различными подструктурами данных. С процедурной точки зрения для построения и изменения структур программист, пишущий на Прологе, должен передать необходимые поля структуры подпроцедурам. Указанные поля используются в процессе вычисления, с ними сопоставляются нужные значения. Сопоставление значений со структурами происходит в процессе унификации. Рассмотрим подробнее характерный случай - построение одного результирующего значения по некоторому данному входу. В качестве примеров используем программу append для соединения двух списков с целью получения третьего и программу 7.8 для перевода списка слов с английского на французский. Вычисление происходит рекурсивно. Исходный вызов сопоставляет результату неполный список [A^|Xs]. Голова списка-X получает значение в результате вызова процедуры, чаще всего в процессе унификации с заголовком предложения. Хвост Xs получает значение постепенно, в процессе рекурсивного обращения. Структура становится окончательно определенной при применении исходных фактов и завершении вычисления. Рассмотрим присоединение списка \_c,d] к списку [я,6], как это описано на рис. 4.3. Результат строится поэтапно в виде Ls = \a\Zs\Zs = \b,Zsl\ и, наконец, применение исходного факта программы append даст результат Zsl = \_c,d\. Каждое рекурсивное обращение частично определяет начально неполный список. Заметим, что рекурсивные вызовы процедуры append не должны обращаться к уже вычисленному списку. Это - построение рекурсивных структур нисходящим методом, типичное для программирования на Прологе. Построение рекурсивных структур данных нисходящим методом имеет одно ограничение. Построенные части глобальных структур данных не могут использоваться в дальнейших вычислениях. Проиллюстрируем это на примере программы, определяющей отношение no-doubles(XXs,Xs), выполненное, если список Xs содержит все элементы списка XXs без повторных вхождений элементов. Попытаемся определить отношение по-doubles с помощью построения нисходящим методом. Заголовок рекурсивного правила, по которому нам следует заполнить пробел, должен иметь вид no_doubles([X | Xs],...) «- 4 1402
98 Чисть П. Г.шва 7 Пробелы заполняются рекурсивным обращением к процедуре по-doubles с входом Xs и результатом Ys и последующим присоединением X к Ys. Если X еще не вошел в результат, то X следует добавить к результату и в правой части должен находиться терм [Х| Ys]. Если X уже включен в результат, то его добавлять не следует, поэтому надо использовать терм Ys. У нас нет простого способа выразить это утверждение. Невозможно установить, как устроена уже построенная часть результата. В программе по-doubles использован иной подход к проблеме. Вместо того чтобы определять, входит ли элемент в уже построенную часть результата, мы можем определить, появится ли он в дальнейшем. Для каждого элемента X проверяется, появится ли он еще раз в хвосте списка-в Xs. Если X появится, то результатом будет Ys - результат рекурсивного обращения к процедуре по-doubles. Если X не появится, то X добавляется к результату рекурсивного обращения. Эта схема определения отношения no-doubles реализована в программе 7.9. В ней использована программа 7.5, определяющая отношение nonmember. по-doubles (Xs, Ys) <- список Ys получен удалением всех повторных вхождений из списка Xs. no_doubles([X|Xs],Ys)<- member(X,Xs), no_ doubles(Xs,Ys). no_doubles([X | Xs],[X | Ys]) <- nonmember(X, Xs), no_doubles(Xs, Ys). nonmember(X,Xs) <- См. программу 7.5. Программ;! 7.9. Удаление повторов из списка. Программа 7.9 может оказаться неудобной, так как список, не включающий повторные вхождения, может содержать элементы не в надлежащем порядке. Например, вопрос no-doubles(\_a,b,c,b],Xs)1 имеет решение Xs = [a,c,b], в то время как решение Xs = \_a,b,c] может быть предпочтительнее. Данное требование будет выполнено, если переделать программу. Как только мы находим какой-то элемент в списке, мы удаляем его из остатка списка. Это можно сделать, заменив в программе 7.9 два рекурсивных вызова на правило. no_doubles([X | Xs],[X | Ys]) <- delete(X,Xs,Xsl), no_doubles(Xsl,Ys). Новая программа строит результат нисходящим методом. Однако для больших списков, как будет показано в гл. 13, она не эффективна, поскольку каждое обращение к процедуре delete приводит к перестройке всей структуры списка. Альтернативной для построения структур является стратегия снизу вверх (восходящий метод). Программа 3.16Ь служит простым примером подобного построения структур данных. В этой программе обращаются списки: reverse(Xs,Ys) <- reverse(Xs,[ ],Ys). reverse([X | Xs],Acc,Ys) <- reverse(Xs,[X | Acc],Ys). reverse([ ],Ys,Ys). К аргументам отношения reverse/2 добавлен дополнительный аргумент, предназначенный для накопления в процессе вычисления значений обращенного списка. Данная процедура reverse строит результат не нисходящим, а восходящим методом. Протокол, приведенный на рис. 7.3, описывает решение цели reverse{[a,b,c\Xs\ последовательные значения среднего аргумента при обращении к процедуре reverse/3-\_ ], [я],[6,я], \_с,Ьд]. Эта последовательность соответствует создаваемой структуре.
Программирование на чистом Прологе 99 reverse ([a,b ,c] ,Xs) reverse([a,b,с],[ ],Xs) reverse ([b ,c], [a] ,Xs) reverse([c] ,[b,a] ,Xs) reverse([ ],[c,b,a],Xs) Xs=[c,b,a] true Ргк. 7.З. Протокол вычисления reverse. При построении восходящим методом разрешен доступ в процессе вычисления к частичным результатам. Рассмотрим отношение nd-reverse (Xs,Ys), объединяющее в себе свойства отношений по-doubles и reverse. Смысл отношения nd-reverse состоит в том, что Ys- список элементов из Xs, расположенных в обратном порядке и без повторов. Так же как и в случае отношения reverse, отношение nd-reverse использует предикат nd-reverse 13 с дополнительным аргументом, предназначенным для построения результата восходящим методом. Этот аргумент и используется при проверке вхождения отдельных элементов в отличие от программы 7.6, в которой при проверке используется хвост списка. Искомой программой является программа 7.10. nd_reverse(Xs,Ys) <- Список Ys является обращением списка, полученного удалением всех повторных вхождений в список Xs. nd._reverse(Xs,Ys) <- nd_reverse(Xs,[ ],Ys). nd_.reverse([X | Xs],Revs,Ys) <- member (X, Revs), nd_reverse (Xs, Revs, Ys). nd_reverse([X | Xs],Revs, Ys) <- nonmember(X,Revs), nd_reverse(Xs,[X | Revs],Ys). nd_reverse([ ],Ys,Ys). nonmember(X,Xs) <- См. программу 7.5. Программ;: ".10. Обращение без повторов. Опишем характерные особенности рассмотренной конструкции, построенной восходящим методом. Один аргумент используется для накопления результирующей структуры данных. В процессе рекурсивных обращений размер данного аргумента увеличивается, поэтому его структура в теле предложения сложнее, чем в заголовке. В противоположность этому при использовании конструкции, построенной нисходящим методом, более сложные структуры создаваемых данных входят в заголовок правила. Другой аргумент используется только для возврата результата, т.е. для конечного накопленного значения. Этот аргумент получает значение при использовании исходного факта. Никаких явных изменений в данном аргументе при рекурсивных обращениях не происходит. Техника добавления «накопителя» к программе допускает обобщения. Мы используем эту технику в следующей главе, рассматривая арифметические программы на Прологе. Накопители можно также рассматривать как специальный случай неполных структур данных, как будет показано в гл. 15. Упражнение к разд. 7.5 1. Перепишите программу 7.9, задающую отношение по-doubles, используя конструкцию снизу вверх. 4*
100 Часть II. Глава 8 7.6. Дополнительные сведения Пролог рассматривался в качестве первого приближения к логическому программированию, и предполагалось, что в результате дальнейших исследований он будет вытеснен. Средства управления в Прологе всегда считались ограниченными и упрощенными. Популярный лозунг, провозглашенный Ковальским (Kowalski, 1979b), «Алгоритм = Логика + Управление». Средства управления, использованные в чистом Прологе, рассматривались как первый этап на пути к декларативному программированию и интеллектуальному управлению. Но время показало обратное. Средства управления Пролога были признаны достаточными для многих приложений, и язык не только выдержал испытание временем, но и завоевал популярность. Несмотря на это, в работах по логическому программированию предлагались и другие виды управления. Например, LOGLISP (Robinson, Siber, 1982) использует обход дерева в ширину, IC-Пролог (Clark, McCale, 1978) использует сопрограммы. MU-Пролог (Naish, 1985a) допускает приостановку вычислений для обеспечения корректной реализации отрицания и исключения возможных случаев поиска по бесконечным ветвям. Условия ожидания были обобщены (Naish, 1985a) в связи с условиями завершения программ на Прологе, приведенными в разд. 7.2. Другие исследования, касающиеся свойств программ на Прологе, обсуждаются в (Mellish, 1985). Глава 8 Арифметика Логические программы разд. 3.1, реализующие арифметику, очень элегантны, но практически непригодны. Любой приличный компьютер имеет эффективную аппаратную реализацию арифметических операций, и ни один реальный язык логического программирования не может игнорировать этот факт. Время выполнения операций, таких, как сложение, на большинстве компьютеров не зависит от размера слагаемых (если они не превосходят некоторой большой константы). Время же работы рекурсивной программы plus (программа 3.3) пропорционально размеру первого слагаемого. И хотя временные характеристики могут быть улучшены за счет использования двоичной или десятичной записи, но все же не на столько, чтобы быть сравнимыми с характеристиками мощной аппаратной реализации. 8.1. Системные арифметические предикаты Роль арифметических предикатов, встроенных в Пролог,-обеспечить непосредственный интерфейс с арифметическими возможностями компьютера. Плата за эффективность состоит в потере некоторыми машинно-ориентированными арифметическими операциями общности, присущей их логическим аналогам. Указанный интерфейс - это арифметический вычислитель, использующий особенности арифметики применяемого компьютера. Edinburgh-Пролог содержит бинарный оператор is, предназначенный для арифметических вычислений. Мы предпочитаем более привычный бинарный оператор: = для обозначения того же предиката.
Арифметика 101 Операторы используются для того, чтобы сделать программу более удобной для чтения. Люди способны обучаться и приспосабливаться ко многому-например, они могут привыкнуть к чтению программ и на Лиспе, и на Фортране. Тем не менее мы считаем, что синтаксис имеет важное значение; роль удачных обозначений в математике общеизвестна. Возможность определять и использовать операторы составляет существенную часть удобного синтаксиса языка Пролог. Такие операторы, как #, <, использовались и в предыдущих главах. Мы рассмотрим несколько операторов Пролога, которые будут вводиться по мере необходимости. Большинство версий Пролога предоставляет пользователю возможность определять свои собственные бинарные инфиксные и унарные префиксные и постфиксные операторы. При этом необходима некоторая форма объявления операторов для указания старшинства, имени и ассоциативных свойств каждого оператора. Способ описания этой информации в различных версиях различен. Форма объявления операторов в версиях Edinburgh-Пролог и Wisdom-Пролог приведена в приложении С, там же дан список всех операторов, используемых в книге, и указано их старшинство. Основной вопрос для арифметического вычислителя имеет вид Значение: = Выражение ?и читается: «Значение есть Выражение». Его интерпретация происходит следующим образом. Выражение вычисляется как арифметическое выражение. Результат успешного вычисления унифицируется с термом Значение, в соответствии с этим цель решается успешно или безуспешно. Приведем несколько примеров простого сложения, поясняющих способ реализации и использования вычислителя. Цель (X: = 5 + 3)1 имеет решение X = 8. Это обычное использование вычислителя, сопоставляющее переменной значение арифметического выражения. Цель (8: = 5 + 3)1 решается успешно. Конкретизация обоих аргументов оператора «: =» позволяет проверять значение арифметического выражения. Вопрос (3 + 5:= 3 + 5)1 приводит к отказу, так как левый аргумент 3 + 5 не унифицирован с термом 8, значением вычисленного выражения. Вычислитель допускает использование стандартных операторов сложения, вычитания, умножения и деления ( + , —, *, /), старшинство операций такое же, как в математике. В данной книге мы ограничимся целочисленной арифметикой. Поэтому / обозначает целочисленное деление, a mod обозначает целочисленный остаток. Что происходит в тех случаях, когда вычисленный терм не является правильным арифметическим выражением? Выражение может быть неправильным по одной из двух причин, которые, по крайней мере концептуально, следует различать. Терм вида 3 + х, где х-константа, не может быть вычислен. В отличие от этого терм 3+Y, где У-переменная, может быть вычислен или не вычислен в зависимости от значения переменной Y Семантика любой логической программы полностью определена, и в этом смысле при работе логической программы не может возникнуть ошибки. Например, цель X:= Y+3 имеет решения {X = 3,Y= 0}. Однако при выполнении логических программ на компьютере следует учитывать аппаратные ограничения. Ошибка возникает в тех случаях, когда машина не может определить результат вычислений из-за недостаточности информации, т.е. из-за неопределенности переменных. Эта ситуация отличается от той, в которой решение цели приводит к отказу. Расширения Пролога и других логических языков реагируют на подобные «ошибки» приостановкой вычисления выражения до тех пор, пока значения требуемых переменных не станут известны. Наша вычислительная модель Пролога не допускает приостановки вычислений. Поэтому в подобных случаях мы будем говорить не о безуспешных вычислениях, а о возникновении ошибочной ситуации (сообщении об ошибке). Цель (X: = 3 + х)1 приводит к отказу, так как правый аргумент не может быть вычислен как арифметическое выражение. Цель (X: = 3 + У)? является примером
102 Часть И. Глава 8 цели, которая может быть успешно решена, если У сопоставлен с арифметическим выражением. В случае такой цели должно быть предусмотрено сообщение об ошибке. Общее для новичков заблуждение состоит в трактовке оператора «: =» как оператора присваивания, знакомого по обычным языкам программирования, что приводит к такой записи цели: (N := N + 1)? Подобный вопрос бессмыслен. Решение цели приведет к отказу, если N сопоставлено со значением, и к ошибке, если N- переменная. Предикат «: = » является примером системного предиката. Системные предикаты реализуются системой Пролога и доступны программисту. Другое название системных предикатов-вычиса.че.^ые предикаты. Приложение В содержит описание системных предикатов Wisdom-Пролога, используемых в книге. Другими арифметическими системными предикатами являются операторы сравнения. Вместо того чтобы использовать логически определенные отношения <, <, >, ^, Пролог непосредственно использует арифметику применяемого компьютера. Мы опишем использование предиката <, остальные предикаты используются аналогично. Для ответа на вопрос {А < В)! А и В вычисляются как арифметические выражения. Численные результаты сравниваются. Если результат вычисления А меньше результата вычисления В, то решение цели успешно. Как и раньше, если А или В не является арифметическим выражением, то вычисление безуспешно, если А и В -не основной пример, то выдается сообщение об ошибке. Приведем несколько простых примеров. Вычисление цели (1 <2)? успешно, то же самое относится к цели (3 — 2<2*3 + 1)1. С другой стороны, вычисление цели (2<7)? приводит к отказу, a (N<1)! приводит к сообщению об ошибке, если N- переменная. Проверка равенства и неравенства значений арифметических выражений производится с помощью системных предикатов =:= и =/ = , которые вычисляют значения обоих аргументов и сравнивают результаты. 8.2. Повторное рассмотрение арифметических логических программ Выполнение арифметики с помощью вычислений, а не логики требует пересмотра арифметических логических программ, приведенных в разд. 3.1. Очевидно, вычисления могут выполняться более эффективно. Например, при нахождении минимума двух чисел можно использовать арифметическое сравнение. При этом программа синтаксически не будет отличаться от программы 3.7. Аналогично наибольший общий делитель двух целых чисел можно эффективно вычислить с помощью обычного алгоритма Евклида, как это показано в программе 8.1. Заметим, что явное условие J<0 необходимо, чтобы избежать неоднозначности решений при J = 0 и ошибок при обращении к предикату mod с нулевым аргументом. greatest_common_divisor (X, Y,Z) <- Z-наибольший общий делитель целых чисел X и Y. greatest_common_divisor (I, 0, I). greatest_common_divisor (I, J, Gcd) <- J > 0, R: = I mod J, greatest_common_divisor(J,R,Ged). flpoi na\i\i»i Ь.1. Нахождение наибольшего делителя двух целых чисел.
Арифметика 103 Арифметические программы на Прологе теряют два свойства арифметических логических программ. Прежде всего использование программ становится более ограниченным. Предположим, что мы хотим с помощью оператора : = построить предикат plus(X,Y,Z), подобный описанному ранее. Очевидное определение- plus(X,Y,Z)<-Z:= x + Y. Оно пригодно, если X и У сопоставлены с целыми числами. Однако мы не можем использовать эту программу для вычитания, задавая вопросы вида plus (3,X, #)?, поскольку это вызовет сообщение об ошибке. Для того чтобы одну и ту же программу использовать и для сложения, и для вычитания, необходимы металогические проверки. Мы отложим этот вопрос до рассмотрения металогических предикатов в гл. 10. Эффективность программы обусловливается специализацией ее использования, и поэтому трудно понять, что происходит при непредусмотренных использованиях программы. Например, программа 3.7, задающая отношение minimum, может быть надежно использована только при нахождении минимума двух целых чисел. Другое свойство, отсутствующее в арифметических программах на Прологе,-рекурсивная структура чисел. В логических программах эта структура использовалась для определения применяемого правила и для гарантии завершения вычислений. Программа 8.2, вычисляющая факториал на Прологе, похожа на программу 3.6. Однако рекурсивное правило стало более громоздким. Первый аргумент рекурсивного обращения к процедуре factorial должен быть вычислен явно, он не может быть получен в результате унификации. Кроме того, следует явно указать условие применимости рекурсивного правила N>0. Это препятствует возникновению незавершающихся вычислений при решении таких вопросов, как factorial (— l,iV)? или factorial(3,F)1 В логической программе унификация рекурсивной структуры очевидным образом запрещает незавершающиеся вычисления. factorial (N,F) <- F равно факториалу целого числа N. factorial (N,F)«- N>0,N1 := N-l,factorial(Nl,Fl), F:= N*F1. factorial (0,1). I ip- i p;i\i\i:i XI Вычисление факториала. Программа 8.2 соответствует обычному рекурсивному определению факториала. В отличие от программы 3.7 данная программа может использоваться только для нахождения факториала заданного числа. Вопросы, в которых первый аргумент предиката factorial-переменная, вызовут сообщение об ошибке. Нам следует изменить понятие корректности программ на Прологе с учетом особенностей выполнения арифметических проверок. Другие системные предикаты, вызывающие сообщения об ошибках, учитываются сходным образом. Программа на Прологе тотально корректна в области D, если решение любой цели, принадлежащей области D, завершается, не приводит к сообщению об ошибке и дает правильный ответ. Программа 8.2 тотально корректна во множестве целей, у которых первый аргумент - целое число. Упражнении к разд. 8.2 1. Треугольное число с индексом N-это сумма всех натуральных чисел до N включительно. Напишите программу, задающую отношение triangle(N,Т), истинное, если Т-треугольное число с индексом N. (Указание: используйте программу 8.2.)
104 Часть II. Глава 8 2. Напишите программу на Прологе, задающую предикат power (X,N,V), выполненный, если V равно X^N. Как она может быть использована? (Указание: возьмите за основу программу 3.5, задающую отношение ехр.) 3. Напишите программы на Прологе, аналогичные другим арифметическим логическим программам, приведенным в основном тексте и упражнениях разд. 3.1. 8.3. Замена рекурсии итерацией В Прологе итерационные конструкции как таковые отсутствуют, более общее понятие - рекурсия используется как в рекурсивных, так и в итерационных алгоритмах. Главное преимущество итерации перед рекурсией состоит в эффективности, особенно эффективности использования памяти. При выполнении рекурсии каждый рекурсивный вызов, не завершенный к данному моменту, требует определенной структуры данных (называемой фрагментом стека). Таким образом, размер области памяти для вычисления, включающего п рекурсивных обращений к процедурам, линейно зависит от п. С другой стороны, итерационные программы обычно используют фиксированный объем памяти, не зависящий от числа итераций. Тем не менее существует ограниченный класс рекурсивных программ, достаточно точно соответствующих обычным итерационным программам. При некоторых условиях, описанных в связи с оптимизацией рекурсии в разд. 11.2, такие программы можно реализовать на Прологе почти так же эффективно, как итерационные программы в обычных языках. По этой причине желательно по возможности задавать отношение с помощью итерационной программы. В данном разделе мы покажем, как использование накопления позволяет преобразовать рекурсивную программу в итерационную. Напомним, что предложение чистого Пролога называется итерационным, если в теле предложения содержится один рекурсивный вызов. Мы распространим это определение на общий Пролог, допуская использование любого числа системных предикатов перед рекурсивным вызовом. Программа на Прологе называется итерационной, если она содержит лишь единичные и итерационные предложения. Большинство простых арифметических вычислений может быть задано с помощью итерационных программ. Например, факториал может быть выполнен с помощью цикла, в котором перемножаются все числа, вплоть до числа, факториал которого мы хотим найти. Соответствующая процедура на языке, подобном языку Паскаль, приведена на рис. 8.1. Итерационный характер этой процедуры может быть непосредственно выражен в итерационной программе на Прологе. factorial(N); 1: = 0; Т:= 1; while I < N do I: = I + 1; T: = T*I end; return T Рис. 8.1. Итеративное вычисление факториала. В Прологе отсутствуют «локальные» переменные для удержания промежуточных результатов и их изменения в процессе вычисления. Поэтому для реализации итерационных алгоритмов, требующих сохранения промежуточных результатов, процедуры Пролога дополняются аргументами, называемыми накопите.{.ими. Обычно промежуточное значение доответствует результату вычисления при завершении итерации. Это значение сопоставляется результирующей переменной с помощью единичного предложения процедуры.
Арифметика 105 Этот метод демонстрируется на примере программы 8.3, которая содержит определение на Прологе отношения factorial, соответствующее циклу «while» рис. 8.1. В программе используется отношение factorial (I,N, T, F), выполненное, если F равно факториалу числа N, а значения переменных / и Гсовпадают со значениями соответствующих переменных в цикле перед (1+1)-й итерацией цикла. factorial(N.F) *- F равно факториалу целого числа N. factorial(N,F) *- factorial(0,N, l,F). factorial(I,N,T,F) I<N, II : = 1 + 1, Tl := T*I1,factorial(Il,N,T1,F). factorial(N,N,F,F). Программа 8.З. Итерационное вычисление факториала. Основной итерационный цикл реализован с помощью итерационной процедуры factorial/4. Каждая редукция цели, использующая процедуру factorial/4, соответствует итерации цикла «while». Обращение к процедуре factorial/4 в процедуре factorial/2 соответствует начальному этапу. Первый аргумент отношения factorial/4, соответствующий счетчику цикла, полагается равным 0. Третий аргумент отношения factorial/4 используется для накопления текущего значения произведения. Он устанавливается равным / при обращении к процедуре factorial/4 в процедуре factorial/'2. Использование обоих накопителей в программе 8.3 типично для программирования на Прологе. Оно очень похоже на использование накопителей в программах 3.16Ь и 7.10 для отбора элементов списка. Накопители являются логическими переменными, а не ячейками памяти. В процессе итерации передается не адрес, а значение. Так как логические переменные обладают свойством «одноразовой записи», то измененное значение - новая логическая переменная - передается каждый раз. Чтобы выразить этот факт, мы используем для указания измененных значений суффикс / в обозначениях переменных, например 77 и /У. Вычисление остановится, когда счетчик достигнет значения N. Правило для предиката factorial/4 в программе 8.3 более не применяется, и факт достигнут. После успешного завершения редукции значение факториала «возвращается» как результат унификации с накопителем в базисном предложении. Отметим, что логическая переменная, представляющая решение (последний аргумент отношения factorial/4), должна следовать по всему вычислению, чтобы получить значение при заключительном вызове процедуры factorial/4. Подобная передача значений с помощью аргументов типична для программ на Прологе, хотя новичку может показаться странной. Программа 8.3 является точным отражением цикла «while», задающего факториал на рис. 8.1. Другой вариант итерационного вычисления отношения factorial может быть получен путем изменения счетчика otN до 0, а не от 0 до N. Основная структура программы остается той же и приведена как программа 8.4. Здесь имеются начальное обращение, устанавливающее значение накопителя, рекурсивное и базисное предложения, реализующие цикл «while». Программа 8.4 несколько эффективнее программы 8.3. Вообще, чем меньше аргументов имеется в процедуре, тем легче понять ее запись и тем быстрее она выполняется. Полезным итерационным предикатом является предикат between (I, J,К), истинный, если целое число К лежит между / и J включительно. Предикат может быть использован для недетерминированного порождения целых чисел в интервале. Это
106 Часть П. Глава 8 factorial (N,F) <- F равно факториалу целого числа N. factorial(N,F) <- factorial(N,1,F). factorial(N,T,F) <- N>0,T1 := T*N, N1 := N-l, factorial(Nl,Tl,F). factorial (0,F,F). Программа М.4. Другой вариант итерационного вычисления факториала. between (I,J, К) <- число К лежит между / и J включительно. between (I.J.I) <- I^J. between (I, J, K) <- I<J,I1 := 1+1, between (II, J, K). IlpoipaviMa M.5. Порождение множества чисел. полезно в программах порождения и проверки, рассматриваемых в разд. 14.1, и циклах, управляемых отказом, рассматриваемых в разд. 12.5. Итерационные программы можно также использовать в работе со списками целых чисел. Рассмотрим отношение sumlist(IntegerList,Sum), в котором Sum есть сумма элементов списка Integer List, состоящего из целых чисел. Приведем две программы, вычисляющие данное отношение. Программа 8.6а представляет собой рекурсивное описание. Чтобы просуммировать список, следует просуммировать хвост списка и к результату прибавить голову списка. Программа 8.66 использует накопитель для подсчета текущей суммы, подобно тому как программа 8.3, задающая отношение factorial, использовала накопитель для подсчета текущего произведения. Вводится вспомогательный предикат sumlist/З с дополнительным аргументом для накопления, начальное значение 0 которого устанавливается при исходном обращении к процедуре sumlist/З. Сумму сопоставляют результату вычислений в заключительном вызове процедуры при унификации с базисным фактом. Единственное различие между программой 8.66 и итерационными версиями программы factorial состоит в том, что управление итерациями происходит с помощью рекурсивной структуры списка, а не с помощью счетчика. Рассмотрим еще один пример. Скалярным произведением двух векторов Х{ и Yt назьшается сумма Xt • Yt +... + Xn-Yn. Если векторы представляются в виде списков, то нетрудно написать программу, задающую отношение inner-product(Xs,Ys,IP), где IP-скалярное произведение векторов Xs и Ys. Программы 8.7а и 8.76 являются соответственно рекурсивным и итерационным вариантами. Взаимосвязь итерационного и рекурсивного вариантов программы inner_product аналогична взаимосвязи программ 8.6а и 8.66. Программы 8.7а и 8.76 корректны, если аргументы Xs и Ys цели inner_pro- duct(Xs,Ys,Z) являются списками равной длины, состоящими из целых чисел. В программах имеется проверка равенства длин. Вычисление приведет к отказу, если Xs и Ys имеют разную длину. Аналогия между взаимосвязью программ 8.6а и 8.66 и взаимосвязью программ 8.7а и 8.76 наводит на мысль, что одна программа может быть автоматически преобразована в другую. Эквивалентное преобразование рекурсивных программ в итерационные представляет интересную область исследований. Ясно, что это может быть сделано для приведенных здесь простых примеров. Сложность программы на Прологе зависит от исходных логических отношений,
Арифметика 107 sumIist(Is,Sum) <- число Sum равно сумме всех элементов списка Is, состоящего из целых чисел. sumlist([I|Is]),Sum)<- sumlist(Is,IsSum), Sum : = I + IsSum. sumlist([ ],0). IlpoipaMvia 8.6a. Суммирование списка целых чисел. sumlist(Is,Sum) число Sum равно сумме всех элементов списка Is, состоящего из целых чисел. sumlist(Is,Sum) <- sumlist(Is,0,Sum). sumlist([I | Is],Temp,Sum) <- Tempi := Temp + I,sumlist(Is,Templ,Sum). sumlist([ ],Sum, Sum). Ilpoipavivia S 66. Итерационный вариант суммирования списка целых чисел с использованием накопителя. inner_product (Xs, Ys, Value) <- Value есть скалярное произведение векторов, представленных списками целых чисел Xs и Ys. inner_product([X | Xs],[Y | Ys],IP) <- inner_product(Xs,Ys,IPl), IP:= X*Y + IP1. inner_product([ ],[ ],0). Программа 8.7а. Вычисление скалярного произведения векторов. inner_product (Xs, Ys, Value) <- Value есть скалярное произведение векторов, представленных списками целых чисел Xs и Ys. inner_product(Xs,Ys,IP) <- inner_product(Xs,Ys,0,IP). inner_product([X | Xs],[Y| Ys],Temp,IP) <- Templ := X*Y + Temp,inner_product(Xs,Ys, Tempi,IP). inner_product([ ],[ ],IP,IP). IIpoipaMNia 8 76. Итерационное вычисление скалярного произведения векторов. аксиоматизируемых в программе. Приведем пример простой программы на Прологе, решающей сложную задачу. Рассмотрим следующий вопрос: дан плоский замкнутый многоугольник {РРР2,...,РЛ}. Требуется найти площадь ориентированного многоугольника. Площадь вычисляется с помощью линейного интеграла l/2jxdy-ydx, где интегрирование производится по границе многоугольника. Решением является программа 8.8, задающая отношение area (Chain, Area). Chain является списком координат вершин, например [(4,6), (4,2), (0,#), (4,6)]. Значением переменной Area будет площадь многоугольника с данными вершинами. Площадь положительна, если многоугольник обходится против часовой стрелки, и отрицательна, если обход-по часовой стрелке. Вопрос area([{4,6\(4,2)~(0,8),(4,6)'], Area)! имеет решение Area = —8. Ориентация многоугольника изменяется, если пары перечисляются в обратном порядке. Решением вопроса ш*ея([(4,6),(0,#),(4,2),(4,6)], Area)! будет Area = 8. Приведенная программа не итерационна. Преобразование
108 Часть И. Глава 8 area (Points, Area) <- число Area равно площади, ограниченной замкнутой ломаной линией, вершины ломаной представлены списком Points, каждая точка в списке представлена парой целых чисел (X,Y), где X, У-координаты точки. area([Tuple],0). area([(Xl,Yl),(X2,Y2)|XYs],Area)*- area([(X2,Y2)|XYs],Areal), Area := (Xl*Y2-Yl*X2)/2 +Area 1. Программа 8.8. Вычисление площади многоугольника. данной программы в итерационную составляет задание упражнения (5) в конце раздела. Можно написать итерационную программу, находящую максимальный элемент в списке целых чисел. Реляционная схема отношения - maximum (Xs, Max), программа 8.9 представляет собой такую программу. Вспомогательный предикат maximum (Xs,X, Мах) истинен, если число Мах является максимумом числа X и элементов списка Xs. Исходное значение переменной X совпадает с первым элементом списка Xs. Заметьте, что максимум пустого списка в программе не определен. maximum (Xs, N) <- N равно максимальному элементу списка Xs. maximum([X | Xs],M) <- maximum(Xs,X,M). maximum([X|Xs],Y,M)*- X^ Y, maximum(Xs,Y,M). maximum([X|Xs],Y,M) <- X>Y,maximum(Xs,Y,M). maximum([ ],M,M). Программа 8.9. Нахождение максимального элемента в списке целых чисел. Стандартная рекурсивная программа нахождения максимального элемента списка основана на несколько ином алгоритме. Рекурсивный алгоритм состоит в нахождении максимального элемента хвоста списка и сравнении его с головой списка. В отличие от этого программа 8.9 запоминает текущий максимум пройденного начального отрезка списка. Программа 3.17, определяющая длину списка, интересна потому, что на ее примере можно продемонстрировать несколько способов преобразования логической программы в программу на Прологе со своими свойствами. Одной из таких программ является итерационная программа 8.10. Вопрос length (Xs,N'/? решается данной программой правильно, если N - натуральное число. В этом случае программа или проверяет длину списка или порождает список из N неопределенных элементов или приводит к безуспешным вычислениям. Однако программа не предназначена для нахождения длины списка. Вопрос вида length([l,2,3],N)l приводит к сообщению об ошибке. length (Xs,N) <- Xs -список длины N. length([X|Xs],N)*- N>0,N1 := N-1, length(Xs,Nl). length([ ],0). Программа 8.10. Проверка длины списка.
Арифметика 109 Длину списка можно найти с помощью программы 8.11. Однако эта программа не может быть использована для порождения списка из N элементов. В отличие от программы 8.10 вычисление программы 8.11 не завершается, если первый аргумент-неполный список. Таким образом, разные использования требуют наличия разных программ length. length (Xs,N) *- N длина списка Xs. lcngth([X | Xs],N) *- length(Xs,Nl), N := N1 + 1. lcngth([ JO). Программа S.ll. Нахождение длины списка. При определении отношения range(M,N,Ns), выполненного, если Ns-список целых чисел, находящихся между М и N включительно, следует, как и в случае предыдущей программы, рассмотреть предполагаемые использования предиката. Программа 8.12 имеет специфическое использование - порождение списка чисел в заданном интервале. Программа тотально корректна в области всех целей range(M,N,Ns) с определенными значениями переменных М и N. Однако эта программа непригодна для нахождения верхней и нижней грани набора чисел, так как в программе имеется проверка М < N. Удаление этой проверки позволит решать с помощью программы вопросы вида range(M,N,[l,2,3~\yi, но теперь возникнут незавершающиеся вычисления при решении вопросов, для которых программа была предназначена первоначально (таких, как range (J,3,Ns)l). range (M,N,Ns) <- Na-список целых чисел, расположенных между М и N включительно. rangc(M,N,[M|Ns])*- M<N, Ml := М+1, range(Ml,N,Ns). range(N,N,[N]). IIpoipaMua 8.12. Построение списка целых чисел в заданном интервале. Упражнения к разд. 8.3 1. Напишите итерационный вариант программы triangle(N,Т), описанной в упр. 8.2(1). 2. Напишите итерационный вариант программы power(X,N,V), описанной в упр. 8.2(2). 3. Перепишите программу 8.5 так, чтобы последовательные целые числа появлялись в убывающем порядке. 4. Напишите итерационную программу, задающую предикат timeslist (IntegerList;Product). Этот предикат вычисляет произведение (Product) элементов списка IntegerList, состоящего из целых чисел. Данная программа аналогична программе sumlist - программе 8.166. 5. Перепишите программу 8.8, вычисляющую площадь многоугольника так, чтобы программа стала итерационной. 6. Напишите программу нахождения минимального элемента в списке целых чисел. 7. Перепишите программу 8.11, вычисляющую длину списка так, чтобы она стала итерационной. (Указание: используйте счетчик, как это сделано в программе 8.3.) 8. Перепишите программу 8.12 так, чтобы список целых чисел строился не восходящим, а нисходящим методом. 8.4. Дополнительные сведения Программа, преобразующая рекурсивные программы в итерационные и пригодная для преобразования программ из разд. 8.3, описана в работе (Bloch, 1984).
1 10 Часть II. Глава 9 О программе 8.8, вычисляющей площадь многоугольника, мы узнали от Мартина Нильсона (Martin Nillson). Глава S) Анализ структуры термов Все реализации Пролога содержат некоторое число системных предикатов, связанных со структурой термов. В главе рассматриваются предикаты, используемые в данной книге. 9.1. Типовые предикаты Типовые предикаты -это унарные отношения, связанные с типом терма. Такие предикаты проверяют, является ли данный терм константой или структурой. Можно уточнить вид константы - является ли она целым числом или атомом. В данной книге допускаются четыре типовых предиката: integer/1, atom/1, constant/1 и compound/1. Список этих предикатов вместе с описанием приведен на рис. 9.1. integer(X) <- Х-целое число. atom(X) <- Х-атом. constant(X) <- Х-константа (целое число или атом). compound(X) <- Х-основной терм. Рис. 9.1. Системные типовые предикаты. Можно считать, что каждый тйповый предикат, описанный на рис. 9.1, как бы определен бесконечной таблицей фактов: таблицей целых чисел-integer(0), integer (1), integer ( — 1), ...; таблицей атомов в программе - atom (foo), atom(bar),...; таблицей функциональных символов в программе с переменными аргументами- compound(отец(X,Y)), compound(сын(X,Y)),... Отношение constant определяется таблицей, являющейся объединением таблицы целых чисел и таблицы атомов. Это выражается двумя правилами: constant(X) <- integer(X). constant(X) <- atom(X). Хотя в разных версиях Пролога предикаты реализуются по-разному, мы полагаем, что вычисления происходят так, будто предикаты заданы таблицами. Однако данные предикаты могут быть использованы лишь целями, имеющими конечное число решений. Если подобный предикат использовать в цели, имеющей бесконечное число решений, то возникнет сообщение об ошибке. Рассмотрим цель integer (X р.. Если Х-целое число, то цель решится успешно; если Х-атом или структура, то решение цели безуспешно. Если X - переменная, то появится сообщение об ошибке. Эта ситуация аналогична вычислению арифметического выражения, содержащего переменную. Отметим, что большинство реализаций Пролога не
Апа.пп сфукт\ры тсрмон 1 1 1 учитывают ошибочные ситуации, и цель integer (X), где X - переменная, приводит к безуспешным вычислениям. Заманчиво использовать вопрос вида atom(X)l для получения списка всех атомов с помощью механизма возврата в системе. Однако подобный способ использования предиката atom недопустим. Единственные термы, не рассматриваемые в наборе предикатов на рис. 9.1,- переменные. Пролог содержит системные предикаты, связанные с переменными. Но способ использования таких предикатов существенно отличается от способа использования системных предикатов, описываемых в данной главе. Металогические предикаты (таково формальное название подобных предикатов) являются предметом рассмотрения следующей главы. Приведем пример применения типовых предикатов в программе, раскрывающей список списков. Отношение flatten (Xs,Ys) истинно, если Ys- список элементов, встречающихся в списке списков Xs. Элементы списка Xs сами могут быть элементами или списками, так что элементы могут находиться на любой глубине вложенности. Пример цели, принадлежащей значению программы flatten, —flat- ten([[aUb,lc,d]leUa,b,c,d,e]). Простейший вариант программы flatten использует двойную рекурсию. Для раскрытия произвольного списка [XIXs], где X само может быть списком, раскрывается голова списка X, раскрывается хвост списка Xs и результаты соединяются: flatten([X|Xs],Ys)<- flatten (X, Ys 1), flatten (Xs, Ys2), append (Ys 1, Ys2, Ys). Что является исходным случаем? Раскрытие пустого списка приводит к пустому списку. Для другого исходного случая следует использовать тйповый предикат. Результат раскрытия константы приводит к списку, состоящему из константы flatten(Х,[Х])«- constant(X), X Ф [ ]. Условие constant (X) необходимо для того, чтобы данное правило не применялось в случае, когда X-список. Полной программой flatten является программа 9.1а. flatten (Xs,Ys) <- IS-список элементов, содержащихся в Xs. flatten([X|Xs],Ys)<- flatten(X,Ysl), flatten(Xs,Ys2), append(Ysl,Ys2,Ys). flatten(X,[X]) <- constant(X),X ф [ ]. flatten([ ],[ ]). Программа (>.ia. Раскрытие списка с помощью двойной рекурсии. Хотя декларативный смысл программы 9.1а очевиден, она реализует не самый эффективный способ раскрытия списков. В худшем случае, которым является левое линейное дерево, число редукций, используемых в программе, квадратично зависит от числа элементов в результирующем списке. Программа flatten, строящая результирующий список сверху вниз, несколько сложнее программы с двойной рекурсией. В ней используется вспомогательный предикат flatten (Xs, Stack, Ys), где Ys- раскрытый список, содержащий элементы из Xs, а стек Stack содержит списки, подлежащие раскрытию. Стек представляется списком. При обращении к процедуре flatten/3 в процедуре flatten/2 устанавливается начальное значение стека-пустой список. Перечислим случаи, рассматриваемые в
112 Часть II. Глава 9 процедуре flatten/ 3. Общий случай - раскрытие списка [X \ Xs]y где X -список. В этом случае список Xs помещается в стек и список X раскрывается рекурсивно. Для распознавания списка используется предикат list(X), определяемый фактом list{[_X | Xs\): flatten([X|Xs],S,Ys) <- list(X), flatten(X,[Xs|S],Ys). Если голова списка является константой, отличной от пустого списка, то она добавляется к результирующему списку и рекурсивно раскрывается хвост списка: flatten([X|Xs],S,[X| Ys]) <- constant(X), X Ф [ ], flatten(Xs,S,Ys). При достижении конца списка возможны две ситуации, зависящие от состояния стека. Если стек не пуст, то считывается элемент из вершины стека и вычисления продолжаются: flatten([ ],[X|S],Ys) <- flatten(X,S,Ys). Если стек пуст, то вычисления завершаются: Папеп([ ],[],[ ]). Полностью программа представлена как программа 9.16. flatten (Xs,Ys) <- Ys -список элементов, содержащихся в Xs. flatten (Xs,Ys) <- flatten (Xs,[ ],Ys). flatten([X | Xs],S,Ys) <- list(X), flatten(X,[Xs | S],Ys). flatten([X|Xs],S,[X|Ys])<- constant(X), X Ф [ ], flatten(Xs,S,Ys). flatten([ ],[X|S],Ys)<- flatten(X,S,Ys). natten([ ],[],[ ]). list([X|Xs]). Программа 9Л6, Раскрытие списка с помощью стека. Программа 9.16 демонстрирует основные приемы работы со стеком. Стеком управляет унификация. Объекты помещаются в стек при рекурсивных обращениях к рассматриваемому списку и извлекаются из стека при унификации с головой списка и рекурсивных обращениях к хвосту списка. Другое применение стеков описано в программах 14.13 и 14.15, моделирующих магазинный автомат. Заметим, что параметр стека является примером накопителя. Читатель может убедиться, что число редукций в данном варианте программы линейно зависит от размера результирующего списка. Упражнение к разд. 9.1 1. Перепишите программу 9.1а, задающую отношение flatten (Xs,Ys), используя накопитель вместо процедуры append и сохраняя двойную рекурсию. 9.2. Составные термы Терм распознается как составной на структурном уровне. Существуют предикаты, обеспечивающие доступ к имени функтора, арности и аргументам составного терма. Одним из таких системных предикатов является functor (Term, F, Arity). Этот предикат истинен, если главный функтор терма Term имеет имя F и арность Arity.
Анализ структуры термов 113 Пример успешной цели-functor (отец (аран, лот), отец, 2). Предикат functor, как и типовые предикаты, может быть определен таблицей фактов вида functor (f (XIf ..., XN),f N) для каждого функтора / арности N. Такими фактами будут, например, functor, (отец (X, Y), отец, 2), functor (сын (X, Y), сын, 2) ... .В большинстве версий языка Пролог константы рассматриваются как функторы арности 0, что приводит к соответствующему расширению таблицы. Использование предиката functor может по разным причинам привести к безуспешным вычислениям. Такая цель, как functor (отец (X, Y), сын, 2), не может быть унифицирована ни с одним фактом таблицы. Кроме того, на типы аргументов целей functor наложены некоторые ограничения. Например, третий аргумент предиката functor, соответствующий арности терма, не может быть атомом или составным термом. Нарушение подобных ограничений приводит к безуспешным вычислениям. Следует различать безуспешные вычисления и вычисления, приводящие к сообщению об ошибке. Сообщение об ошибке может возникнуть из-за бесконечного числа решений, например, при вопросе functor (X, Y, Z)?. Предикат functor обычно используется одним из двух способов. Первый способ-нахождение имени функтора и арности заданного терма. Например, ответом на вопрос functor (отец (аран, лот), X, Y)? будет {X = отец, У= 2}. Второй способ - построение терма с определенным именем функтора и арностью. Такой вопрос, как functor (Т, отец, 2), имеет ответ Т= отец (X, Y). Парным системным предикатом к предикату functor является предикат arg (N, Term, Arg), обеспечивающий доступ не к имени функтора, а к аргументам. Цель агд (N, Term, Arg) истинна, если Arg-N-й аргумент терма Term. Пример истинной цели-агд (1, отец (аран, лот), аран). Как и functor/3, предикат агд/3 обычно используется одним из двух способов. Первый способ - нахождение определенного аргумента составного терма. Вопрос, поясняющий подобное использование, - агд (2, отец (аран, лот), X)?'. Ответом на вопрос будет X = лот. При втором способе использования происходит конкретизация переменного аргумента терма. Например, при успешном решении вопроса агд (1, отец (X, лот), аран?) устанавливается соответствие X = аран. Можно, как и раньше, считать, что предикат агд определен так, будто существует бесконечная таблица фактов. Пример фрагмента такой таблицы: arg (1, отец (X, Y), X). arg (2, отец (X, Y), Y). arg (1, сын (X, Y), X,...). Вычисления, использующие предикат агд, безуспешны, если цель не может быть унифицирована с соответствующим фактом таблицы. Пример такой цели- агд (1,отец (аран, лот), авраам). Безуспешные вычисления возникают и в случае нарушения типовых ограничений, например если первый аргумент-атом. Цели вида arg(l,X,Y) приводят к сообщениям об ошибке. Рассмотрим пример использования предикатов functor и агд при анализе термов. Программа 9.2 задает отношение subterm (Tl, T2), истинное, если 77 -подтерм терма 72. По причинам, изложенным ниже, ограничимся случаем, когда 77 и 72-основные термы. Первое предложение программы 9.2, определяющее отношение subterm/2, утверждает, что любой терм является собственным подтермом. Второе предложение утверждает, что Sub является подтермом составного терма Term, если Sub-подтерм одного из аргументов терма Term. Определяется число аргументов, т. е. арность главного функтора терма. Это число используется в качестве счетчика цикла во вспомогательном предикате subterm/З, итерационно проверяющем все аргументы. Первое предложение процедуры subterm/3 уменьшает значение счетчика и рекурсивно обращается к процедуре subterm. Второе предложение процедуры рассмат-
1 14 Часii, II. Глава 9 subterm (Sub, Term) <- Sub-подтерм основного терма Term. subterm (Term, Term), subterm (Sub, Term) <- compound (Term), functor(Term,F,N), subterm (N, Sub, Term). subterm(N, Sub,Term) <- N>1,N1 : = N-1, subterm(N1,Sub,Term), subterm (N, Sub, Term) <- arg(N,Term,Arg), subterm (Sub, Arg). Ilpoi p;i\'.\i:i lM Нахождение подтермов терма. ривает случай, когда Sub является подтермом JV-ro аргумента терма. Процедура subterm может быть использована двумя способами: для проверки того, что первый аргумент является подтермом второго, и для порождения подтермов заданного терма. Заметим, что порядок предложений определяет порядок порождения подтермов. Порядок предложений программы 9.2 приводит к тому, что сначала порождаются подтермы первого аргумента, потом - второго и т. д. Изменение порядка предложений приводит к изменению порядка появления решений. Рассмотрим вопрос subterm (a,f(X,Y))?, в котором второй аргумент-неосновной. В процессе решения в некоторый момент возникает цель subterm (а,Х). Применение первого правила приведет к решению X = а. Эта же цель будет редуцироваться с помощью второго правила, редукция построит цель compound (X), решение которой приведет к ошибке. Это, конечно, нежелательный эффект. Мы рассмотрим проблемы, связанные со структурным анализом неосновных термов позже, после того как в следующей главе будут введены металогические предикаты, обладающие подходящими выразительными средствами. До конца этой главы будем считать, если не оговорено противное, что аргументы у всех программ основные. Программа 9.2-типичная программа, использующая анализ структуры. Рассмотрим еще один пример: подстановку подтерма в терм. Реляционная схема общей программы подстановки подтерма в терм -substitute (О Id, New, ОIdTerm, NewTerm), где New Term -терм, полученный заменой всех вхождений подтерма Old в терме Old Term на подтерм New. Отношение, задаваемое программой, обобщает отношение подстановки элемента в список [см. упражнение 3.3(1)] и отношение подстановки элемента в бинарное дерево (логическая программа 3.26). Программа 9.3 немного сложнее программы 9.2, но следует той же основной схеме. Предложения процедуры substitute/4 анализируют три основных случая. В последнем, относящемся к составным термам, происходит обращение к вспомогательному предикату substitute/5, который реализует итерационную подстановку в подтермы. Арность главного функтора терма является начальным значением счетчика цикла, это значение последовательно уменьшается и используется для управления итерациями. Приведем конкретный пример, проясняющий интересные детали, скрытые в тексте программы. Протокол решения вопроса substitute (cat, dog, owns (jane, cat),X)? приведен на рис. 9.2. Унификация вопроса с фактом программы 9.3 безуспешна. Терм owns (jane, cat) не является константой, поэтому второе правило тоже неприменимо. Применимо третье правило substitute. Рассмотрим второе обращение к предикату functor. Переменным Name и Arity уже сопоставлены значения owns и 2 в предыдущем обращении к этому предикату, поэтому в рассматриваемом обращении
Анализ структуры термов 1 1 5 substitute (Old, New,OldTerm, NewTerm) <- NewTerm получается из терма OldTerm заменой всех вхождений подтерма Old на подтерм New. substitute(01d, New, Old, New). substitute(01d,New,Tcrm,Term) «- constant(Term), Term Ф Old. substitute(01d,New,Term,Terml) <- compound (Term), functor(Term, F, N), functor (Term 1, F, N), substitute(N,01d,New,Term,Terml). substitute(N,Old,New,Term,Term 1) «- N>0, arg(N,Term,Arg), substitute(01d, New, Arg, Arg 1), arg(N,Term1,Argl), N1 := N-l, substi tute (N1, Old, New, Term, Term 1). substitute(0,Old,New,Term,Terml). Пгчмрамма L).3. Программа подстановки термов. substitute(cat,dog,owns(jane,cat),X) {X=owns (jane,с at)} constant(owns(jane,cat)) f substitute(cat,dog,owns(jane,cat),X) compound(owns(jane,cat)), functor(owns(jane,cat),F,N) {F=owns,N=2} functor(X,owns,2) {X=owns(Xl,X2)} substitute(2,cat,dog,owns(jane,cat),owns(Xl,X2)) 2>0 arg(2, owns (jane,cat), Arg) {Arg=cat} substitute(cat,dog,cat,Argl) {Argl=dog} arg(2,owns(Xl,X2),dog) {X2=dog} N1 := 2-1 {N1 = 1} substitute(l,cat,dog,owns(jane,cat),owns(Xl,dog)) 1 >0 arg(l,owns(jane,cat),Arg2) {Arg2=jane} substitute(cat,dogjane,Arg3) {Arg3=jane} constant (jane) jane Ф cat arg(l,owns(Xl,dog) jane) {Xl=jane} N2 := 1-1 {N2=0} substitute(0,cat,dog,owns(jane,cat),owns(jane,dog)) 0>0 f substitute(0,cat, dog, owns (jane, cat), owns (jane, dog)) true Результат: X = owns(jane,dog) Рис. 9.2. Протокол предиката подстановки.
1 16 Часть II. Глава 9 строится терм, служащий шаблоном ответа, который заполняется в процессе вычисления. Это явное построение терма успешно выполнялось с помощью неявной унификации в предыдущих программах на Прологе. Обращение к цели substitute/5 приводит к успешному сопоставлению значений аргументам терма Term 1. В нашем примере второму аргументу сопоставляется значение dog, а аргументу XI сопоставляется значение jane. Два обращения к процедуре агд в процедуре substitute/5 используются по-разному. Первое обращение выделяет аргумент, второе-сопоставляет аргументу значение. В обычных языках подстановка в терм производится с помощью деструктивного присваивания, что в Прологе исключено. Программа 9.3 является типичным примером управления изменением структуры данных в Прологе. Новый терм строится рекурсивно по мере анализа исходного терма с помощью логических взаимосвязей между подтермами термов. Заметим, что вторую цель агд и рекурсивное обращение к substitute/5 можно поменять местами. Измененное предложение процедуры substitute/5 логически эквивалентно исходному и в процессе работы программы 9.3 приводит к тому же результату. Однако процедурно эти два варианта совершенно различны. Другой системный предикат, применяемый при анализе структур,-бинарный оператор = .., имеющий в силу исторических причин название univ. Цель Term = .. List выполнена, если голова списка List совпадает с именем функтора терма Term, а хвост является списком аргументов этого терма. Например, цель (отец (аран, лот) = .. [отец, аран, лот]) выполнена. Подобно отношениям functor и агд, отношение univ используется двояко-или для построения терма по заданному списку (например, вопрос (X = .. [отец, аран, лот])1 имеет решение X = отец (аран,лот)), или для построения списка по заданному терму (например, вопрос (отец (аран, лот) = . .Xs)? имеет решение Xs = [отец, аран, лот]). Программы, использующие предикаты functor и агд, в общем случае могут быть преобразованы в программы с отношением univ. Программа 9.4, дающая иное определение отношения subterm, эквивалентна программе 9.2. Как и в программе 9.2, здесь используется вспомогательный предикат, анализирующий аргументы, - в данном случае subtermjist. Предикат univ обеспечивает доступ к списку аргументов Args, подтермы которого находятся с помощью рекурсивного обращения к процедуре subtermjist. subterm (Sub, Term) <- Sub подтерм основного терма Term. subterm (Term,Term). subtcrm(Sub,Term) <- compound (Term), Term =.. [F| Args], subterm_list(Sub,Args). subterm, list(Sub,[Arg | Args]) «- subtcrm(Sub,Arg). subterm _list(Sub,[Arg| Args]) «- subtermjist (Sub, Args). Программа 9.4. Определение подтерма с помощью =.. . Программы для анализа структур, использующие предикат univ, обычно оказываются проще. Однако использование предикатов functor и агд приводит в
Анализ структуры термов 117 общем случае к более эффективным программам за счет отсутствия вспомогательных структур. Использование предиката univ позволяет изящно задать правило дифференцирования сложных функций. Правило утверждает, что d/dx{f(g(x))} = d/dg(x){f(g(x))} x d/dx{g(x)}. В разд. 3.5 мы заметили, что данное правило нельзя выразить с помощью одного предложения, являющегося частью логической программы 3.29. В Прологе правило дифференцирования сложной функции можно задать следующим образом: derivative (F_G_X, X, DF * DG) <- F..G_X = ..[F,G_X], derivative (F_G_X,G_X,DF), derivative (G_X, X, DG). Предикат univ расщепляет функцию F_G_X на функтор F и аргумент G_X, при этом одновременно проверяется, является ли она функцией одного аргумента. Рекурсивно вычисляются производная функции F от своего аргумента и производная функции G_X. Объединение этих вычислений приводит к решению. Предикат univ может быть определен в терминах предикатов functor и агд. Однако для того, чтобы строить списки по термам и термы по спискам, нужны два различных определения. Одного определения недостаточно из-за ошибочных ситуаций, возникающих при использовании неопределенных переменных. Другие системные предикаты не допускают разнообразных использований по тем же причинам. Программа 9.5 а правильно строит список по терму. Обращение к предикату functor строит функтор F, а аргументы находятся рекурсивно с помощью предиката orgs. Первый аргумент предиката orgs-счетчик, значение которого возрастает по мере вычислений, так что аргументы будут входить в результирующий список в нужном порядке. Если значение переменной Term в программе 9.5 а не определено, то обращение к предикату functor приведет к сообщению об ошибке. Term = .. List «- List список, состоящий из функтора терма Term, за которым следуют аргументы терма Term. Term =.. [F | Args] «- functor(Term,F,N), args(0,N,Term,Args). args(I,N,Term,[Arg| Args]) «- I<N, II := 1+ 1, arg(Il,Term,Arg), args(11,N,Term,Args). args(N,N,Term,[ ]). Программа 9.5а. Построение списка, соответствующего терму. Term =.. List <-- функтор терма Term является первым элементом списка List, остальные элементы списка аргументы терма. Term =.. [F|Args] <- length(Args,N),functor(Term,F,N),args(Args,Term,l). args([Arg | Args],Term,N) «- arg(N,Term,Arg), N1 := N + 1, args(Args,Term, N1). args([ ],Term,N). length(Xs,N)«- См. программу 8.11. Программа 9.56. Построение терма, соответствующего списку.
118 Часть II. Глава К) Программа 9.5 б правильно строит терм по списку. Длина списка используется для определения числа аргументов. Обращение к предикату functor создает шаблон терма, а для заполнения аргументов шаблону используется другой вариант программы args. Если попытаться с помощью программы 9.5 б строить список, то при решении цели length (Args,N) с неопределенными аргументами возникнет ошибочная ситуация. Упражнения к разд. 9.2 1. Определите предикат occurances (Sub, Term, N), истинный, если число N равно числу вхождений подтерма Sub в терм Term. Предполагается, что терм Term основной. 2. Определите предикат position (Subterm, Term, Position), где Position - список номеров аргументов, задающий положение подтерма Sub в терме Term. Например, цель position (X,(2-sin(X)\ [2,/]) истинна, так как sin (X) является вторым аргументом бинарного оператора «•», а Х-первым аргументом терма sin (X). (Указание: добавьте дополнительный аргумент к программе 9.2, задающей отношение subterm, и стройте список номеров методом сверху вниз.) 3. Перепишите программу 9.5 а так, чтобы значение счетчика убывало. (Указание: используйте накопитель.) 4. Определите предикаты functor и агд через предикат univ. Как можно использовать эти программы? 5. Перепишите программу 9.3, задающую отношение substitute, с использованием предиката univ. .9.3. Дополнительные сведения В стандартном Прологе не различают типовые предикаты объектного уровня и мета- уровня. Мы использовали другой подход, рассматривая отдельно предикаты проверки типов, применяемые только к термам с определенными значениями, и предикаты металогических проверок (такие, как var/l, описанный в разд. 10.1). Предикаты для анализа и построения термов, т. е. functor, агд и = .., берут свое начало в версиях, разрабатывавшихся в Эдинбурге. Обозначение = .. связано со старым синтаксисом Пролога-10 для списков, в котором при построении списков использовался оператор «,..» вместо оператора «|», например [a,b,c,..Xs~\ вместо \_a,b,c\Xs]. Две точки «..» справа должны подсказывать или напоминать о том, что правая сторона равенства является списком. Некоторые примеры данной главы основаны на программах из работы (O'Keefe, 1983). Упражнения (1) и (2) будут использованы в гл. 22 в программе, решающей уравнения. Глава 10 Металогические предикаты Металогические предикаты служат полезным расширением выразительных средств логических программ. Эти предикаты выходят за рамки логики первого порядка, поскольку служат для анализа структуры доказательства, рассматривают переменные (а не обозначаемые ими термы) как объекты языка и допускают преобразование структур данных в цели. Металогические предикаты помогут решить две проблемы, связанные с ис-
Металогические предикаты 119 пользованием переменных в предыдущих главах. Первая проблема относится к поведению переменных в системных предикатах. Например, вычисление арифметического выражения, содержащего переменную, приводит к ошибке. То же происходит при обращении к типовому предикату, аргумент которого - переменная. Все это приводит к уникальности программ на Прологе в отличие от эквивалентных логических программ. Вторая проблема связана с ненужными сопоставлениями значений переменным в процессе анализа структуры, когда переменные приходится рассматривать как специфические объекты, а не как обозначение для произвольного неопределенного терма. В предыдущей главе мы обходили эту проблему, ограничиваясь анализом лишь основных термов. Данная глава состоит из четырех разделов в соответствии с различными классами металогических предикатов. В первом разделе обсуждаются типовые предикаты, определяющие, является ли терм переменной. Во втором разделе рассматривается сравнение термов. В следующем разделе описываются предикаты, позволяющие рассматривать переменные как объекты преобразования. И наконец, описываются средства преобразования данных в обрабатываемые цели. \()Л. Типовые металогические предикаты Основным типовым металогическим предикатом является предикат var (Term), проверяющий, представляет ли собой терм Term переменную с неопределенным значением. Поведение этого предиката аналогично поведению типовых предикатов, рассматривавшихся в разд. 9.1. Решение вопроса var (Term) успешно, если терм Term - переменная, и безуспешно, если терм Term отличен от переменной. Например, var (X) выполнено, а решение обоих вопросов var (ар. и var (\_X\Xs]p. безуспешно. Предикат var является расширением для программ на чистом Прологе. Для задания имен всех переменных таблицу использовать невозможно. Дело в том, что факт var (X) означает, что все примеры X являются переменными, а не то, что буква «X» обозначает переменную. Возможность работы с именами переменных находится вне логики первого порядка вообще и вне чистого Пролога в частности. Значение предиката nonvar (Term) противоположно значению предиката var. Цель nonvar (Term) выполнена, если Term не переменная, и приводит к безуспешному вычислению, если Term переменная. Типовые металогические предикаты можно применять для восстановления некоторой гибкости программ, использующих системные предикаты, а также для управления порядком целей. Мы покажем это на примере новых версий программ, приведенных в предыдущих главах. Рассмотрим отношение plus (X,Y,Z). Программа 10.1 является версией plus, которая может быть использована и для сложения, и для вычитания. Идея состоит в проверке, какие установлены аргументы, до обращения к арифметическому вычислителю. Например, второе правило гласит, что если первый и третий аргументы, X и Z, не переменные, то второй аргумент Y может быть определен как их разность. Заметим, что если аргументы - не целые числа, то вычисление, как и требуется, будет безуспешным. Работа программы 10.1 подобна работе программы 3.3-логической программы plus. В частности, она не приводит к ошибкам. И все же программе 10.1 не в полной мере присуща гибкость рекурсивной логической программы - она не может быть, например, использована для разбиения числа на два меньших числа. Разбиение числа использует порождение чисел, а для этого нужна другая программа. Данная задача сформулирована в виде упражнения в конце раздела.
120 Часть II. Глава 10 plus(X.Y.Z) <- сумма чисел X и У равна Z. plus(X,Y,Z) <- nonvar(X), nonvar(Y), Z := X + Y. plus(X,Y,Z) <- nonvar(X), nonvar(Z), Y := Z-Y. plus(X,Y,Z) <- nonvar(Y), nonvar(Z), X := Z-Y. Программа 10.1. Программа plus, допускающая различные использования. Металогические цели, расположенные в начале тела предложения для определения, какое предложение процедуры следует выполнять, называются металогическими тестами. Приведенная выше программа/?/ms управляется металогическими тестами. Такие тесты относятся к текущему состоянию вычисления. Для понимания работы тестов требуется знание операционной семантики Пролога. Многие версии Пролога фактически содержат типовые предикаты с металогическими свойствами. Так, в Edinburgh-Прологе цель integer(X) приводит, если X -переменная, к безуспешным вычислениям, а не к ошибке. Это позволяет использовать в правилах программы 10.1 предикат integer вместо предиката nonvar, например: plus (X,Y,Z)<- integer (X), integer (Y), Z: = X + Y. Мы считаем предпочтительным разграничивать типовую проверку, являющуюся законным оператором первого порядка, и металогические тесты, представляющие более мощное средство. Другим отношением, призванным восстановить универсальность программ на Прологе, является отношение length (Xs, N) - определение длины N списка Xs. Ранее для определения длины данного списка и для порождения произвольного списка данной длины потребовалось ввести две отдельные программы на Прологе (8.10 и 8.11), хотя обе функции выполняет одна логическая программа (3.17). Программа 10.2 использует металогические тесты для определения единого отношения length. Ее дополнительное преимущество перед программами 8.10 и 8.11 состоит в невозможности незавершающихся вычислений, присущих обеим программам, когда оба аргумента не определены. length (Xs,N) <- список Xs имеет длину N. length(Xs,N)«- nonvar(Xs), lengthl(Xs,N). length(Xs,N)«- var(Xs), nonvar(N), length2(Xs,N). length l(Xs,N) <- См. программу 8.11. Iength2 (Xs,N)«- См. программу 8.10. Программа 10.2. Программа length, допускающая различные использования. Металогические тесты можно также использовать для выбора наилучшего порядка целей в предложениях программы. В разд. 3.7 обсуждалось определение отношения дедушка_или_бабушка: дедушка или бабушка (X, Z)«- родитель (X, Y), родитель (Y, Z). Оптимальный порядок целей зависит от того, ищутся ли внуки данных бабушки и дедушки или ищутся бабушка и дедушка данного внука. Программа 10.3, задающая требуемое отношение, осуществляет поиск эффективнее. Используя основные типовые металогические предикаты, можно определять более сложные металогические процедуры. Рассмотрим отношение ground (Term), истинное, если терм Term основной. Программа 10.4 определяет это отношение.
Металогические предикаты 121 дедушка, или бабушка (X, У) <- Z-внук Х. дедушка__или. бабушка (X,Z) «- nonvar(X), родитель(X,Y), родитель(Y,Z). дедушка или_бабушка(Х,7), «- nonvar(Z), родитель(Y,Z), родитель(X,Y). Программа 10.3. Более эффективная версия программы дедушка_или_бабушка. ground (Term) «- терм Term основной. ground (Term) «- nonvar(Term), constant (Term), ground (Term) «- nonvar(Term), compound(Term), functor(Term, F, N), ground (N,Term), ground(N,Tcrm) <- N>0, arg(N,Term,Arg) ground (A rg), N1 : = N-l, ground (N1, Term), ground (0, Term). Программа 10.4. Проверка, является ли терм основным. Программа написана в стиле программ разд. 9.2, анализировавших структуры, в частности, в стиле программы 9.3, определяющей отношение substitute. Два предложения процедуры ground/\ достаточно очевидны. В обоих предложениях используется металогический тест, позволяющий избежать возникновения ошибочных ситуаций. Первое предложение утверждает, что константа является основным термом. Во втором предложении рассматриваются структуры. В нем происходит обращение к вспомогательному предикату ground/2, который итерационно проверяет, что все аргументы структуры - основные термы. Рассмотрим более искусное применение типовых металогических предикатов на примере программирования алгоритма унификации. Необходимость в Прологе унификации для сопоставления цели и заголовка предложения означает доступность явного определения унификации. Исходная унификация Пролога может быть задана тривиальным определением unify (Х,Х), что совпадает с определением системного предиката = /2, задаваемого фактом X = Х. Заметим, что данное определение зависит от используемого в Прологе алгоритма унификации и, следовательно, не влияет на наличие проверки на вхождение. Типовые металогические предикаты позволяют определить унификацию в Прологе более явно. Будучи более громоздким и менее эффективным, такое определение может оказаться полезным в качестве основы более совершенных алгоритмов унификации. Одним примером служит унификация с проверкой на вхождение, описанная в следующем разделе. Другой пример - унификация в других языках логического программирования, которая может быть встроена в Пролог, подобно унификации без записи в Параллельном Прологе.
122 Часть II. Глава 10 В программе 10.5 приводится явное определение унификации. Отношение unify (Term I, Term 2) истинно, если терм Term 1 унифицирован с термом Term 2. Предложения программы unify описывают возможные случаи. Первое предложение программы утверждает, что две переменные унифицируемы. Следующее предложение является записью правила унификации, утверждающего, что если X переменная, то X унифицируемо с Y unify (TermI, Теrm2) <- Terml и Тегт2 унифицируемы без проверки на вхождение. unify (X,Y) <- var(X), var(Y), X = Y. unify (X,Y) <- var(X), nonvar(Y), X = Y. unify(X,Y) <- var(Y), nonvar(X), Y = X. unify (X,Y) <- nonvar(X), nonvar(Y), constant(X), constant(Y), X = Y. unify (X,Y)<- nonvar(X), nonvar(Y), compound(X), compound(Y), term. unify(X,Y). term_unify(X,Y)^ functor(X,F,N), functor(Y,F,N), unify.args(N,X,Y). unify_args(N,X,Y) <- N > 0, unify_arg(N,X,Y), N1 := N - I, unify„args(Nl,X,Y). unify_args(0,X,Y). unify _arg(N,X,Y)<- arg(N,X,ArgX), arg(N, Y,ArgY), unify(ArgX,ArgY). Программа 10.5. Алгоритм унификации. Другой случай, являющийся предметом рассмотрения программы 10.5, состоит в унификации двух составных термов, как это описано предикатом term_unify (X, Y). В данном предикате проверяется, имеют ли два терма X и У одинаковые главный функтор и арность, а затем проверяется, что все аргументы попарно унифицируемы. В этой процедуре предикат unify_arg используется примерно так, как в рассмотренных ранее программах анализа структуры. Упражнения к разд. 10.1 1. Напишите вариант программы range (программа 8.12), допускающий различные использования. 2. Напишите вариант программы plus (программа 10.1), пригодный для сложения, вычитания и разбиения числа на слагаемые. (Указание: Используйте для порождения чисел отношение between). 10.2. Сравнение неосновных термов Рассмотрим задачу расширения программы явной унификации (программа 10.5) средством проверки на вхождение. Напомним, что проверка на вхождение является частью формального определения алгоритма унификации, которая препятствует унификации переменной с термом, содержащим эту переменную. Для того чтобы выразить это отношение на Прологе, нам потребуется проверка идентичности
Металогические предикаты 123 переменных (а не просто их унифицируемости, так как унифицируемы любые две переменные). Это - металогический тест. Для этой проверки в Прологе имеется системный предикат = =/2. Цель (X = = Y)? выполнена, если X и Y - идентичные константы, идентичные переменные или структуры с одним и тем же главным функтором и одинаковой арностью, а для соответствующих аргументов Xi и YJ структур X и Y соотношение Х( = = Y?. выполняется рекурсивно. В противном случае цель (X = = У) не выполнена. Например, цель (X = = 5) не выполнена (в отличие от цели X = 5). Имеется также системный предикат, значение которого противоположно значению предиката = =. Цель X \ = = Y? выполнена в тех случаях, когда X и Y не являются идентичными термами. Предикат \ = = может быть использован в определении отношения not_oc- curs_in(Sub,Term), истинного, если терм Sub не входит в терм Term. Данное отношение требуется для построения алгоритма унификации с проверкой на вхождение. Предикат not_occurs_in (Sub, Term) является металогическим предикатом, анализирующим структуру. Он используется в реализующей унификацию с проверкой на вхождение программе 10.6, варианте программы 10.5. unify (Term 1 ,Term2) <- Term] и Тегт2 унифицируемы с проверкой на вхождение. unify (X,Y) <- var(X), var(Y), X = Y. unify(X,Y) <- var(X), nonvar(Y), not_occurs_in(X,Y), X = Y. unify(X,Y) «- var(Y), nonvar(X), not_occurs_in(X,Y), Y = X. unify (X,Y) <- nonvar(X), nonvar(Y), constant(X), constant(Y), X = Y. unify (X,Y) <- nonvar(X), nonvar(Y), compound(X), compound(Y), term_unify(X,Y). not_occurs_in(X,Y) <- var(Y),X\= = Y. not_occurs_in(X,Y) <- nonvar(Y), constant(Y). not_occurs_in(X,Y) <- nonvar(Y), compound (Y), functor(Y,F,N), not_occurs_in (N, X, Y). not_occurs_in(N,X,Y) <- N > 0, arg(N,Y,Arg), not_occurs_in(X,Arg), N1: = N - 1, not_occurs_in(N 1 ,X, Y). not_occurs_in (0, X, Y). term_unify(X,Y) <- См. программу 10.5. ripoi pa\i.\Ki 10.6. Алгоритм унификации с проверкой на вхождение. Заметим, что при определении предиката not_occurs_in область определения не ограничена основными термами. Снять подобное ограничение в случае программы 9.2, задающей отношение Subterm, не так просто. Рассмотрим вопрос Subterm (X, У)?. Он успешно решается программой 9.2 сопоставлением переменной X терма Y. Определим металогический предикат occurs_in (Sub, Term) обладающий требуемыми свойствами.
124 Часть II. Глава 10 Наличие предиката = = позволяет использовать программу 9.2, задающую предикат Subterm как основу определения предиката occurs_in. Механизм возврата порождает все подтермы данного терма, и каждый подтерм проверяется на совпадение с переменной. Записью программы является программа 10.7(a). occurs_in (Sub, Term) <- Sub является подтермом (возможно, неосновного) терма Term. а: С использованием предиката = = occurs_in(X,Term) <- subterm (Sub, Term), X = = Sub. в: С использованием предиката freeze occurs_in(X,Term) <- frccze(X,Xf), freeze(Term,Termf), subterm (Xf, Termf). subterm(X,Term) <- См. программу 9.2. Программа 10.7. Вхождение. Исходное определение предиката Subterm корректно только для основных термов. Однако добавление типовых металогических тестов, подобных использованным при определении отношения no_occurs_in в программе 10.6, позволяет легко преодолеть это затруднение. 10.3. Использование переменных в качестве объектов Нетривиальная проблема, связанная с явной обработкой переменных при определении отношения occurs_in в предыдущем разделе, подчеркивает слабость выразительных средств Пролога. Работа с переменными связана с некоторыми сложностями, и при анализе и обработке термов между переменными и значениями могут возникнуть случайные соответствия. Такая проблема появляется при построении программы 9.3, определяющей отношение Substitute. Рассмотрим цель Substitute (a, b,X,Y), задающую подстановку константы а вместо b в переменную X. Результатом подстановки является терм Y Имеются две правдоподобные интерпретации отношения Substitute в этом случае. Логически обоснованным решением является сопоставление переменной X константы а, а переменной У-константы Ь. Это решение действительно находится программой 9.3 при унификации цели с основным фактом Substitute(OldfNew,OldfNew). Однако на практике предпочтительнее иная интерпретация. Будем считать термы X и а различными; таким образом, переменной У следует сопоставить терм X. При такой интерпретации следовало бы воспользоваться другим предложением программы 9.3, а именно: Substitute(01d, New, Term, Term) <-constant (Term), Term Ф Old. Цель, однако, не будет доказана, так как переменная не является константой. Мы можем избежать первого (логического) решения, используя металогический тест, гарантирующий подстановку в основной терм. Тогда неявная унификация с заголовком предложения становится выполняемой лишь в случае, если тест выполнен. Следовательно, унификация должна быть задана явно. Основной факт превращается в правило: Substitute(01d, New, Term, New) <- ground (Term), Term = Old.
Металогические предикаты 125 Рассмотрение переменных отдельно от констант треОует специального правила, также использующего металогический тест: Substitute(01d,New,Var,Var) <- var(Var). Добавление двух рассмотренных предложений к программе 9.3, определяющей отношение Substitute, а также добавление других металогических тестов позволяет применять программу к неосновным термам. Однако полученная программа будет громоздкой. В ней смешаны процедурный и декларативный подходы. Для понимания такой программы требуется знание механизма управления, реализованного в Прологе. Используя аналогию с медициной, можно сказать, что мы лечим симптомы (нежелательное сопоставление значения переменной), а не болезнь (отсутствие средств, позволяющих использовать переменные в качестве объектов). Для «исцеления» необходимы дополнительные металогические примитивы. Сложности, возникающие при смешивании обработки термов на метауровне и на объектном уровне, связаны с некоторой теоретической проблемой. Строго говоря, программы метауровня должны рассматривать переменные объектного уровня как константы и должны иметь возможность ссылаться на такие переменные по имени. Расширяя Пролог двумя системными предикатами: freeze (Term,Frozen) и melt (Frozen, Thawed) n, можно частично решить указанные проблемы. «Замораживание»2) терма Term с помощью предиката freeze создает копию терма Frozen. В этом терме все переменные исходного терма, которым не были сопоставлены значения, заменяются уникальными константами. Замороженный терм подобен основному терму и может использоваться так же, как и основной терм. При унификации замороженные переменные рассматриваются как основные атомы. Две замороженные переменные унифицируемы тогда и только тогда, когда они совпадают. Аналогично, если переменная, которой не сопоставлено значение, унифицируется с замороженным термом, то значением переменной становится замороженный терм. Применение системного предиката к замороженной переменной приводит к тому же результату, что и применение к константе. Например, использование замороженных переменных в арифметических вычислениях приводит к безуспешным вычислениям. Предикат freeze, как и предикат var, является металогическим предикатом. Он позволяет анализировать структуру терма непосредственно в процессе вычисления. Наличие предиката freeze позволяет иначе определить предикат occurs_in, рассматривавшийся в предыдущем разделе. Идея состоит в замораживании терма, что превращает переменные в основные обекты. Это позволяет использовать программу Subterm (программа 9.2), правильно обрабатывающую основные термы. Метод реализован в программе 10.7(b). Замораживание позволяет решать вопрос идентичности двух термов. Два замороженных терма X и У унифицируемы тогда и только тогда, когда их незамороженные варианты идентичны, т. е. когда X = = У Это свойство существенно для обоснования корректности программы 10.7(b). Отличие замороженных термов от основных состоит в том, что замороженные термы можно «разморозить»3), превратив их в неосновные термы. Предикатом, двойственным к freeze, является предикат melt (Frozen, Thawed). Вычисление цели melt(X,Y) создает копию терма У терма X, в которой замороженные переменные становятся обычными переменными языка Пролог. При размораживании терма У учитываются все сопоставления значений переменным терма X, сделанные пока терм был заморожен. 1) Freeze- заморозить, melt разморозить. Прим. перев. 2) Далее дастся без кавычек. - Прим. ред. 3) См. сноску 2) на с. 125.- Прим. ред.
126 Часть II. Глава 10 Используя предикаты freeze и melt, можно построить вариант программы substitute, а именно non_ground__substitute, в котором не происходит нежелательных сопоставлений значений переменным. Процедурный подход к программе поп_ ground_substitute состоит в следующем: перед подстановкой терм замораживается; далее, используя программу substitute, правильно обрабатывающую основные термы, производится подстановка в замороженный терм; наконец, новый терм размораживается: non_ground__substitute(X,Y,01d,New) <- freeze(01d,01dl), substitute^, Y,01d,01d 1), melt(01d 1, New). Замороженный терм можно также использовать как шаблон для изготовления копий. Системный предикат melt_new (Frozen, Term) создает копию Term терма Frozen, в которой замороженные переменные заменены переменными с новыми именами. Предикат melt_new используется для копирования термов. Предикат copy (Term, Сору) создает новую копию терма Term. Этот предикат можно задать одним правилом: copy (Term, Copy) <- freeze (Term, Frozen), melt_new(Frozen, Copy). Примитивы freeze, melt и melt_new полезны при задании вводимых в гл. 12 внелогических предикатов и описании их вычислений. 10.4. Доступность метапеременных Характерной особенностью Пролога является эквивалентность программ и данных-и то и другое может быть представлено логическими термами. Для того чтобы можно было использовать эту эквивалентность, необходимо, чтобы программы можно было рассматривать в качестве данных, а данные можно было бы превращать в программы. В данном разделе описывается средство, позволяющее превращать терм в цель. Смысл предиката Call(X) в Прологе состоит в передаче терма X в качестве цели для решения. На практике в большинстве реализаций языка Пролог отсутствует введенное нами ограничение для логических программ, а именно: цели в теле предложения должны быть термами, отличными от переменных. Доступность метапеременных означает, что в качестве целей в конъюнктивных вопросах и в теле предложений разрешается использовать переменные. В процессе вычисления в момент обращения к такой переменной ей должно быть сопоставлено значение-терм. Если переменной не сопоставлено значение в момент обращения, то возникает ошибочная ситуация. Доступность метапеременных является синтаксическим средством, обеспечивающим условия применения предиката call. Доступность метапеременных является важным инструментом метапрограмми- рования, используемым, в частности, при построении метаинтерпретаторов и оболочек. Два важных примера подобных программ - простая оболочка (программа 12.6) и метаинтерпретатор (программа 19.1)-будут обсуждаться в последующих главах. Это же средство существенно используется при определении отрицания (программа 11.5) и при определении предикатов высших порядков, описанных в разд. 17.3. В заключение приведем пример использования метапеременных в определении логической дизъюнкции, обозначенной инфиксным бинарным оператором «;». Цель (X; Y) выполнена, если X и У выполнено. Определение оформлено как программа 10.8.
Отсечения и отрицание 127 *;Y<- ХилиХ X; Y <- X. X; Y <- Y. i Ipoi ;\iM\iii 10.X. Логическая дизъюнкция. 10.5. Дополнительные сведения Блестящее обсуждение системных металогических предикатов в Прологе-10 и описание их использования можно найти в (O'Keefe, 1983b). Процедура унификации Параллельного Пролога, написанная на Прологе, имеется в работе (Shapiro, 1983). Предикаты freeze, melt и melt_new были введены в статье (Nakashima, Ueda, 1984), там же обсуждается их использование в Прологе-10. Название freeze предполагалось использовать для других раширений чистого Пролога. Наиболее радикальным из них является предикат geler (Colmerauer, 1982 b), позволяющий приостанавливать решение целей и предоставляющий программисту дополнительные средства управления порядком целей. Глава 11 Отсечения и отрицание В Прологе имеется единственный системный предикат, отсечение9 затрагивающий процедурное поведение программ. Основное назначение этого предиката состоит в ограничении пространства поиска при вычислениях за счет динамического сокращения дерева поиска. Отсечение можно использовать для отбрасывания бесплодных путей вычислений, которые, как известно программисту, не могут привести к решениям. Отсечение может быть также использовано умышленно или по недосмотру для изъятия путей вычислений, которые должны содержать решения. Таким образом можно реализовать слабую форму отрицания. Полезность применения отсечений спорна. Во многих приложениях отсечения имеют лишь процедурное истолкование в противоположность поддерживаемому нами декларативному стилю программирования. Однако аккуратное использование отсечений может повысить эффективность программ без нарушения их ясности. 11.1. Зеленые отсечения: выражение детерминизма Рассмотрим программу merge (Xs,Ys,Zs) (программа 11.1), сливающую два упорядоченных числовых списка Xs и Ys в единый упорядоченный список Zs.
128 Часть II. Глава 11 merge (Xs, Ys,Zs) <- упорядочений список целых чисел Zs получен слиянием упорядоченых списков целых чисел Xs и Ys. merge([X | Xs], [Y | Ys], [X | Zs]) - X < Y, merge(Xs,[Y | Ys],Zs). merge([X | Xs], [Y | Ys], [X, Y | Zs]) - X = Y, merge(Xs,Ys,Zs). merge([X | Xs], [Y | Ys], [Y, Zs] *- X > Y,merge([X|Xs],Ys,Zs). merge(Xs,[ ],Xs). merge([ ],Ys,Ys). Программа 11.1.Слияние упорядоченных списков. Слияние двух упорядоченных числовых списков является детерминированной операцией. В этом вычислении к каждой нетривиальной цели можно применить лишь одно из пяти правил. Если одна из проверок выполнилась, то невозможно, чтобы выполнилась и какая-либо другая. Отсечение, обозначаемое символом «!», может быть использовано для выражения взаимоисключающего характера проверок. В таких случаях символ отсечения записывается после арифметических проверок. Например, первое предложение программы merge может быть записано в виде merge([X | Xs], [Y | Ys], [Z, | Zs]) *- X < Y,!, merge(Xs,[Y | Ys],Zs). Операционно отсечение реализуется следующим образом. После унификации порождающей цели с заголовком предложения, содержащего отсечение, цель выполняется и фиксируется для всех выборов предложений. Хотя данное определение является полным и точным, его частные случаи и следствия не всегда являются интуитивно ясными и очевидными. Непонимание механизма отсечения является основным источником ошибок даже у опытных программистов. Ошибки бывают двух видов: отсекаются пути вычислений, которые нельзя отбрасывать, и не отсекаются те решения, которые следует отбросить. Перечисленные следствия могут помочь понять приведенное выше определение: • Отсечение отбрасывает все расположенные после него предложения. Если цель р унифицирована с предложением, содержащим отсечение, и отсечение выполнено, то эта цель не может быть использована для построения решений с помощью предложений, расположенных ниже данного. • Отсечение отбрасывает все альтернативные решения конъюнкции целей, расположенных в предложении левее отсечения, т. е. конъюнкция целей, стоящих перед отсечением, приводит не более чем к одному решению. • С другой стороны, отсечение не влияет на цели, расположенные правее его. В случае возврата они могут порождать более одного решения. Однако если эта конъюнкция не выполнится, то поиск переходит к последнему выбору, сделанному перед выбором предложения, содержащего отсечение. Рассмотрим фрагмент дерева поиска решений вопроса merge([I,2,3],[2,3],Xs)! относительно программы 11.2- полной версии программы merge с добавлением отсечений. Этот фрагмент приведен на рис. 11.1. Сначала исходный вопрос сводится к конъюнктивному вопросу 1 < 2,!, merge([3,5], \2,3\Xsl)4, цель 1 < 2 решается успешно, что приводит к вершине дерева поиска, помеченной символом «*». В результате применения отсечения отбрасываются ветви с метками (а) и (Ь).
Отсечения и отрицание 129 merge(X,YS,Zs) *- упорядоченный список целых чисел Zs получен слиянием упорядоченных списков целых чисел Xs и Ys. merge([X|Xs],[Y|Ys],[X|Zs])- X < Y,!, merge (Xs,[Y | Ys],Zs). merge([X | Xs],[Y | Ys],[X, Y | Zs]) <- X = Y,!, merge (Xs,Ys,Zs). merge([X|Xs],[Y|Ys],[Y|Zs])^ X > Y,!, merge ([X | Xs], Ys,Zs). merged ],[X|Xsl[X|Xs]) «-!. merge(Xs,[ ],Xs) <- !. Программа 11.2 Слияние упорядоченых списков с использованием отсечения. merge([l,3,5],[2,3],Xs) I. г (а) l=2,!,merge([3,5],[3],Xsl) (b) l<2,!,merge([3,5),[2,3],Xsl) l>2,!,merge((l,3,5],[3],Xsl) Ф (*) !,merge([3,5],[2,3],Xel) * merge([3,5],[2)3],Xsl) Рис. 11.1. Результат отсечения. Приступим к обсуждению программы 11.2. В трех рекурсивных правилах программы merge отсечение помещено после проверокп. Два исходных случая программы также детерминированы. Подходящее предложение определяется в них при унификации, поэтому отсечение размещается как первая (а в действительности и единственная) цель в теле такого правила. Заметим, что отсечение устраняет лишнее решение цели тегде([ ],[ ],Xs). Раньше мы добивались этого более сложным способом, указывая, что список Xs (или Ys) должен содержать хотя бы один элемент. Опишем еще раз эффект применения отсечения в общем предложении С = = А «- ВР...,В^\, Вк + 2>-,Вп процедуры, определяющей А. Если текущая цель G унифицируется с заголовком предложения С и цели В1,...,Вк выполняются, то отсечение приводит к следующему результату. В программе фиксируется выбор предложения С для редуцирования цели (7, и любые другие предложения процедуры А, которые могут быть унифицированы с целью (/, игнорируются. Кроме того, если некоторое Bi при / > к не выполняется, то возврат происходит не далее чем к моменту отсечения. Все другие выборы в дереве поиска, относящиеся к вычислению целей В{ при / ^ А:, отбрасываются. Если процесс возврата фактически привел к моменту отсечения, то отсечение становится невыполненным и процесс поиска возвращается к последнему выбору, сделанному перед сопоставлением цели G предложения С. Отсечения, содержащиеся в программе merge, отражают детерминированный характер этой программы. Это значит, что для каждой доказуемой цели к ее п Отсечение после третьего предложения программы является излишним с любой прагматической точки зрения. Оно никак не сокращает поиск в процессе вычисления, однако придает программе более симметричный вид, и, как сказано в старой шутке про куриный бульон, вреда от тгого нет. 5-1402
130 Часть II. Глава 11 доказательству ведет применение лишь одного правила программы. Отсечение фиксирует выбор такого правила, как только вычисление достигло стадии, на которой это можно определить. Сведения, зафиксированные при отсечении, используются для сокращения дерева поиска, что уменьшает выполняемый в Прологе путь обхода дерева и, таким образом, экономит время вычисления. Но более важным на практике является тот факт, что применение отсечений в программах уменьшает объем используемой памяти. Интуитивно ясно, что детерминированность вычислений означает, что для возврата требуется сохранение меньшего объема информации. Этот факт можно использовать в реализациях Пролога, допускающих оптимизацию остатка рекурсии, обсуждаемую далее. Рассмотрим некоторые другие примеры. Отсечение можно добавить к программе, вычисляющей минимум двух чисел (программа 3.7) точно таким же образом, как и в случае программы merge. При выполнении одной арифметической проверки оказывается невозможным выполнение другой проверки. Программа 11.3 является соответствующей модификацией программы minimum. minimum (X, Y, Min) <- Min-минимум чисел X и Y. minimum(X,Y,X)«- X^Y,!. minimum(X,Y,Y)<-X>Y,!. Программа 11.3. Нахождение минимума с помощью отсечения. Более содержательным примером, в котором добавление отсечений используется для указания детерминированности программы, служит программа 3.28. Программа определяет отношение polynomial (Тегт,Х ), которое истинно, если терм Тегт- многочлен от переменной X. Типичным правилом является polynomial (Term 1 + Term2,X)<- polynomial(Terml,X),polynomial(Term2,X). Как только выяснено (унификацией с заголовком правила), что терм является суммой, никакое другое правило программы polynomial не будет применено. Программа 11.4 является полной программой распознавания многочленов с добавлением отсечений. Это - детерминированная программа, в которой используются как отсечения после условий, так и отсечения после унификаций. polynomial(Term,X) <- терм Term является многочленом от X. polynomial(X,X) <- !. polynomial(Term,X) <- constant(Term),!. polynomial (Term 1 + Term2,X) <- !, polynomial (Term 1, X), polynominal(Term2, X). polynomial (Term 1 — Term2,X) <- !, polynomial (Term 1,X), polynomial(Term2,X). polynomial (Term 1 xTerm2,X) <- !, polynomial (Term 1,X), polynomial(Term2,X). polynomial (Term l/Term2,X) <- !, polynomial (Term 1,X), constant(Term2). polynomial(Term|N,X) <- !, natural number(N), polynomial (Term, X). Программа 11.4. Распознавание многочленов.
Отсечения и отрицание 131 Сравнивая арифметические программы на Прологе, использующие арифметические возможности компьютера, с рекурсивными логическими программами, мы указывали, что платой за повышение эффективности является уменьшение степени гибкости. При переводе логических программ на Пролог теряется возможность разнообразного использования программ. Программы на Прологе с отсечениями также менее гибки, чем их аналоги без отсечений. Однако это не существенно, если, как это часто бывает, с самого начала предполагается использовать программу лишь одним способом. До сих пор рассматривались примеры, в которых отбрасывались варианты, бесполезные с точки зрения возможности унификации порождающей цели. Приведем теперь пример, в котором основной смысл отсечения заключается в устранении избыточных вычислений при решении порожденных целей. Рассмотрим рекурсивное правило программы сортировки перестановками: Sort(Xs,Ys)<- append(As,[X,Y|Bs],Xs), X>Y, append(As,[Y,X|Bs],Xsl), sort(Xsl,Ys). В программе отыскивается пара соседних элементов, расположенных с нарушением порядка. Эти два элемента переставляются, и процесс продолжается, пока список не будет упорядочен. Исходное предложение- Sort(Xs,Xs)<-ordered(Xs). Рассмотрим цель Sort([3,2,l]9Xsl). Упорядочение происходит путем перестановки 3 и 2, далее 3 и 1 и, наконец, 2 и /, что приводит к упорядоченному списку [7,2,5]. То же решение можно получить, переставив сначала 2 и У, затем 3 и 1 и, наконец, 3 и 2. Мы знаем, что может получиться лишь один упорядоченный список. Таким образом, если некоторая перестановка выполнена, то нет никакого смысла рассматривать другие варианты. Это можно указать с помощью отсечения, помещаемого после проверки X > У. Отсечение производится, как только выяснено, что перестановка необходима. Программа 11.5 является программой сортировки перестановками с отсечениями. sort(Xs,Ys) <-. список К? упорядоченная перестановка списка Xs- sort(Xs,Ys)<- append(As,[X,Y|Bs],Xs), X>Y, \t append(As,[Y,X|Bs],Xsl), sort(Xs1,Xs). sort(Xs,Xs) <- ordered (Xs), !_ ordered(Xs) <- См. программу 3.20 Программа 11.5. Сортировка перестановками. Добавление отсечений в программы данного раздела не влияло на их декларативное значение. Для данного вопроса находились все решения. И наоборот, удаление отсечений также не повлияет на значение программы. К сожалению, так бывает не всегда. В литературе различают зеленые отсечения и красные отсечения. Зеленые отсечения рассматривались в данном разделе. Добавление и удаление 5*
132 Часть П. Глава 11 зеленых отсечений не влияет на значение программы. Зеленые отсечения лишь отбрасывают те пути вычисления, которые не приводят к новым решениям. Отсечения, не являющиеся зелеными, называются красными Упражнения к разд. 11.1 1. Добавьте отсечения к процедуре partition в программе quiksort (программа 3.22). 2. Добавьте отсечения к программе дифференцирования (программа 3.29). 3. Добавьте отсечения к программе сортировки вставкой (программа 3.21). 11.2. Оптимизация остатка рекурсии Как было отмечено ранее, основное отличие рекурсии от итерации с точки зрения исполнения состоит в том, что рекурсия в общем случае требует объем памяти, линейно зависящий от числа выполняемых рекурсивных обращений, в то время как объем памяти, требуемый для выполнения итераций, ограничен константой, не зависящей от числа выполняемых итераций. Хотя рекурсивные программы, свободные по определению от побочных эффектов, представляются более изящными и привлекательными, чем их итерационные аналоги, определяемые в терминах итераций и локальных переменных, объем памяти-слишком важная мера сложности, чтобы ею расплачиваться за подобные свойства. К счастью, существует класс рекурсивных программ (в точности тех, которые могут быть преобразованы непосредственно в итерационные программы), выполняемых с использованием постоянного объема памяти. Метод реализации, приводящий к подобной экономии памяти, называется оптимизацией остатка рекурсииили, точнее, оптимизацией последнего обращения В общих чертах основная идея оптимизации остатка рекурсии состоит в выполнении рекурсивной программы так, как если бы она была итерационной. Рассмотрим редукцию цели А с помощью предложения и пусть наибольший общий унификатор равен 0. Оптимизация потенциально применима к последнему обращению в теле предложения - обращению к Вп. При этом для решения цели Вп повторно используется область памяти, выделенная для решения порождающей цели А. Ключевая предпосылка для применения подобной оптимизации состоит в том, что с момента редуцирования цели А с помощью данного предложения до момента редуцирования последней цели Вп не осталось нерассмотренных вариантов выбора. Другими словами, не осталось иных предложений для редуцирования цели А и не осталось вариантов вычисления целей левее Вп, т.е. вычисление конъюнктивной цели {Вр В2>...,Вп_ ;) было детерминированным. В большинстве реализаций оптимизации остатка рекурсии некоторые случаи возникновения данной предпосылки распознаются в процессе вычисления. Это распознавание происходит путем сравнения информации, используемой в процессе возврата и относящейся к целям А и Вп. Другой метод, используемый при реализации Пролога,-индексирование предложений-также тесно связан с оптимизацией остатка рекурсии. Он позволяет увеличить число случаев, в которых Пролог обнаруживает выполнимость предпосылки. Индексирование состоит в некотором анализе цели для определения предложений, которые можно использовать для редукции до фактического выполнения унификации. Обычно индексирование производится над типом и значением первого аргумента цели.
Отсечения и отрицание 133 Рассмотрим программу соединения списков: append([X | Xs], Ys,[X | Zs]) <- append(Xs,Ys,Zs). append ([ ],Ys,Ys). Если она используется для соединения двух полных списков, то в момент исполнения рекурсивной цели программы append выполнены предпосылки оптимизации остатка рекурсии. Никакое иное предложение не может быть использовано для редукции порождающей цели (если первый аргумент унифицируем с [X | Xs], то ясно, что он не может быть унифицирован с [ ], так как мы предположили, что первый аргумент - полный список). В теле правила нет других целей, кроме цели append, так что второе условие здесь выполнено тривиально. Однако для реализации знания о применимости оптимизации необходимо знать, что второе, еще не рассмотренное правило неприменимо к данной цели. Здесь на помощь приходит индексирование. Рассматривая первый аргумент цели append, можно, не пытаясь применить второе правило, установить, что оно приведет к безуспешному вычислению, и применить оптимизацию в рекурсивном обращении к цели append. Не все реализации Пролога обеспечивают индексирование и не все случаи детерминизма могут быть опознаны с помощью доступных методов индексирования. Поэтому в интересах программиста помочь реализации, использующей оптимизацию остатка рекурсии, в распознавании предпосылок для применения оптимизации. Для этого имеется радикальный метод - добавление отсечения перед последней целью в тех предложениях, к которым всегда применима оптимизация остатка рекурсии. Предложение будет иметь вид Такое отсечение отбрасывает предложения, оставшиеся нерассмотренными при редуцировании порождающей цели А, и любые варианты вычисления конъюнкции В общем случае невозможно определить, является ли подобное отсечение зеленым или красным, так что программист должен тщательно исследовать этот вопрос. Следует заметить, что эффективность оптимизации остатка рекурсии существенно возрастает, если происходит совместно с хорошей сборкой мусора. Лучше даже сказать так: использование оптимизации без сборки мусора не дает существенного эффекта. Причина здесь в том, что большинство рекурсивных программ с оптимизацией остатка порождает при каждом обращении некоторые структуры данных. Большинство этих структур являются временными и могут быть утилизованы (смотри, например, редактор, описанный в программе 12.5). При использовании сборки мусора такие программы в принципе могут работать сколь угодно долго. Если сборки мусора не производится, то, хотя область, используемая программой в качестве стека, остается постоянной, выделение областей под новые временные структуры данных приведет к перерасходу памяти. 11.3. Отрицание Использование зеленого отсечения не влияет на декларативное значение программ на Прологе. Однако с учетом процедурного поведения отсечение может быть в ограниченной мере использовано для выражения безрезультатной информации. Отсечение является основой реализации в Прологе ограниченной формы отрицания, называемой отрицание как безуспешное выполнение. Программа 11.6 задает
134 Часть II. Глава 11 стандартное определение отношения not (Goal), которое истинно, если цель Goal не выполнена. Она основана на использовании метапеременной и системного предиката fail, который никогда не выполняется (т.е. отсутствуют определяющие его предложения). Конъюнкция отсечения и предиката fail представляет комбинацию «отсечение-fail». Мы употребляем not как префиксный оператор. not X X недоказуемо. not X <- X,!, fail. notX. Программа 11.6. Отрицание как безуспешное выполнение. Рассмотрим, как работает программа 11.6 при поиске ответа на вопрос notG! Применяется первое правило, и использование метапеременной задает обращение к цели G. Если цель G выполнена, то мы сталкиваемся с отсечением. В вычислении фиксируется выбор первого правила, и цель not не выполнена. Если цель G не выполнена, то применяется второе правило программы 11.6, приводящее к успешному вычислению. Следовательно, цель notG не выполнена, если выполнена цель (7, и наоборот. Для того чтобы программа 11.6 функционировала должным образом, требуется именно такой порядок правил. Здесь мы сталкиваемся с новой, не всегда желательной ситуацией в программировании на Прологе. До сих пор порядок правил влиял только на порядок появления решений. Теперь порядок правил может изменить значение программы. Процедуры, в которых порядок правил является критическим в этом смысле, следует рассматривать как единый объект, а не как набор отдельных предложений. Остановка вычислений цели not G зависит от остановки вычислений цели G. Если вычисления цели G остановятся, то остановятся и вычисления цели notG. Если вычисления цели G не остановятся, то вычисления цели notG могут остановиться или не остановиться в зависимости от того, будет ли в дереве поиска найдена успешная вершина, прежде чем вычисление пойдет по бесконечной ветви. Рассмотрим незавершающуюся программу: женаты (авраам, сара). женаты (X, Y) <- женаты (Y,X). Решение вопроса not женаты (авраам, сара)! приведет к остановке (безуспешного) вычисления, хотя решение вопроса женаты (авраам,сара)! не останавливается. Значение отношения not, определяемого в программе 11.6, отличается от строгого логического отрицания1*. Нельзя также сказать, что программа реализует полное и точное определение отрицания как безуспешного выполнения в смысле теории гл. 5. Неполнота такой реализации проистекает от неполноты реализации в Прологе вычислительной модели логического программирования. В логическом программировании отрицание как безуспешное выполнение определяется в терминах дерева конечного безуспешного поиска. Вычисления в Прологе не гарантируют нахождение такого дерева, даже если оно существует. Имеются цели, которые могут быть не выполнены при подобном отрицании, однако решение этих целей в соответствии с правилом вычислений в Прологе не останавливается. Например, решение вопроса not(p(X),q(X))! приводит к незавершающейся работе программы 1) В действительности Пролог-10 использует предикат \ +, а не not, но это не должно вводить в заблуждение.-Прим. перев.
Отсечения и отрицание 135 p(s(X))*-p(X). q(a). Вопрос можно решить, если выбрать сначала цель q(X), так как этот выбор приводит к дереву конечного безуспешного поиска. Неадекватность программы 11.6 связана также с порядком обхода дерева поиска, что проявляется в случае использования отношения not в конъюнкции с другими целями. Рассмотрим использование отношения not в определении предиката не- женатый_студент ( X) для кого-то, кто и студент, и не женат. Определение приведено в программе 5.1: неженатый студент(Х) <- п(Иженат(Х), студент(Х). студент (билл.) женат (джо). Решение вопроса неженатый_студент (X)! с использованием приведенных сведений безуспешно, несмотря на то что решение X = билл логически следует из правила и двух фактов. Это связано с тем, что цель not женат (X) не выполнена из-за решения X = джо. В данном случае проблема может быть устранена перестановкой целей в теле правила. Аналогичным примером является вопрос not(X = 1), X = 2?, который, несмотря на то что он верен, приводит к безуспешному вычислению. Как показывают приведенные выше примеры, реализация отрицания как безуспешного выполнения с помощью комбинации «отсечение-fail» приводит к некорректной работе для неосновных целей. В большинстве стандартных реализаций языка Пролог ответственность за то, что под отрицанием находятся основные цели, ложится на программиста. Проверка может выполняться или при статическом анализе программы, или в процессе вычислений с помощью предиката ground, описанного в программе 10.4. Предикат not очень полезен. С его помощью можно вводить интересные понятия. Рассмотрим, например, предикат disjoint (Xs,Ys), выполненный, если списки Xs и Ys не имеют общих элементов. Этот предикат можно задать следующим образом: disjoint(Xs,Ys) <- not(member(Z,Xs), member(Z,Ys)). В программах, приведенных в книге, встретятся и другие примеры применения предиката not. Комбинация «отсечение-fail», использованная в программе 11.6, имеет и более общее применение. Она позволяет прервать вычисление на ранней стадии. В предложении, содержащем «отсечение-fail», утверждается, что поиск не должен быть (и не будет) продолжен. Некоторые отсечения в комбинации «отсечение-fail» являются зелеными отсечениями. Это означает, что значение программы не изменится, если удалить предложение, содержащее данную комбинацию. Рассмотрим в качестве примера программу 10.4, определяющую предикат ground. Можно добавить правило, сокращающее поиск и не влияющее на значение программы: ground (Term) <- var(Term),!, fail. Использование отсечения в программе 11.6, задающей предикат not, является красным, а не зеленым. Работа программы после удаления отсечения будет отличаться от требуемой. Комбинация «отсечение-fail» применяется и при реализации других системных предикатов, использующих отрицание. Например, программа 11.7 дает простую реализацию предиката ф с помощью унификации и комбинации «отсечений-fail», а не с помощью бесконечной таблицы. Данная программа также работает правильно лишь для основных целей.
136 Часть II. Глава 11 X ФУ+- X и У неунифицируемы. X Ф X <- !, fail. X ф Y. Программа 11.7. Реализация отношения Ф. Изобретательность, хорошее понимание алгоритма унификации и процесса вычисления в Прологе позволяют строить интересные определения многих металогических предикатов. Уровень требуемых ухищрений можно оценить, рассмотрев программу, определяющую предикат same_var(X, Y). Этот предикат выполнен в том случае, если X и У-одна и та же переменная, в противном случае он ложен. Same._var(foo, Y) <- var(Y),!, fail. Same_var(X, Y) <- var(X), var(Y). Довод в пользу правильности определения: «Если аргументы предиката Sa- те_уаг-одна и та же переменная, то сопоставление значения foo аргументу X приведет к такому же сопоставлению и для второго аргумента, так что первое правило будет неприменимо. Применение второго правила приведет к успешному решению. Если какой-либо из аргументов не является переменной, то оба правила неприменимы. Если аргументы - различные переменные, то первое правило приведет к безуспешному вычислению, а отсечение воспрепятствует рассмотрению второго правила» (O'Keef, 1983). Упражнения к разд. 11.3 1. Определите предикат \ = =, используя предикат = = и конструкцию «отсечение-fail». 2. Определите предикат nonvar, используя предикат var и конструкцию «отсечение-fail». 11.4. Красные отсечения: устранение явных условий Последовательный перебор правил в Прологе и способ его реализации при использовании отсечения является основой, необходимой для построения программы not. Программист может принимать во внимание, что при определенных условиях Пролог будет выполнять только часть процедуры. Это наводит на мысль о новом (вводящем в заблуждение) стиле программирования на Прологе с устранением явных условий, регламентирующих применение правила. Исходным (плохим) примером в литературе является модификация программы 11.3, задающей отношение minimum. Можно отбросить сравнение во втором предложении программы, что приводит к программе minimum(X, Y,X) <- X ^ Y,!. minimum (X,Y,Y). Законность этой программы объясняют так: «Если X не больше У, то минимум - X. В противном случае минимум равен У и сравнение X с Уне требуется». Однако в программе 11.3 такое сравнение выполняется. Эти доводы имеют серьезный изъян. Значение модифицированной программы отличается от значения стандартной программы minimum. В модифицированной программе выполняется цель minimum (2,5,5). Модифицированная программа является ложной логической программой. Можно избавиться от ложных целей, выводимых из модифицированной программы. Для этого необходимо неявную унификацию первого и третьего аргумента
Отсечения и отрицание 1 37 в первом правиле сделать явной. Измененное правило- minimum(X, Y,Z) <- X ^ Y,!, Z = X. Такой способ использования отсечения для фиксации правила после частичного выполнения унификации является достаточно обычным приемом. Но в случае программы minimum в результате получается слишком мудреный текст. Гораздо лучше просто записать корректную логическую программу, добавив для эффективности отсечение, как это сделано в программе 11.3. Использование отсечения в операционной среде Пролога весьма проблематично. Оно приводит к появлению Пролог-программ, которые неверны, если их рассматривать как логические программы, т.е. из таких программ выводимы ложные утверждения. Однако они же работают правильно, поскольку в Прологе невозможно доказать эти ложные утверждения. Например, если в цели вида minimum (X,Y,Z) аргументам X и У сопоставлены значения, а аргументу Z-нет, то модифицированная программа работает правильно. Единственным следствием применения зеленых отсечений в разд. 11.1 является отбрасывание заведомо бесполезных ветвей в дереве поиска. Отсечения в программе, которые приводят к изменению ее значения, называются красными отсечениями . Удаление красного отсечения изменяет значение программы, т.е. множество выводимых целей. Обычный способ использования красных отсечений в Прологе-это устранение явных условий. Знание метода вычислений в Прологе и порядка использования правил в программе позволяет опускать заведомо выполнимые условия. В практическом программировании это иногда существенно, так как явные условия, особенно использующие отрицание, затрудняют описание и снижают эффективность выполнения программ. Однако такое устранение чревато ошибками. Устранение явного условия возможно, если безуспешное применение предыдущих правил влечет выполнение этого условия. Например, из безуспешности сравнения X ^ У в программе minimum следует, что X больше У Следовательно, проверка X > У может быть опущена. Обычно такое явное условие эквивалентно отрицанию предыдущих условий. Использование красных отсечений для устранения условий позволяет задавать отрицание неявно. Рассмотрим программу 11.5-программу сортировки перестановками. Первое (рекурсивное) правило применимо всегда, когда в списке имеется пара неупорядоченных соседних элементов. Когда применяется второе правило sort, такие пары в списке отсутствуют и он должен быть упорядочен. Следовательно, условие ordered (Xs) может быть опущено, что превращает второе правило в факт sort(Xs,Xs). Как и в случае программы minimum, данное предложение логически некорректно. Как только из программы удалено условие Ordered, цвет отсечения меняется с зеленого на красный. Удаление отсечений из варианта программы, не содержащего условия ordered, приводит к программе, дающей ложные решения. Обратимся к другому примеру устранения явного условия. Рассмотрим программу 3.18, удаляющую элементы из списка. В двух рекурсивных предложениях рассматриваются различные случаи, зависящие от того, совпадает ли голова списка с удаляемым элементом. Взаимоисключащий характер этих случаев может быть выражен с помощью отсечений, как это сделано в программе 11.8а. Ввиду того что безуспешное выполнение первого предложения означает несовпадение удаляемого элемента с головой списка, явную проверку на неравенство во втором предложении можно опустить. Программа 11.86 содержит соответствующие изменения. Все отсечения программы 11.8а зеленые в отличие от красного отсечения в первом предложении программы 11.86.
138 Часть II. Глава 11 delete (Xs,X,Ys) <- список Ys получен в результате удаления всех вхождений элемента X из списка Xs. delete([X | Ys],X,Zs) <- !, delete(Ys,X,Zs) delete([Y | Ys],X,[Y | Zs]) <- Y Ф X,!, delete(Ys,X,Zs). delete([ ],X,[ ]). Программа 11.8a. Удаление всех вхождений элемента из списка. delete (Xs,X,Ys) <- список Ys получен в результате удаления всех вхождений элемента X из списка Xs. delete([X | Ys],X,Zs) <-!, delete(Ys,X,Zs). delete([Y | Ys],X,[Y | Zs]) +- !,deletc(Ys,X,Zs). delete([ ],X,[ ]). Программа 11.86. Удаление всех вхождений элемента из списка. В общем случае удалять простые проверки, как это сделано в программе 11.86, нецелесообразно. Выигрыш в эффективности, который дает такое удаление, ничтожен по сравнению с потерями удобочитаемости текста программы и его модифицируемости. Рассмотрим использование отсечения при задании управляющей конструкции if_then_else. Программа 11.9 определяет отношение if_then_else (P,Q,R). Декларативное понимание предиката: предикат выполнен, если Р и Q истинны или не Р и R истинны. Операционное понимание: мы пытаемся доказать Р, в случае успеха доказываем Q, в противном случае доказываем R. if_.then_else(P,Q,R) <- Или Р и Q, или не Р и R. if_then_else(P,Q,R) <- Р,!, Q. if_then_else(P,Q,R)+- R. Программа 11.9. Конструкция if_then_else. Очевидно, что использование красного отсечения в данном случае разумно. Альтернатива к использованию отсечения состоит в явном указании условия, при котором доказывается R. В этом случае второе предложение будет иметь вид if._then_else(P,Q,R) <- not P, R. С вычислительной точки зрения это может привести к большим затратам. При вычислении предиката not придется полностью повторить вычисление цели Р. До сих пор мы встречались с двумя вариантами использования красных отсечений. В первом красное отсечение было встроено в программу, как в определениях отношений not и ф. Во втором красное отсечение возникало из зеленого при удалении условий из программы. Однако имеется и третий вариант красного отсечения. Отсечение, вводимое в программу в качестве зеленого, предназначенного лишь для повышения эффективности, может оказаться красным, меняющим значение программы. В качестве примера рассмотрим попытку создания эффективного варианта программы member, который при неоднократном вхождении элемента в список допускал бы лишь одно успешное вычисление. Применяя процедурный подход, для устранения возврата при обнаружении вхождения элемента в список, будем использовать отсечение. Текст соответствующей программы: member(X,[X|Xs])<-!. member(X, [Y | Ys]) <- member(X, Ys).
Отсечения и отрицание 1 39 Добавление отсечения действительно изменило функционирование программы. Однако полученная программа некорректна, так как решение, например, цели member (Х,[1,2,3'])! дает лишь одно значение X = 1. Эта программа является вариантом программы member_check (программа 7.3), в которой опущено явное условие X Ф У, и, следовательно, отсечение является красным. Упражнения к разд. 11.4 1. Рассмотрите возможность добавления отсечений в программу 9.3, задающую отношение Substitute. Полезно ли использовать комбинацию «отсечение-fail» и имеет ли смысл исключать явные условия? 2. Исследуйте связь между программой Select (программа 3.19) и программой, полученной добавлением одного отсечения: select(X,[X|Xs],Xs)+-!. select(X,[Y | Ys],[Y | Zs]) +- select(X, Ys,Zs). (Указание: Рассмотрите варианты программы select.) 11.5. Правила по умолчанию Логические программы с красными отсечениями фактически состоят из последовательности специальных случаев и правила по умолчанию. Например, в программе 11.6, определяющей отношение not, имеется специальный случай для выполненности цели G и факт по умолчанию notG, используемый в остальных случаях. Второе правило программы 11.9, задающей отношение if_then_else, имеет вид if._then_else(P,Q,R)+-R. Оно используется по умолчанию, когда Р не выполнено. Использование отсечений для функционирования по умолчанию входит в фольклор логического программирования. Мы покажем на примере простой программы, что во многих случаях лучше прибегать к иным логическим формулировкам. Программа 11.10а является упрощенной программой, описывающей выплаты по социальному обеспечению. Отношение пенсия (Человек, Пенсия) определяет, какая Пенсия назначена Человеку. Первое правило программы пенсия утверждает, что инвалиду положена пенсия по инвалидности. Второе правило утверждает, что человеку старше 65 лет положена пенсия по старости, если он в течение достаточно долгого времени делал взносы в соответствующий пенсионный фонд. Это условие кратко обозначено предикатом выплачено. Людям, не выплатившим взнос, все равно назначается дополнительное пособие, если им больше 65 лет. Рассмотрим расширение программы 11.10а правилом о том, что человеку не выплачивается ничего, если он не подпадает ни под одно из правил. Процедурное «решение» состоит в добавлении отсечений после каждого из трех правил и введении дополнительного факта по умолчанию: пенсия (X,ничего). Программа 11.106 является таким вариантом. Программа 11.106 функционирует правильно, когда речь идет об установлении вида пенсии человеку, например, при решении вопроса пенсия (мактэвиш, Xр.. Однако программа некорректна. Ответ на вопрос пенсия (мактэвиш, ничего р. будет положителен, что вряд ли обрадует человека по фамилии Мактэвиш. Вопрос пенсия (X,пенсия__по_старости) имеет единственное решение X = мактэвиш, что
140 Часть П. Глава 11 пенсия ( Человек, Пенсия) <- Пенсия -вид пенсии, назначенной Человеку. пенсия(Х,пенсия_по_инвалидности) <- инвалид(X). пенсия(X,пенсия по.старости) <- старше. 65(Х), выплачено(Х). пенсия(Х,дополнительное_.пособие) <- старше_65(Х). инвалид (мактэвиш). старше_65(мактэвиш). старше.65(макдональд). старше 65(макдаф). выплачено(мактэвиш). выплачено (макдональд). Программа 11.10а. Установление пенсии. пенсия ( Человек, Пенсия) Пенсия - вид пенсии, назначенной Человеку. пенсия (Х,пенсия_по._инвалидности) <- инвалид(X).!. пенсия(X,пенсия по старости) <- старше 65(X), выплачено(Х),!. пенсия(X,дополнительное, пособие) <- старше 65(Х),!. пенсия(Х, ничего). Программа 11.106. Установление пенсии. также неверно1*. Отсечение препятствует поиску иных решений. Программа 11.106 правильно работает только при установлении пенсии, назначенной конкретному человеку. Для решения поставленой задачи лучше ввести новое отношение назначено (X,Y), которое выполнено, если человеку X назначена пенсия Y. Оно задается с помощью двух правил и использует программу 11.10а, определяющую отношение пенсия: назначено(X, Y) <- пенсия(X, Y). назначено (X, ничего) <- not пенсия(X,Y). Этой программе присущи все достоинства программы 11.106 и ми один из указанных выше недостатков. Она показывает, что установка человеку пенсии вида «ничего» по умолчанию является действительно новым понятием, которое должно быть представлено надлежащим образом. 11.6. Дополнительные сведения Отсечение было введено уже в Marseille-Прологе (Colmerauer et al., 1973), и, возможно, это-наиболее важное решение, принятое при разработке Пролога. Прежде чем прийти к окончательному определению, Колмероэ экспериментировал с некоторыми другими конструкциями, соответствующими частным случаям отсечения. Терминология зеленых и красных отсечений была введена в работе (van Emden, 1982) для того, чтобы разделить допустимые и недопустимые применения отсечения. Постоянно рассматриваются и другие управляющие конструкции, более структурированные, чем отсечения, однако отсечение все еще остается «рабочей лошадкой» программиста. Примерами расширений конструкции «отсечение» являются конструкции if__then else (O'Kecfc, 1985), способы объявления функциональных, или детерминированных, отношений, а также «слабые отсечения», «разрезы», удаленные отсечения (Chikayma, 1984) и само отношение not, которое, будучи реализовано описанным выше способом, может рассматриваться в качестве структурированного применения отсечения. «На бобах» остались Макдаф и, что уж совсем обидно, Макдональд. Прим ред.
Внелогические предикаты 141 Отсечение является также предшественником оператора фиксации , используемого в параллельных языках логического программирования. Впервые этот оператор был введен в работе (Clark, Gregory, 1981) при разработке Реляционного Языка. Оператор фиксации свободен от одного из главных недостатков отсечения - нарушения модульности предложений. Отсечение несимметрично, так как оно исключает рассмотрение предложений, расположенных ниже предложения, содержащего отсечение, но не влияет на предложения, расположенные выше. Следовательно, отсечение, входящее в некоторое предложение, влияет на значение других предложений. В отличие от этого оператор фиксации симметричен, поэтому он не может быть использован при реализации отрицания как безуспешного выполнения и не нарушает модульности программы. Были предприняты попытки описать семантику отсечения в Прологе. Один из подходов содержится в работе (Lloyd, 1984). Оптимизация остатка рекурсии была впервые описана в работе (Warren, 1981) и реализована в Прологе-10. Одновременно эта оптимизация была реализована в системе Пролога, описанной в (Bruynooghe, 1982). Ссылки на применение отрицания в логическом программировании указаны в аналогичном разделе гл. 5. Примерами корректной реализации правила отрицания, как безуспешное выполнение в диалектах Пролога, являются Пролог-Н (van Caneghem, 1982) и MU-Пролог (Naish, 1985a). Программа same..var и обоснование ее правильности заимствованы из работы (O'Keefe, 1983). Программа 11.10а, описывающая отношение пенсия, является видоизменением примера Сэма Стила (Sam Steel) из курса Пролога в Эдинбургском университете - отсюда шотландский оттенок. Излишне напоминать о том, что программа не предназначена для точного описания (и не является таковой) шотландской или британской системы социального обеспечения. Глава 12 Внелогические предикаты В Прологе имеется ряд предикатов, существующих за рамками модели логического программирования. Они называются внелогическими предикатами. Такие предикаты в процессе решения логических целей порождают побочный эффект. Существуют три основных вида внелогических предикатов: предикаты, относящиеся к вводу-выводу, предикаты, обеспечивающие доступ к программе и ее обработку, и предикаты для связи с внешней операционной системой. В данной главе рассматриваются предикаты Пролога, связанные с вводом-выводом, и предикаты, обеспечивающие обработку программы. Интерфейс с операционной системой слишком системозависим, чтобы рассматривать его в данной книге. В приложении В приведен список системных предикатов Wisdom-Пролога в операционной системе Unix. 12.1. Ввод-вывод Очень важный класс предикатов, вызывающих побочные эффекты, образуют предикаты, относящиеся к вводу-выводу. Любой практический язык программирования должен обеспечивать средства ввода и вывода. Однако вычислительная
142 Часть II. Глава 12 модель Пролога препятствует введению операций ввода-вывода в виде чистых компонентов языка. Основной предикат BBOjxa-read(X). При решении такой цели считывается терм из текущего входного потока; обычно это данные, вводимые с терминала. Введенный терм унифицируется с X, цель read выполнена или не выполнена в зависимости от результата унификации. Основной предикат ъыъола.-шИе(Х). Решение этой цели приводит к записи терма X в текущий выходной поток, определяемый операционной системой; обычно это данные, выводимые на терминал. Ни read, ни write не дают альтернативных решений при возврате. Обычно в предикате read в качестве аргумента используется переменная, которая приобретает значение первого терма в текущем входном потоке. Сопоставление переменной X какого-либо значения, определенного вне программы, не входит в логическую модель, так как каждое новое обращение к процедуре read(X) будет выполняться с (возможно) различными значениями X. Предикат read проводит синтаксический анализ следующего терма в потоке ввода. Если найдены синтаксические ошибки, то на терминале печатается сообщение об ошибке и предпринимается попытка прочесть следующий терм. Внелогическая природа предикатов read и write различна. Если в программе заменить все обращения к предикату write на обращения к всегда выполняемому предикату true, то семантика программы не изменится. Для предиката read это не так. Различные версии Пролога содержат различные системозависимые вспомогательные предикаты. Полезным вспомогательным предикатом является предикат writeln(Xs), аналогичный соответствующей команде языка Паскаль. Решение цели writeln(Xs) приводит к печати списка термов Xs в виде выходной строки. Предикат описан в программе 12.1. В ней использован системный предикат я/, вызывающий переход к выводу с новой строки. Writln([X | Xs])«- write(X), writeln(Xs) Writeln([ ])«- nl. Программа 12.1. Вывод списка термов. Строка литер заключается в апострофы. Например, решение цели writeln (['Значение X равно', XJ) приводит к печати текста Значение X равно 3 если переменной X сопоставлено число 3. В Прологе можно выполнять и более общие операции со строками литер, обрабатывая строки как списки кодов литер. Если, например, используются коды ASCII, то список [80,114,111,108,111,103'] соответствует строке «Prolog». Код ASCII для литеры Р равен 80 для литеры г-114 и т.д. Строка литер, заключенная в кавычки, является другим способом обозначения списка, состоящего из кодов ASCII. В нашем примере список можно задать с помощью строки «Prolog». Такие строки просто являются более удобной синтаксической записью подобных списков. Обработка строк может производиться с помощью стандартных методов работы со списками. Системный предикат name(X,Y) используется для преобразования названия константы в строку литер и обратно. Цель name(X,Y) выполнена, если Х-атом, а У-список кодов ASCII, соответствующих литерам атома X, например, цель пате(1од, [108,111,103 ])1 выполнена.
Внелогические предикаты 143 Ввод-вывод может выполняться не только на уровне термов с помощью предикатов read и write, но и на более низком уровне-на уровне литер. Основной предикат вывода на уровне литер имеет вид put(N), он помещает литеру, соответствующую коду ASCII для N, в текущий выходной поток. Основной предикат ввода get(X)l\ сопоставляющий аргументу X код ASCII первой литеры во входном потоке. Программа 12.2 задает вспомогательный предикат read_word_list(Words), читающий список слов Words. В построении программы использован предикат get. Слова могут быть разделены произвольным числом пробелов (код ASCII для пробела-32) и могут состоять из любого числа строчных и прописных букв и символов подчеркивания. Последовательность слов завершается точкой. read word list(Ws) <- get(C), read_word_list(C,Ws). read_word_list(C,[W | Ws]) <- word_char(C), read_word(C,W,Cl), read_word._list(C 1, Ws). read_word_list(C,Ws) <- filLchar(C), get(Cl), read_word_list (C1, Ws). read_word_..list(C,[ ]) <- end_of_words_char(C). read_word(C,W,Cl)<- word_chars (C, Cs, С1), name(W,Cs). word_chars(C,[C | Cs],C0) <- word_char(C), |5 get(Cl), word_chars(Cl,Cs,CO). word_chars(C,[ ],C) <- not word__char(C). word_char(C) <- 97 ^ С, С ^ 122 word_char(C) <- 65^C, С ^90. word „char (9 5). fill_char(32). end_of_ words._char(46). Программа 12.2. Чтение списка слов. Предикат read_word_list читает литеру С и обращается к процедуре read_word_list (С, Words). В этой процедуре выполняется одно из трех действий в зависимости от значения литеры С. Если С-литера, входящая в слово, т.е. прописная или строчная буква, или символ подчеркивания, то обрабатывается следующее слово и далее рекурсивно обрабатывается последовательность оставшихся слов. Второе действие состоит в игнорировании пробелов, т. е. считывается следующая литера и работа программы рекурсивно продолжается. Если, наконец, % строчные буквы %прописные буквы %подчеркивание %пробел % точка Это несколько отличается от операторов Edinburgh-Пролога.
144 Часть II. Глава 12 встретилась литера, обозначающая конец последовательности слов, то программа завершает работу и возвращает список слов. Существенно, что всегда программа прежде читает литеру, а затем уже проверяет, что следует делать. Если это полезная литера, например литера слова, то она должна быть включена в слово. Иначе литеры могут быть потеряны при возврате. Рассмотрим следующий цикл ввода и обработки: process([ ])<- get(C), end_of_words_char(C). process ([W | Words]) <- get(C), word _char(C), get_word(C, W), process (Words). Если первая литера слова не удовлетворяет предикату end_of_words_char, то первое предложение не выполнится, а второе предложение приведет к вводу следующей литеры. Возвращаясь к программе 12.2, заметим, что предикат read_\vords(C\W,Cl) читает слово W, начинающееся с текущей литеры С, и возвращает литеру, следующую за словом-С/. Список литер, образующих слово, находится с помощью процедуры word_chars/3 (аргументы те же, что и у процедуры read_words). Слово строится по списку литер с помощью системного предиката пате. Процедура words_chars также обладает свойством опережающего просмотра одной литеры, так что литеры не теряются. Такие предикаты, как fill_char/l и word_char/1, упрощают представление данных в Прологе. Упражнение к разд. 12.1 1. Измените программу 12.2 так, чтобы апострофы (код 39) и цифры (коды 48 57) воспринимались как литеры, входящие в слово, а вопросительный знак (код 63) и восклицательный знак (код 33)-как завершение последовательности. Что следует добавить для чтения предложений, содержащих запятые и прочие знаки пунктуации? 12.2. Доступ к программам и обработка программ До сих пор предполагалось, что программы размещены в памяти компьютера, и вопрос о том, как они там представлены и каким образом были туда занесены, не рассматривался. Однако многие приложения Пролога зависят от того, как организован доступ к предложениям программы. Более того, если программа должна изменяться во время вычислений, то следует иметь средства, позволяющие добавлять и удалять предложения. Системным предикатом, обеспечивающим доступ к программе, является предикат clause (Head, Body). К цели clause (Head, Body)! можно обращаться, если аргументу Неаd сопоставлено значение. Ищется первое предложение в программе, заголовок которого унифицируем с термом Head. Далее заголовок и тело этого правила унифицируются с аргументами Head и Body. При возврате каждое предложение, унифицируемое с аргументами цели, дает одно решение. Отметим, что доступ к правилам, минуя заголовки, невозможен. Считается, что телом факта является атом true. Конъюнктивная цель изображается с помощью бинарного функтора «,». Впрочем, можно легко абстрагироваться от конкретного представления. Рассмотрим программу 3.12, задающую отношение member member(X,\_X\Xs]). тетЬег(Х,[У| Ys~\) <- member(X,Ys).
Внелогические предикаты 145 Цель clause(member(X,YS), Body) имеет два решения: {Ys = [X\Xs~],Body = true} и {Ys = [У| Ysl],Body = member(X,Ys 1)}. Отметим, что каждое выполнение унификации приводит к созданию новых копий переменных, входящих в предложение. В терминах металогических примитивов freeze и melt можно сказать, что предложения программы хранятся в замороженном виде. Каждое обращение к предложению clause вызывает размораживание ранее замороженного предложения. Описанный метод обращения является логическим аналогом повторной входимости в традиционном программировании. Существуют системные предикаты, выполняющие добавление предложений к программе и удаление предложений из программы. Основной предикат для добавления предложений -assert (Clause), он присоединяет предложение Clause в качестве последнего предложения соответствующей процедуры. Например, решение цели assert (отец (арап,лот))? добавляет в программу факт отец. При добавлении правил следует вводить дополнительные скобки, чтобы учесть старшинство термов. Например, синтаксически правильным является выражение assert ((родитель (X, Y) <- отец(Х^))). Имеется вариант предиката assert, а именно asserta, добавляющий предложение в начало процедуры. Если аргументу Clause значение не сопоставлено (или если значение аргумента Clause имеет вид Н <- В, а переменной Н значение не сопоставлено), то возникает ошибочная ситуация. Предикат retract (С) удаляет из программы первое предложение, унифицируемое с С. Заметим, что для удаления правила вида а <- Ь, с, d следует задать цель retract ((а <- С)). Обращение к предикату retract может только пометить удаляемое предложение, а не удалить его физически из программы. Реальное удаление может произойти только при решении вопроса верхнего уровня Пролога. Это объясняется методом реализации предиката и может привести к неправильным действиям в некоторых версиях Пролога. Добавление лредложения замораживает имеющиеся в предложении термы. Удаление того же предложения размораживает новые копии термов. Во многих версиях Пролога это используется в качестве простейшего способа копирования термов. В частности, предикат сору, введенный в гл. 10, может быть задан следующим правилом: copy(X,Y)«- asserta($tmp(X)), retract($tmp(Y)). При этом предполагается, что функтор %tmp больше нигде в программе не используется. Предикаты assert и retract вводят в Пролог возможность появления побочных эффектов при программировании. Программы, успешное выполнение которых зависит от побочных эффектов, трудны для чтения, отладки и формального анализа. Поэтому эти предикаты являются до некоторой степени спорными, и в ряде случаев их использование свидетельствует о лености ума или некомпетентности. При программировании следует по возможности реже прибегать к этим предикатам. Многие программы, приведенные в данной книге, могли быть записаны с использованием предикатов assert и retract, но при этом были бы потеряны ясность и эффективность. Более того, по мере совершенствования компиляторов с Пролога неэффективность применения предикатов assert и retract будет становиться все более очевидной. Однако можно привести логическое обоснование некоторого ограниченного использования предикатов assert и retract. Например, добавление правила оправданно, если уже выяснено, что это правило является логическим следствием программы. Такое добавление не влияет на логическое значение программы, так как не
146 Часть И. Глава 12 позволяет выводить новые следствия, но может повлиять на эффективность, поскольку некоторые следствия теперь могут быть выведены быстрее. Такое использование продемонстрировано в конструкции «лемма», описанной в разд. 12.3. Аналогично удаление правила обоснованно, если правило логически излишне. В этом случае удаление играет роль логической сборки мусора, предназначенной для уменьшения размеров программы. Укажем некоторые другие примеры законного применения предикатов assert и retract. Одно из них состоит в установке и использовании глобальных переключателей, влияющих на выполнение программы. Это применение будет рассматриваться в разд. 13.2, посвященном программистским трюкам. Другое применение возникает при решении задач, которые по определению требуют модификации программы (например, программа consult в разд. 12.5 и такие метапрограммы, как редакторы). 12.3. Запоминающие функции Запоминающие функции сохраняют промежуточные результаты с целью их использования в дальнейших вычислениях. Запоминание промежуточных результатов в чистом Прологе невозможно, поэтому реализация запоминающих функций основана на побочных эффектах. Такой способ программирования может быть назван программированием по восходящей методологии. Исходной запоминающей функцией является функция lemma (Goal). Операционное понимание состоит в следующем: предпринимается попытка доказать цель Goal, и если попытка удалась, результат доказательства сохраняется в виде леммы. Отношение задается следующим образом: lemma (Р) <- Р, asserta((P <- !)). Следующая попытка доказать цель Р приведет к применению нового правила, что позволит избежать ненужного повторения вычислений. Отсечение введено для того, чтобы воспрепятствовать повторному рассмотрению всей программы. Применение отсечения обоснованно лишь в тех случаях, когда цель Р имеет не более одного решения. Использование лемм демонстрируется на примере программы 12.3, описывающей решение задачи о ханойской башне. В ней радикально улучшены характеристики программы 3.30, решающей данную задачу. Хорошо известно, что решение задачи о ханойской башне с N дисками требует 2N -1 перемещений. Например в случае 10 дисков требуются 1023 перемещения или, в терминах программы 3.30, требуются 1023 обращения к процедуре Hanoi(l,A,B,C,Xs). Суммарное число общих вызовов процедуры hanoi/5 будет существенно больше. Решение задачи о ханойской башне состоит в повторяющемся решении подзадач о перемещении одного и того же количества дисков. Запоминающая функция может быть использована при повторном обращении к поиску перемещений, необходимых для решения задачи с меньшим числом дисков. В последующих попытках решить ту же подзадачу могут использоваться уже вычисленные последовательности перемещений вместо их перевычисления. Эта идея реализована в рекурсивном предложении процедуры Hanoi в программе 12.3. В первом обращении к процедуре Hanoi решается задача с N — 1 диском. Это решение запоминается и может быть использовано при втором обращении к процедуре Hanoi с N —1 диском. Программа проверяется с помощью предиката test_hanoi(N,Pegs,Moves). Здесь N-число дисков. Pegs-список названий трех стержней, Moves-список перемещений, которые следует выполнить. Заметим что для эффективного использования
Внелогические предикаты 147 hanoi(N, А, В, С, Moves) Moves последовательность перемещений, необходимых для переноса N дисков со стержня А на стержень В с использованием промежуточного диска С. Перемещения производятся по правилам задачи о ханойской башне. hanoi(1,A,B,C,[AtoB]). hanoi(N,A,B,C,Moves)<- N> 1, N1:= N - 1, lemma(hanoi(Nl,A,C,B,Msl)), hanoi(Nl,C,B,A,Ms2), append(Msi,[A to В | Ms2],Moves). lemma(P) <- P, asserta((P <- !)). Проверка lest hanoi(N, Pegs, Moves) «- hanoi(N,A,B,C,Moves), Pegs = [A,B,C]. Программа 12.3. Решение задачи о ханойской башне с использо/ ^нием запоминающих функций. запоминающих функций следует сначала решить общую задачу. Только после того, как общая задача полностью решена и запоминающие функции записали результат своей работы, можно задавать названия стержней. Упражнение к разд. 12.3 1. Два игрока по очереди называют какое-либо число в интервале от 1 до 3 включительно. Названные числа суммируются, выигрывает игрок, сумма чисел которого равна 20. Напишите, используя запоминающие функции, программу, выигрывающую в данную игру. 12.4. Интерактивные программы Наиболее распространенный случай использования побочного эффекта - интерактивный цикл. С терминала читается некоторая команда, выполняются соответствующие действия и затем читается следующая команда. В обычных языках программирования интерактивный цикл чаще всего реализуется с помощью цикла while. Программа 12.4 описывает общую структуру подобных программ. В данной программе введенная команда повторяется путем вывода на экран. echo <- read (X), echo(X). echo(X) <- end_of_ file(X), echo(X) <- write(X), nl, read(Y),!, echo(Y). Программа 12.4. Основной интерактивный цикл. Цикл чтение-ответ запускается с помощью цели echo. Основу программы составляет отношение echo(X), где А^-терм, который следует вывести на экран. Программа использует предикат end_of_fille(X), который выполнен, если Х-символ конца файла. Конретный вид этого символа зависит от операционной системы. Если обнаружен конец файла, то цикл завершается. В противном случае терм выводится на экран и считывается новый терм. Заметим, что чтение терма и проверка терма выполняются раздельно. Это
148 Часть П. Глава 12 необходимо для того, чтобы избежать потери терма, так как терм не может быть прочитан повторно. С такой же ситуацией мы встретились в программе 12.2 при обработке литер. Литера сначала читалась, а уж потом отдельно обрабатывалась. Программа 12.4 является итерационной и детерминированной. Если система включает в себя оптимизацию остатка рекурсии, то программа может эффективно выполняться, всегда используя один и тот же небольшой объем памяти. Мы приведем два примера программ, использующих основной цикл чтения и последующей обработки термов. Первый пример - редактор строк. Вторым примером является интерактивная программа, представляющая собой оболочку, обрабатывающую команды Пролога. По существу она является написанным на Прологе интерпретатором верхнего уровня для Пролога. Первое решение, которое следует принять при создании редактора строк на Прологе,-выбор представления файла. Необходимо иметь доступ к каждой строке файла и к позиции курсора, соответствующей текущей позиции редактируемого файла. Мы используем структуру file (Before, After), где Before - список строк перед курсором; After - список строк, расположенных после курсора. Курсор может размещаться только в конце некоторой строки. Строки, расположенные перед курсором, размещены в обратном порядке, что облегчает доступ к строкам, ближайшим к курсору. В основном цикле с клавиатуры вводится команда, с помощью которой строится новая версия файла. Редактор записан в виде программы 12.5. edit <- edit(file([ ],[ ])). edit(File) <- write prompt, read (Command), edit(File, Command). edit(File,exit)«- !. edit(File,Command) «- apply (Command, File, Filel),!, edit(Filel). edit(File,Command) «- writeln(Command, 'is not applicable'l), l5 edit(File). apply (up, file([X | Xs],Ys),file(Xs,[X | Ys])). apply(up(N),file(Xs,Ys),file(Xsl,Ysl)<- N > 0,up(N,Xs,Ys,Xsl,Ysl). apply(down,file(Xs,[Y | Ys]),file([Y | Xs],Ys)). apply(insert(Line),file(Xs,Ys),file(Xs,[Line|Ys])). apply(delete,file(Xs,[Y|Ys],file(Xs,Ys)). apply(print,file([X | Xs], Ys),file([X | Xs], Ys)) <- write(X),nl. apply(print(*),filc(Xs,Ys),file(Xs,Ys))<- reverse(Xs,Xsl), write_file(Xsl), write_file(Ys). up(N,[ ],Ys,[ ],Ys). up(0,Xs,Ys,Xs,Ys). up(N,[X|Xs],Ys,Xsl,Ysl)<- N >0,N1 is N- l,up(Nl,Xs,[X|Ys],Xsl,Ysl). write file([X | Xs]) <- write(X), nl, write_file(Xs). write_file([ ]). write prompt «- write('»'), nl. ripoipuM.via 12.5. Редактор строк. Процесс редактирования начинается обращением к процедуре edit, которая использует цель file{\_ ], [ ]) для задания пустого файла в качестве начального
Внелогические предикаты 149 значения редактируемого файла. Интерактивный цикл начинается с помощью процедуры edit (File). Решение цели edit_prompt приводит к выводу подсказки на экран, далее читается и выполняется команда. При выполнении используется основной предикат - edit (File, Command). Этот предикат применяет команду Command к файлу File. Применение команды осуществляется в процессе решения цели apply (Command,File,File/), где File1 -новая версия файла, полученная после применения команды. Редактирование продолжается путем обращения к процедуре edit/1 с аргументом Filel. Третье предложение процедуры edit/2 используется в тех случаях, когда команда неприменима, т.е. в тех случаях, когда цель apply не выполнена. В этих случаях на экран выводится соответствующее сообщение и редактирование продолжается. Процесс радактирования завершается по команде exit, анализируемой в отдельном предложении процедуры edit/2. Давайте рассмотрим несколько предложений процедуры apply, чтобы представить себе, как же происходит анализ команд. Наиболее простыми являются команды перемещения курсора. В предложении apply(up,filc([X | Xs],Ys), filc(Xs,[X, Ys])). указано, что перемещение курсора вверх состоит в перемещении строки, находящейся непосредственно перед курсором, на место строки, находящейся непосредственно за курсором. Команда не выполнится, если курсор находится в начале файла. Команда перемещения курсора вниз аналогична команде перемещения курсора вверх и также описана в программе 12.5. Перемещение курсора вверх на N строк, а не на одну строку производится с помощью вспомогательного предиката ир/5, изменяющего позицию курсора в файле. Правильность выполнения данного предиката легко усматривается из его определения. Заметим, что в процедуре apply перед обращением к процедуре up проверяется, допустимы ли аргументы процедуры up, т.е. является ли число строк положительным. В самом предикате up учитывается то, что число строк, на которое требуется переместить курсор, может быть больше числа строк в файле. В этом случае выполнение команды приводит к установке курсора в начало файла. Задача перемещения курсора на N строк вниз рассматривается в упражнениях. Другие команды программы 12.5 вставляют и удаляют строки. Команда вставки insert (Line) содержит один аргумент, а именно вставляемую строку. Команда удаления выполняется непосредственно. Она неприменима, если курсор находится в конце файла. В редакторе имеются также команды печати строки, находящейся под курсором,-команда print и команда печати всего файла -print (*). Команды редактора-взаимоисключающие. Для любой команды редактора применимо лишь одно правило процедуры apply. Это указывается с помощью отсечения во втором предложении процедуры edit/2. После выполнения цели apply путь вычисления однозначно определен. Такой способ указания детерминизма несколько отличается от описанного в разд. 11.1, где отсечения требуется применять непосредственно к самим фактам процедуры apply. Различие между этими двумя подходами носит скорее косметический характер. Можно усовершенствовать редактор, связав с каждой командой собственное сообщение об ошибке. Предположим, например, что при попытке переместиться вверх из начала файла желательно получать более содержательное сообщение, чем «команда неприменима». Это можно сделать за счет усложнения предложения процедуры apply, относящегося к перемещению в файле вверх. Перейдем от редактора к оболочкам. Оболочка получает команды с терминала и выполняет их. В качестве примера рассмотрим оболочку, решающую цели на Прологе. Она описана в программе 12.6.
150 Часть II. Глава 12 shell <- shell, prompt, read (Goal), shell (Goal). shell(exit) <- ! shell (Goal) «- ground (Goal),!, shell_solve_ground(Goal), shell, shell (Goal) «- shell_.solve(Goal), shell. shelLsolve(Goal) <- Goal, write(Goal), nl, fail. shelLsolve(Goal) <- write ('Решений (больше) нет'), nl. shell_solve_ground(Goal) <- Goal,!, write('fla'), nl. shell _solve_ground (Goal) <- wnte('HeT'), nl. shelLprompt <- write ('Следующая команда?'). Программа 12.6. Интерактивная оболочка. Процесс работы начинается с помощью решения цели shell. Начало работы подобно началу редактирования. С помощью процедуры shelLprompt выводится подсказка, потом читается цель и предпринимается попытка решить эту цель с помощью обращения к процедуре shell (Goal). Решение основных целей, дающее ответ да/нет, отличается от решения неосновных целей, дающего в качестве ответа цель, в которую подставлены соответствующие значения. Эти два случая рассматриваются соответственно в процедурах shell_solve_ground и shell_solve. Оболочка прекращает работу при вводе цели exit. И shell_solve_ground, и shell_solve используют доступность метапеременных для обращения к решаемой цели. Успешность или безуспешность решения цели определяют вид выдаваемого сообщения. Эти два предиката являются простейшими примерами метаинтерпретаторов - предмета обсуждения гл. 19. Процедура shell_solve содержит интересную конструкцию «решение-вывод-fail», полезную при выявлении всех решений цели путем принудительного возврата. Для того чтобы избежать безуспешных вычислений, добавлено еще одно предложение. Оно выполняется, когда исчерпаны все решения цели. Любопытно отметить, что невозможно непосредственно получить все решения целей, не прибегая к некоторой форме побочного эффекта. Причина этого явления разъясняется дальше в гл. 17, посвященной программированию второго порядка. На основе данной оболочки может быть создано средство хранения протокола сеанса работы с Прологом. Такая система описана в программе 12.7. Эта новая оболочка начинает работать с вызова цели log, которая обращается к основному интерактивному предикату shell (Flag), аргумент Flag вначале равен константе log. Переключатель Flag принимает одно из двух значений log или nolog; это значение определяет, следует ли результат текущей работы заносить в протокол. Данная программа является усовершенствованием программы 12.6. Основное отличие заключается в добавлении в главные предикаты дополнительного аргумента, значение которого определяет, включен ли режим хранения. Добавлены две дополнительные команды-/яд и nolog для включения и выключения режима хранения. Переключатель используется в предикатах, относящихся к вводу-выводу. Каждое сообщение, выдаваемое на экран, должно быть также занесено в файл протокола. Аналогично каждая вводимая цель заносится в протокол, что облегчает чтение
Внелогические предикаты 151 log<- shell (log), shell (Flag) <^ shelLprompt, shelLread (Goal, Flag), shell (Goal, Flag). shell (exit, Flag) <- !, closejogging .file, shell(nolog, Flag) <- !. shell (nolog). shell (log, Flag) <- !, shell (log), shell (Goal, Flag) <- ground (Goal),!, shell_solve_ground (Goal, Flag), shell (Flag), shell (Goal, Flag) <- shell sol ve(Goal, Flag), shell (Flag). shell._solve(Goal, Flag) <- Goal, shell_write(Goal,Flag), nl, fail. shelLsolve(Goal) <- shell write ('Решений (больше) нет1, Flag), nl. shell_solve_ground(Goal, Flag) <- Goal,!, shell.writef Да1, Flag), nl. sheILsolve_.ground (Goal, Flag) <- shell _write('HeT\ Flag), nl. shelLprompt <- \угке('Следующая команда ?'). shelLread(X,log) <- read(X), п1е_\Угке(['Следующая команда ?\X], 'prologJog'). shelLread (X, nolog) <- read(X). shell. write(X,nolog) <- write(X). shell_write(X,log) <- write(X), Гlle_write([X],,prolog•log,). file_write(X,File)^ telling(Old), tell(File), writeln(X),nl,tell(01d). closeJogging__file <- tellCprologJog'), told. Про! рамма 12.7Реализация средства хранения протокола сеанса. протокола. Поэтому обращение к предикату read имевшееся в программе 12.6, заменено на обращение к процедуре shelLread, а обращение к write заменено на обращение к shell_write. Определение предиката shelLwrite описывает наши действия: shell_write(X, nolog)«- write(X). shell_write(X,log) <- write(X), file_write([X],'prolog, log'). Если текущее значение переключателя -nolog, то результат просто выводится на экран. Если значение-/од, то дополнительная копия результата помещается в файл prolog.log. Предикат file_write(X,File) записывает строку X в файл File. В программе 12.7-только два системозависимых предиката file_write и close_ logging_file. Они зависят от дополнительных системных предикатов, обрабатывающих файлы. Их определение использует примитивы Edinburgh-Пролога tell, told и telling, описанные в приложении Б. Еще одно соглашение, принятое в тексте программы - результирующий протокол записывается в файл prolog.log.
152 Часть II. Глава 12 Упражнения к разд. 12.4 1. Усовершенствуйте редактор, описанный в программе 12.5, так, чтобы он исполнял следующие команды: а: Передвинуть курсор на N строк вниз. Ь: Удалить N строк. с: Перейти к строке, содержащей данный терм. d: Заменить один терм на другой. е: Любая команда по вашему выбору. 2. Модифицируйте программу 12.7, описывающую систему протоколирования так. чтобы пользователь мог задавать файл, предназначенный для хранения протокола. 12.5. Циклы, управляемые отказом Интерактивные программы, описанные в предыдущем разделе, были основаны на неминимальных рекурсивных циклах. Существует другой способ записи циклов в Прологе, аналогичный записи циклов repeat в обычных языках программирования. Выполнение таких циклов управляется с помощью отказов, поэтому их называют циклами, управляемыми отказами . Такие циклы бывают полезны лишь при совместном использовании с внелогическими предикатами, имеющими побочный эффект. Работа таких циклов может быть понята только с операционной точки зрения. Простым примером цикла, управляемого отказом, служит вопрос Goal, write (Goal),nl,fail?, который приводит к выводу всех решений цели goal на экран. Такой цикл использован в оболочках, описанных в программах 12.6 и 12.7. Цикл, управляемый отказом, может быть использован при определении системного предиката tab(N), предназначенного для вывода на экран N пробелов. В нем используется отношение between, описанное в программе 8.5: tab(N)<-between(l,N,I), put(32), fail. ' Каждая интерактивная программа предыдущего раздела может быть переписана с использованием циклов, управляемых отказом. Новый вариант основного интерактивного цикла приведен в программе 12.8. Он основан на незавершающемся системном предикате repeat, который может быть задан с помощью минимальной рекурсивной процедуры, приведенной в программе 12.8. В отличие от программы 12.4 в данной программе решение цели echo(X) приводит к безуспешному вычислению, если только X не символ конца файла. Безуспешное вычисление вызывает возврат к цели repeat, цель выполняется, считывается и затем выводится на экран следующий терм. Отсечение в определении предиката echo гарантирует от позднейшего повторения цикла repeat. echo «- repeat, read(X), echo(X),!. echo(X)<-end_ofJile(X),!. echo(X) <- write(X), nl, fail. repeat. repeat <- repeat. Г1ро1ра.мма 12.8. Основной интерактивный цикл repeat. Цикл, управляемый отказом и использующий предикат repeat, имеет название repeat-цикл. Такие циклы аналогичны циклам вида repeat в обычных языках программирования. Repeat-циклы применяются в Прологе для описания интер-
«Знелогические предикаты 1 53 активного взаимодействия с внешней системой путем повторяющегося ввода и (или) вывода. В repeat-цикле обязательно должен присутствовать предикат, гарантированно приводящий к безуспешным вычислениям (в программе 12.8 это-цель echo(X)), определяющим продолжение итерации. Такой предикат вычисляется успешно лишь в момент выхода из цикла. Можно сформулировать полезное эвристическое правило построения repeat-циклов: в теле правила, содержащего цель repeat, должно быть отсечение, предохраняющее от незавершающихся вычислений при возврате в цикл. Мы используем repeat-цикл для определения системного предиката consult (File), предназначенного для чтения и последующего добавления к программе предложений из некоторого файла. Определение приведено в программе 12.9. Системные предикаты see (File) и seen используются соответственно для открытия и закрытия входного файла. consult (File) <- see(File), consult Joop, seen consult_coop <- repeat, read (Clause), process(Clause),!. process(X) <- encLofJile(X),!. process (Clause) <- assert(Clause), fail. Программа 12.9. Обращение к файлу. Рекурсивные циклы предпочтительнее repeat-циклов, поскольку последние не имеют логической интерпретации. На практике, однако, repeat-циклы часто необходимы при выполнении большого объема вычислений, особенно в реализациях Пролога без оптимизации остатка рекурсии и (или) без сборки мусора. Обычно явный отказ приводит к требованию некоторого зависящего от реализации объема памяти. Упражнение к разд. 12.5 1. Определите лредикат abolish(F,N), удаляющий все предложения для процедуры F арности N. 12.6. Дополнительные сведения Ввод-вывод никогда по-настоящему не гармонировал с остальной частью языка Пролог. Обычная реализация ввода-вывода с побочными эффектами полагается исключительно на процедурную семантику Пролога и никак не связана с исходной моделью логического программирования. Например, если оператор вывода появляется на безуспешной ветви вычислений, то возврат не способен исправить выведенные данные. Считанный входной терм теряется при возврате, так как входной поток не допускает возвратов. В параллельных языках логического программирования предприняты попытки решить данную проблему и добиться более гармоничного совмещения ввода-вывода с моделью логического программирования. Это делается путем отождествления входных-выходных потоков внешних устройств с логическими потоками в языке (Shapiro, 1984). Бесконечные рекурсивные процессы способны возрастающе производить или потреблять такие потенциально неограниченные потоки. Самомодифицируемые программы представляют собой устаревшую концепцию программирования. Современные языки программирования исключают возможность построения таких программ и даже в разумном программировании на ассемблере избегают подобных
154 Часть II. Глава 13 программистских трюков. Ирония судьбы состоит в том, что язык, пытающийся открыть новую эру в программировании, открыл ворота архаическим методам, использующим предикаты assert и retract. Эти предикаты обработки программ первоначально были введены в качестве средств нижнего уровня для загрузки и перезагрузки программ и реализованы в Прологе-10 с помощью предикатов consult и reconsult. Однако, как и в случаях других конструкций языка, они в конце концов стали применяться при решении таких задач, для которых, как мы полагаем, первоначально не были предназначены. В современных реализациях Пролога пытаются возместить некоторый ущерб за счет введения альтернативных конструкций, таких, как slot в ESP (Chikayama, 1984), или специальных объявлений изменяемых предикатов, как в Quintus-Прологе (Quintus, 1985). В параллельных языках логического программирования можно полностью отказаться (и отказываются) от подобных конструкций (Silverman etal., 1986), так как в таких языках глобальные, модифицируемые структуры данных могут быть реализованы с помощью мониторов, имеющих чисто логическое определение, как, например, бесконечный рекурсивный процесс (Shapiro, 1984). В Edinburgh-Прологе имеются два системных предиката для ввода литер -getO(X) и get(X). Различие между ними заключается в том, что getO(X) возвращает следующую литеру, в то время как get(X) получает следующую печатаемую литеру, т.е. такую, чей код ASCII больше 32. Необходим только один, более общий оператор, который мы называем get, а не getO. С программой, решающей задачу о ханойской башне, нас познакомил Шмуль Сафра (Shmuel Safra). Редактор строк взят из работы (Warren, 1982 b). Глава 13 Практические рекомендации Описывая в предыдущих главах программы на чистом Прологе и его расширениях, почти все внимание мы уделяли разъяснению основополагающих понятий. Однако при практическом программировании следует заботиться об эффективности программ, учитывать ограничения, связанные с конкретной реализацией, используемой системой программирования и другими факторами. В фольклоре логического программирования имеется большое число приемов и трюков, необходимых для создания нетривиальных рабочих программ на Прологе. В этой главе наши интересы смещаются к более прагматичным задачам разработки програ*мм на Прологе. Технология программирования для логических языков имеет тот же смысл, что и для процедурных языков. Методология разработки и сопровождения больших программных комплексов для Пролога так же необходима, как и для любого другого языка программирования. Не меньшую важность представляет хороший стиль программирования. В четырех разделах этой главы обсуждаются эффективность программ, классифицированные программистские трюки, стиль программирования, компоновка программ и методы их разработки.
Практические рекомендации 1 55 13.1. Эффективность программ на Прологе В практическом программировании на Прологе следует обращать внимание на эффективность программ. Для обсуждения этого понятия нам следует установить критерии оценки программ. Основной оцениваемый параметр-число выполненных унификаций и число попыток унификации в процессе вычисления. Этот параметр связан с временем работы программы. Еще один параметр-глубина вложенной рекурсии. Если в процессе вычислений глубина станет больше максимально допустимой, то вычисления прервутся. На практике эта проблема является основной. Третий параметр - число порожденных структур данных. Мы последовательно рассмотрим эти параметры. Можно предположить, что при разумной записи детерминированного последовательного алгоритма в виде программы на Прологе ожидаемая эффективность алгоритма сохранится. Обычно результирующие программы на Прологе не основываются на унификациях общего вида или глубоких возвратах. Сложности могут возникнуть при реализации алгоритмов, связанных с существенной перестройкой структур данных, например при использовании переменных указателей или массивов. Подобные структуры данных можно непосредственно моделировать в Прологе, например используя деревья, при этом затраты будут возрастать логарифмически. Однако во многих случаях более естественно было бы модифицировать сам алгоритм, приспосабливая его к принципу однократного присваивания, присущему логическим переменным. Для указанных алгоритмов применимы обычные методы анализа сложности вычислений. Если не используется полная унификация (унификация двух произвольных термов цели), то можно показать, что время выполнения редукции цели с помощью предложения программы ограничено константой. Величина константы зависит от программы. Поэтому число выполняемых в программе редукций, как функция от размера ее входа, является хорошей оценкой временной сложности программы. При программировании на Прологе средства, представляемые языком, могут использоваться полностью. Можно писать недетерминированные программы и программы, использующие полную унификацию. Анализ сложности таких программ представляет собой более трудную задачу и требует оценки затрат на просмотр при поиске и размера унифицируемых термов. Основной способ повышения эффективности программ-совершенствование алгоритма. Хотя речь идет о декларативном языке, понятие алгоритма в Прологе остается тем же, что и в других языках программирования. Примеры удачных и неудачных алгоритмов решения одной и той же задачи приведены в предыдущих главах вместе с записью этих алгоритмов на Прологе. Ясно, что линейное обращение списков с использованием накопителей (программа ЗЛ6Ь) эффективнее непосредственного обращения (программа 3.16а). Быстрая сортировка (программа 3.22) лучше сортировки перестановкой (программа 3.20). Помимо выбора наиболее удачного алгоритма можно использовать еще несколько способов для увеличения эффективности программ на Прологе. Один из них состоит в выборе наилучшей реализации. Эффективная реализация характеризуется исходной скоростью, возможностями индексирования, применением оптимизации остатка рекурсии и методом сборки мусора. Единицей измерения скорости выполнения программы на языке логического программирования обычно служит LIPS- чисАо Аогических выводов в секунду. При вычислениях логический вывод соответствует редукции. При фиксированной реализации сами программы могут быть улучшены за счет
156 Часть II. Глава 13 • упорядочения целей в соответствии с правилом: чем раньше возникает отказ, тем лучше, • устранения недетерминированности путем введения явных условий и отсечений, • использования возможностей индексирования с помощью соответствующего упорядочения аргументов. Проблема упорядочения целей рассматривалась в разд. 7.3. В разд. 11.1 обсуждалось, как использовать зеленые отсечения для выражения детерминизма программы. Как было сказано в разд. 11.2, индексирование существенно при обработке конъюнктивных целей в процессе оптимизации остатка рекурсии. Даже в программах без остатка рекурсии индексирование может привести к увеличению эффективности, так как оно ограничивает поиск применимых правил. Это может иметь значение в программах, представляющих наборы фактов в виде таблиц, например в синтаксическом анализаторе и компиляторе. В литературе, посвященной Прологу, не много внимания уделяется минимизации числа порождаемых структур данных. Рассмотрим эту проблему на примере. Предикат sublist (Xs,Ys), устанавливающий, является ли список Xs подсписком списка Ys, может иметь несколько определений. Сравним эффективность двух таких определений, отличающихся порождаемыми структурами данных. При этом ограничимся одним определенным использованием - проверкой, является ли один данный список подсписком другого данного списка. Два рассматриваемых варианта определения предиката sublist используют программу 3.13 для вычисления суффикса и префикса списка. Предложение (i) определяет подсписок как префикс суффикса, а предложение (п)-как суффикс префикса: sublist(Xs,AXBs)<^ suffix(XBs,AXBs),prefix(Xs,XBs). (i) sublist(Xs, AXBs) <- prefix(AXs, AXBs), suffix (Xs, AXs). (ii) Хотя эти две программы логически эквивалентны, эффективность их различна. Если оба аргумента предиката sublist - полные списки, то предложение (i) означает просмотр второго списка от конца к началу с возвращением суффикса, а затем проверку, является ли первый список префиксом суффикса. Такое вычисление не порождает новых промежуточных структур данных. С другой стороны, выполнение предложения (ii) приводит к созданию нового списка, являющегося префиксом второго списка. Далее проверяется, является ли первый список суффиксом этого списка. Если проверка выполнилась неудачно, то происходит возврат и создается новый префикс первого списка. Хотя число редукций в среднем в этим двух программах одинаково, эффективность их выполнения все же различна. Первая программа не создает новых структур данных (на жаргоне языка Лисп-«не применяет cons»). Вторая программа создает новые структуры. При анализе программ на Лиспе обычно тщательно исследуется сяия-характеристика программы, поскольку при оценке эффективности программы существенно, применяется ли в ней cons. Нам кажется, что этот вопрос столь же важен и для программ на Прологе. Однако искусство анализа больших программ на Прологе, вероятно, не достигло еще должной стадии. 13.2 Программистские трюки Для каждого языка программирования имеется набор программистских трюков, и Пролог не составляет исключения. Мы приведем несколько полезных трюков для Пролога. Трюками здесь будут предикаты, которые или обрабатывают сопостав-
Практические рекомендации 1 57 ления значений переменным, или демонстрируют использование глобальных переключателей. Интересная особенность предиката not состоит в том, что при его выполнении переменным никогда не сопоставляются значения. Это связано с явным указанием безуспешности вычисления после выполнения цели, что исключает проведение любых сопоставлений. Указанная особенность используется при определении предиката verify (Goal) в программе 13.1. Этот предикат проверяет, выполнена ли цель Goal, причем проверка не нарушает текущего сопоставления значений переменным. Предикат определяется через двойное отрицание. verify (Goal) <- not not Goal. Программа 13.1. Проверка цели. В качестве курьеза отметим, что реализация отрицания в Прологе имеет что-то общее с отрицанием в естественных языках. Например смысл двойного отрицания высказывания не совпадает со смыслом эквивалентного утверждающего высказывания. Полезным системным предикатом Edinburgh-Пролога является предикат пит- bervars(Term,Nl,N2), последовательно сопоставляющий переменным терма Term строки вида '$VAR (N), где число N лежит на отрезке от N1 до N2-1. Например, при успешном решении цели numbervars(foo(X,Y),l,N)? переменной X сопоставляется строка '$VAR'(1J, переменной У-строка '$VAR'(2), а переменной N-число 3. Этот предикат является еще одним примером предиката, анализирующего структуру терма; определение предиката приведено в программе 13.2. Счетчики в программе возрастают, а не убывают, и это позволяет нумеровать переменные слева направо в возрастающем порядке. Чтобы не возникла ошибка, следует обращаться к процедуре numbervars только тогда, когда второму аргументу сопоставлено значение. numbervarsO$VAR'(N),N,Nl) «- N1:=N+ l. numbervars (Term, N1,N 2) <- nonvar{Term),functor(Term,Name,N), numbervars (0, N,Term, N1, N2). numbervars(N,N, Term,Nl,Nl). numbervars (I, N, Term, N1, N3) <- I<N, I1:=I+ 1, arg(Il,Term,Arg), numbervars(Arg,Nl,N2), numbervars(Il,N,Term,N2,N3). Программа 13.2.Нумерация переменных в терме. Предикат numbervars позволяет определять металогические предикаты неожиданным образом. Например, ground(Term) <- numbervars(Term,0,0). Этот же предикат можно использовать при определении отношения freeze следующим образом: freeze(X,Term) <- copy(X,Term), numbervars(Term,0,N). Определение отношения melt мы отложим до гл. 15. В другом полезном примере используется как отношение verify, так и отношение numbervars. Для обоснования этого примера мы вернемся к обсуждению металогического сравнения термов, начатому в разд. 10.2.
158 Часть II. Глава 13 Предикат = =/2 задает более сильное отношение эквивалентности, чем унифицируемость, т.е. чем предикат =/2. В Прологе существует понятие промежуточной эквивалентности, заимствованное из логики, а именно: два терма эквивалентны, если они являются алфавитными вариантами. Напомним, что два терма являются алфавитными вариантами, если они совпадают с точностью до переименования переменных, т.е. они могут быть сделаны синтаксически идентичными путем последовательного изменения имен переменных в одном из термов. Примерами служат пары термов f(X,Y) и f(Y,Z) f(a,X) и f(a,Y) и пара f(X,X) и f(Y.Y). Предикат variant (Terml,Term2) выполнен, если термы Terml и Тегт2 являются алфавитными вариантами. Определение этого предиката приведено в программе 13.3. Трюк состоит в следующем: сначала с помощью предиката numbervars (программа 13.2) переменным сопоставляются значения, далее проверяется унифицируемость полученных термов, после чего построенные сопоставления отменяются. Отмена может быть выполнена с помощью программы verify. Variants (Term l,Term2) <- Verify ((numbervars (Term 1,0, N), numbervars(Term2,0,N), Terml = Term2)). Программа 13.3. Алфавитные варианты. Три указанных вида сравнения термов: = /2, verify/2 и = = /2 - расположены в строго возрастающем порядке, где унифицируемость является самым слабым и самым общим отношением. Идентичные термы являются алфавитными вариантами, а алфавитные варианты унифицируемы. Различие между сравнениями исчезает для основных термов: в этом случае все три сравнения дают один и тот же результат. Предикат verify может быть также использован для представления термов, содержащих переменные, в удобном для чтения виде. Дело в том, что в процессе работы интерпретатора Пролога переменным назначаются некоторые внутренние имена. Обычно они совершенно невразумительны, что затрудняет чтение результатов, содержащих переменные. Программа 13.4 определяет отношение write-vnames (Term)-вспомогательную процедуру печати, вводящую вместо чисел обычные имена переменных. Предикат lettervars(X), определенный в программе 13.4, заменяет переменные терма X литерными строками в соответствии с предварительно заданным списком имен. Другой программистский трюк состоит в моделировании глобальных переменных с помощью предикатов assert и retract. Отношение flag (Name, Value) обеспечивает доступ к текущему значению переключателя, а отношение set-flag (Name, Value) задает значение переключателя. Определение предиката set-flag приведено в программе 13.5. Одно из применений процедур, управляющих переключателями, - порождение в процессе вычислений имен констант. Отметим, однако, что в нашем случае необходимость в такой функции гораздо меньше, чем в других языках программирования. Обычно такие «gensum-функции» применяются для частичного моделирования свойств логических переменных. Изначальное присутствие логических переменных лишает рассматриваемую функцию большинства ее достоинств. Чаще всего функция "gensum" выполняется следующим образом: к заданному корневому префиксу, например к х, добавляются числовые суффиксы в возрастающей последовательности, например xl, х2,... и т. д. Переключатель используется для сохранения количества выбранных числовых суффиксов. Предикат gensum (Prefix,Constant), определенный в программе 13.6, сопоставляет аргументу Constant новое имя константы с заданным префиксом.
Практические рекомендации 1 59 write_vnames(Term) <- lettervars(Term), write (Term), fail. write_vnames(Term). lettervars(Term) <- list_of_variables(Term, Vars), variable_names(Names), unify „variables (Vars, Names). list_of_variables(V,[V]) <- var(V),!. list_of_variables(V,[ ]) <- constant(V),!. list_of_variables(Term, Vs) <- functor (Term, F, N), list_of_variables(N,Term, Vs 1), flatten(Vsl,Vs). list._of_variables(N,Term,[VArgs | Vs]) <- N>0, arg(N,Term,Arg), list_of _variables(Arg, VArgs), N1:= N-l, list_of_variables(Nl,Term,Vs). list_of_variables(0,Term,[ ]). unify .variables ([V | Vs],[V | Ns]) <- !, unify_variables(Vs,Ns). unify_variables([V | Vs],Ns) <- !, unify_variables(Vs,Ns). unify_variables(Vs,Ns). % Исчерпан список переменных или имен variable_names(|7X\T\,Z\,U\'V\,W\'Xl\Tl\'Zl\,Ul\'Vl\'Wl\'X2\T2\'Z2\,U2','V2',,W2', 'ХЗ',' Y 3', 'Z3', 'U 3', 'V3\ 'W3']). Программа 13.4. Вывод терма с нечисловыми именами переменных. set_flag(Name,X)<- nonvar(Name), retract(flag(Name,Val)),!, asserta(flag(Name, X)). sct_.flag(Name,X) <- nonvar(Name), asserta(flag(Name,X)). Программа 13.5. Использование глобальных переключателей. gensum(Prefix,V) <- var(V), atom (Prefix), old..value(Prefix,N), N1 := N+l, set_.flag(gensum (Prefix), N1), string_conca tenate (Prefix, N1, V), I old_value(Prefix,N) <- flag(gensum(Prefix),N),!. old_value (Prefix, 0). string_concatenate(X,Y,XY) <- name(X,Xs), name(Y,Ys), append (Xs,Ys,XYs), name(XY,XYs). Программа 13.6. Порождение новых символов.
160 Часть II. Глава 13 13.3. Стиль программирования и запись программ Один из основных принципов построения программ в данной книге - стремиться к тому, чтобы программы были «насколько возможно декларативны», так как это облегчает чтение и понимание программ. Программу следует рассматривать как нечто целое. Простота чтения программы зависит от ее размещения на бумаге и от выбора используемых имен. В этом разделе рассматриваются основные правила, которыми мы руководствуемся, создавая программы. Восприятие программы связано с именованием ее объектов. Выбор названий предикатов, имен переменных, констант и структур оказывает влияние на ясность программы. Основная цель такого выбора - облегчение декларативного понимания программы. В качестве имени предиката мы выбираем слово (или несколько слов), описывающее взаимосвязь между объектами программы, а не действия, выполняемые в программе. Придумать удачное декларативное название процедуры сложно. При создании программы используется процедурный подход. Зачастую легче дать процедурное имя, чем декларативное (к тому же программы с процедурными именами обычно выполняются быстрее). Однако после отладки программы мы часто заменяем процедурные имена на декларативные. В процессе составления программ происходит непрерывная переработка наименований предикатов. В новых именах отражается уточняемое значение описываемых отношений. Эти имена должны помочь (не только разработчикам) легче понять программу. Мнемонические имена переменных также влияют на простоту чтения программы. Именем может быть некоторое содержательное слово (или слова) или стандартная форма, такая, как Xs для обозначения списков. Переменные, входящие в предложение лишь однократно, могут быть помечены особым образом. Их можно рассматривать как анонимные, и с точки зрения реализации их именование излишне. В некоторых версиях языка Пролог придерживаются специальных синтаксических соглашений в обозначениях анонимных переменных. Например, в Edinburgh-Прологе для этой цели используется отдельный символ подчеркивания. С использованием такого соглашения программа 3.12, задающая отношение member, будет иметь такой вид: member(X,[X | „._]). member(X,[ | Ys]) <- member(X,Ys). Достоинство этого соглашения состоит в том, что явно выделяются переменные, унификация которых существенна. Недостаток связан с достоинством - правило понимается процедурно, а не декларативно. Синтаксические правила, которых мы придерживаемся при разделении отдельных слов в именах переменных и в именах функторов, различны. В случае переменных слова пишутся слитно, каждое новое слово начинается с прописной буквы. Отдельные слова в названии предиката разделяются подчеркиванием. Синтаксические правила-дело вкуса, но желательно, чтобы стиль программирования был последовательным. Способ размещения отдельных предложений также влияет на простоту понимания программы. Мы пришли к выводу, что наиболее удобный способ таков: foo« Аргументы)) «- bar! (Аргументы ^)), Ьаг2« Аргументы2)), Ьагп«Аргументып».
Практические рекомендации 161 Заголовки всех предложений выравнены, цели в теле предложений записаны с отступом, каждая цель занимает отдельную строку. Процедуры отделяются пустыми строками, но между отдельными предложениями одной процедуры пустые строки не используются. Размещение программ в книгах и при типографском оформлении не вполне следует этим правилам. Если все цели в теле правила невелики, то они записываются в одной строке. Иногда мы используем таблицы фактов, записывая в одной строке более одного факта. Программа может не нуждаться в дополнительных пояснениях, если указанным принципам уделялось достаточное внимание и программа оказалась достаточно простой. Такая ситуация была бы идеальна, поскольку программисты испытывают естественную неприязнь к комментариям и документированию. На практике текст программы часто нуждается в дополнительных пояснениях. Одной из существенных частей документирования является построение реляционных схем. Реляционная схема приводится перед предложениями, описывающими отношение, и при необходимости снабжается дополнительными разъяснениями. Разъяснения, приводимые в данной книге, описывают вычисляемое процедурой отношение. Записать на естественном языке ясное декларативное определение отношения, вычисляемого логической программой, иногда не так просто. Однако неспособность написать такое определение обычно свидетельствует о том, что программист сам не вполне понимает, что же он разработал, хотя его программа действительно работает. Поэтому мы призываем использовать принятые в данной книге правила документирования. Эти правила предоставляют удобное средство разъяснить смысл программы. Кроме того, они дисциплинируют мышление, помогают программисту анализировать и описывать собственные разработки. 13.4. Разработка программ Программирование на (чистом) Прологе настолько близко к записи спецификаций, насколько это доступно для практического языка программирования. Поэтому кому-нибудь может показаться, что в программах на чистом Прологе не бывает ошибок. Это, конечно, не так. Уже процесс аксиоматизации понятий и алгоритмов может привести к широкому спектру ошибок, совершенно аналогичных ошибкам в обычных языках программирования. Другими словами, при любом формализме найдется достаточно много сложных задач, для которых нет, очевидно, правильных записей решения. Таким образом, грань между языками низкого и высокого уровня определяется лишь тем, достаточно или нет простой проверки для выяснения правильности программы. Существую! два различных подхода к анализу правильности программ. При «верификационном» подходе предполагается, что сложные программы можно проверить, доказав, что они корректны относительно некоторой абстрактной спецификации. Непонятно, как подобный подход может быть использован при анализе логических программ, так как для таких программ различие между спецификацией и программой существенно меньше по сравнению с другими языками программирования. Если программа на Прологе не очевидна, то мало надежды и на очевидность спецификации, на каком языке она ни была бы записана. В случае Пролога можно предложить использовать в качестве языка записи спецификаций полный язык логики первого порядка. Авторы на собственном опыте убедились, что очень редко спецификации на полном языке логики первого порядка короче, проще или понятнее простейшей программы на Прологе, определяющей то же отношение. 6-1402
162 Часть II. Глава 13 В подобной ситуации имеются менее радикальные решения. Одно из них состоит в доказательстве того, что одна программа на Прологе - возможно более эффективная, хотя и более сложная - эквивалентна более простой программе на Прологе, которая, уступая в эффективности, может служить спецификацией первой. Другое решение состоит в доказательстве того, что программа удовлетворяет некоторому ограничению типа «инвариант цикла», которое хотя и не гарантирует корректность программы, однако повышает нашу уверенность в ее правильности. В некотором смысле программы на Прологе являются выполняемыми спецификациями. Вместо того чтобы разглядывать программы, стремясь убедиться в их истинности, программы можно запустить и посмотреть, работают ли они так, как нам хотелось. Существуют обычные приемы тестирования и отладки, применяемые при разработке программ во всех прочих языках программирования. Все классические методы и подходы, включая здравый смысл, используемые обычно при тестировании и отладке программ, в равной степени применимы и в случае Пролога. В чем же тогда состоит разница в разработке программ на традиционном, пусть даже на символьном, языке программирования и на Прологе? Один из ответов состоит в том, что, хотя программирование на Прологе-это «просто» программирование, в случае Пролога имеется преимущество в простоте записи и скорости отладки по сравнению с другими формализмами более низкого уровня. Мы надеемся, что у читателей уже была возможность убедиться в этом. Другой ответ состоит в том, что декларативное программирование проясняет ваше мышление. Говоря менее возвышенно, вообще программирование некоторых понятий, а программирование на декларативном языке и языке высокого уровня в особенности, позволяет уточнить соответствующие концепции и идеи. Для опытных программистов Пролог-это не просто формализм для «кодирования» машинных команд, но формализм, позволяющий записывать и реализовывать идеи, т.е. инструмент мышления. Третий ответ заключается в том, что особенности логического формализма высокого уровня могут в конце концов привести к набору средств практической разработки программ, существенно более мощных, чем существующие в настоящее время. Примерами таких средств являются автоматический преобразователь программ, частично вычисляющая программа, программа типового вывода и алгоритмический отладчик. Последнее средство рассматривается в разд. 19.3, где описаны алгоритмы программной диагностики и их реализация на Прологе. К сожалению, среды практического программирования на Прологе, включающие эти новые идеи, пока еще не широко доступны. Лучшее, на что можно рассчитывать,-это простой трассировщик, описанный в разд. 19.1. Тем не менее даже имеющиеся средства программирования позволяют создавать большие и сложные программы на Прологе существенно проще, чем на других доступных языках программирования. Имеющиеся средства и системы программирования не навязывают и не поддерживают какую-либо специфическую методику разработки программ. Однако, как и в других символьных языках программирования, наиболее естественной стратегией разработки программ является стратегия быстрой смены прототипов. При такой стратегии на каждой стадии разработки имеется лучше работающий прототип программы. Разработка происходит путем переделки или расширения прототипа. Другой подход к разработке программ, иногда комбинируемый с первым, состоит в «нисходящем анализе и восходящей реализации». Хотя проектирование системы следует проводить нисходящим способом, основываясь на анализе цели, реализацию системы лучше всего строить «снизу вверх». В процессе восходящего программирования каждый написанный фрагмент программы может быть немед-
Практические рекомендации 163 ленно отлажен. Глобальные проектные решения, такие, как представление, могут быть проверены на небольших сегментах системы, которые будут приведены в порядок и очищены от ошибок на начальной стадии программирования. Кроме того, эксперименты с одной подсистемой могут привести к изменению проектных решений, относящихся к другим подсистемам. Часть текста, которую следует целиком написать и отладить, может иметь разную длину. Она возрастает по мере того, как программист приобретает опыт. Опытный программист, пишущий на Прологе, может сразу написать программу, текст которой занимает несколько страниц. При этом он знает, что после записи текста осталось выполнить лишь весьма простую и прозаическую отладку. Для менее опытного программиста может оказаться сложным следить за функциональностью и взаимодействием одновременно большого числа процедур. Нам хотелось бы закончить этот раздел несколькими нравоучительными замечаниями. Независимо от языка программирования, его ясности, изящества, высокого уровня всегда найдутся программисты, пишущие на этом языке «грязные», неестественные, нечитаемые программы. Пролог не составляет исключения. Однако мы уверены, что для большинства задач, имеющих изящное решение, существует изящная запись этих решений на Прологе. Цель нашей книги-обосновать эту уверенность и предоставить средства для ее реализации в конкретных случаях, тем самым показав, что эстетика и практичность не обязательно должны конфликтовать. 13.5. Дополнительные сведения Практические методы анализа сложности программ в Прологе развиты хуже, чем в обычных языках программирования. Мы полагаем, что это объясняется историческими и социальными причинами и не связано со свойствами самих программ на Прологе. Кроме убеждения, что LIPS, вероятно, наилучшая характеристика, существует очень мало общепринятых оценок реализаций Пролога. Одной из стандартных оценок является время работы программы 3.16а-программы reverse, непосредственно обращающей списки. Обращение списка из 30 элементов требует 496 редукций. Некоторые собрания программистских трюков для Пролога широко известны. Главное из них- библиотека Пролога в Станфордском узле сети Агра, построенная на утилитах Эдинбургского университета. Стиль программирования развивается по мере накопления опыта и в процессе общения с другими программистами. Большое влияние на первого автора оказало Общество программистов на Прологе в Эдинбургском университете, и в частности, Лоуренс Байд (Lawrence Byrd) и Ричард О'Киф (Richard O'Keefe).
Часть III Современные методы программирования на Прологе Выразительная мощность и высокий уровень логического программирования позволяют писать программы, которые трудно разрабатывать с помощью обычных языков программирования. Средства логического программирования поддерживают парадигмы, пригодные для решения различных задач, используют альтернативные структуры данных и механизмы доступа к ним. Рассмотренные в предыдущей части простые программы на Прологе являются примерами использования основных методов программирования, реализуемых в контексте логического программирования. В этой части книги собраны более совершенные методы, развитые в рамках логического программирования и использующие специальные свойства логических программ. Будет показано, к каким преимуществам приводит их применение. Глава 14 Недетерминированное программирование Одним из отличий вычислительной модели логического программирования от моделей обычного программирования является недетерминизм. Недетерминизм- это техническое понятие, используемое для сжатого определения абстрактных моделей вычислений. Однако недетерминизм не только мощная теоретическая идея, но и полезное средство описания и реализации алгоритмов. В данной главе будет показано, как при недетерминированном подходе можно конструировать компактные и эффективные программы. Интуитивно ясно, что недетерминированная машина, перед которой возникло несколько альтернативных путей решения, осуществляет корректный выбор очередного действия. Подлинно недетерминированную машину реализовать нельзя, однако ее можно моделировать или аппроксимировать. В частности, Пролог-интерпретатор, как описано в гл. 6, аппроксимирует недетерминированное поведение интерпретатора абстрактных логических программ с применением механизма последовательного поиска и возвратов. Однако тот факт, что недетерминизм только «моделируется», но «реально не присутствует», для недетерминированного мышления во многих случаях несуществен, точно так же как несущественны для символьного мышления детали обработки указателей в процессе унификации.
Недетерминированное программирование 165 14.1. Метод «образовать и проверить» Метод «образовать и проверить»-общий прием, используемый при проектировании алгоритмов и программ. Суть его состоит в том, что один процесс или программа генерирует множество предполагаемых решений задачи, а другой процесс или программа проверяет эти предполагаемые решения, пытаясь найти те из них, которые действительно являются решениями задачи. Обычно программы, реализующие метод «образовать и проверить», конструировать проще, чем программы, в которых решение находится непосредственно, однако они менее эффективны. Стандартный прием оптимизации программ типа «образовать и проверить» заключается в стремлении погрузить программу проверки в программу генерации предполагаемых решений настолько «глубоко», насколько это возможно. В пределе программа проверки полностью переплетается с программой генерации предполагаемых решений, которая начинает порождать только корректные решения. Используя вычислительную модель Пролога, легко создавать логические программы, реализующие метод «образовать и проверить». Обычно такие программы содержат конъюнкцию двух целей, одна из которых действует как генератор предполагаемых решений, а вторая проверяет, являются ли эти решения приемлемыми: find(X)*-gencrate(X), test(X). Эта Пролог-программа действует подобно обычной процедурной программе, выполняющей генерацию вариантов и их проверку. Если при решении вопроса find(Xр. успешно выполняется цель generate?X) с выдачей некоторого X, то затем выполняется проверка test(X). Если проверка завершается отказом, то производится возвращение к цели generate, с помощью которой генерируется следующий элемент. Процесс продолжается итерационно до тех пор, пока при успешной проверке не будет найдено решение с характерными свойствами или генератор не исчерпает все альтернативные решения. Однако программисту нет необходимости интересоваться циклом «образовать и проверить». Он может рассматривать этот метод более абстрактно, как пример недетерминированного программирования. В этой недетерминированной программе генератор вносит предположение о некотором элементе из области возможных решений, а затем просто проверяется, корректно ли данное предположение генератора. В качестве генератора обычно используется программа для предиката member (программа 3.12), порождающая множество решений. На вопрос member{X,[a,b,c])? будут даны в требуемой последовательности решения Х = а, Х = ЬиХ = с. Таким образом, предикат member можно использовать в программах, реализующих метод «образовать и проверить» для недетерминированного выбора корректного элемента из некоторого списка. Программа 14.1 представляет собой простой пример реализации метода «образовать и проверить» с использованием в качестве генератора предиката member. Эта программа предназначена для идентификации частей речи предложения. Предполагается, что предложение представлено списком слов и существует база данных фактов, задающих части речи для определенных слов. Для задания частей речи используются унарные предикаты, аргументами которых являются слова, например, предикат существительное (man) указывает, что man - существительное. Отношение глагол{Предложение, Слово) истинно, если Слово в предложении Предложение является глаголом. Аналогичный смысл имеют предикаты сущест- вительное/2 и артикль/2. На вопрос eAaeoA([a,manJoves&,womari],Vfl методом «образовать и проверить» будет дан ответ V= loves. Слова предложения генерируются
166 Часть III. Глава 14 глагол (Предложение, Глагол) <- Глагол -глагол в списке слов Предложение. глагол (Предложение, Слово) <- member (Слово, Предложение), глагол (Слово), су ществител ьное (Предложение, Слово) тетЬег(Слово,Предложение),существительное(Слово). артикль (Предложение, Слово) member (Слово, Предложение), артикль (Слово). Словарь существительное (man). существительное (woman), артикль (а). глагол (loves). Программа 14.1. Отыскание частей речи в предложении. с помощью предиката member и затем проверяется, являются ли они глаголами. Другой простой пример-проверка существования в двух списках общего элемента. Рассмотрим предикат intersect(Xs,Ys), который истинен, если списки Xs и Ys имеют общий элемент. Определим его предложением intersect(Xs,Ys) <- member(X,Xs), member(X,Ys). Первая цель member в теле этого предложения генерирует элементы из первого списка, а с помощью второй цели member проверяется, входят ли эти элементы во второй список. Описывая эту программу как недетерминированную, можно говорить, что первая цель делает предположение о том, что X содержится в списке Xs, а вторая цель проверяет, является ли X элементом списка Ys. Отметим, что рассматриваемое предложение в виде Пролог-программы эффективно реализуется двумя вложенными циклами. Внешний цикл обеспечивает перебор элементов первого списка, а во внутреннем цикле проверяется, является ли выбранный из первого списка элемент элементом второго списка. Следовательно, поведение этой недетерминированной логической программы в принятой модели исполнения Пролог-программ напоминает поведение аналогичных программ, разработанных средствами Фортрана, Паскаля или Лиспа. Следующее определение предиката member с использованием предиката append member(X,Xs) <- append(As,[X | Bs],Xs) само по существу является программой, в которой реализуется принцип «образовать и проверить». Однако в этой программе два шага метода сливаются в процессе унификации. С помощью цели append производится расщепление списка и тут же выполняется проверка, является ли X первым элементом второго списка. Остановимся на вопросе оптимизации программ, реализующих принцип «образовать и проверить» посредством внедрения шага проверки в генератор предполагаемых решений. Рассмотрим еще один пример применения обсуждаемого принципа - программу 3.20 для сортировки перестановками. Предложение верхнего уровня программы имеет вид sort(Xs,Ys) <- permutation(Xs,Ys), ordered(Ys). Абстрактно эта программа, действуя недетерминированно, генерирует с помощью цели permutation(Xs,Ys) предположительно корректную перестановку, а с помощью цели ordered проверяет, действительно ли эта перестановка упорядочена должным образом.
Недетерминированное программирование 167 В операционной интерпретации данная программа выглядит следующим образом. Вопрос, включающий отношение sort, сводится к вопросу с отношениями permutation и ordered. В результате возникает цикл, управляемый отказами. Некоторая перестановка элементов списка генерируется посредством отношения permutation и проверяется с помощью отношения ordered. Если перестановка оказывается неупорядоченной, то происходит возврат к цели permutation, которая обеспечивает генерацию новой перестановки, подлежащей проверке на упорядоченность. В конце концов будет сгенерирована упорядоченная перестановка и процесс вычислений прекратится. Сортировка перестановками - крайне неэффективный алгоритм сортировки, имеющий экспоненциальную по размеру сортируемого списка сложность. Более приемлемый алгоритм получается, если внедрить проверку в часть алгоритма, ответственную за генерацию. Тогда генератор permutation выбирает из списка произвольный элемент и рекурсивно выполняет перестановку остальных элементов списка. Предикат ordered проверяет упорядоченность первых двух элементов этой перестановки, затем рекурсивно проверяет остальные элементы списка. Если рассматривать объединение рекурсивных целей permutation и ordered как рекурсивный процесс сортировки, то последний составляет основу алгоритма сортировки вставками (см. программу 3.21). Для сортировки списка сортируется его хвост, а головка списка вставляется на место, сохраняющее порядок расположения элементов. Выбор произвольного элемента должен быть заменен выбором первого элемента. Еще один пример преимущества «сплетения» процессов генерации и проверки дает программа для решения задачи об N ферзях. Требуется разместить N ферзей на квадратной доске размера N х N так, чтобы на каждой горизонтальной, вертикальной или диагональной линии было не больше одной фигуры. В первоначальной формулировке этой задачи шла речь о размещении 8 ферзей на шахматной доске таким образом, чтобы они, согласно правилам игры в шахматы, не угрожали друг другу. Отсюда пошло название задачи о ферзях. Эта задача была хорошо изучена в литературе по занимательной математике. Для N = 2 и N = 3 решения не существует; единственное, без учета симметричного, решение при N = 4 показано на рис. 14.1. Для N = 8 существует 88 (а с учетом симметричных - 92) решений этой задачи. Программа 14.2-упрощенная программа для решения задачи об N ферзях. Отношение queen(N,Qs) истинно, если Qs-решение задачи об N ферзях. Решение представляется некоторой перестановкой списка от 1 до N. Порядковый номер элемента этого списка определяет номер вертикали, а сам элемент - номер горизонтали, на пересечении которых стоит ферзь. Так, решение [2,4,1,3'] задачи о четырех ферзях соответствует решению, изображенному на рис. 14.1. Подобное описание решений и программа их генерации неявно предполагают, что в любом решении задачи о Лг ферзях на каждой горизонтали и на каждой вертикали будет находиться по одному ферзю. Q Q Q Q Рис. 14.1. Решение задачи о четырех ферзях.
168 Часть III. Глава 14 queens(N, Queens) <- Queens-размещение ферзей, которое является решением задачи о N ферзях, представляемое перестановкой списка чисел [7,2 JV]. queens(N,Qs) «- range(l,N,Ns), permutation(Ns,Qs), safe(Qs) safe(Qs) <- Qs- безопасное размещение ферзей. safe([Q | Qs]) <- safe(Qs), not attack (Q,Qs). safe([ ]). attack(X,Xs) <- attack(X, l,Xs). attack (X, N,[Y | Ys]) ч-Х := Y + N;X: = Y-N. attack (X,N,[Y|Ys])«-Nl := N+l; attack(X,Nl,Ys). permutation(Xs,Ys) <- См. программу 3.20. range(l,N,Ns)<- См. программу 8.12. Программа 14.2. Простая программа для решения задачи о N ферзях с использованием метода «образовать и проверить». Программа 14.2 выполняется следующим образом. Сначала с помощью предиката range образуется список Ns, содержащий числа от / до N. Затем начинается выполнение цикла «образовать и проверить». Посредством предиката permutation генерируется перестановка Qs элементов списка Ns, которая затем проверяется предикатом safe(Qs), принимающим значение «истинно», если перестановка Qs является решением задачи. Поскольку на одной и той же вертикали или горизонтали одновременно не могут размещаться два ферзя, этот предикат должен обеспечить лишь проверку того, не угрожают ли друг другу два ферзя по какой-нибудь диагонали. Предикат safe имеет рекурсивное определение. Размещение ферзей безопасно, если ферзи, представляемые хвостом этого списка, не атакуют друг друга, а ферзь, представляемый головой этого списка, не атакует никакого другого ферзя. В определении предиката attack(Q,Qs) использована изящная инкапсуляция взаимосвязи диагоналей. Два ферзя находятся на одной и той же диагонали на расстоянии М вертикалей друг от друга, если номер горизонтали одного ферзя на М больше или на М меньше, чем номер горизонтали другого ферзя. В программе 14.2 это выражается первым предложением attack/3. Смысл предиката attack (Q,Qs) можно выразить словами: «Ферзь Q атакует некоторого ферзя из списка Qs». Диагонали проверяются итерационно до тех пор, пока не будет достигнут конец доски. Программа 14.2 не способна распознавать симметричные решения. Например, на вопрос queens(4,Qs)? она дает два ответа, а именно решения: Qs = [2,4,1,3] и U. 1A.21 Хотя логическая программа 14.2 написана грамотно, она весьма неэффективна. В ней генерируется много перестановок, которые заведомо не могут быть решениями. Как и в случае с программой сортировки перестановками, рассматриваемая программа может быть улучшена внедрением проверки, в данном случае предиката safe, в генератор перестановок. Вместо генерации полной перестановки, т.е. размещения всех ферзей и последующей проверки размещения, можно выполнять проверку корректности размещения каждого ферзя непосредственно после его размещения. Программа 14.3 предназначена для решения задачи об N ферзях при последовательном размещении ферзей. В этой программе сохраняется реализация принципа «образовать и прове-
Недетерминированное программирование 169 queens(N, Queens) <- Queens- размещение ферзей, которое является решением задачи о N ферзях, представляемое перестановкой списка чисел [/,2 JV]. queens(N,Qs)<- range(l,N,Ns), queens(Ns,[ ],Qs). queens( Unplaced Qs, Safe Qs,Qs) <- select (Q, Unplaced Qs, Unplaced Qsl), not attack (Q, Safe Qs), queens(UnplacedQsl,[Q|SafeQs],Qs). queens([ ],Qs,Qs). select(X,Xs,Ys) *- См. программу 3.19. attack(X,Xs) *- См. программу 14.2. Программа 14.3. Программа для решения задачи о N ферзях посредством последовательного размещения ферзей. рить», что отличает ее от программы сортировки вставками, которая после преобразования стала детерминированной. Генератором в рассматриваемой программе является предикат select, проверка реализуется предикатом attack, или, более точно, его отрицанием. Чтобы проверить, в безопасном ли положении находится новый ферзь, необходимо знать позиции ранее размещенных ферзей. Следовательно, искомое решение строится по принципу снизу вверх с применением накопителя. Здесь используется базовый метод, описанный в разд. 7.5. Использование накопителя приводит к размещению ферзей, начиная с правой границы досжи. Программа 14.3 на вопрос queens (4, Qs) дает два решения в порядке, обратном порядку решений по программе 14.2. Следующая задача состоит в раскрашивании плоской карты так, чтобы никакие две смежные области на ней не были окрашены в одинаковый цвет. Эта знаменитая задача, известная уже сотни лет, была решена в 1976 г., когда было доказано, что для раскрашивания любой плоской карты достаточно использовать четыре краски. На рис. 14.2 показана простая карта, для корректного раскрашивания которой требуется четыре цвета. Это можно доказать путем перечисления всех возможных вариантов раскраски. Следовательно, для решения задачи использование четырех красок является необходимым и достаточным. В программе 14.4, предназначенной для решения задачи о раскрашивании карты, также использован принцип «образовать и проверить». Программа реализует следующий недетерминированный алгоритм: для каждой области карты - выбрать цвет, - выбрать из оставшихся цветов (или проверить) цвета соседних областей Для реализации алгоритма необходимо выбрать подходящие структуры данных. Карта представляется списком областей, каждая из которых имеет имя, цвет и а Ъ с е d f Рис. 14.2. Раскрашивание карты четырьмя цветами.
170 Часть III. Глава 14 color_тар (Map, Colors) <- Плоская карта Map раскрашивается красками Colors так, чтобы никакие две соседние области не были окрашены в одинаковый цвет. Карта представляется списком смежных областей region (Name,Color,Neighbors), где Name -имя области, Color-ее цвет, Neighbors -цвета раскраски соседних областей. Программа может быть использована без начальной конкретизации всех цветов. color_map([Region | Regions],Colors) <- color_region (Region, Colors), color_map(Regions,Colors). color_map([ ], Colors). color_region(Region, Colors) <- Область Region и смежные с ней области окрашиваются в цвета Colors так, чтобы цвет этой области отличался от цвета, в который окрашены соседние области. color_region (region (Name, Color, Neighbors), Colors) <- select (Color, Colors, Colors 1), members(Neighbors,Colorsl). select(X,Xs,Ys)<- См. программу 3.19. members(Xs,Ys)<- См. программу 7.6. Программа 14.4. Раскрашивание карты. список цветов, в которые окрашены смежные области. Например, карта, изображенная на рис. 14.2, представляется списком region (а, А, [В, С, D]), region (b, В, [А, С, Е]), region(c,С,[А,В,D,E, F]), region(d,D,[А,С, F]), region(e,E,[B,C,F]), region(f,F[C,D,E])]. Для того чтобы избежать раскраски одной и той же области в разные цвета на разных итерациях алгоритма, используются общие переменные. Отношением верхнего уровня рассматриваемой программы является color_ тар (Map,Colors), где Map -карта, представляемая описанным выше способом, Colors - список цветов, используемых для раскрашивания карты. Выберем цвета: красный, желтый, голубой и белый. Ядро алгоритма - определение отношения color_ region (Region, Colors): colorjregion (region (Name, Color, Neighbors), Colors) <- select (Color,Colors, Colors!), member (Neighbors, Colors!). Цели select и members в зависимости от того, конкретизированы или нет их аргументы, могут либо производить генерацию вариантов, либо выполнять проверку. Итогом выполнения программы о раскрашивании карты является конкретизация структуры данных-карты. Вызовы предикатов select и members могут рассматриваться как описания локальных ограничений. Предикаты либо генерируют предполагаемое решение посредством конкретизации элементов структуры, либо проверяют, удовлетворяют ли конкретизированные значения локальным ограничениям. В качестве последнего примера рассмотрим решение логической головоломки. Поведение этой программы будет подобно поведению программы для решения задачи о раскрашивании карты. Логическая головоломка состоит из нескольких фактов относительно небольшого числа объектов, которые имеют различные атрибуты. Минимальное число фактов относительно объектов и атрибутов связано с желанием выдать единственный вариант назначения атрибутов объектам.
Недетерминированное программирование 171 Тестовые данные test_color(Name,Map) *- map(Name,Map), colors (Name, Colors), color_map(Map,Colors). map (test, [region (а, А, [В, С, D]), region(b,B,[A,C,E]),region(c,C,[A,B,D,E,F]), region (d, D, [A,C, F]), region(e, E, [B,C, F]), region(f,F,[C,D,E])]). map(west_europe, [region (portugal, P, [E]), region(spain, E, [F, P]), region(france,F, [E,I,S,B,WG,L]), region(belgium,B,[F,H,L,WG]), region(holland, H, [B, WG]), region(west_germany, WG, [F, A, S, H, B, L]), region(luxembourg,L,[F, B, WG]), region(italy, I,[F, A,S]), region(switzerland,S, [F,I, A, WG]), region(austria, A,[I,S, WG])]). colors(X,[red,yellow, blue, white]). Программа 14.5 Тестовые данные для задачи о раскрашивании карты. Метод решения логических головоломок опишем на следующем примере. Три друга заняли первое, второе и третье места в соревнованиях универсиады. Друзья - разной национальности, зовут их по-разному, и любят они разные виды спорта. Майкл предпочитает баскетбол и играет лучше чем американец. Израильтянин Саймон играет лучше теннисиста. Игрок в крикет занял первое место. Кто является австралийцем? Каким спортом занимается Ричард? Подобные логические головоломки изящно решаются посредством конкретизации значений подходящей структуры данных и выделения значения, приводящего к решению. Каждый ключ к разгадке преобразуется в факт относительно структуры данных. Это может быть сделано с использованием абстракции данных до определения точной формы структуры данных. Проанализируем первый ключ к разгадке: «Майкл предпочитает баскетбол и играет лучше чем американец». Очевидно, речь идет о двух разных людях. Одного зовут Майклом и он занимается баскетболом, в то время как второй-американец. Кроме того, Майкл лучше играет в баскетбол, чем американец. Предположим, что Друзья-структура данных, подлежащая конкретизации, тогда наш ключ может быть выражен следующей конъюнкцией целей: играет.. лучше(мужчина 1 ,Мужчина2,Друзья), имя(Мужчина!,Майкл),спорт(Мужчина1,баскетбол), национальность(Мужчина2,американец). Аналогично второй ключ можно представить конъюнкцией целей: играет_лучше(мужчина 1 ,Мужчина2,Друзья), имя(Мужчина 1 ,Саймон),национальность(Мужчина 1 израильтянин), спорт(Мужчина2,теннис). Наконец, третий ключ к разгадке выразится следующим образом: первый(Друзья,Мужчина),спорт(Мужчина,крикет). Базовая программа для решения головоломок представлена программой 14.6. Вычислению подлежит отношение решить _голово ломку (Голово ломка,Решение), где Решение является решением головоломки Головоломка. Головоломка представля-
172 Часть III. Глава 14 решить, головоломку (Головоломка, Решение) *- Решение -решение головоломки Головоломка, которая представляется структурой головоломка (Ключи, Вопросы, Решения). решить_ головоломку (головоломка (Ключи, Вопросы, Решение), Решение) ♦- решить(Ключи), решить (Вопросы). решить ([Ключ | Ключи]) <- Ключ, решить (Ключи), решить([ ]). Программа 14.6. Решатель головоломок. ется структурой головоломка(Ключи,Вопросы,Решение), где структура данных, подлежащая конкретизации, представляется ключами и вопросами, а получаемые значения определяются аргументом Решение. Программа решить _г олово ломку тривиальна. Все, что она делает, состоит в последовательном решении каждого ключа и вопроса, которые представляются как цели Пролога и выполняются с использованием метапеременных. Ключи и вопросы для нашего примера даны в программе 14.7. Рассмотрим структуру, представляемую ключами, для решения этой головоломки. Каждый человек имеет три атрибута и может быть представлен структурой друг(Имя,Страна,Спорт). Есть три друга, распределение мест которых в итоге соревнований имеет существенное значение. Это наводит на мысль выбрать в качестве структуры данных для решения задачи упорядоченную последовательность из трех элементов, т.е. список: Opyr(Nl,Cl,Sl), Apyr(N2,C2,S2), друг (N3,C3,S3)]. Тестовые данные тест_^ля .головоломки (Имя, головоломка (Ключи, Вопросы, Решение)) *- структура (Имя, Структура), ключи(Имя,Структура, Ключи), вопросы (Имя, Структура, Вопросы, Решение). cTpyKTypa(TecT,[Apyr(Nl,Cl,Sl),apyr(N2,C2,S2),;ipyr(N3,C3,S3)]). ключи (тест, Друзья, [(играет_лучше (Мужчина 1 Ключ 1,Мужчина2Ключ1, Друзья), % Ключ 1 имя (Мужчина 1 Ключ 1, майкл), спорт (Мужчина 1 Ключ 1, баскетбол), национальность (Мужчина2Ключ1, американец)), (играет. лучше(Мужчина!Ключ2,Мужчина2Ключ2, Друзья), % Ключ 2 имя(Мужчина1 Ключ2,саймон), национальность(Мужчина 1 Ключ2, израильтянин), спорт (Мужчина2Ключ2, теннис)), первый(Друзья, МужчинаКлючЗ), спорт(МужчинаКлючЗ, крикет)) % Ключ 3 ])• вопросы (тест, Друзья, [принадлежит (Q1, Друзья), имя(01,Имя), национальность (01, австралиец), % Вопрос 1 принадлежит (Q2, Друзья), имя(С?2,ричард), cnopT(Q2,Спорт) % Вопрос2 ], [['Имя австралийца-',Имя], ['Ричард играет в ' ,Спорт]]
Недетерминированное программирование 173 ). играет_лучше(А, В, [А, В, С]), играет, лучше (А, С, [А, В, С]), играет лучше(В, С, [А, В, С]). имя(друг(А,В,С),А). национальность (друг (А. В, С), В). спорт(друг(А,В,С),С). первый ([X|Xs],X). Программа 14.7. Описание головоломки. В программе 14.7 даны определения условий играет_лучше, имя, национальность, спорт и первый, которые, очевидно, легко программируются. Объединение программ 14.6 и 14.7 дает нечто гигантское на тему «образовать и проверить». Каждая из целей играет_лучше и принадлежит (member) имеет дело с людьми, а остальные цели обращаются к атрибутам людей. Какие функции они выполняют-генерацию или проверку, зависит от того, конкретизированы их аргументы или нет. Для любопытных сообщаем ответ нашей головоломки: Майкл- австралиец, а Ричард играет в теннис. Упражнения к разд. 14.1 1. Напишите программу вычисления целочисленного квадратного корня из натурального числа N, определяемого как число /, такое, что I2 ^ JV, но (/ + /)2 > N. Используйте определение предиката between/З (см. программу 8.5) для генерирования последовательности натуральных чисел с помощью механизма возвратов. 2. Напишите программу для решения задачи об устойчивых браках (Sedgewick, 1983), формулируемую следующим образом. Предположим, что N мужчин и N женщин желают вступить в брак. Каждый мужчина имеет список всех женщин, упорядоченных по его вкусу. Подобный список, но, конечно, мужчин, есть и у каждой женщины. Задача состоит в нахождении устойчивого множества браков. Множество браков неустойчиво, если два человека, не состоящие друг с другом в браке, хотят образовать такой союз. Предположим, например, что существуют двое мужчин (А и В) и две женщины (X и У), такие, что А предпочитает X, В предпочитает У, X предпочитает А и У предпочитает В. Пара браков А- Ун В-Х неустойчива, так как А предпочитает X, а не У, в то время как X отдает предпочтение А, а не В. Ваша программа должна в качестве входных данных иметь списки предпочтений, а результатом ее выполнения должно быть устойчивое множество браков, т.е. множество браков, которое не является неустойчивым. В теории графов есть теорема, в которой утверждается, что это всегда возможно. Проверьте вашу программу на группе из пяти мужчин и пяти женщин, которым соответствуют следующие списки предпочтений: авраам: беньямин: хайм: давид: елеазар: звия: хана: руфь: сара: тамар: хана тамар звия руфь сара звия хана руфь сара тамар хана руфь тамар сара звия звия руфь хана сара тамар тамар руфь хана звия сара елеазар авраам давид беньямин хайм давид елеазар беньямин авраам хайм авраам давид беньямин хайм елеазар хайм беньямин давид авраам елеазар давид беньямин хайм елеазар авраам
174 Часть III. Глава 14 3. Используйте программу 14.4 для раскраски карты Западной Европы. Названия государств даны в программе 14.5. 4. Напишите программу для решения следующей логической головоломки. В пяти домах, окрашенных в разные цвета, обитают мужчины разных национальностей. Они держат разных животных, предпочитают разные напитки и курят сигареты разных марок. Известно, что: 1. Англичанин живет в красном доме. 2. У испанца есть собака. 3. Кофе пьют в зеленом доме. 4. Украинец пьет чай. 5. Зеленый дом-первый по правую руку от дома цвета слоновой кости. 6. Курильщик «Уинстона» держит улиток. 7. Сигареты «Кул» курят в желтом доме. 8. Молоко пьют в среднем доме. 9. Норвежец живет в крайнем слева доме. 10. Мужчина, курящий «Честерфилд», живет в доме, соседнем с домом мужчины, у которого есть лиса. 11. Сигареты «Кул» курят в доме, соседнем с домом, где имеется лошадь. 12. Мужчина, предпочитающий «Лаки страйк», пьет апельсиновый сок. 13. Японец курит сигареты «Парламент». 14. Норвежец живет в доме рядом с голубым домом. Вопросы: «У кого есть зебра?», «Кто пьет воду?». 5. Используя алгоритм Хопкрофта и Тарьяна (Deo, 1974; Even, 1979), напишите программу проверки планарности графа. 14.2. Недетерминизм с произвольным выбором альтернативы и недетерминизм с неизвестным выбором альтернативы В литературе по логическому программированию различают два вида недетерминизма. Отличаются они принципом выбора следующей альтернативы, которая должна быть выполнена. Недетерминизм с произвольным выбором альтернативы в рамках модели вычислений логического программирования означает, что к решению ведет редукция любой цели, и не имеет значения, какое частное решение найдено. В случае недетерминизма с неизвестным выбором альтернативы выбор имеет значение, но на момент выбора неизвестно, какой выбор корректен. Большинство примеров появления недетерминизма с произвольным выбором альтернативы не представляет интереса для программирующих на Прологе. Иллюстративный пример недетерминизма с произвольным выбором альтернативы дает программа minimum (см. программу 3.7). В случае когда X и Y совпадают, программа minimum(X,Y,X)<-X ^ Y. minimum (X,Y,Y)<- Y ^ X. ведет себя как недетерминированная, с произвольным выбором альтернативы. В разд. 7.4 мы назвали это избыточностью и возражали против ее использования. С другой стороны, программы, демонстрирующие недетерминизм с неизвестным выбором альтернативы, являются вполне обычными. Рассмотрим программу для проверки изоморфности двух бинарных деревьев. Такая проверка обеспечивается программой 3.25, которая выглядит следующим образом: isotree (void, void). isotree (tree (X, L1, R1), tree (X, L2, R2)) <- isotree (L1, R1), isotree (L2, R2). isotree (tree (X, L1, R1), tree (X, L2, R2)) <- isotree (L1, R2), isotree (L2, R1).
Недетерминированное программирование 175 Каждое отдельно взятое предложение рассматриваемой программы корректно. Однако, представляя программе два изоморфных бинарных дерева, мы не знаем, какое из двух рекурсивных предложений будет использовано для доказательтва их изоморфности. В операционном смысле корректный выбор становится известным только тогда, когда вычисление успешно завершается. Процесс разработки Пролог-программ с любой формой недетерминизма может и не отличаться от процесса разработки детерминированных программ. Каждое предложение пишется независимо от другого. Для программиста безразлично, одному или нескольким предложениям сопоставляются входные данные. Действительно, это проявляется во множественных применениях Пролог-программ. Программа с аргументами одного вида может быть детерминированной, с аргументами другого вида - недетерминированной, как, например, append. Поведение Пролог-программ, внешне имеющих недетерминизм с неизвестным выбором альтернативы, таких, как программа isotree, понятно. Некоторая данная логическая программа и связанный с ней вопрос определяют, как обсуждалось в гл. 5, дерево поиска, на котором Пролог-система осуществляет поиск в глубину. Написание программы, обладающей недетерминизмом с неизвестным выбором альтернативы, в действительности представляет собой спецификацию алгоритма поиска в глубину для решения определенной задачи. Рассмотрим эту точку зрения более подробно на примере задачи определения связности двух вершин в графе. На рис. 14.3 изображены два графа, которые будут использованы при обсуждении предлагаемых идей. Слева изображено дерево, а справа-граф с циклом. Деревья, или ориентированные ациклические графы, как будет видно из примеров наших программ, позволяют получать более простые решения, чем графы с циклами. Наша первая программа представляет собой незначительную модификацию программы, представленной в разд. 2.3. Программа 14.8 определяет отношение connected (X,Y), которое истинно, если вершины X и У графа связны. Ребра графа ориентированы, факт edge (X, Y) устанавливает существование ориентированного ребра из X и У Декларативно эта программа представляет собой сжатое рекурсивное описание связности вершин графа. Операционная интерпретация в виде Пролог-программы состоит в том, что это - реализация алгоритма поиска в глубину для определения наличия связи двух вершин графа. Вопрос connected (а, X) для графа, изображенного слева на рис. 14.3, приводит к следующим решениям: b,c,f,h,i,g,d,j,e и к. Их порядок определяется алгоритмом обхода дерева в глубину. Программа 14.9 для поиска пути между двумя вершинами является расширением этой простой программы. Предикат path (X,Y, Path) истинен, если Path -путь в h* м Рис. 14.3. Ориентированные графы.
176 Часть III. Глава 14 connected(X,Y) <- вершина X связана с вершиной У, определено некоторое отношение edge/2, описывающее ориентированный ациклический граф. connected(A,A). connected (А, В) «- Данные cdge(a,b). edge(dj). edge(e,k). edge(z,x). edge(A,N), connected(N, B). cdge(a,c). edge(a,d). edgc(c,f). edge(c,g). edge(f,i). edge(x,y). cdge(y,u). cdge(z,v). edge (a, c). edge(f,h). edge(y,z). Программа 14.8. Определение связности конечного ориентированного ациклического графа. path (X,Y, Path )^ Path-путь между двумя вершинами X и У в ориентированном ациклическом графе, определенном отношением edge/2. path(X,X,[X]). path(X,Y,[X | P]) <- edge(X,N), path(N,Y,P). Программа 14.9. Определение пути в графе методом поиска в глубину. графе из вершины X в вершину Y. Обе концевые точки включаются в путь. Путь строится по нисходящей методологии, которая удачно сочетается с рекурсивным определением отношения connected. Легкость вычисления пути является прямым следствием обхода дерева в глубину. Эквивалентное расширение программы для реализации обхода в ширину значительно сложнее (соответствующие модификации будут обсуждаться в разд. 17.2 и 18.1). С помощью поиска в глубину осуществляется корректный обход любого конечного дерева или ориентированного ациклического графа. Однако встречаются задачи, в которых требуется производить обход графа с циклами. В процессе вычислений может образоваться бесконечный (буквально!) программный цикл по одному из циклов графа. Например, на вопрос connected (x, Node)! для графа, изображенного справа на рис. 14.3, будет дано решение \\z и х, которое бесконечно повторяется, и никогда не будут достигнуты вершины и и v. Неприятностей с зацикливанием можно избежать подходящей модификацией отношения connected. К аргументам отношения добавляется дополнительный аргумент, используемый для накопления уже пройденных при обходе вершин. Для исключения повторного просмотра одного и того же состояния применяется проверка. Указанные изменения введены в программе 14.10. connected (X,Y) «- Вершина X связана с вершиной У в графе, определенном отношением edge/2. connected(X, Y) <- connected(X,Y,[X]). connected(A, A, Visited), connected (A, B, Visited) <- edge(A,N), not member(N,Visited), connected(N,B,[N | Visited]). Программа 14.10. Определение связности графа.
Недетерминированное программирование 177 Программа 14.10 успешно реализует обход конечного ориентированного графа в глубину. Программа на чистом Прологе для корректного обхода ориентированных ациклических графов должна быть расширена отрицанием. Добавление накопителя просмотренных путей для борьбы с зацикливанием приводит к разбиению циклов в графе, что препятствует просмотру ребра, замыкающего цикл. Программа не гарантирует достижения каждой вершины в бесконечном графе. Для обеспечения такой возможности необходимо использовать при обходе поиск в ширину. Этот вопрос будет обсуждаться в разд. 17.2. Последняя программа этого раздела связана с разработкой простых проектов в мире кубиков. Это недетерминированная программа, существенно использующая поиск в глубину. В ней объединены две ранее описанные функции: использование накопителя для учета просмотренных вершин и отыскание пути в графе. Задача состоит в формировании плана сооружения из кубиков, т.е. описании последовательности действий по перестановке кубиков, приводящей к определенной конфигурации. На рис. 14.4 представлены исходное состояние и искомое конечное состояние в данной задаче. Имеются три кубика a, b и с и три места p,q и г. Разрешенными являются два действия: перемещение верхнего в конструкции кубика на свободное место и перемещение верхнего в конструкции кубика на другой кубик. Для корректных действий необходимо, чтобы была свободна верхняя грань перемещаемого кубика и свободны были место или кубик, на которые устанавливается перемещаемый кубик. В программе 14.11, предназначенной для решения рассматриваемой задачи, на верхнем уровне определена процедура transform (Statel,State2, Plan). Аргумент Plan определяет план действий, преобразующих состояние Statel в состояние State2. Состояния представляются списком отношений вида оп(Х, Y), где Х-кубик, а Y- кубик или место, на котором стоит кубик. Они представляют факты, истинные в этом состоянии. Например, исходное и конечное состояния, показанные на рис. 14.4, представляются списками Ion (a, b),on (b,p),on (с, г) ] и Ion (a, b),on (b, с), on (с, г) ] соответственно. Отношение on для а предшествует отношению on для Ь, которое в свою очередь предшествует отношению on для с. Такие описания состояний позволяют легко проверить, является ли в данный момент свободным кубик или место X. Для этого достоточно убедиться, что отношение оп(Л,Х) ложно. В предикатах clear/2 и оп/3 программы 14.11 использованы преимущества такого представления. Использованное в программе планирования действий рекурсивное предложение transform/4 определяет недетерминированный алгоритм: пока желаемое состояние не достигнуто, найти допустимое действие, изменить текущее состояние, проверить, что это состояние ранее не встречалось. Имеются два возможных действия: перемещение на кубик и перемещение на место. Для каждого из них должны быть специфированы условия допустимости действия и правила изменения состояния. а ГаП ПГ I Ь I с I с Рис. 14.4. Начальное и конечное состояния в задаче сооружения из кубиков.
178 Часть III. Глава 14 Программа 14.11 успешно решает простую задачу, представленную программой 14.12. Первый выбранный ею план просто нелеп, однако это все-таки план: to_place (a, b, q), to_block (a, q, с), to_place (b, p, q), to_place (а, с, р), to_block (a, p, b), to_place (c, r, p), to_place (a, b, r), to_block (a, r, c), to_place (b, q, r), to_place (a, c, q), to_block (a, q, b), to_place (c, p, q), to_place (a, b, p), to_block (a, p, c), to_place (b, r, p), to_place (a, c, r), to_block (b, p, a), to_place (c, q, p), to_block (b, a, c), to_place (a, r, q), to_block (b, c, a). to_place (c, p, r), to_block (b, a, c), to_place (a, q, p), to_block (a, p, b). transform (Statel,State2,Plan) <- План Plan действий для преобразования состояния Statel в состояние State2. transform(Statel,State2,Plan) <- transform(State 1, State2, [State 1 ], Plan). transform(State, State,Visited, [ ]). transform(State 1, State2, Visited, [Action | Actions]) <- legaLaction (Action, State 1), updete(Action, State 1, State), not member (State, Visited), transform(State, State 2, [State | Visited], Actions). legal_action(to_place(Block,Y,Place),State)<- on(Block,Y,State),clear(Block,State), place(Place),clear(Place, State). legaLaction (to_block (Block 1, Y,Block2), State) «- on(Blockl,Y,State), clear(Block 1,State), block(Block2), Block 1 Ф Block2,clear(Block2, State). clear(X, State) <- not member(on(A,X),State). on(X,Y,State) <- member(on(X,Y),State). update (to_block (X, Y, Z), State, State 1) <- substitute(on (X, Y), on (X, Z), State, State 1). update (to_place (X, Y, Z), State, State 1) <- substi tute(on (X, Y), on (X, Z), State, State 1). substitute(X,Y,Xs,Ys)<- См. упражнение 3.3(1). Программа 14.11. Программа планирования с использованием поиска в глубину. Предложения и данные для тестирования test_plan (Name, Plan) <- initial_state(Name, I), final_state(Name, F), transform(I, F, Plan). initiaLstate (test, [on (a, b), on (b, p), on (c, r)]). final_state(test,[on(a,b),on(b,c), on(c,r)]). block (a). block (b). block (c). place (p). place (q). place (r). Программа 14.12. Программа и данные для тестирования программы 14.11. Сначала кубик а перемещается на место q, затем ставится на кубик с. После того как кубик b перемещается на место q, кубик а перемещается на место р и затем ставится на кубик b и после двадцати еще более хаотичных перемещений достигается окончательная конфигурация.
Недетерминированное программирование 179 Легко привнести в алгоритм чуточку интеллекта, пытаясь сразу достичь одного аз целевых состояний. Предикат legaLaction можно заменить предикатом choo- se_action (Action, Statel, State2), с помощью которого производится выбор действия. Следующего простого определения достаточно для более разумного поведения программы при решении нашей задачи: choose_action (Action, State 1, State2) «- suggest (Action, State2), legaLaction (Action, Statel). choose_action (Action, State 1, State2) <- legaLaction (Action, State 1). suggest (to_place(X,Y,Z), State) <- member (on (X, Z), State), place (Z) suggest (to_block (X, Y, Z), State) <- member (on (X, Z), State), block (Z). Теперь будет получен первый план следующего вида: \to_place (a,b,q),to_block (b,p, с), tojblock (a, q, b) ]. 14.3. Моделирование недетерминированных вычислений В этом разделе представлены простые программы, имитирующие некоторые фундаментальные модели вычислений. На Прологе очень легко писать интерпретаторы для автоматов различных классов. Имитационные программы являются естественной областью применения недетерминированного программирования. Действие недетерминированного автомата хорошо иллюстрирует недетерминизм с неизвестным выбором альтернативы. Интересно, что вследствие присущего Прологу недетерминизма недетерминированный автомат имитируется так же легко, как и детерминированный. Рассмотрим недетерминированный конечный автомат (НКА), определяемый пятеркой (Q,S,D,I,F), где б-множество состояний, S-множество символов, D - отображение из Q x S в Q, /-начальное состояние и F- множество конечных состояний автомата. Если отображение является функцией, то рассматриваемый автомат оказывается детерминированным. В Прологе конечный автомат может быть описан тремя наборами фактов: initial (Q), истинным, если Q- начальное состояние автомата; final (Q), истинным, если Q-конечное состояние автомата; delta(Q,A,Ql), истинным, если НКА переходит из состояния Q в состояние Ql при получении символа А. Множество состояний и множество входных символов определяются неявно как константы, входящие в предикаты. Программа 14.13 является абстрактным интерпретатором НКА. Основной предикат программы - accept (S) истинен, если строка символов S, представляемая списком символов, допускается НКА. Чтобы воспользоваться интерпретатором для имитации поведения определенного конечного автомата, этот автомат следует специфицировать. Это влечет за собой определение его начального состояния, конечного состояния и отношения переходов delta. Программа 14.14 определяет НКА, распознающий язык (ab)*. Этот автомат изображен на рис. 14.5. Он имеет два состояния: qO и ql. Если автомат находится в состоянии qO и считывается символ а, то автомат переходит в состояние ql\ переход из состояния ql в состояние qO происходит при считывании символа Ь. Другая базовая модель вычислений представляется магазинным автоматом, который распознает языки из класса контекстно-свободных языков. Недетерминированный магазинный автомат (НМА) получается посредством расширения модели НКА добавлением магазинной памяти для внутреннего состояния автомата. Фор-
180 Часть III. Глава 14 b Рис. 14.5. Простой автомат. accept (S) <- Строка символов, представленная списком S, принимается НКА, определенным предикатами initial/1, delta/3 и final/1. accept(S) <- initial(Q), accept(Q,S) accept(Q,[X|Xs]) <- delta(Q,X,Ql), accept(Ql,Xs). accept(Q,[ ])^final(Q). Программа 14.13. Интерпретатор для недетерминированного конечного автомата. initial(qO). final (q0). delta(qO,a,ql). delta(ql,b,qO). Программа 14.14. НКА, распознающий язык (ab)*. мально НМА представляется семеркой (Q,S,G,D,I,Z,F), где Q,S,I и F определяются так же, как и для НКА, G- список символов, которые можно поместить в стек, Z-начальный символ, содержащийся в стеке, D (дельта-функция) - функция переходов, учитывающая состояние стека, текущий символ и внутреннее состояние автомата. Функционирование НМА с заданной функцией D определяется его состоянием, первым символом во входной строке и элементом в вершине стека. За одну операцию НМА может поместить в стек или вытолкнуть из стека один символ. Абстрактный интерпретатор НМА (программа 14.15) сходен с интерпретатором НКА, представленным программой 14.13. Как и в случае НКА, для имитации действия НМА необходимы предикаты, определяющие начальное и конечное состояния автомата. Множества символов определяются неявно. В программе 14.15 предполагается, что первоначально стек пуст. Отношение delta(Q,A,S,Ql,Sl) имеет незначительные отличия от аналогичного отношения для НКА. Оно истинно, если, находясь в состоянии Q при входном символе А и состоянии стека 5, НМА переходит в состояние Q1 и переводит стек в состояние S1. Частный случай НМА представлен программой. Этот автомат распознает палиндромы над конечным алфавитом. Палиндромом называется непустая строка, которая слева направо и справа налево читается одинаково. Например, строки abba и abaabaaba- палиндромы. Рассматриваемый автомат имеет два состояния: состояние q0, в котором символы помещаются в стек, и состояние ql, в котором символы выталкиваются из стека и сравниваются с символами входного потока. Решения о завершении записи в стек и о начале считывания из стека принимаются недетерминированно. В программе определены два факта delta, которые соответствуют переходам из состояния q0 в состояние ql и учитывают, что палиндромы могут иметь нечетную или четную длину.
Недетерминированное программирование 181 accept (S) «- Строка, представленная списком S, принимается НМА, определенным предикатами initial/7, delta/5 и final/1. accept(Xs) <- initial(Q), accept(Q,Xs,[ ]). accept (Q, [X | Xs], S) <- delta(Q,X, S, Q1, S1), accept(Q1, Xs, S1). accept(Q,[ ],[ ])<-final(Q). Программа 14.15. Интерпретатор недетерминированного магазинного автомата. Интерпретатор и программу, определяющую автомат, можно представить в виде одной программы. В частности, программа 14.17 получена в результате объединения программ 14.15 и 14.16. В ней определено отношение palindrome (Xs), используемое для проверки того, является ли строка Xs палиндромом. initial(qO). final(ql). delta(qO,X,S,qO,[X|S]). delta(qO,X,S,ql,[X|S]). delta(qO,X,S,ql,S). delta(ql,X,[X|S],ql,S). Программа 14.16. Определение НМА для палиндромов над конечным алфавитом. palindrome (Xs) <- Строка, представленная списком A\s,-палиндром. palindrome(Xs)«- palindrome(qO,Xs,[ ]). palindrome(qO,[X | Xs],S) <- palindrome(qO,Xs,[X | S]). palindrome(qO,[X j Xs],S) <- palindrome(ql,[X [Xs],S). palindrome(qO,[X | Xs],S) <- palindrome(ql,Xs,S). palindrome(ql,[X j Xs],[X | S] <- palindrome(ql,Xs,S). palindrome(ql,[ ],[ ]). Программа 14.17. Программа, распознающая палиндромы. В процессе преобразования программ 14.15 и 14.16 в программу 14.17 использован метод раскрытия целей. Раскрытие - полезный прием преобразования программ, используемый и в других местах книги. Сделаем небольшое отступление, чтобы дать определение этому методу. Рассмотрим некоторую цель Ах в предложении Н*-Аг, ..., Ап и предложение С = (А <- В1г...,Вт), такие, что Ах и А унифицируемы с наиболее общим унификатором 0. Раскрытие цели А{ относительно ее определения С дает предложение (Я4-А1,...^1._1./?1,...,2?|ИМ| + 4....^я)в. Это определение аналогично определению раскрытия в языках функционального программирования. Например, раскрытие цели initial (Q) в предложении accept(X)<- initial(Q),accept(Q,X,[ ]). с использованием ее определения initial (qO) дает предложение accept (X) <- accept (qO, X, [ ]). Если цель определяется несколькими предложениями, то раскрытие даст столько предожений, сколько их было в определении цели. Например, раскрытие цели delta в предложении accept(Q,[X | Xs],S) <- delta(Q,X,S,Ql,Sl),accept(Ql,Xs,Sl).
182 Часть III. Глава 14 с использованием определения цели delta в программе 14.16 приведет к четырем предложениям. Теперь должно быть понятно, каким образом была получена программа 14.17: раскрытием целей initial и delta в первых двух предложениях программы 14.15 и раскрытием цели final в оставшемся предложении. Кроме того, предикат accept был заменен в новой программе предикатом palindrome. Подобным образом можно построить интерпретатор для машины Тьюринга. Этот факт, между прочим, говорит о том, что Пролог имеет мощность машины Тьюринга, а следовательно, и всех других известных моделей вычислений. Упражнение к разд. 14.3 1. Определите НМА, распознающий язык, в котором за цепочкой из п символов а следует цепочка из п символов Ъ. 14.4. Классические интеллектуальные программы: ANALOGY, ELIZA и McSAM Современные учителя часто прибегают к штампу: «Лучший способ изучить предмет - учить его». По аналогии современные программисты могут сказать, что лучший способ понять программу-написать ее заново. Признавая этот тезис, представим логические реконструкции трех программ, созданных в процессе исследований по искусственному интеллекту. В этих программах легко разобраться и произвести в них желаемые изменения. В упражнениях к разделу читателю предлагается добавить к этим программам новые факты и правила. Итак, рассмотрим следующие три программы: программу ANALOGY, разработанную Эвансом для решения задач на геометрические аналогии из тестов для оценки умственных способностей; программу ELIZA, созданную Вейценбаумом, которая имитирует или, скорее, пародирует беседу; программу McSAM (версия программы SAM; Mc-сокращение от micro), разработанную группой исследования систем обработки концептуальной информации в Йельском университете и предназначенную для «понимания» сюжетов. Каждая из этих логических реконструкций записывается на Прологе весьма просто. Благодаря недетерминизму программ Пролога программисту не приходится заниматься организацией поиска решений. Рассмотрим, в чем заключаются задачи на геометрические аналогии, которые обычно используют для испытания умственных способностей. В каждой задаче такого рода предлагается несколько фигур. На рис. 14.6 представлен вариант простой задачи на отыскание геометрических аналогий. Часть фигур (на рисунке-нижний ряд) - возможные ответы. Используя фигуры А, В и С и фигуры, представляющие собой варианты решений, следует дать ответ на вопрос: «Фигура А относится к фигуре В, как фигура С относится к ... «К какой фигуре? Ответ надо выбрать из трех вариантов решений в нижнем ряду». Интуитивные представления позволяют записать следующий алгоритм для решения этой задачи (такие понятия, как «найти», «применить» и «правило», здесь не описаны): найти правило, определяющее связь фигур А и В, применить это правило к фигуре С, чтобы перейти к фигуре X, найти среди ответов фигуру X или ее ближайший эквивалент. В рассматриваемой задаче (см. рис. 14.6) связь фигуры А с фигурой В определяется обменом местами (с соответствующим изменением масштабов) изображений квадрата и треугольника. «Очевидный» ответ для фигуры С-обмен местами изображе-
Недетерминированное программирование 183 как и I 2 Рис. 14.6. Задача на геометрические аналогии. ний квадрата и круга. Соответствующая фигура имеет в нижнем ряду номер 2. Программа 14.18 предназначена для решения простых задач на геометрические аналогии. Ее основным отношением является отношение analogy (Pair 1, Pair2, Answers) , в котором каждая пара Pair представляется формой X is_to Y. В программе, конечно, должно быть дано определение инфиксному оператору is_to. Элементы пары Pairl находятся й таком же отношении, как и элементы пары Pair2, а второй элемент пары Pair2 принадлежит множеству ответов Answers. Следующее определение отношения analogy соответствует приведенному выше интуитивному алгоритму: analogy(A is_to В, С is_to X, Answers) match (А, В, Operation), match (С, X, Operation), member (X, Answers). Большое значение имеет выбор способа представления фигур в рассматриваемой задаче. Этот выбор оказывает существенное влияние на «интеллектуальность» программы. В программе 14.18 фигуры представлены термами Пролога. Например, фигура А на рис. 14.16-квадрат в треугольнике - представляется термом inside (square, triangle). Связь между фигурами отыскивается с помощью предиката match (А,В,Operation). Это отношение истинно, если операция Operation сопоставляет А и В. Для решения рассматриваемой задачи применяется операция invert, которая обеспечивает обмен местами ее аргументов. analogy (Pairl, Pairl, Answers) <- Существует аналогия между двумя парами геометрических фигур Pairl и Pair2. Второй элемент в паре Pair2 - один из элементов списка возможных ответов Answers. analogy (A is_.to В, С is_to X, Answers) <- match(A,B,Rule), match(C,X,Rule), member(X, Answers). match (inside(Figure 1, Figure 2), inside(Figure2, Figure l),invert). match (above(Figure 1, Figure2), above(Figure2, Figure 1), invert). HpoinaMMu 14.18. Программа для нахождения геометрических аналогий.
184 Часть III. Глава 14 Предложения и данные для тестирования test_analogy(Name,X) <- figures(Name,A,B,C), answers(Name, Answers), analogy (A is_to B,C is_to X, Answers), figures (test 1 ,inside(sq uare,triangle), inside(triangle, square), inside(circle, square)). answers(test 1, [inside(circle, triangle), inside (sq uare, circle), inside(triangle, square)]). Программа 14.19. Тестовые данные для программы решения задач на геометрические аналогии. Предикат match в программе 14.18 используется двояко. В первом случае с его помощью производится сопоставление двух данных фигур. Во втором случае для заданных операции и фигуры подбирается вторая фигура. Однако эти детали несущественны для недетерминированного поведения программы. Наконец, с помощью предиката member проверяется, принадлежит ли данная фигура множеству ответов. Теперь рассмотрим следующий классический пример программы из области искусственного интеллекта - упрощенную версию программы ELIZA. Эта программа предназначена для имитации беседы. Пользователь печатает предложение, а программа ELIZA отвечает либо соответствующим вопросом, либо комментарием. На самом деле ELIZA не понимает смысла вводимых предложений, а лишь распознает словесные шаблоны и реагирует на них подходящими шаблонными ответами. Чтобы сделать шаблоны ответов более достоверными, при их формировании учитываются психологические аспекты ведения беседы. Пример ведения беседы с программой ELIZA дан в виде протокола интерактивного взаимодействия, представленного на рис. 14.7. Ответам компьютера предшествует знак ». I am unhappy. Я опечален. » How long have you been unhappy. Сколько времени длится Ваша печаль. Six months. Can you help me. Шесть месяцев. Не могли бы Вы мне помочь. » What makes you think I help you. Как Вы думаете, чем я могу помочь Вам. You remind me of my father and brother. Вы напоминаете мне отца и брата. » Can you tell me more about father. Могли бы Вы рассказать мне подробнее об отце. I like teasing father. Я люблю дразнить отца. » Does anyone else in your family like teasing father. Любит ли кто-нибудь еще в Вашей семье дразнить отца. No, only me. Нет, только я. » Please, go on. Продолжайте, пожалуйста. Рис. 14.7. Пример беседы с программой ELISA.
Недетерминированное программирование 185 Центральной частью программы ELIZA является процедура для сопоставления входного предложения с шаблоном. Полученная в результате сопоставления пара применяется к другому шаблону для определения ответа программы. Пара шаблонов может рассматриваться как пара стимул/отклик; входное предложение сопоставляется со стимулом, а ответ генерируется с использованием отклика. Типичной парой стимул/отклик является пара: I am (утверждение) How long have you been (утверждение)? При использовании этой пары ответом программы на входное заявление "I am unhappy" будет вопрос "How long have you been unhappy?" Элемент предложения (утверждение) может рассматриваться как поле в формате ответа, подлежащее заполнению. Программа 14.20-простая версия программы ELIZA. В ней реализуется следующий алгоритм: считать входное предложение. пока поступают входные данные, выбрать пару стимул/отклик, сопоставить входное предложение со стимулом, генерировать ответ с использованием отклика и результата сопоставления на предыдущем шаге, выдать ответ, считать следующее входное предложение. Пары стимул/отклик представляются фактами вида pattern (Stimulus, Response), где и стимул Stimulus, и отклик Response являются списками слов и полей. Поля в шаблонах представляются целыми числами. Предикат match(Pattern,Table,Words) используется на втором и третьем шагах приведенного выше алгоритма. Он выражает связь между шаблоном Pattern, списком слов Words и таблицей Table, в которую производится запись при заполнении полей шаблона. Центральную роль в процедуре match играет недетерминированное использование предиката append для разбиения списка слов. Таблица представляется неполной структурой данных; такие структуры данных будут более подробно изучаться в следующей главе. Текст процедуры lookup/3 будет дан в разд. 15.3. Ответ генерируется процедурой reply (Words)-вариантом процедуры written, который обеспечивает печать пробелов между словами. Последней в этом разделе рассматривается программа Micro SAM, или McSAM. Она представляет собой упрощенную версию программы SAM (Script Applier Mechanism), разработанную в группе, проводящей исследования в области «понимания» естественных языков с помощью ЭВМ в Йельском университете. Цель программы McSAM-«понимать» сюжеты. По данному сюжету с помощью программы McSAM находится подходящий сценарий и осуществляется сопоставление отдельных событий сюжета с шаблонами сценария. В этом процессе сценарий пополняется событиями, явно не упоминаемыми в сюжете. Для представления сюжета и сценария используется теория концептуальной зависимости, развитая Р. Шенком. Рассмотрим, например, сюжет на рис. 14.8, он используется для примера в нашей версии программы McSAM. Сюжет, записанный на английском языке, "John went to Leones, ate a hamburger and left" представляется в программе в виде списка списков: [[ptransjohn John, X1, leones], [ingest, X2, hamburger, ХЗ], [ptrans, Actor, Actor, X4, X 5]].
186 Часть III. Глава 14 eliza <- Имитирует беседу с использованием побочных эффектов. eliza <- read_word_list(Input), eliza(Input),!. eliza([bye]) <- writeln(['Goodbye. I hope I have helped you']). eliza(Input) <- pattern(Stimulus, Response), match(Stimulus, Dictionary, Input), match(Response, Dictionary, Output), reply (Output), read_word_list(Input 1), !, eliza(Inputl). match (Pattern,Dictionary, Words) <- Шаблон Pattern сопоставляется со списком слов Words, результат сопоставления записывается в словарь Dictionary. match([N | Pattern], Dictionary,Target) <- integer(N), lookup(N, Dictionary,LeftTarget), append (LeftTarget,RightTarget, Target), match(Pattern, Dictionary, RightTarget). match ([Word | Pattern], Dictionary, [Word | Target]) <- atom (Word), match (Pattern, Dictionary, Target). match([ ], Dictionary,[ ]). lookup(Key, Dictionary, Value) <-См. программу 15.8. pattern (Stimulus, Response) <- Response - шаблон ответа, соответствующий шаблону Stimulus. pattern ([i, am, l],[how,long,have, you, been, 1,?]). pattern ([ 1, у ou, 2, me], [what, makes, you, think, i, 2, you, ?]). pattern ([i, like, 1 ], [does, anyone, else, in, your, family, like, 1, ?]). pattern ([i, feel, 1 ], [do, you, often, feel, that, way,?]). pattern([l,X,2],[can,you,tell,me,more,about,X]) <- important(X). pattern ([ 1 ], [please, go, on]). important (father). important (mother), important (son). important (sister), important (brother). important (daughter). reply([Head | Tail]) <- write(Head), write(' '), reply(Tail). reply([ ]) <- nl. read_word_list(Xs)«- См. программу 12.2. Программа 14.20. Программа ELIZA. Первый элемент каждого списка, например ptrans или ingest-термин из теории концептуальной зависимости. Выбор представления сюжета в виде списка списков-дань первоначальной версии этой программы, написанной на Лиспе. Программа McSAM легко записывается на Прологе (см. программу 14.21). Отношение верхнего уровня в этой программе mcsam(Story, Script) обеспечивает развитие некоторого сюжета Story в ее «понятный» эквивалент относительно подходящего сценария Script. Сценарий ищется посредством предиката find (Story, Script, Defaults). Текст сюжета просматривается для отыскания устойчивого аргумента, который инициирует имя соответствующего сценария. В нашем примере Джон
Недетерминированное программирование 187 mcsam(Story, Script) <- Сценарий Script описывает сюжет Story. mcsam (Story, Script) <- find (Story, Script, Defaults), match, (Script, Story), name_defaults( Defaults). find (Story, Script, Defaults) <- filler(Slot, Story), frigger(Slot,Name), script(Name, Script, Defaults). match (Script,Story) <- Сюжет Story является подпоследовательностью сценария Script. match(Script,[ ]). match([Line | Script], [Line | Story])<- match(Script,Story). match([Line | Script], Story)<- match (Script, Story). filler (Slot, Story;<- Slot - некоторое слово в сюжете Story. filler(Slot, Story) <- member([Action | Args], Story), member(Slot, Args). namejdefaults (Defaults) <- Унифицирует пары по умолчанию в Defaults. name_defaults([ ]). name_defaults([[N,N] | L])<- name_defaults(L). name_defaults([[Nl,N2] | L]) <- N1 Ф N2,name_defaults(L). Программа 14.21. Программа McSAM. Предложения и данные для тестирования программы lest_mcsam (Name, Understood Story) <- Story (Name, Story), mcsam (Story, Understood Story). Story (test, [[ptrans, John, john, XI, leones]. [ingest, X2, hamburger, X3], [ptrans, Actor, Actor, X4, X5]]). script(restaurant, [[ptrans, Acta, Actor, Earlier_place, Restaurant], [ptrans, Actor, Actor, Door, Seat], [mtrans, Actor, Waiter, Food], [ingest, Actor, Food [mouth, Actor]], [atrans, Actor, Money, Actor, Waiter], [ptrans, Actor, Actor, Restaurant, Gone]], [[Actor, customer], Earlier_place, place 1], [Restaurant, restaurant], [Door, door], [Seat, Seat], [Food, meal], [Waiter, waiter], [Money, check], [Gone, place2]]). trigger(leones, restaurant). trigger(waiter, restaurant). Программа 14.22. Тестовые данные для программы McSAM.
188 Часть III. Глава 14 посещает Леониса и атом leones запускает сценарий restaurant, идентифицированный фактом trigger (leones.restaurant) в программе 14.22. Сопоставление сюжета сценарию выполняется отношением match (Script,Story), которое связывает строки сюжета со строками в сценарии. Остающиеся поля в сценарии заполняются с помощью предиката name_defaults(Defaults). В итоге программой формируются списки. [ptrans John John, place 1, leones] [ptransjohnjohn, door, seat] [ptrans John, waiter, hamburger] [ingestjohn, hamburger, [mouthjohn] [atransjohn, check, leones, waiter] [ptransjohnjohn, leones, place2]. Результат преобразования этих списков в текст на английском языке представлен на рис. 14.8. Вход: John went to Leones, ate a humburger and left. Выход: John went to Leones. He was shown from the door to a seat. A waiter brought John a hamburger, which John ate by mouth. The waiter brought John a check, and John left Leones for another place. Рис. 14.8. Разработка сюжета программы McSAM. Вся работа первоначальной программы McSAM состояла в выполнении поиска и сопоставлении с шаблоном. Эти операции выполняются в Прологе механизмами недетерминированного программирования и унификации. Упражнения к разд. 14.4 1. Измените программу 14.18 так, чтобы с ее помощью можно было решить три задачи на геометрические аналогии, представленные на рис. 14.9. 2. Расширьте программу ELIZA путем добавления новых шаблонов для пар стимул/ отклик. 3. Если седьмое предложение на рис. 14.7 заменить на предложение "I like teasing my father", то программа ELIZA даст отвег: "Does any one else in your family like teasing my father". Модифицируйте программу 14.20 так, чтобы она могла действовать указанным образом, изменив для этого такие ссылки, как I, my, to you, your и т.д. 4. Перепишите программу McSAM для использования структур. 14.5. Дополнительные сведения Использование Пролога и метода «образовать и проверить» для решения задачи о N ферзях и задачи о раскрашивании плоской карты рассматривалось многими исследователями. Они прибегали к этим примерам как к свидетельству несовершенства управления в Прологе. Предлагались такие усовершенствования, как использование сопрограмм, реализованных в IC-Прологе (Clark and McCabe, 1979), и «интеллектуальный» механизм возвратов (Bruynooghe and Pereira, 1984). Хорошее обсуждение решения задачи о N ферзях средствами Пролога можно найти в (Elcock, 1983). Логическая головоломка «зебра» [упражнение 14.1(4)] появлялась в Prolog Digest в начале 80-х гг. и была использована в качестве неофициальной контрольной задачи для оценки эффективности реализаций Пролога и способности программистов писать на Прологе ясные программы. Обстоятельное обсуждение недетерминизма с произвольным выбором альтернативы и недетерминизма с неизвестным выбором альтернативы в логическом программировании содержится в работе (Kowalski, 1979).
0) Дано: Недетерминированное программирование 189 О о g о ■ °а 3 4 (2) Л*"*: ^^ ^/\^ (п^ D E F © ■ 0 О Л 6 7 8 (З)Дано: П а а а II 12 13 14 Рис. 14.9. Три задачи на определение аналогии. 15 Программа 14.11 (планирование с использованием поиска в глубину) является вариантом примера, заимствованного в (Kowalski, 1979). Первой программой планирования на Прологе была программа Warplan (Warren, 1976), воспроизведенная затем в (Coelho et al., 1980). Описание автомата заимствовано из работы (Hopcroft and Ullrnan, 1979). Работа (Burstall и Darlington, 1977) является классической работой по методу раскрытия в логическом программировании. Программа ANALOGY была составной частью докторской диссертации Т. Эванса в MIT в середине 1960-х гг. Хорошее описание этой программы можно найти в книге Semantic Information Processing (Minsky, 1968). В программе Эванса уделялось внимание многим аспектам задачи, которые стали элементарными при нашем выборе представления фигур, например средствам описания фигур, вписанных в данную фигуру. Наш вариант программы (программа 14.17) возник в ходе обсуждений, проводимых первым автором книги со студентами-эпистемологами в Эдинбургском университете. Программа ELIZA первоначально была опубликована в работе (Weizenbaum, 1966). Ее характеристики убедили некоторых, что достигнута некоторая ограниченная форма известного теста Тьюринга. Ее автор, Вайцснбаум, был шокирован таким мнением о возможностях этой программы и вообще искусственного интеллекта. Он страстно умолял (Weizenbaum, 1976) не
190 Часть III. Глава 15 воспринимать эту программу слишком серьезно. Наша версия программы (программа 14.20)- вариант учебной программы (авторство приписывается Алану Банди, Ричарду О'Кифу и Генри Томпсону), использованной в курсе по искусственному интеллекту в Эдинбургском университете. Программа McSAM-версия программы SAM, которая была подготовлена Шенком и Рисбеком как учебная (Schank и Riesbeck, 1981). Наша версия (программа 14.21) разработана Эрни Дэвисом и вторым автором книги-Шапиро. Дополнительные сведения о принципе концептуальной зависимости можно найти в работе Шенка и Айбельсона (Shank и Abelson, 1977). Глава 15 Неполные структуры данных До сих пор в рассмотренных программах использовались отношения между полными структурами данных. Как будет показано в настоящей главе, на основе применения неполных структур данных могут быть развиты мощные методы программирования. В первом разделе обсуждаются разностные списки-структура данных, отличающаяся от списков для представления последовательности элементов. Они могут быть использованы для упрощения и повышения эффективности программ обработки списков. Разностные списки вводят в употребление идею накопления. Во втором разделе обсуждаются структуры данных, строящиеся как разности неполных структур, отличных от списков. В третьем разделе показано, как путем наращивания в процессе вычислений могут быть построены в виде неполных структур таблицы и справочники. В последнем разделе обсуждается применение разностных списков для представления очередей. 15.1. Разностные списки Рассмотрим последовательность 1,2,3. Она может быть представлена как разность двух списков-списков [1,2,3,4,5] и [4,5], или списков [1,2,3,8] и [#], или списков [1,2,3] и [ ]. Каждый из этих случаев представляет собой пример разности между двумя неполными списками [l,2,3\Xs] и Xs. Для обозначения разности двух списков будем использовать структуру As\Bs, которая названа разностным списком- В этой структуре As-голова разностного списка, a Bs-qto хвост. Для рассмотренного примера [l,2,3\Xs]\Xs есть наиболее общий разностный список, представляющий последовательность 1,2,3 (здесь [1,2, 3\Xs]-голова, a Xs-хвост разностного списка). Логические выражения не вычисляются, а унифицируются, поэтому имя бинарного функтора, используемого для обозначения разностного списка, может быть произвольным, пока это не приводит к противоречиям. Оно может быть и вообще опущено, тогда голова и хвост разностного списка становятся двумя отдельными аргументами предиката. Обычные и разностные списки тесно связаны. И те и другие используются для
Неполные структуры данных 191 представления последовательностей элементов. Любой список L может быть тривиально представлен как разностный список L\[ ]. Пустой список представляется посредством любого разностного списка, голова и хвост которого одинаковы, наиболее общей формой при этом будет разностный список As\As. Разностные списки успешно и широко используются в логическом программировании. Применение разностных, а не обычных списков ведет к более выразительным и эффективным программам. Возможность улучшения программ связана с эффективностью соединения разностных списков. Два неполных разностных списка м/эгут быть соединены для образования третьего разностного списка за константное время. В противоположность этому соединение обычных списков с использованием стандартной программы append происходит за линейное время, пропорциональное длине первого списка. Рассмотрим списки на рис. 15.1. Разностный список Xs\Zs- это результат присоединения разностного списка Ys\Zs к разностному списку Xs\ Ys. Это может быть выражено одним фактом. Программа 15.1 определяет предикат append_dl(As,Bs, Cs), который истинен, если разностный список Cs является результатом присоединения разностного списка Bs к разностному списку As. Суффикс _dl используется для обозначения варианта предиката (соединения), в котором аргументами являются разностные списки. Необходимым и достаточным условием возможности соединения двух разностных списков As\Bs и Xs\Ys с помощью программы 15.1 является унифицируемость Bs и Xs. В этом случае соединенные разностные списки совместимы. Если хвост разностного списка не конкретизирован, то он совместим с любым разностным списком. Более того, в таком случае программа 15.1 будет соединять списки за константное время. Например, вопросу append_dl(\_a,b,c\Xs~\\Xs, [7,2]\[ ], Ys)l будет соответствовать результат (Xs = [7,2], Ys = [a,b,c,l,2~\\\_ ]). Разностные списки в логическом программировании являются аналогом функции rplacd языка Лисп, которая также используется для соединения списков за константное время (при этом не размещаются новые ячейки в области списочной памяти). Существует различие между разностными списками и указанной функцией: разностные списки свободны от побочного эффекта и могут рассматриваться в терминах абстрактной модели вычислений, тогда как функция rplacd- деструктивная XS\YS *—vr—' Рис. 15.1. Соединение разностных списков. операция, которая может быть описана лишь с использованием машинного представления S-выражений. Хорошим примером программы, которую можно усовершенствовать путем использования разностных списков, является программа 9.1а, предназначенная для раскрытия списков. В ней использованы двойная рекурсия для раздельной линеаризации головы и хвоста списков и последующее соединение полученных списков друг 2s
192 Часть III. Глава 15 append_dl(As,Bs,Cs) <- разностный список Cs - результат присоединения разностного списка Bs к разностному списку As; As и Bs -совместные разностные списки append_dl(Xs\Ys,Ys\Zs,Xs\Zs). Программа 15.1. Соединение разностных списков. flatten (Xs, Ys) <- Ys- раскрытый список, составленный из элементов, содержащихся в Xs. flatten(Xs,Ys) <- natten_dl(X, Ys\[ ]). natten_dl([X|Xs],Ys\Zs)<- Hatten_dl (X, Ys\ Ys 1), flatten_dl (Xs, Ys 1 \Zs). natten_dl(X,[X|Xs]\Xs)<- constant(X),X Ф [ ]. flatten_dl([ ],Xs\Xs). Программа 15.2. Программа раскрытия списка списков с использованием разностных списков. с другом. Приспособим эту программу для вычисления отношения flatten_dl(Ls, Xs), где Xs - разностный список, представляющий элементы, которые располагаются в списке списков Ls в правильном порядке. Ниже представлен результат прямой трансляции программы 9.1а в эквивалентную программу с разностными списками. fllatten_dl(X|Xs],Ys\Zs)<- natten_dl(X, As\Bs), natten_dl(Xs,Cs\Ds), append_dl(As\Bs,Cs\Ds,Ys\Zs). natten_dl(X,[X|Xs]\Xs)<- constant(X), X Ф [ ]. flatten_dl([ ],Xs\Xs). Предложение с двойной рекурсией может быть упрощено посредством развертывания цели по отношению к ее определению в программе 15.1. Тем самым получим правило natten_dl([X | Xs],As\Ds) <- natten_dl(X,As\Bs),natten_dl(Xs,Bs\Ds). Программа для отношения flatten_dl может быть использована для реализации отношения flatten посредством выражения связи между списком, подлежащим раскрытию, и разностным списком, вычисляемым программой flatten_dl. Необходимая связь устанавливается следующим правилом: natten(Xs,Ys) <- natten_dl(Xs, Ys\[ ]). Наконец, выполнив необходимые переименования переменных, получим программу 15.2. При декларативном прочтении программа 15.2 выглядит простой. Благодаря тому что раскрытие исходного списка приводит к разностному, а не обычному списку, явное обращение к предикату append оказывается необязательным. Полученная программа более эффективна, так как размер ее дерева доказательства линейно, а не квадратично зависит от числа элементов в списке списков. Операционное поведение программ с разностными списками, подобными программе 15.2, оказывается для понимания труднее. Кажется, что раскрытый список строится словно по волшебству. Рассмотрим, как действует эта программа. На рис. 15.2 представлена трассировка выполнения программы, вызванного вопросом: flatten^a\\b,[с]]],Xs)!.
Неполные структуры данных 1 93 flatten([[a],[b,[c]]],Xs) flatten_dl([[a],[b,[c]]],Xs\[ ]) flatten ..dl([a],Xs\Xsl) flatten .. dl(a,Xs \ Xs 1) Xs = [a | Xs2] constant(a) a*[] flatten _ dl([ ],Xs2 \ Xs 1) Xs2 = Xs 1 flatten_dl([[b,[c]]],Xsl\[ ]) flatten _dl([b,[c]],Xsl\Xs3) flatten _ dl(b,Xs 1 \ Xs4) Xs 1 = [b | Xs4] constant(b) b*[ ] flatten _dl([[c]],Xs4\Xs3) flatten. dl([cj,Xs4\Xs5) flatten.. dl(c,Xs4 \ Xs6) Xs4 = [с | Xs6] constant(c) c*[ ] flatten _ dl([ ],Xs6 \ Xs5) Xs6 = Xs5 flatten _ dl([ ],Xs5\ Xs3) Xs5 = Xs3 flatten _dl([ ],Xs3\[ ]) Xs3 = [ ] Выход: Xs = [a,b,c] Рис. 15.2. Трассировка вычислений с использованием разностных списков. Трассировка показывает, что результат Xs формируется по принципу «сверху вниз» (в терминологии разд. 7.5). Хвост разностного списка действует подобно указателю конца неполной структуры данных. Указатель устанавливается посредством унификации. Благодаря использованию таких «указателей» не требуется в отличие от программы 9.1а строить промежуточные структуры. Противоречие между ясностью декларативного и трудностью процедурного понимания программы обусловлено мощью логической переменной. Можно описывать логические отношения неявно и предоставлять их интерпретацию Пролог- системе. Здесь соединение разностных списков выражается неявно, и его появление в программе кажется таинственным. Программы, написанные с использованием разностных списков, иногда структурно подобны программам, в которых используются накопители. В упражнении 9.1(1) предлагалось написать с использованием двойной рекурсии программу для отношения flatten, в которой вместо обращения к предикату append используются накопители. Решением будет следующая программа: flattcn(Xs,Ys) 4- flatten(Xs,[ ],Ys). flattcn([X|Xs],Zs,Ys)«- flatten(Xs,Zs,Ysl), flatten(X,Ysl,Ys). flatlen(X,Xs,[X|Xs])«- constant(X), X ф [ ]. flatten([ ],Xs,Xs). Она весьма схожа с программой 15.2, за исключением двух отличий. Первое отличие-синтаксическое. Разностный список представляется двумя аргументами, однако они расположены в обратном порядке: хвост предшествует голове списка. Второе отличие заключается в порядке целей в рекурсивном предложении )latten. В результате раскрытый список строится по принципу «снизу вверх», т.е. начиная с хвоста. Рассмотрим еще один пример сходства разностных списков и накопителей. Программа 15.3 является результатом преобразования «наивной» версии предиката reverse, а именно программы 3.16а. В новом варианте обычные списки заменены разностными списками, а операция append вообще не применяется. 7 1402
194 Часть III. Глава 15 reverse(Xs, Ys) <- список Ys-есть обращение списка Xs. reverse(Xs,Ys) <- reverse_dl(Xs,Ys\[ ]). reverse_dl([X | Xs],Ys\Zs) <- reverse_dl(Xs,Ys\[X|Zs]). reverse_dl([ ],Xs\Xs). ripoipHM.via i 5.3. Обращение с использованием разностных списков. quicksort (List, SortedList) <- sortedlist- упорядоченная перестановка элементов списка List. quicksort(Xs,Ys)«- quicksort_dl(Xs,Ys\[ ]). quicksort_dl([X | Xs],Ys\Zs) <- partition (Xs, X, Littles, Bigs), quicksort_dl(Littles,Ys\[X | Ysl]), quicksort_dl(Bigs, Ys 1 \Zs). quicksort_dl([ ],Xs\Xs). partition(Xs,X,Ls,Bs)<- См. программу 3.22. Программа 15.4. Программа быстрой сортировки с использованием разностных списков. В каких же случаях разностные списки являются подходящими структурами данных для Пролог-программ? Обычно программы с явными обращениями к предикату append могут оказаться более эффективными, если в них использовать не простые, а разностные списки. Типичным примером является программа с двойной рекурсией, в которой результат получается путем присоединения результатов двух рекурсивных вызовов. Вообще программа, в которой части списка строятся независимо с целью последующего их объединения, является хорошим кандидатом на применение в ней разностных списков. Программа 3.22 {quicksort)-логическая программа, реализующая алгоритм быстрой сортировки, является примером программы с двойной рекурсией, в которой конечный результат-упорядоченный список - получается соединением двух упорядоченных подсписков. С помощью применения разностных списков можно получить более эффективную программу сортировки. Как показано в программе 15.4, все операции append, используемые при объединении частичных результатов, могут быть выполнены неявно. В программу 15.4, аналогично тому как это было сделано в программе 15.2 для отношения flatten, первым включено предложение с заголовком quicksort и целью quicksort_dl. Рекурсивное предложение в этой программе определяет алгоритм быстрой сортировки, реализованный с использованием разностных списков, в котором конечный результат достигается неявным соединением частичных результатов. Последнее предложение quicksort_dl определяет, что результат сортировки пустого списка есть пустой разностный список. Отметим, что при выполнении унификации цели quicksort_dl{Littles, Ys\\X\YsY§ обеспечивается размещение «разделяющего» элемента X после наименьшего элемента списка Ys и перед наибольшим элементом из Ysl. Программа 15.4 получена преобразованием программы 3.22 таким же образом, что и программа 15.2 из программы 9.1а. Списки заменяются разностными списками, а цель append_dl раскрывается соответствующим образом. Предложение quicksort с целью quicksort_dl выражает отношение между списком, подлежащим сортировке, и разностным списком, используемым в процессе вычислений. Замечательным примером преимущества использования разностных списков
Неполные структуры данных 195 является решение упрощенной версии задачи Дейкстры о голландском флаге. Задача формулируется следующим образом: «Дан список элементов, окрашенных в красный, белый и голубой цвета. Упорядочить список так, чтобы все красные элементы расположились вначале, за красными должны следовать белые элементы, за которыми в свою очередь должны быть расположены голубые элементы. При переупорядочении должен быть сохранен исходный относительный порядок элементов одного и того же цвета». Например, список [red(l),white(2),blue(3),red(4),white(5)] после переупорядочения должен принять вид [red(1), red(4),white(2),white(5),blue(3)]. В простом и разумном решении этой задачи, представленном программой 15.5, элементы исходного списка сначала распределяются по трем различным спискам, которые затем соединяются друг с другом в нужной последовательности. Базовое отношение в программе-dutch(Xs,Ys), где Xs-исходный список окрашенных элементов, а К?-переупорядоченный список с требуемым расположением элементов по цветам. dutch (Xs, Reds Whites Blues) <- RedsWhitesBlues- список элементов списка Xs, упорядоченных по цветам: красные, белые, голубые. dutch(Xs, RedsWhitesBlues) «- distributees, Reds, Whites, Blues), append(Whites, Blues, WhitesBlues). append(Reds,WhitesBlues, RedsWhitesBlues). distribute (Xs, Reds, Whites, Blues) <- Reds, Whites и Blues-списки красных, белых и голубых элементов списка Xs соответственно. distribute([red(X)|Xs],[red(X)| Reds],Whites, Blues) <- distributees, Reds, Whites, Blues). distribute([white(X) | Xs], Reds, [white(X) | Whites], Blues) <- distributees, Reds, Whites, Blues). distribute([blue(X) | Xs], Reds, Whites, [blue(X) | Blues]) <- distributees, Reds, Whites, Blues), distributed ],[],[],[ ]). append(Xs,Ys,Zs) <- См. программу 3.15. 1 Ipoi pavi\i:i 15.5 Решение задачи о голландском флаге. Ядро программы-процедура distribute, которая строит три одноцветных списка. Списки строятся по принципу «сверху вниз». Если использовать предикат distribute _dls для построения трех различных разностных списков вместо трех обычных, то два обращения к процедуре append могут быть исключены из программы. Соответствующая модификация представлена в виде программы 15.6. При унификации цели distribute_dls в предложении dutch происходит неявное соединение разностных списков. Полный список окончательно собирается из отдельных частей, когда удовлетворяется последнее предложение distribute_dls в программе. Решение задачи о голландском флаге является примером программы, в которой независимо строятся части решения, объединяемые в конце решения. Разностные списки использовались здесь более сложным образом, чем в предыдущих программах. Использование явного конструктора для разностных списков, хотя и облегчает чтение программы, влечет за собой заметные дополнительные затраты времени и памяти. В связи с этим целесообразно использовать два отдельных аргумента. В тех 7*
196 Часть III. Глава 15 dutch (Xs,RedsWhitesBlues) <- RedsWhitesBlues -список элементов списка Xs, упорядоченных по цвету: красные, белые, голубые. dutch (Xs,RedsWites Blues) — distribute„_dls(Xs,RedsWhitesBlues\WhitesBlues,WhitesBlues\Blues,Blues\[ ]). distribute_dls (Xs, Reds, Whites, Blues) <- Reds, Whites и Blues разностные списки красных, белых и голубых элементов в списке Xs соответственно. distribute_dls([red(X)| Xs],[red(X)| Reds]\Redsl, Whites, Blues) <- distribute dls(Xs, Reds\Redsl,Whites,Blues). distribute_dls([whitc(X)|Xs],Reds,[white(X)|Whites]\Whitcsl,Blues) *- distribute. dls(Xs,Reds,Whites\Whites 1, Blues), distribute. dls([blue(X) | Xs], Reds, Whites, [blue(X) | Blues]\Blues 1) - distribute. dls(Xs, Reds, Whites, Blues\Blues 1). distribute_dls([ ],Reds\Reds,Whites\Whites,Blues\Blues). Программа 15.6. Решение задачи о голландском флаге с использованием разностных списков. случаях, когда требуется повысить эффективность программы, соответствующие простые преобразования могут быть выполнены вручную или автоматически. Упражнения к разд. 15.1 1. Перепишите программу 15.2 так, чтобы порядок элементов в окончательном списке был обратным по отношению к порядку их расположения в списке списков. 2. Перепишите программы 3.27 для отношений/;/-? order (Tree,List), in order (Tree,List) и post_order(Tree,List), собирающие элементы при обходе двоичного дерева, используя разностные списки и исключая явное применение предиката append. 3. Перепишите программу 12.3 для решения задачи о ханойских башнях, используя для формирования списка перемещений не обычный, а разностный список. 15.2. Разностные структуры Идея, лежащая в основе построения и применения разностных списков, состоит в использовании для представления частичных результатов вычислений разности неполных структур данных. Такой подход применим не только к спискам, но и к другим рекурсивным структурам данных. В этом разделе рассматривается один специфический пример, связанный с алгебраическими суммами. Рассмотрим задачу нормализации арифметических выражений, например сумм. На рис. 15.3 представлены две суммы: (a + b) + (c + d) и (а + (Ь + (с + d))). Согласно стандартному синтаксису Пролога, скобки в терме а + Ь + с расставляются следующим образом: ((с/ + Ь) + с). Опишем процедуру преобразования суммы в нормализованную сумму, в которой сгруппированы правые скобки. Например, выражение, представленное на рис. 15.3 слева, должно быть преобразовано в нормализованное выражение, показанное справа. Такая процедура полезна для упрощения алгебраических выражений, облегчения написания программ проверки эквивалентности выражений. Введем понятие разностной суммы как некоторого варианта разностного списка. Разностная сумма представлена структурой вида El + + Е2, где El и Е2 неполные нормализованные суммы. Предполагается, что + Л- является обозначением бинар-
Неполные структуры данных 197 + /\ а + /\ Ь + /\ с + /\ d О Рис. 15.3. Ненормализованная и нормализованная суммы. ного инфиксного оператора. Условимся также использовать 0 для обозначения пустой суммы. Программа 15.7 является программой для нормализации сумм. Реляционной схемой программы является отношение normalize (Exp,NormExp), где NormExp-ъы- ражение, эквивалентное выражению Ехрч в котором сгруппированы правые скобки при сохранении порядка констант, определенного в выражении Ехр. normalize (Sum, NormalizedSum) <- NormalizedSum результат нормализации выражения Sum. normalize(Exp,Norm) <- normalize _ds( Exp, Norm + +0). normalize ds(A + B,Norm-f + Space) <- normalize ds(A,Norm + +NormB), normalize ds(B,NormB+ + Space), normalize ds(A,(A + Space) + + Space) <- constant(A). Программа 15.7. Нормализация выражений типа «сумма». По структуре эта программа подобна программе 15.2 для раскрытия списков с использованием разностных списков. Существует начальный этап установки разностной структуры, на котором обычно выполняется обращение к предикату с тем же именем, но имеющим другую арность или другой вид аргументов. Последнее предложение normalize_ds в программе распространяет хвост неполной структуры данных, в то время как цели в теле рекурсивного предложения передают хвост первой неполной структуры во вторую структуру, где он занимает место головы структуры данных. Программа строит нормализованную сумму по принципу сверху вниз. По аналогии с программами, использующими разностные списки, и эта программа может быть легко модифицирована, с тем чтобы структура строилась снизу вверх (см. упражнение в конце раздела). Эти программы имеют простой декларативный смысл. Операционное их толкование состоит в представлении процесса построения структуры путем ее наращивания, когда явно указывается «место» для последующих результатов. Здесь видна полная аналогия с разностными списками. Упражнения к разд. 15.2 1. Определите предикат normalized}_sum(Expression), который истинен, если выражение Expression -нормализованная сумма. + /\ + + /\/\ abed
198 Часть III. Глава 15 2. Перепишите программу 15.7 так, чтобы а) нормализованная сумма строилась по принципу «снизу вверх», б) получился обратный порядок элементов. 3. Напишите программу для нормализации произведения, используя разностное произведение, определенное по аналогии с разностной суммой. 15.3. Справочники Отличный от рассмотренных способ использования неполных структур данных позволяет реализовывать справочники. Рассмотрим задачу создания, использования и обновления набора значений, индексированных ключами. Существуют две основные операции, выполняемые со словарем: поиск значения, записанного под определенным ключом, и ввод новых ключей и связанных с ними значений. Эти операции должны выполняться с обеспечением непротиворечивости, так, например, один и тот же ключ не должен встречаться дважды с двумя разными значениями. Оказывается, можно выполнять обе операции-поиск значений по ключу и ввод новых ключей и значений-с использованием одной простой процедуры, в которой применяются неполные структуры данных. Рассмотрим линейную последовательность пар ключ-значение. Преимущества использования для их представления неполной структуры данных очевидны. Программа 15.8 определяет отношение lookup (Key,Diet, Value), которое истинно, если запись с ключом Key в справочнике Diet имеет значение Value. Словарь представляется как неполный список пар вида (Key,Value). lookup (Key,Dictionary, Value) <- Dictionary содержит значение Value, индексированное ключом Key. Dictionary представляется списком пар (Key, Value). lookup(Key, [(Key, Value) | Dictionary], Value). lookup(Key,[Keyl,Valuel)| Dictionary],Value) <- Key ф Keyl,lookup(Key,Dictionary,Value). Программа 15.8. Поиск в словаре, представленном списком кортежей. Рассмотрим пример справочника, используемого для хранения номеров телефонов, фамилии владельцев которых играют роль ключей. Предположим, что справочник Diet первоначально представлен списком [(арнольд,8881), (барри,4513), (кэ- ти,5950)|Лг5]. Тогда на вопрос lookup (арнольд, Dict,N)l, используемый для поиска номера телефона человека по имени Арнольд, будет получен ответ N = 8881. Вопрос lookup (6appH,D/c/,4513), используемый для проверки номера телефона Барри, имеет утвердительный ответ. Ввод новых ключей и значений рассмотрим на примере вопроса lookup (Дэвид, Diet, 1199)?. Синтаксически этот вопрос кажется проверкой номера телефона Дэвида. Но он приводит к иному результату. Этот вопрос успешно решается: справочник Diet будет теперь представлен списком [(арнольд, 8881), (барри,4513), (кэти,5950), (дэвид, 1199)1X57]. Таким образом, с помощью процедуры lookup вводится новое значение. Что произойдет, если проверить номер телефона Кэти с помощью вопроса lookup (кэти, Diet, 5951)1, в котором номер задан неправильно? Вторая запись в справочник номера телефона Кэти сделана не будет: вследствие проверки Key ф Keyl решение вопроса будет безуспешным. Процедура lookup представлена программой 15.8, завершающей программу 14.20-упрощенную программу ELIZA. Отметим, что перед началом выполнения
Неполные структуры данных 199 программы справочник пуст и обозначается некоторой переменной. Построение справочника происходит при поступлении запросов. Сконструированный справочник используется для выработки корректных ответов. Отметим, что записи помещаются в справочник, когда их значения еще неизвестны. Это замечательный пример мощности логической переменной. Как только обнаруживается некоторая запись, она помещается в справочник, а ее значение определяется позже. При большом числе пар ключ-значение поиск в линейном списке становится не очень эффективным. Упорядоченные двоичные деревья позволяют вести поиск информации более эффективно по сравнению с поиском в линейном списке. Понятно, что неполная структура данных может быть использована для организации бинарного дерева, которое позволит вводить новые ключи и значения, а также искать значение по заданному ключу. Бинарные деревья, описанные в разд. 3.4, преобразуются в четырехместную структуру diet (Key, Value,Left,Right), где Left и Right- левый и правый подсправочники соответственно, a Key и Value -как и прежде, ключ и значение. Функтор diet напоминает, что речь идет о справочнике. Поиск по дереву-справочнику имеет очень элегантное определение, напоминающее по духу определение процедуры поиска в программе 15.8. Он выполняется рекурсивно по двоичным деревьям, а не по спискам и основан на унификации для конкретизации переменных в структурах справочника. В программе 15.9 определена процедура lookup (Key,Dictionary, Value), которая, как и прежде, обеспечивает поиск значения по заданному ключу и вводит новые значения. На каждом шаге поиска ключ сравнивается с ключом, записанным в текущем узле. Если ключ поиска меньше ключа в текущем узле, то рекурсивно продолжается поиск по левой ветви, если больше, то выбирается правая ветвь. Если ключ не числовой, то предикаты < и > необходимо обобщить. В программе 15.9 в отличие от программы 15.8 следует использовать отсечение. Это обусловлено «нелогическим» характером действий операторов сравнения, которые будут приводить к ошибкам, если ключи не конкретизированы. lookup (Key,Dictionary, Value) <- Dictionary содержит значение Value, индексированное ключом Key. Dictionary представляется упорядоченным бинарным деревом. lookup(Key,dict,(Key,X,Left,Right), Value) <- !, X = Value. lookup(Key,dict(Keyl,X,Left,Right),Value) <- Key < Key l,lookup(Key, Left, Value). lookup(Key,dict(Keyl,X,Left,Right), Value) <- Key > Keyl,lookup(Key,Right,Value). !(poip;iM\iii l\9. Поиск в словаре, составленном в виде бинарного дерева. Если имеется ряд пар ключей и значений, то справочник их определяет не единственным образом. Форма справочника зависит от порядка, в котором поступали вопросы. Справочник может использоваться и для размораживания терма, который был заморожен с помощью программы 13.2 для предиката numbervars. С этой целью может использоваться программа 15.10. Каждая размороженная переменная вводится в справочник так, чтобы значения были присвоены соответствующим общим переменным.
200 Часть III. Глава 15 freeze (А, В) <- Замораживание терма А в терме В. freeze (А, В) <- сору (А, В), numbervars(BAN). melt_new(А,В) <- Освобождение (размораживание) терма А в терме В. melt_new(A,B) <- melt(A, В,Dictionary),!. meltfSVARXN^Dictionary) <- lookup(N, Dictionary,X). melt(X,Y,Dictionary) <- constant(X). melt(X,Y, Dictionary) <-- compound(X), functor(X,F,N), functor(Y,F,N), melt(N,X,Y,Dictionary). melt(N,X,Y,Dictionary) «- N>0, arg(N,X,ArgX), melt(ArgX, ArgY, Dictionary), arg(N,Y,ArgY), N1:=N-1, melt(Nl,X,Y,Dictionary). melt(0,X,Y,Dictionary). copy (A, B) <- assert C$foo'( A)), retract fSfoo'fB)). numbervars(Term,Nl,N2) <-См. программу 13.2. lookup(Key,Dictionary, Value) <- См. программу 15.9. Программа 15.10. Размораживание терма. 15.4. Очереди Интересным применением разностных списков является организация очередей. Очередь-структура данных для хранения списков, используемая по принципу: «первым записан - первым считан». Голова разностного списка представляет начало очереди, хвост-ее конец, а объекты разностного списка это элементы очереди. Очередь пуста, если пуст разностный список, т. е. голова и хвост тождественны. Управление очередью отличается от управления описанным выше справочником. Рассмотрим отношение queue fS), предназначенное для обработки потока команд, представленных списком 5. Существуют две основные операции с очередью -включение элемента в очередь и исключение элемента из очереди, представляемые структурами enqueue (X) и dequeue (X) соответственно, где X - интересующий нас элемент. В программе 15.11 эти операции реализуются абстрактно. Предикат queue (S) обращается к предикату queue(S,Q), где Q-первоначально пустая очередь. Предикат queue/2 является интерпретатором для потока команд включения и исключения. Он воспринимает каждую команду и соответственно изменяет состояние очереди. При включении элемента используется незаполненность хвоста очереди; производится конкретизация его новым элементом и соответствующее обновление хвоста
Неполные структуры данных 201 queue(S) <- S последовательность операций включения элементов в очередь и исключения элементов из очереди, представленных списком термов enqueue(X) и dequeue(X). queue(S) - qucuc(S,Q\Q). queue([enqueue(X) | Xs],Q) <- enqueue(X,Q,Q 1 ),queue(Xs,Q 1). queue([dequeue(X)|Xs],Q) «- dequeue(X,Q,Ql),queue(Xs,Ql). qucuc([ ],Q). enqueuc(X,Qh\[X | Qt],Qh\Qt). dequeue(X,[X | Qh]\Qt,Qh\Qt). Программа 15.11. Выполнение операций над очередью. очереди. Понятно, что отношения enqueue и dequeue могут быть и неразвернутыми, что дает более выразительную и эффективную, однако более трудную для чтения программу. Выполнение программы завершается, когда исчерпывается поток команд. Эта программа может быть расширена так, чтобы после выполнения последней команды очередь становилась пустой; для этого один базисный факт можно заменить следующим предложением: qucuc([ ],Q)«-- empty(Q). Очередь пуста, если ее голова и хвост конкретизируются пустыми списками, выраженными фактом empty(\_ ] \[ ]). Логически для этого должно быть достаточно предложения empty (Х\Х)Ч однако из-за отсутствия в Прологе контроля на вхождение, рассмотренного в гл. 4, оно может быть удовлетворено ошибочно на непустой очереди, образующей некоторую циклическую структуру данных. Продемонстрируем использование очередей для раскрытия списка в программе 15.12. Хотя этот пример до некоторой степени надуманный, он ясно показывает, как могут быть использованы очереди. flatten(Xs.Ys) «- Xv раскрытый список, содержащий элементы из списка Xs. flatten(Xs,Ys) <- flatten q(Xs,Qs\Qs,Ys). flatten .q([X|Xs],Ps\[Xs|Qs],Ys) «- flatten.q(X,Ps\Qs,Ys). flatten. q(X,[Q | Ps]\Qs,[X | Ys]) «- constant(X),X Ф [ ],flatten..q(Q,Ps\Qs,Ys). flatten q([ ],[Q | Ps]\Qs,Ys) - flatten q(Q,Ps\Qs,Ys). flatten q([ ],[ ]\[ ],[ ]). Программа 15.12. Программа раскрытия списка с использованием очереди. Основное отношение в программе flatten...q (Ls,Q,Xs), где Ls- список списков, подлежащий раскрытию, Q- очередь списков, ожидающих раскрытия, и Xs -список элементов в Ls. Первое предложение flatten..qj3 посредством отношения flatten/2 инициализирует пустую очередь. Базовой операцией являются включение в очередь хвоста списка и рекурсивное раскрытие головы списка: flatten q([X|Xs],Q,Ys)-enqueue(Xs,Q,Ql),natten_q(X,Ql,Ys). Явно раскрывая отношение enqueue, получим
202 Часть III. Глава 15 natten_q([X | Xs],Qh\[Xs | Qt],Ans) <- natten_q(X,Qh\Qt,Ys). Если элемент, подлежащий раскрытию, оказывается константой, он добавляется в выходную структуру, строящуюся сверху вниз, и некоторый элемент извлекается из очереди (при унификации с головой разностного списка) для последующего раскрытия в рекурсивном вызове отношения flatten_q\ natten_q(X,[Q|Qh]\Qt,[X|Ys])<-constant(X),^ Ф [ ],flatten_q(Q,Qh\Qt,Ys). Когда при раскрытии встречается пустой список, либо верхний элемент исключается из очереди flatten_q([ ],[Q | Qh]\Qt,Ys) <- natten_q(Q,Qh\Qt,Ys), либо очередь оказывается пустой и вычисления прекращаются: Рассмотрим операционный смысл программы 15.11. В процессе предполагаемого использования очереди сообщения enqueue (X) передаются с определенными X, а dequeue (X)- с неопределенными X. Пока в очередь включено больше элементов, чем из нее исключено, очередь ведет себя нормально; разность между головой очереди и ее хвостом дает элементы, содержащиеся в очереди. Однако, если число полученных команд исключения из очереди превышает число команд включения элементов в очередь, происходит интересное явление-содержимое очереди становится «отрицательным». Голова опережает хвост очереди, что приводит к появлению в очереди отрицательной последовательности неопределенных элементов. Число таких элементов определяется числом избыточных операций исключения из очереди. Интересно понаблюдать, как такое поведение согласуется с ассоциативностью присоединения разностных списков. Если очередь Qs\\_XJ,X2,X3\Qs'], содержащая три неопределенных отрицательных элемента, присоединяется к очереди [а,Ь, c,d,e\ XsJ\Xs, состоящей из пяти элементов, то результатом присоединения будет очередь [d,e\XsJ\Xs с двумя элементами, где отрицательные элементы Х1,Х2,ХЗ унифицируются с а,Ь,с 15.5. Дополнительные сведения Разностные списки сразу вошли в фольклор логического программирования. Первое их описание в литературе дали Clark и Tarnlund (1977). Описание метода автоматического преобразования программ без разностных списков в программы с разностными списками можно найти в (Bloch, 1984). Элегантную процедуру поиска lookup на упорядоченных двоичных деревьях описал Warren (1980). Эта процедура, как будет показано в гл. 23, используется в качестве центрального метода при написании компиляторов на Прологе. Van Emden (1984) и Lloyd (1984) подвели определенную теоретическую базу под процедуру управления справочниками и очередями, рассматривая ее как некоторый бесконечный процесс. Особое значение приобретают очереди в параллельных языках логического программирования, поскольку их «вход» не обязательно должен быть списком запросов. Он может быть потоком, последовательно формируемым процессами, которые требуют обслуживания.
Синтаксический анализ для грамматик 203 Глава 16 Синтаксический анализ для грамматик, задаваемых определительными предложениями] Синтаксический анализ является очень важным приложением Пролога и логического программирования. В действительности, происхождение Пролога связано с попыткой использовать логику для выражения грамматических правил и формализации процесса синтаксического разбора. Наиболее распространенным подходом к реализации синтаксического разбора средствами Пролога является использование грамматик, задаваемых определительными предложениями (definite clause grammar, DCG). DC-грамматики являются некоторым обобщением контекстно-свободных грамматик. Они представляют собой нотационный вариант определенного класса программ на Прологе и потому являются исполняемыми. Разбор с использованием DC-грамматик обсуждается здесь, поскольку эта тема связана с темами двух предыдущих глав. Разработка программ синтаксического разбора на Прологе является идеальной иллюстрацией программирования на Прологе с использованием недетерминированных программ и разностных списков. Начнем с обсуждения контекстно-свободных грамматик. Контекстно-свободные грамматики определяются множеством правил вида (нетерминал) -► (тело), где нетерм una л является нетерминальным символом, а тело - последовательностью из одного или нескольких элементов, разделяемых запятыми. Каждый элемент-это либо нетерминальный символ, либо последовательность терминальных символов. Смысл правила в том, что тело есть возможная форма грамматической группы нетерминального типа Нетерминальные символы записываются как атомы Пролога, а последовательности терминальных символов-в виде списков атомов. Это облегчает трансляцию грамматик в Пролог-программы. Для каждого нетерминального символа S грамматика определяет язык, который представляет собой множество последовательностей терминальных символов, получаемых путем повторного недетерминированного применения правил грамматики, начиная с символа S. Рассмотрим простую контекстно-свободную грамматику для небольшого подмножества английского языка. Грамматика, показанная на рис. 16.1, не требует специальных пояснений. Первое правило грамматики читается так: «Предложение состоит из группы существительного, за которым следует группа глагола». Примером предложения, распознаваемого этой грамматикой, является предложение «The decorated pieplate contains a surprise». Грамматика может быть непосредственно записана в виде программы на языке Пролог. В этой программе каждому нетерминальному символу соответствует унарный предикат, идентифицируемый аргумент которого есть предложение или группа. Для представления предложения можно выбрать просто список слов. Части предложения также легко представить списками слов. Первое правило грамматики 1} В литературе на русском языке наряду с этим существуют термины грамматика определенных дизъюнктов, дизъюнктивно-определяемая грамматика. - Прим. ред.
204 Часть III. Глава 16 Грамматические правила предложение -► группа_существительного, группа .глагола. группа .существительного -► артикль, группа_существительного2. группа, существительного -► группа сущсствительного2. группа_.существительного2 -► прилагательное, группа _суидествительного2. группа _сущсствитсльного2 -► существительное. группа.. глагола -► глагол. группа _ глагола -► глагол, группа существительного. Словарь артикль -► [the]. прилагательное -► [decorated], артикль -► [а]. существительное -► [pieplate]. глагол -► [contains], существительное -► [surprise]. Рис. 16.1. Простая контекстно-свободная грамматика. на языке Пролог запишется следующим образом: предложение(S)«- append(NP, VP,S), группа существительного (NP), группа .глагола (VP). Словарные правила, связанные с терминальными символами, могут быть выражены как факты, например, так: артикль([1пс]). существительное([р1ер1а1е]). Таким образом, представленную на рис. 16.1 полную грамматику можно записать на языке Пролог в виде программы синтаксического разбора, однако такая программа будет неэффективной. Использование в программе предиката append наводит на мысль, что более подходящей структурой данных для синтаксического анализа является разностный список. Программа 16.1 является результатом преобразования грамматики, представленной на рис. 16.1, в Пролог-программу, в которой грамматические группы представлены разностными списками. Базовая реляционная схема-это предикат предложение (S), где S - разностный список слов, образующих некоторое предложение согласно правилам грамматики. Подобным же образом предикаты группа существительного(S), группа,.сущест- вителыюго2(S), артикль (S), группа_глагола(Б), существительное(S) и глагол (S) истинны, если их аргумент, некоторый разностный список слов 5, является частью речи, соответствующей их именам. Программа 16.1 -это синтаксический анализатор, реализующий нисходящий рекурсивный разбор слева направо, в процессе которого производится возврат, когда требуется выбрать некоторое альтернативное решение. Хотя анализаторы с возвратами легко конструируются, в общем случае они неэффективны. Однако эффективность механизма возврата, присущего Прологу, позволяет создавать на основе DC-грамматик практически эффективные анализаторы. Преобразование контекстно-свободной грамматики в эквивалентную Пролог- программу осуществляется просто. Программа 16.2 преобразует отдельное правило грамматики в его эквивалент на Прологе. Преобразование всей грамматики заключается в последовательном преобразовании отдельных правил. Предполагается, что правила грамматики представлены термами вида А -* В, где А-нетерми-
Синтаксический анализ для грамматик 205 предложение^ \ SO) <- группа сущсствитсльног o(S\Sl), группа, глагола (SI \S0). группа существительного(8\80) <- аргикль(8\81), группа. существительного2(81 \S0). ♦ группа существительного^) <- группа существительного2(8). группа существитсльного2(8\80) <- прилагательное(8\81), группа существителыюго2(81 \ SO), группа сущсствительного2(8) *- существительное^). группа глагола^) *- глагол (S). группа, глагола(S\SO) ♦- niar4ui(S\Sl), группа существительного (SI \S0). apTHKjib([thc | S]\S). прилагательное([с1есога1сс11 S]\S). артикль([а|8]\8). существительное ([pieplate | S]\S). глагол ([contains | S]\S). существительное ([surprise | S]\S). Программа 16.1. Программа на Прологе для разбора предложений языка, определенного грамматикой, представленной на рис. 16.1. нальный символ, а В- конъюнкция нетерминальных символов и списков терминальных символов. Нетерминальные символы являются атомами Пролога. Предполагается также, что для знака -► существует подходящее объявление оператора. Основная реляционная схема программы 16.2-это предикат преобразовать(Правило Грамматики, Предложение Пролога). преобразовать (ПравилоГрамматики, ПредложениеПро.юга) «- Предложение Пролога эквивалент гга Прологе правила контекстно-свободной грамматики Правило Грамматики. преобразовать((Lhs -► Rhs),(Hcad <- Body)) <- преобразовать (Lhs, Head, Xs \ Ys), преобразовать(Rhs, Body,Xs\ Ys). npeo6pa3o»aTb((A,B),(Al,Bl),Xs\Ys) «- npeo6pa30BaTb(A,Al,Xs\Xsl), преобразовать (В, В1, Xs 1 \ Ys). преобразовать(А,А1,8) ♦- не терминал (A), functor(Al,A, 1), arg(l,Al,S). преобразовать (Xs, true, S) *- терминалы (Xs), последовательность (Xs, S). не тсрминал(А) *- atom(A). терминалы([X | Xs]). послсдоватсльность([Х | Xs],[X | S]\S0) ♦- последовательность (Xs, S \ SO), последовательность([ ],Xs\Xs). Программа 16.2. Программа преобразования грамматических правил в предложения Пролога.
206 Часть III. Глава 16 Процедура преобразовать/2 преобразует левую и правую части данного правила грамматики в заголовок и тело эквивалентного предложения на Прологе. Основная идея состоит в добавлении некоторого разностного списка к каждому нетерминальному символу. Необходим новый вариант предиката преобразовать, а именно предикат преобразовать (Символ,Цель, Xs). Это отношение истинно, если Символ преобразуется в Цель при добавлении разностного списка Xs в качестве аргумента. Три предложения преобразовать/3 покрывают возможные термы в грамматическом правиле. Если терм является конъюнкцией, то каждый ее элемент рекурсивно преобразуется с выполнением подходящих соединений разностных списков. Нетерминальный символ преобразуется в унарную структуру, функтором которой является символ, а аргументом - разностный список. Последовательность терминальных символов с помощью предиката последовательность (Символ,Xs) унифицируется с разностным списком соответствующего правила. Некоторый постпроцессор может удалить избыточные «истинные» цели true. Однако можно предотвратить их появление, используя разностную структуру. Следующие две программы дополняют программу 16.1. Эти программы представляют собой хотя и простые, но типичные примеры использования DC-грамматик для построения синтаксических анализаторов. Значительную роль в них играют логические переменные. Первая программа предназначена для конструирования дерева разбора анализируемого предложения. К предикатам программы 16.1 должны быть добавлены аргументы, представляющие определенные части дерева разбора. Это расширение подобно рассмотренному в разд. 2.2 добавлению структурных аргументов к логическим программам. Так, предикат предложение/1 заменяется бинарным отношением предложение (Дерево,С лова), где Дерево - дерево разбора слов, представленных переменной Слова и анализируемых согласно правилам грамматики. Другие унарные предикаты, представляющие грамматические группы и части речи, должны быть подобным же образом заменены бинарными предикатами. Здесь предполагается, что дерево разбора является первым аргументом предикатов. Первое предложение программы 16.1 после изменения принимает следующий вид: предложение (предложение (NP, VP), S\S1) ♦— группа_существител ьного (NP, S\S0), группа_глагола (VP, S0\S 1). Составной терм предложение (NP, VP) представляет дерево разбора, в котором NP и VP в свою очередь представляют подлежащие разбору группу существительного и группу глагола соответственно. Программа, кроме того, может быть расширена за счет дополнения правил грамматики. К нетерминальным символам может быть добавлен дополнительный аргумент, что превращает их в структуры. Приведенное выше правило представляется теперь следующим образом: предложение (предложение (NP, VP)) -* гру ппа_существител ьного (ЫР),группа_глагола(УР). Добавление аргументов к нетерминальным символам контекстно-свободных грамматик делает их более полезными. Программа 16.2, преобразующая правила контекстно-свободных грамматик в программы на языке Пролог, может быть расширена для преобразования DC-грамматик в Пролог-программы. Такое расширение предлагается выполнить в качестве упражнения [см. упражнение 16(3)]. Поэтому, используя DC-грамматики в нотации грамматических правил, мы понимаем, что по существу пишем Пролог-программы. DC-грамматики в программе 16.3 являются расширением программы 16.1. В программе 16.3 выполняются синтаксический анализ предложения и построение
Синтаксический анализ для грамматик 207 предложение (предложение (N,V)) -* группа_существительного(М), группа_глагола(У). группа_существительного (группа_существительного (D, N)) -* артикль(D), группа_существительного2(Ы). группа_существительного (группа_существительного(N)) ♦- группа_существител ьного2 (N). группа_существительного2(группа_существительного2(А,1Ч)) -► прилагательное (А), группа_существительного2 (N). группа._существительного2 (группа_существительного2 (N)) -* существительное (N). группа_глагола(группа_глагола(У)) -► глагол (V). группа_глагола (группа_глагола (V, N)) -► глагол (V), группа_существительного (N). Словарь артикль (артикль (the)) -* [the], артикль (артикль ((a)) -> [а]. существительное (существительное (pieplate)) -* [pieplate]. существительное (существительное (surprise)) -* [surprise]. прилагательное (прилагательное (decorated)) -* [decorated], глагол (глагол (contains)) -* [contains]. Программа 16.3. DC-грамматика построения дерева разбора. дерева разбора. Как логическая программа, она подобна программе 2.3, которая в дополнение к идентификации компонентов схемы строила некоторую структуру. Программа выполняет нисходящее построение дерева разбора, используя при этом возможности логической переменной. Следующее расширение связано с согласованием подлежащего и дополнения по числу. Предположим, что по нашей грамматике необходимо разобрать такое предложение: "The decorated pieplates contain a surprise". Простейшим способом управления множественно-числовыми формами существительных и глаголов, достаточным для целей нашего изложения, является обработка разных числовых форм как различных слов. Расширим словарь путем добавления следующих фактов: существительное (существительное (pieplates)) -► [pieplates]. глагол (глагол (contain)) -► [contain]. Новая программа должна быть способна разбирать предложение "The decorated pieplates contain a surprise", но, кроме того, она, к сожалению, должна выполнять и разбор предложения "The decorated pieplates contains a surprise". Необязательно, чтобы существительное и глагол были либо оба в единственном, либо оба во множественном числе. Согласование в числе может быть осуществлено с помощью добавления аргумента к частям речи, которые должны иметь одинаковое число. Этот аргумент указывает, в каком числе (единственном или множественном) представлены соответствующие части речи. Рассмотрим правило грамматики: предложение (предложение (NP, VP)) -► группа_существительного (NP, Num), группа_глагола (VP, Num). Согласно этому правилу, требуется, чтобы и группа существительного (подлежащее), и группа глагола (дополнение) имели одно и то же число - единственное или множественное. Согласование в числе указывается с помощью общей переменной
208 Часть 11Г. ГЛава 16 Num. Информация, определяющая согласование в числе субъекта и объекта предложения, является контекстно-зависимой. Такие контекстно-зависимые требования, очевидно, не могут быть определены средствами контекстно-свободной грамматики. Программа 16.4, в которой осуществляется корректное согласование в числе, является расширением программы 16.3. Группы существительного и группы глагола должны иметь одно и то же число- единственное или множественное. Аналогично в группе существительного должны согласовываться в числе артикли и существительные. Чтобы определить, какие слова имеют единственное, а какие - множественное число, производится расширение словаря. Следует отметить, что число не является грамматической категорией некоторых частей речи, например наречия, в таких случаях вводить дополнительные аргументы не требуется. Артикль может использоваться как в случае единственного, так и в случае множественного числа. При этом аргумент, указывающий число, остается неконкретизированным. предложение (предложение (N,V)) -► группа_существительного(N,Num), группа, глагола(V,Num). группа, существительного (группа .существительного (D,N),Num) -> артикль(П,1Мит), группа существитсльного2(М,Мит). группа, существительного(группа существительного(N),Num) -► группа .существительного2(N, Num). группа. существительного2(группа существительного2(А,1Ч|),1Ч1шп) -► прилагательное(А),группа. сущсствительного2(М,Мшп). группа существительного2(группа_существительного2(>>1),1Ч1игп) -► существительное^, Num). группа, глагола (группа .глагола (V),Num) -► глагол (V,Num). группа глагола (группа глагола (V,N),Num) -► глагол(V,Num), группа существительного(Ы,1Мит1). Словарь артикль(артикль(1пе),Ь1ит) -► [the]. артикль(артикль(а),единственное) -► [а]. существительное (существительное (pieplate),единственное) -► [pieplate]. существительное(существительное(р1ер1а1е8),множественное) -► [pieplates]. существительное(существительное(8игрп5е), единственное) -► [surprise]. существительное(существительное(8игрп8С8), множественное) -► [surprises]. прилагательное (прилагательное(decoreted)) -► [decorated]. глагол (глагол (contains),единственное) -► [contains]. глагол(глагол(сотат,)множественное) -► [contain]. Программа 16.4. DC-грамматика с согласованием в числе субъекта и объекта. В следующем примере DC-грамматики используется еще одно свойство Пролога-возможность ссылаться в теле правила Пролога на произвольную цель. Программа 16.5 представляет собой грамматику для распознавания чисел, не превышающих одну тысячу. Числа записываются по-русски. При этом значение распознанного числа вычисляется с использованием арифметических средств Пролога. Основным отношением в данной грамматике является отношение number(N), где N- численное значение числа, подлежащего распознаванию. Согласно грамматике, специфицированной программой 16.5, некоторое число-это либо нуль, либо число N, состоящее не более чем из трех цифр. Такое число представляется предикатом xxx(N). Аналогично предикат xx(N) представляет числа, состоящие не
Синтаксический анализ для грамматик 209 number(O) -► [нуль]. number(N) -► xxx(N). xxx(N) -► цифра(О), [сто], остаток_ххх(К1), {N := D*100 + N1}. остаток.ххх(О) -► [ ]. остаток, ххх(N) -► [и], xx(N). хх(N) -» цифра(N). xx(N) -+ дцать(Ы). xx(N) -► десятки(Т), остаток .xx(Nl), {N := T + Nl}. остаток_хх(0) -► [ ]. остаток_хх(Ы) -► цифpa(N). цифра(1) -► [один], цифра(2) -► [два], цифра(3) -► [три], цифра(4) -► [четыре]. цифра(5) -► [пять], цифра(6) -* [шесть], цифра(7) -► [семь], цифра(8) -► [восемь], цифра(9) -► [девять]. десятки(20) -► [двадцать], десятки(30) -► [тридцать], десятки(40) -► [сорок], десятки(50) -► [пятьдесят], десятки(60) -► [шестьдесят], десятки(70) -► [семьдесят], десятки(80) -► [восемьдесят] десятки(90) -► [девяносто]. Программа 16.5. DC-грамматика для распознавания чисел. более чем из двух цифр. Предикаты остаток_ххх и остаток_хх обозначают остатки чисел из трех и двух цифр соответственно после удаления ведущей цифры. Предикаты цифра, дцатъ и десятки используются соответственно для распознавания цифр, чисел от десяти до девятнадцати включительно и чисел, кратных десяти, от 20 до 90 включительно. В грамматику необходимо ввести новую синтаксическую конструкцию, которая позволяла бы использовать произвольные цели Пролога. Такая возможность осуществляется путем помещения цели Пролога в фигурные скобки. Для иллюстрации рассмотрим следующее правило для определения чисел из трех цифр: xxx(N) -► цифра(О), [сто], остаток, ххх(N1), {N: = D*100 + N1}. Согласно этому правилу, в числе N из трех цифр первая цифра имеет значение D, за ней следует слово «сто», за которым в свою очередь следует остаток числа, имеющий значение N1. Значение числа N получается посредством умножения D на 100 и прибавления значения ЛГУ. DC-грамматики наследуют еще одно свойство логического программирования-использование возвратов при выполнении программы. Программа 16.5 может использоваться для порождения записи заданного числа, не превышающего тысячи. Эта грамматика, говоря техническим языком, порождает и допускает. При этом реализуется классический принцип - «порождать и проверять». Все допустимые грамматикой числа порождаются одно за другим и проверяются на правильность дцать(Ю) -* [десять]. дцать(П) -► [одиннадцать]. дцать(12) -► [двенадцать]. дцать(13) -+ [тринадцать]. дцать(14) -+ [четырнадцать]. дцать(15) -► [пятнадцать]. дцать(16) -► [шестнадцать]. дцать(17) -► [семнадцать] дцать(18) -► [восемнадцать]. дцать(19) -» [девятнадцать].
210 Часть III. Глава 17 значения до тех пор, пока не будет получено фактически заданное число. Конечно, это скорее необычный, нежели эффективный способ записи чисел. В общем случае порождающее свойство DC-грамматик не является полезным. Во многих грамматиках используются рекурсивные правила. Например, правило на рис. 16.1, которым группа_существителъного2 определяется как прилагательное, за которым следует группа_существительного2, является рекурсивным. Рекурсивно определенные грамматики применяются для порождения результатов в недетерминированных вычислениях. В грамматике, представленной программой 16.3, сначала обрабатываются группы существительных с произвольно большим количеством прилагательных, а затем рассматривается группа глагола. Упражнении к rvi. 16 1. Напишите простую грамматику для французского языка, в которой предусматривается согласование по родам. 2. Расширьте и измените программу 16.5 с целью разбора чисел, так чтобы она допускала разбор всех чисел, не превышающих одного миллиона. Не забудьте включить объекты типа «тридцать пять сотен» и не включайте «тридцать сотен». 3. Расширьте программу 16.2 для преобразования правила DC-грамматики в предложение Пролога. 16.1, Дополнительные сведения Язык Пролог с самого начала был связан с синтаксическим разбором. Как упоминалось выше, появление языка Пролог было подготовлено работами Колмероэ по синтаксическому анализу и Q-системам (Colmerauer, 1973). Разработчики языка Пролог-10 также были нацелены на обработку текстов на естественном языке; ими написан один из наиболее подробных отчетов по DC-грамматикам (Pereira и Warren, 1980). В этой публикации содержится хорошее обсуждение преимущества DC-грамматик как формализма для синтаксического разбора по сравнению с грамматиками ATN. Во многих версиях языка Пролог, например в языке Wisdom-Пролог, предусмотрены специальные средства, позволяющие автоматически преобразовывать правила грамматики по мере просмотра соответствующего файла. Несмотря на то что управляющие структуры Пролога непосредственно пригодны для построения синтаксических анализаторов, работающих по методу рекурсивного спуска и нисходящего разбора, средствами Пролога могут быть довольно легко реализованы и другие алгоритмы разбора. Например, Matsumoto и др. (1986) описывают реализованную на Прологе программу синтаксического анализа для восходящего разбора. Грамматика, представленная в программе 16.1, заимствована из книги Винограда по вычислительной лингвистике (Winograd, 1983). Глава 17 Программирование второго порядка В гл. 14 и 15 были рассмотрены методы программирования на Прологе, основанные непосредственно на логическом программировании. В этой главе программирование на Прологе расширяется введением методов, отсутствующих в
Программирование второго порядка 21 1 модели логического программирования. Эти методы основаны на свойствах языка, выходящих за рамки логики первого порядка. Они названы методами второго порядка, поскольку речь здесь идет о множествах и их свойствах, а не об отдельных элементах. В первом разделе вводятся множественные предикаты, которые в качестве решений вырабатывают множества. Вычисления с использованием множественных выражений приобретают особую силу, когда применяются в сочетании с уже рассмотренными методами программирования. Второй раздел посвящен некоторым приложениям множественных предикатов. В третьем разделе обсуждаются лямбда-выражения и предикатные переменные, которые позволяют рассматривать функции и отношения как «первоклассные» объекты данных. 17.1. Множественные выражения Решение какого-либо вопроса в программе на Прологе вызывает поиск некоторого примера вопроса, выводимого из программы. Что произойдет, если осуществлять поиск всех примеров вопроса, выводимых из программы? На декларативном уровне такой вопрос лежит вне модели логического программирования, представленной в гл. 1. Это вопрос второго порядка, поскольку он касается множества элементов с определенными свойствами. В случае операционного толкования он выходит и за рамки вычислительной модели чистого Пролога. В чистом Прологе вся информация относительно определенной ветви вычисления при осуществлении возврата теряется. Поэтому в чистом Прологе не существует простого способа нахождения множества всех решений для некоторого вопроса или даже определения того, сколько существует решений для данного вопроса. В этом разделе обсуждаются предикаты, позволяющие отвечать на вопросы второго порядка. Будем называть такие предикаты множественными. Их можно считать новыми примитивами, однако нельзя трактовать как подлинные расширения Пролога, поскольку они могут быть определены в самом Прологе посредством его внелогических предикатов, особенно таких, как assert и retract. Мы представляем их в языке в виде некоторого расширения более высокого порядка-квантификации по всем решениям-и покажем далее, как они могут быть реализованы. Как и при стандартной реализации отрицания, реализация множественных предикатов лишь аппроксимирует их логическое описание. Однако эта аппроксимация очень полезна во многих применениях, что будет показано в следующем разделе. Рассмотрим пример применения множественных предикатов, используя представленную на рис. 17.1 часть библейской базы данных программы 1.1. отец(терах,авраам). отец(аран,лот). отец(терах,нахор). отец(аран, милка). отец(терах,аран). отец(аран,иска). отец(авраам,исаак). мужчина(авраам). мужчина(аран). женщина(иска). мужчина(исаак). мужчина(нахор). женщина(милка). мужчина(лот). Рис. 17.1. Часть библейской базы данных. Пусть требуется отыскать всех детей определенного отца. Естественно представить себе предикат дети(X,Детки), где Детки - список детей X, которые должны быть «извлечены» из фактов отец, представленных на рис. 17.1. Наивный подход, основанный на применении накопителя, реализуется следующим образом:
Ч;)сп> III. Глава 17 дети(Х,Сз) <- дети(Х,[ ],Cs). дети(Х,А,Сз) <- отец(Х,С), not member(C,A), !, дети(Х,[С | A],Cs). Ae™(X,Cs,Cs). Эта программа на такой вопрос, как дети (терах,Xs)?, успешно отвечает Xs = [арандахордвраам]. Однако подход с использованием накопителей имеет два серьезных недостатка, которые препятствуют развитию более общих множественных предикатов. Первый недостаток состоит в том, что всякий раз, когда некоторое решение добавляется в накопитель, полное дерево поиска обходится заново. В общем случае повторные вычисления должны быть запрещены. Второй недостаток связан с тем, что существуют задачи с общностью. На такой вопрос, как дети (X,Cs)!9 будет дан ответ X = терах, Cs = [аран,нахор,авраам], а альтернативное решение при возврате из-за использования отсечения оказывается недоступным. Как только «свободные» переменные конкретизированы, получить альтернативное решение становится невозможно. Удаление же отсечения вызовет некорректное поведение программы при задании вопроса дети(терах,Н). Предпочтительный способ реализации множественных предикатов в Прологе основан на операционном поведении Пролога, в частности вызывающем побочные эффекты программы. Как это сделать, будет показано в конце раздела. Остановимся на двух примитивных множественных предикатах. Отношение bag-of( Term,Goal,Instances) истинно, если Instances - мультимножество всех примеров терма Term, для которых цель Goal истинна. Кратные тождественные решения сохраняются. Отношение set-of (Term,Goal,Instances) является некоторым усовершенствованием отношения bag-of, где Instances упорядочено, а повторяющиеся элементы удалены. Отношение дети легко теперь записать следующим образом: дети(Х,Детки) <- set_of(C,oTeu(X,C),fleTKM). Завершаемость выполнения множественных предикатов зависит от завершения работы с целью, примеры которой собираются. Для этой цели должен быть выполнен обход полного дерева поиска. Следовательно, бесконечная ветвь, встретившаяся в дереве поиска, приведет к незавершаемости поиска. Множественные предикаты могут иметь кратные решения. Рассмотрим вопрос set-of (Y,omeu(X,Y),Детки)1. Существует несколько альтернативных решений, соответствующих различным значениям X. Например, {X = терах,Детки = [авраам, нахордраи]} и {X = авраам,Детки = [исаак]} являются равно законными решениями, которые должны быть получены при поиске с возвратами. Однако существует другая интерпретация, применимая к вопросу set-of (Y, отец(X,Y),Детки)!. Он может рассматриваться как вопрос о всех детях (независимо от отца), упомянутых в фактах программы. В логической интерпретации это означает, что Детки есть множество всех значений У, таких, что существует некоторое X, для которого отношение отец(Х,У) истинно. Для представленных на рис. 17.1 фактов существует единственное решение этого вопроса: Детки = [авраам, нахордрап,исаак,лот,милка,иска]. Чтобы отличать подобные «экзистенциальные» вопросы от обычных, решаемых поиском с возвратами, используем дополнительное обозначение. Наш вопрос в принятой для вопросов второго типа форме запишется так: set-of (Y,X] (отец(X,Y),Детки). В общем случае некоторый вопрос Y]Goal означает, что существует некоторое У, такое, что цель Goal истинна. Множественные предикаты могут быть вложенными. Например, все отношения отец-дети могут быть вычислены при ответе на вопрос set -offОтец- Детки, set-of (X,родитель (Отец,X),Детки), Ys)l. Соответствующим решением будет [терах - [авраамдахордран], авраам - [исаак], арап - [лот,милка,иска]].
Программирование второго порядка 213 Существуют два возможных способа определения поведения предиката set-of (X,Goal,Instances), когда решение цели Goal завершается отказом, т.е. когда она не имеет истинных примеров. Определим предикаты set-of и bag-of так, чтобы их вычисление всегда было успешным; для этого, когда цель Goal не имеет решений, Instances определяется как пустой список. Такое определение предполагает, что «знаниями» в программе является все то, что истинно. Это аналогично аппроксимации отрицания «отрицанием как безуспешным вычислением». Можно определить варианты предикатов set-ofn bag-of, вычисление которых будет приводить к отказу, когда решений не существует. Эти новые отношения обозначим как set-ofl и bag-ofl и дадим их определения в программе 17.1. Множественные предикаты set_.oj7 (X, Goal, Instances) «- Instances -это множество примеров X (если такие существуют), для которых цель Goal истина. set._ofl(X,Goal, Instances) «- set_of(X,Goal, Instances), Instances = [I|Is]. bag..ofI (X, Goal, Instances) <- Instances -это мультимножество примеров X (если такие существуют), для которых цель Goal истинна. Кратность элемента мультимножества равна числу различных путей, которыми может быть доказана цель Goal для этого элемента, используемого в качестве примера X. bag_ of 1(X, Goal, Instances) <- bag of(X,Goal,Instances), Instances = [I | Is]. size_of(X,Goal,N) <- N -число различных примеров X, таких, что цель Goal истинна. size. of(X,Goal,N) <- set_of(X, Goal, Instances), length(Instances,N). length(Xs,N) <- См. программу 8.11. Программа 17.1. Множественные предикаты. Многие из рассмотренных ранее рекурсивных процедур можно переписать с использованием множественных предикатов. Например, программа 7.9 для удаления из списка повторяющихся элементов легко определяется с помощью предикатов для проверки принадлежности элементов множеству: no._doubles(Xs,Ys) <- set of(X,member(X,Xs),Ys). Это определение, однако, значительно менее эффективно, чем непосредственно записанная рекурсивная процедура. Вообще в современных реализациях Пролога рекурсивные программы оказываются более эффективными, чем программы, использующие множественные предикаты. Другие вспомогательные предикаты второго порядка можно определить, используя рассмотренные основные множественные предикаты. Так, подсчитать число различных решений можно с помощью программы size-of(X,Goal,N'), которая определяет число N решений X для некоторой цели Goal. Этот предикат представлен в программе 17.1. Если цель Goal не имеет решений, переменная N принимает значение 0. Если требуется, чтобы выполнение предиката size-of при отсутствии решений завершалось отказом, можно вместо предиката set-of использовать предикат set-ofl. Если же вместо предиката set-of применить предикат bag-of, то рассматриваемый нами предикат будет определять число решений с учетом кратных решений. Еще один вспомогательный множественный предикат предназначен для проверки того, все ли решения вопроса удовлетворяют определенному условию. Программой 17.2 определяется предикат for-all (Goal,Condition), истинный, когда условие
214 Часть III. Глава 17 formal I (Goal, Condition) Для всех решений цели Goal условие Condition истинно. for_all (Goal, Condition) <- set_of (Condition, Goal, Cases), check (Cases). check ([Case | Cases]) <- Case, check (Caces). check([ ]). Программа 17.2. Применение множественных предикатов. Condition истинно для всех значений цели Goal. В его определении использована метапеременная Case. С помощью вопроса/ог-а11(отец(Х,С),мужчина(С))? проверяется, какие отцы имеют детей только мужского пола. Ему соответствуют два ответа: X = терах и X = авраам. Более простой и эффективный, но менее универсальный вариант предиката far-all можно написать без использования множественных предикатов. Подобный эффект дает совместное использование недетерминизма и отрицания как безуспешного вычисления. Рассмотрим новое определение for_all(Goal,Condition) <- not(Goal,notCondition). Теперь предикат успешно отвечает на такой вопрос, как for-all(отец (теpax,X), мужчина(Х))?, однако попытка дать решение вопроса for -all(отец (X, С), мужчи- на(С))? приводит к отказу. В заключение этого раздела покажем, как реализовать простой вариант предиката bag-of Это обсуждение преследует две цели: оно иллюстрирует определенный стиль реализации множественных предикатов и вводит вспомогательный предикат, который пригодится нам в следующем разделе. Предикат find-all-dl (X.Goal,Instances) истинен, если Instances - мультимножество примеров X, представленных разностным списком, для которых цель Goal истинна. Эта процедура отличается от процедуры bag -of тем, что поиск альтернативных решений происходит без использования возвратов. Л Определение предиката find-all-dl дает программа 17.3. Программа имеет только операционное толкование. В ней имеются два этапа, описанные двумя предложениями find-all-dl. Явный отказ в первом предложении гарантирует выполнение второго предложения. Первый этап находит все решения для цели Goal, используя управляемый отказами цикл, добавляющий по мере обработки соответствующие примеры X. Второе предложение выбирает эти решения. Введение "$тагк" существенно для корректного выполнения вложенных множественных выражений-заимствования одним множественным выражением решений, полученных другим множественным выражением. find_all_dl (X, Goal, Instances) <- Instances -это мультимножество примеров X, для которых цель Goal истинна. Кратность элемента мультимножества равна числу различных путей, которыми может быть доказана цель Goal для этого элемента, используемого в качестве примера X. find_all_dl(X,Goal,Xs) <- asserta ($instances (Smark)), Goal, asserta($instance(X)), fail. find_all_dl(X,Goal,Xs\Ys) +- retract($instance(X)), reap(X,Xs\Ys),!.
Программирование второго порядка 215 reap(X,Xs\Ys)<- X Ф $mark, retract($instance(Xl)),!, reap(XI,Xs\[X|Ys]). reap($mark, Xs\ Xs). IIpoipaMMa 17.3. Реализация множественного предиката с использованием разностных списков и внелогических предикатов assert и retract. Упражнения к разд. 17.1 1. Используя множественное выражение, определите предикат intersect(Xs,Ys,Zs) для поиска пересечения Zs двух списков Xs и Ys. Что должно произойти, если указанные списки не пересекаются? Сравните вашу программу с рекурсивным определением предиката intersect. 17.2. Применения множественных выражений Множественные выражения являются существенным дополнением к Прологу. Их использование вкупе с другими, уже рассмотренными методами программирования для многих задач приводит к искусным решениям. В этом разделе в качестве примеров представлены три программы: программа обхода графа в ширину, программа трассировки СБИС по алгоритму Ли и программа порождения ключевого слова в контекстном указателе. В разд. 14.2 были рассмотрены три программы 14.8, 14.9 и 14.10 для обхода графа в глубину. Здесь обсуждаются эквивалентные программы для обхода графа в ширину. connected (XtY) <- Связь вершины X с вершиной У в ориентированном ациклическом графе определяется предложением edge/2. connected(X,Y) <- connected_bfs([X|Xs])\Xs,Y). connected_bfs([ ]\[ ],Y) <- !, fail. connected_bfs([X | Xs] \ Ys,X). connected..bfs([X | Xs]\ Ys,Y) <- find_all_dl(N,edge(X,N),Ys\Zs), connected._bfs(Xs \ Zs, Y). Данные edge (a, b). edge(f,i). edge(e,k). edge(z,x). edge (a, c). edge(c,f). edge(d,j). edge(y,u). edge (a, d). edge (c,g). edge(x,y). edge(z,v). edge (a, e). edge(f,h). edge(y,z). Программа 17.4. Программа и тестовые данные для проверки алгоритма обхода в ширину ориентированного ациклического графа. Основным отношением является connected(X,Y), которое истинно, если вершины X и У связны. Это отношение определено в программе 17.4. Поиск в ширину реализуется посредством ведения такой очереди вершин, следуя которой происходит обход графа в ширину. Соответственно предложение connected вызывает отношение connected-bfs(Queue, Y), которое истинно, если Y принадлежит связному компоненту графа, представленному вершинами в очереди Queue.
216 Часть III. Глава 17 При каждом обращении к предикату connected-bfs текущая вершина извлекается из очереди, находится множество связанных с ней вершин, которые добавляются в конец очереди. Очередь представлена разностным списком. Кроме того, здесь используется множественный предикат find-all-dl. Если очередь пуста, то выполнение программы завершается отказом. Поскольку разностные списки являются неполными структурами данных, необходимо предусмотреть явную проверку пустоты очереди. В противном случае выполнение программы может не завершаться. Рассмотрим факты edge в программе 17.4, представляющие граф, показанный на рис. 14.3 слева. Для них вопрос connected(a,X) дает решения Ь, с, d, e,f, g, j, к, h, i как результат обхода графа в ширину. Программа 17.4, подобно программе 14.8, пригодна для обхода конечного дерева или ориентированного ациклического графа. В случае циклического графа выполнение программы не завершится. connected (X, Y) <- Связь вершины X с вершиной У в графе определяется предложением edge/2. connected(X,Y) <- connected([X|Xs]\Xs,Y,[X]). connected([ ]\[ ],Y,Visited) <- !, fail. connected([A |Xs]\Ys, A, Visited), connected ([A | Xs]\Ys,B, Visited) <- set_of(N,edge(A,N),Ns), filter([Ns,Visited,Visitedl,Xs\Ys,Xsl), connected (Xsl,B, Visited 1). filter([N | Ns],Visited, Visited l,Xs,Xsl) <- member(N, Visited), filter(Ns, Visited, Visitedl,Xs,Xsl). filter([N | Ns],Visited,Xs\[N | Ys],Xs\ Ys) <- not member(N, Visited), filter(Ns,[N | Visited],Visitedl,Xs\ Ys,Xsl). filter([ ],V,V,Xs,Xs). Программа 17.5. Проверка связности графа путем обхода в ширину. По сравнению с программой 17.4 программа 17.5 более совершенна, в ней, в частности, ведется список просмотренных вершин графа. В конец очереди добавляются не все дочерние вершины, а только те, которые еще не просматривались. Такая проверка-в программе 17.5 выполняется с помощью предиката filter. На самом деле программа 17.5 является более мощной, чем ее эквивалент с обходом в глубину-программа 14.10. Она не только осуществляет корректный обход любого конечного графа, но с тем же успехом может использоваться и для обхода бесконечных графов. Полезно заметить, что расширения чистого Пролога необходимы для увеличения эффективности поиска на графах. Используя чистый Пролог, можно писать корректные программы поиска на конечных деревьях и ориентированных ациклических графах. Добавление отрицания позволяет реализовать корректный поиск на конечных графах с циклами, а множественные выражения необходимы для работы с бесконечными графами. На рис. 17.2 эти соображения представлены в сжатом виде. Определение пути между двумя вершинами графа представляет собой несколько более сложную задачу, чем поиск в глубину. Вместе с каждой вершиной в очереди здесь необходимо хранить список вершин, по которым проходит путь, связывающий ее с исходной вершиной. Этот метод демонстрируется в программе 18.6 в следующей главе.
Программирование второго порядка 217 (1) Конечные деревья и ориентированные ациклические графы. Чистый Пролог. (2) Конечные графы. Чистый Пролог + отрицание. (3) Бесконечные графы. Чистый Пролог + множественные выражения + отрицание. Рис. 17.2. Средства Пролога для решения различных задач поиска.на графах. Другой пример объединяет мощность недетерминированного программирования со средствами программирования второго порядка. Это программа определения трассы минимальной стоимости между двумя точками схемы, реализующая алгоритм Ли. Задача формулируется следующим образом. Дана сетка, на которой могут быть размещены препятствия. Необходимо найти кратчайший путь между двумя заданными точками сетки. Пример сетки с препятствиями показан на рис. 17.3. Жирной сплошной линией показан кратчайший путь между точками А и В. Препятствия представлены заштрихованными прямоугольниками. Прежде всего сформулируем задачу в подходящем для программирования виде. СБИС моделируется сеткой точек, обычно располагаемой в первом квадранте декартовой плоскости. Трассой называется путь между двумя точками сетки, составленный только из горизонтальных и вертикальных отрезков, располагаемых в пределах сетки и не проходящих через препятствия. Точки на плоскости представлены своими декартовыми координатами и обозначены как X — Y. На рис. 17.3 точка А имеет координаты / — 7, точка В-координаты 5-5. Обозначение удобно для восприятия и использует «-» в качестве инфиксного бинарного оператора. Путь, определяемый программой, представляется списком точек, лежащих на пути, включая концевые точки. Так, трасса, показанная на рис. 17.3 жирной сплошной линией, есть список 15—5,5 — 4,5 — 3,
218 Часть III. Глава 17 Предикатом верхнего уровня в программе является предикат lee-route (Л,В, Obstacles,Path), где Path - кратчайшая трасса из точки А в точку В схемы, Obstacles-препятствия на сетке. Программа выполняется в два этапа. На первом этапе, начиная с исходной точки, генерируются последовательные волны, состоящие из соседних точек сетки. Процесс продолжается до тех пор, пока не будет достигнута конечная точка. На втором этапе из накопленных волн выделяется искомый путь. Рассмотрим некоторые элементы программы 17.6, представляющей собой полную программу трассировки схемы по алгоритму Ли. Волны определяются индуктивно. Начальная волна-список [А]. Последова- lee_route (Source, Destination,Obstacles, Path) <- Path-путь минимальной длины, ведущий из исходной точки Source к конечной точке Destination и не пересекающий препятствий Obstacles. lee_route(A, В,Obstacles, Path) <- waves(B, [[A], [ ]], Obstacles, Waves), path(A,B, Waves, Path). waves (Destination, WavesSoFar, Obstacles, Waves) <- Waves -список волн, включающий «текущие», волны WavesSoFar (возможно, кроме последней), которые ведут к точке Destination, минуя препятствия Obstacles. waves(B, [Wave | Waves], Obstacles, Waves) <- member (В, Wave),!. waves(B, [Wave, Last Wave | LastWaves], Obstacles, Waves) «- next_wave(Wave, Last Wave, Obstacles, Next Wave), waves(B, [Next Wave, Wave, Last Wave | LastWaves], Obstacles, Waves). next_wave ( Wave, Last Wave, Obstacles, Next Wave) <- NextWave-множество допустимых точек из волны Wave, исключая точки из «последней волны» Last Wave и волны Wave, а также точки, покрытые препятствиями Obstacles. next_wave(Wave, Last Wave, Obstacles, NextWave) «- set_of(X, admissible(X, Wave, Last Wave, Obstacles), NextWave). admissible(X, Wave, Last Wave, Obstacles) <- adjaccnt(X, Waves,Obstacles) <- not member(X, Last Wave), not member(X,Wave), adjacent(X, Wave, Obstacles) «- member(X 1, Wave), neighbor(Xl,X), not obstucted(X,Obstracles). neighbor(Xl - Y,X2-Y) <- next_to(Xl,X2). neighbor(X-Yl,X-Y2) <- next. to(Yl,Y2). next_to(X,Xl)<-Xl := X+l. next_to(X,Xl)<-X>0,Xl := X-l. obstucted(Point,Obstaclcs) «- member(Obstacles, Obstacles), obstructs(Point, Obstacle). obstructs(X-Y,obstacle(X-Yl,X2-Y2))«-Yl<Y,Y<Y2. obstructs(X - Y,obstacle(X 1 - Y1 ,X - Y2)) <- Y1 ^ Y, Y ^ Y2. obstructs(X-Y,obstacle(Xl-Y,X2-Y2))«-Xl^X,X<X2. obstructs(X-Y,obstacle(Xl-Yl,X2-Y))<-XKX,X^X2. path(Source, Destination, Waves, Path) <- Path-путь из точки Sourse в точку Destination, проходящий через волны Waves.
Программирование второго порядка 219 path(A,A,Waves,[A])<-!. path(A,B,[Wave | Waves],[B | Path]) <- member(B 1, Wave), neighbor(Bl,Bl), !, path (А, В1, Waves, Path). Данные и предложения для тестирования test_lee(Name,Path) <- data(Name, А, В, Obstacles). lee_route(A, B,Obstacles, Path). data(test,l-l,5-5,[obstacles(2-3,4-5), obstacles(6-6,8-8)]). Программа 17.6. Программа трассировки по алгоритму Ли. тельные волны-это множества точек сетки, соседние с точками предыдущей волны и не попавшие в предыдущие волны. Это определение иллюстрируется изображением волн на рис. 17.3 тонкими сплошными линиями. Генерация волн осуществляется с помощью предиката waves (B,WavesSoFar, Waves), который определяет список волн Waves, достигнувших конечной точки В. Параметр WavesSoFar- накопитель волн, образованных с начала их генерации от исходной точки к текущему шагу. Вычисления по этому предикату завершаются, когда текущая волна накроет конечную точку. В теле предложения waves используется предикат next-.wave, в котором для поиска соответствующих точек сетки применены множественные выражения. Предполагается, что препятствия являются прямоугольными блоками. Для их представления используется терм obstacle (L,R), где L- координаты левого нижнего угла, R-координаты правого верхнего угла блока. В упражнении к этому разделу предлагается изменить программу так, чтобы она допускала обработку препятствий другой формы. Предикат find-path (А,В,Waves,Path) используется для нахождения обратного пути Path из Л в Л по сгенерированным ранее волнам. При построении пути производится спуск вниз в соответствии с порядком точек от В к А. Этот порядок можно изменить, используя в предикате find-path накопитель. В процессе нахождения пути по алгоритму Ли вывод даных в программе 17.6 не предусмотрен. На практике пользователь может пожелать увидеть результаты текущих вычислений. Это легко осуществить добавлением соответствующих операторов write в процедуры next-wave и find-path. В последнем примере этого раздела решается задача поиска ключевых слов в контексте. И вновь объединение недетерминированного программирования с программированием второго порядка позволяет с помощью простой Пролог-программы решить сложную задачу. Нахождение ключевых слов в контексте связано с поиском всех вхождений множества ключевых слов в определенный текст и выделением контекстов, в которых они встречаются. Здесь будет рассмотрен следующий вариант этой общей задачи: «Для данного списка заголовков сформировать упорядоченный список всех вхождений в данные заголовки множества ключевых слов вместе с их контекстами». Пример входных и ожидаемых выходных данных для такой программы представлен на рис. 17.4. Контекст описывается как вращение заголовка, конец которого отмечен вертикальной чертой «|». В рассматриваемом примере использован следующий набор «нетривиальных» ключевых слов: algorithmic, debugging, logic, problem, program, programming, prolog и solving. В данной задаче подлежит вычислению отношение kwic(Titles,KwicTitles), где Titles-список заголовков, из которых должны быть выделены ключевые слова, а
220 Часть III. Глава 17 Вход: programming in prolog logic for problem solving logic programming algorithmic program debugging Выход: algorithmic program debugging |, debugging | algorithmic program, logic for problem solving |, logic programming,!, problem solving | logic for, program debugging | algorithmic, programming in prolog |, programming | logic, prolog | programming in, solving | logic for problem Рис. 17.4. Вход и выход для задачи поиска вхождений ключевых слов в контексте. KwicTitles- отсортированный список ключевых слов в их контекстах. Предполагается, что входной и выходной тексты представлены в виде списков слов. В более общей программе должны быть предусмотрены предварительный шаг преобразования входного текста в свободной форме в списки слов, а также выдача результатов в удобном для чтения виде. Рассмотрим поэтапное представление программы. Основа программы - недетерминированное описание вращения списка слов. С помощью предиката append ему можно дать следующее элегантное определение: rotate(Xs,Ys)«- append(As,Bs,Xs), append(Bs,As,Ys). Декларативно: Ys- вращение Xs, если Xs состоит из As, за которым следует Bs, a Ys состоит из Яу, за которым следует As. Следующий этап связан с идентификацией отдельных слов как потенциальных ключевых слов. Это выполняется посредством выбора слова в первом вызове предиката append. Отметим, что следующее новое правило является примером предыдущего правила rotate: rotate(Xs,Ys) <- append(As,[Key | Bs],Xs), append([Key | Bs],As,Ys). Кроме того, это определение лучше предыдущего, поскольку в нем предусмотрено удаление одинаковых решений, когда один из расщепленных списков пуст, а другой-полон. Следующее улучшение связано с более детальной проверкой потенциального ключевого слова. Предположим, что каждое ключевое слово Word определяется фактом вида keywordf Word). Решения в процессе rotate могут быть отфильтрованы так, чтобы воспринимались только те слова, которые идентифицированы как ключевые. Соответствующая версия рассматриваемого предложения имеет вид rotate._and_.filter(Xs,Ys) <- append(As,[Key | Bs],Xs), keyword(Key), append([Key | Bs],As,Ys). Операционное поведение данной процедуры: она рассматривает все ключевые слова, отфильтровывая нежелательные альтернативы. Для максимизации эффективности программы в данном случае важен порядок целей. В программе 17.7, окончательной версии интересующей нас программы, предусмотрены дополнительные средства для распознавания ключевых слов. Любое слово Word считается ключевым, если оно не специфицировано с помощью факта вида insignificant(Word) как незначащее. Кроме того, в процедуре выполняется вставка в конец заголовка маркера «|», отмечающего контекстную информацию. Это реализуется добавлением дополнительного символа во втором обращении к предикату append. Соответствующее предложение rotate^and-fllter содержится в программе 17.7. Наконец, для получения всех решений использован множественный предикат. Квантификация необходима по всем возможным заголовкам. Явным преиму-
Программирование второго порядка 221 kwic(Titles, К WTitles) <- К WTitles - К WlС-индеке списка заголовков Titles. kwic(Titles,KWTitles)<^ set. of(Ys,Xs | (member (Xs,Titles), rotate. and_.filter(Xs,Ys)),KWTitles). rotate, and.filter (Xs, Ys) <- Ys вращение списка Xs, такое, что первое слово Ys значимо, и знак '|' вставлен после последнего слова Xs. rotate_andJilter(Xs,Ys) «- append (As, [Key | Bs],Xs), not insignificant(Key), append([Key,T| Bs],As,Ys). Словарь незначимых слов insignificant(a). insignificant(the). insignificant(in). insignificant (for). Предложения и данные для тестирования test kwic(Books,Kwic) <- titlcs(Books,Titlcs),kwic(Titles,Kwic). titles(lp,[[logic,for,problem,solving], [logic, programming], [algorithmic, program, debugging], [programming, in, prolog]]). Программа 17.7. Создание индекса в задаче поиска ключевых слов вместе с их контекстами. ществом использования предиката set-of является сортировка ответов. Полная программа для решения рассматриваемой задачи (программа 17.7)-элегантный пример выразительной мощности Пролога. Для тестирования в программе использован предикат test-kwic/2. Упражнения к разд. 17.2. 1. Измените программу 17.6 так, чтобы можно было обрабатывать препятствия, отличные от прямоугольников. 2. Адаптируйте программу 17.7 таким образом, чтобы она могла выделять ключевые слова из строк текста. 3. Напишите программу для поиска минимального остовного дерева. 4. Используя алгоритм Форда Фалкерсона нахождения максимального потока в сети, напишите соответствующую программу. 17.3. Другие предикаты второго порядка Логика первого порядка позволяет производить квантификацию по отдельным элементам. Логика второго порядка, кроме того, позволяет проводить квантификацию по предикатам. Включение такого расширения в логическое программирование влечет за собой использование правил с целями, предикатные имена которых являются переменными. Имена предикатов становятся «первоклассными» объектами данных, допускающими модификацию и обработку. Простым примером отношения второго порядка является установление, все ли элементы некоторого списка обладают определенным свойством. Для простоты
222 Часть III. Глава 17 предполагается, что это свойство описано как унарный предикат. Определим отношение has -property(Xs,P), которое истинно, если элемент из Xs обладает некоторым свойством Р. Расширение синтаксиса Пролога переменными именами предикатов позволяет определить отношение has-property так, как показано на рис. 17.5. Поскольку в определении предиката has-property можно использовать переменные свойства, он является предикатом второго порядка. Пример использования этого предиката - вопрос has ..property (Xs,мужчина)'?, проверяющий «все ли люди, входящие в список Xs,мужчины!». Еще один предикат второго порядка- тар-list (Xs, P, Ys). Здесь Ys - отображение списка Xs по предикату Р. Это означает, что для каждого элемента X из Xs существует соответствующий элемент У из Ys, такой, что P(X,Y) истинен. В Ys сохраняется порядок элементов списка Xs. Используя предикат тар-.list, можно переписать некоторые программы из предыдущих глав. Например, программа 7.8, отображающая набор английских слов в слова на французком языке, может быть выражена как предикат тар-list(Words,dict Mots). Предикат тар-list, как и предикат has-property, легко определить, используя переменное имя предиката. Это определение дано на рис. 17.5. has_property([X | Xs],P) <- P(X),has._property(Xs,P). has_property([ ],P). map_Jist([X|Xs],P,[Y|Ys])<- P(X,Y), map_Jist(Xs,P,Ys). map_list([ ],P,[ ]). Рис. 17.5. Предикаты второго порядка. В операционном понимании возможность использования переменных имен предикатов влечет за собой динамическую конструкцию целей в процессе решения вопроса. При постановке вопроса вычисляемое отношение не остается статически неизменным, а динамически определяется в процессе вычисления. В некоторых версиях Пролога программист может использовать переменные для имен предикатов и синтаксис, представленный на рис. 17.5. Однако необходимости в таком усложнении синтаксиса нет. Пригодные для реализации предикатов второго порядка средства уже существуют. Достаточно использовать одно базовое отношение, которое будем называть предикатом apply, он предназначен для конструирования цели с переменным функтором. Предикат apply определяется множеством предложений-по одному предложению для каждого имени функтора и арности. Например, для функтора foo арности п требуется записать предложение apply(foo,X 1,.. .,Хп) <- foo(X 1,.. .,Хп). Два предиката, определенные на рис. 17.5, представлены в программе 17.8 на стандартном Прологе. Определения предложений apply даны здесь для упомянутых в книге примеров. Предикат apply осуществляет структурный контроль. Полный набор предложений apply может быть обобщен посредством использования примитива структурного контроля univ. Обобщенный предикат apply (P,Xs) применяет предикат Р к списку аргументов Xs: apply(F,Xs) <- Goal =..[F|Xs],Goal. Эту функцию можно обобщить так, чтобы ее можно было применять, начиная с имени предиката, т.е. некоторого атома, и кончая термом, параметризованным переменными. Примером является подстановка значения в список. Если допускается параметризация, то отношение Substitute/4 в программе 9.3 может рассматриваться как пример предиката mapjist. А именно: предикат map_list(Xs,substitute (Old, New), Ys) дает тот же самый результат (список Ys получается в результате замены в списке
Программирование второго порядка 223 has_property(Xs,P) <- Каждый элемент списка Xs обладает свойством Р. has_property([X | Xs],P) <- apply(P,X), has_property(Xs,P). has_property([ ],P). apply (мужчина, X) <- мужчина(Х). maplist(Xs,P,Ys) <- Каждый элемент списка Xs находится в отношении Р с соответствующим ему элементом списка Ys. map_list([X | Xs],P,[Y | Ys]) <- apply(P,X,Y), map_list(Xs,P,Ys). map_list([ ],P,[ ]). apply(dict,X,Y)<-dict(X,Y). Программа 17.8. Предикаты второго порядка в языке Пролог. Xs элемента Old на элемент New), что и отношение, вычисляемое программой 9.3. Для того чтобы корректно выполнять рассмотренные действия, предложение apply путем незначительных изменений должно быть приведено к следующему виду: apply(P,Xs) <- Р =.. Ll,append(Ll,Xs,L2),Goal =.. L2,Goal. Использование предиката apply в теле предложения mapJist ведет к неэффективным программам. Например, непосредственное применение отношения Substitute по сравнению с реализацией тех же действий с помощью предиката mapJist приводит к значительному уменьшению числа образуемых промежуточных структур и упрощению компиляции. Следовательно, предикаты второго порядка лучше использовать совместно с системой преобразования программ, которая может во время компиляции транслировать выражения с предикатами второго порядка в выражения с предикатами первого порядка. Предикат apply можно также использовать для реализации лямбда-выражений. Лямбда-выражение имеет вид lambda(XJt...,XJ. Expression. Если заранее известно множество подлежащих использованию лямбда-выражений, то они могут быть поименованы. Например, приведенное выше выражение можно заменить некоторым уникальным идентификатором, скажем идентификатором foo, и определить предложением apply: apply (foo, XI,..., Xn) <- Expression. Распространенность лямбда-выражений и предикатов второго порядка типа has_property и mapJist ограничивается языками функционального программирования, такими, как Лисп, но мы полагаем, что это положение обусловлено предубеждением и наличием множества альтернативных методов программирования. Возможно, что активно ведущаяся работа по расширению модели логического программирования предикатами более высокого порядка и по интеграции логического и функционального программирования изменит существующую картину. А пока множественные выражения представляются основными и наиболее полезными конструкциями высокого уровня в Прологе. 17.4. Дополнительные сведения Прекрасное, подробное описание множественных предикатов языка Пролог-10 дано Уорреном (Warren, 1982a). Он объяснил их основные логические свойства. В выборе в качестве
224 Часть III. Глава 18 базового множественного предиката Set_of, а не предиката setjofl (в Прологе-10 он обозначен как setoj) наше мнение расходится с мнением Уоррена. Множественные предикаты являются мощным расширением Пролога. Они могут использоваться (к сожалению, неэффективно) для реализации «отрицания как безуспешного вычисления» и предикатов металогического типа (Kahn, 1984). Если цель G не имеет решений, определяемых таким предикатом, как set_of, то notG истинно. Предикат var(X) реализуется проверкой того, имеет ли цель два решения X = 1; X = 2. Дальнейшее обсуждение такого поведения множественных предикатов и обзор различных реализаций множественных предикатов можно найти в (Naish, 1985b). Более подробное описание алгоритма Ли и общей задачи трассировки СБИС имеется в учебниках по СБИС, например в (Breuer и Carter, 1983). Задача о поиске ключевых слов в контексте была предложена Перлисом как контрольная для сравнения языков программирования высокого уровня. Она была использована для сравнения нескольких языков. Реализацию этой задачи на Прологе мы находим наиболее элегантной из всех известных нам. Наше описание лямбда-выражений дано под влиянием работы (Warren, 1982a). Такие предикаты, как apply и mapjist, были включены в пакет сервисных программ Пролог-системы Эдинбургского университета. Некоторое время они были в моде, но потом утратили благосклонность пользователей из-за неэффективной компиляции и отсутствия средств преобразования на уровне исходной программы. Глава 18 Методы поиска В этой главе рассматриваются программы, охватывающие классические для задач искусственного интеллекта методы поиска. В первом разделе обсуждаются общие понятия и принципы поиска в пространстве состояний для задач, формулируемых в терминах графов пространства состояний. Во втором разделе рассматривается минимаксный алгоритм с альфа-бета отсечением для поиска на дереве игры. 18. L Поиск на графах пространства состояний Графы пространства состояний используются для представления задач. Вершины графа являются состояниями задачи. Две вершины графа связаны ребром, если существует некоторое правило перехода {move), согласно которому производится преобразование одного состояния в другое. Решение задачи состоит в поиске пути из заданного начального состояния в состояние, соответствующее искомому решению, посредством применения последовательности правил перехода. . Программа 18.1 является базовой для решения задач путем поиска на графах пространства состояний с применением поиска в глубину, описанного в разд. 14.2. Никаких ограничений для представления состояний вводить не следует. Переходы будем описывать бинарным предикатом move (State, Move), где Move - правило перехода, применяемое к состоянию State. Предикат update (State, Move, State I) используется для поиска состояния State I, достижимого с помощью применения правила Move к состоянию State. В ряде случаев процедуры move и update целесо-
Методы поиска 225 образно объединять. Здесь они остаются раздельными для простоты изложения и сохранения гибкости и модульности программ, возможно, в ущерб эффективности. Допустимость возможных переходов оценивается предикатом legal (State), который проверяет, удовлетворяет ли состояние задачи State ограничениям задачи. Для того чтобы предупредить зацикливание, программа сохраняет ранее пройденные состояния. Последовательность переходов из начального состояния в конечное строится путем наращивания в третьем аргументе предиката solve_dfs/3. solve_ dfs (State, History, Moves) <- Moves- последовательность переходов до достижения требуемого конечного состояния из текущего состояния State. History содержит ранее пройденные состояния. solve_dfs(State, History,[ ]) <- final state(State). solve...dfs(State,History,[Move | Moves]) <- move(State, Move), update(State, Move, State 1), legal(Statel), not member(Statel,History), solve_.dfs(Statel,[State 11 History],Moves). Предложение для тестирования базовой программы test__dfs(Problem, Moves) <- initiaLstate(Problem, State), sol ve„dfs (State, [State], Moves). Программа 18.1. Базовая программа организации поиска в глубину в пространстве состояний. Чтобы использовать базовую программу организации поиска при решении задачи, программист должен принять решения о представлении состояний и аксиоматизации процедур move, update и legal. Успешность применения базовой программы в значительной степени зависит от выбора подходящего представления состояний. Допустим, базовая программа применяется для решения известной задачи о волке, козе и капусте. Сформулируем эту задачу неформально. У фермера есть волк, коза и капуста, и все они находятся на левом берегу реки. Фермер должен перевезти это «трио» на правый берег, но в лодку может поместиться что-то одно - волк, коза или капуста. Существенно в этой задаче то, что рискованно оставлять волка вместе с козой (волки неравнодушны к козлятине), равно как и козу с капустой (козы обожают капусту). Фермер всерьез озадачен сложившимся положением и не желает нарушать экологическое равновесие ценой потери пассажира. Состояния представляются тройкой wgc(B,L,R), где Я-местонахождение лодки (левый или правый берег), L-список находящихся на левом берегу, R-список находящихся на правом берегу. Начальным и конечным состояниями являются wgc(left,.{wolf,goat,cabbage~],{ ~\) и wgc(right,{ ~\,{wolf goat,cabbage']) соответственно. На самом деле нет необходимости вести списки «обитателей» правого и левого берегов. Зная, кто в данный момент находится на левом берегу, легко определить обитателей правого берега, и наоборот. Использование двух списков лишь упрощает описание переходов. Для проверки зацикливания удобно сохранять списки обитателей в отсортированном виде. Таким образом, волк всегда будет в списке перед козой, и оба они-перед капустой, если, конечно, все «пассажиры» находятся на одном берегу. Переходы из состояния в состояние - это перевозка обитателей с одного берега 8-1402
226 Часть III. Глава IS на другой и поэтому может быть специфицирована конкретным «пассажиром», которого будем называть грузом (Cargo). Ситуация, когда фермер переправляется через реку один, специфицируется грузом alone (без груза). Недетерминированное поведение предиката member позволяет, как показано в программе 18.2, сжато описывать все возможные переходы тремя предложениями: для перевозки с левого берега на правый, для перевозки с правого берега на левый и для одиночного плавания фермера в любом направлении. Состояния в задаче о волке, козе и капусте представляются структурой вида wgc (Boat,Left,Right), где Boat-берег, у которого в данный момент находится лодка, Left -список обитателей левого берега, Right-список обитателей правого берега. initial_state(wgc,wgc(left,[wolf,goat,cabbage], [ ])). final_state(wgc(right, [ ],wolf,goat,cabbage])). move (wgc (left, L,R), Cargo) <- member (Cargo, L). move(wgc(right,L,R),Cargo) <- member(Cargo,R). move(wgc(B, L, R), alone). update(wgc(B,L,R), Cargo, wgc(Bl, LI, Rl))<- update_boate(B, В1), update_banks(Cargo, B, L, R, L1, R1). update_boat (left, right). update_boat (right, left). update_banks (alone, B, L, R, L, R). update_banks(Cargo,left,L,R,Ll,Rl) <- select (Cargo, L, LI), insert (Cargo, R,R1). update_banks (Cargo, right,L, R, L1, R1) <- select (Cargo, R, R1), insert (Cargo, L, L1). insert(X,[Y|Ys],[X,Y|Ys])<- precedes(X,Y). insert(X,[Y|Ys],[Y|Zs])<- precedes(Y,X), insert(X,Ys,Zs). insert(X,[ ],[X]). precedes (wolf, X). precedes (X, cabbage). legal(wgc(left,L,R)) <- not illegal(R). legal(wgc(right,L,R))<- not illegal(L). illegal(L)<- member(wolf,L), member(goat,L). illegal(L)<- member(goat,L), member(cabbage,L). Программи IS.2. Программа для решения задачи о волке, козе и капусте. Для каждого из указанных переходов должна быть специфицирована процедура, которая изменяла бы местонахождение лодки updateJboat/2 и состав обитателей берегов updateJbanks. Использование предиката select позволяет дать компактное описание модифицирующих процедур. Для поддержания списка обитателей берега в упорядоченном состоянии используется процедура update_banks/3, облегчающая проверку на зацикливание. В ней учтены все варианты расширения состава обитателей на берегу. Наконец, должна быть специфицирована проверка допустимости переходов. Существующие здесь ограничения просты. Волк и коза, равно как и коза с капустой, не могут в отсутствие фермера находиться на одном берегу.
Методы поиска Программа 18.2 объединяет в дополнение к базовой программе 18.1 все факты и правила, необходимые для решения задачи о волке, козе и капусте. Ясность программы говорит сама за себя. и-ы 8л 5л 4л 1*150. 18.1. Задача о кувшинах. Теперь обратимся к применению базовой программы поиска в пространстве состояний для решения другой классической в занимательной математике задачи поиска - задачи о кувшинах с водой. Имеются два кувшина вместимостью 8 и 5 л, и необходимо отмерить 4 л из бочки на 20, а может быть, больше литров. Возможными операциями являются: наполнение кувшина жидкостью из бочки, выливание содержимого кувшина в бочку, переливание из одного кувшина в другой до полного опустошения первого, либо до полного заполнения второго. Рис. 18.1 поясняет суть задачи. Рассматриваемая задача может быть обобщена на N кувшинов с емкостями С\ CN. Требуется измерить объем V, отличный от любого из Cit но не превышающий емкости наибольшего кувшина. Решение существует, если величина V кратна наибольшему общему делителю чисел С{. Для случая двух кувшинов задача имеет решение, поскольку 4 кратно наибольшему общему делителю чисел 8 и 54 Рассмотрим решение этой частной задачи для двух кувшинов произвольной емкости, однако этот подход непосредственно обобщается на любое число кувшинов. Предполагаться, что в базе данных содержатся два факта capacity (I,CI) для /, равного / и 2. Естественным представлением состояния будет структура jugs(Vl,V2), где VI и V2 представляют объемы жидкости, содержащейся в соответствующих кувшинах в текущий момент. Начальное состояние задается структурой jugs (0, 0), а желаемое конечное состояние выражается либо структурой jugs (0, X), либо структурой jugs(X,0), где Х-искомый объем. Предполагая, что первый кувшин имеет большую емкость, чем второй, достаточно специфицировать только одно конечное состояние jugs(X,0), так как требуемое количество жидкости легко перелить из второго кувшина в первый (сначала освобождается первый кувшин, а затем в него переливается содержимое второго). Эти данные для решения задачи о кувшинах, объединенные с программой 18.1, дают программу 18.3. В программе определено шесть переходов (предложений move): два-для наполнения каждого из кувшинов, два-для освобождения каждого из кувшинов и два-для переливания из одного кувшина в другой. Примером факта, соответствующего заполнению первого кувшина, является move (jugs (VI, V2)fill(l)). Явное задание состояний кувшинов позволяет данным «сосуществовать» с данными для решения других задач, например, таких, как задача, представленная программой 18.2. Переходы, связанные с опустошением кувшинов, оптимизированы так, чтобы не опустошать уже пустой кувшин. Модифицирующая процедура, связанная с первыми четырьмя переходами, проста, в то время как операция переливания имеет два варианта. Если общий объем жидкости в кувшинах меньше чем емкость наполняемого кувшина, то опустошаемый кувшин освободится, а наполненный п Поскольку 5 и 8 взаимно простые, то можно отлить не только 4 л, ной 1,2, ...,8 л.- Прим. ред. 8*
228 Часть III. Глава 18 initial_state(jugs, jugs(0,0)). final_state(jugs(4,V2)). final_state(jugs(Vl,4)). move(jugs(Vl,V2),fill(l)). move(jugs(Vl,V2),fill(2)). move(jugs(V 1, V2), empty (1)) <- VI > 0. move(jugs(Vl,V2),empty(2)) <- V2 > 0. movc(jugs(V 1, V2), transfer (2,1)). move(jugs(Vl,V2),transfer(l,2)). update(jugs(Vl,V2),empty(1),jugs(0,V2)). update(jugs(Vl,V2),empty(2),jugs(Vl,0)). update(jugs(Vl,V2),fill(l)jugs(Cl,V2))4-capacity(l,Cl). update0ugs(Vl,V2),fill(2)jugs(Vl,C2)) <- capacity(2,C2). update (jugs( V1, V2), transfer (2,1) jugs( W1, W2)) <- capacity (1, CI), Liquid := V1+V2, Excess := Liquid —CI, adjust(Liquid, Excess, W2, W1). adjust(Liquid,Excess,Liquid,0) <- Excess ^0. adjust(Liquid,Excess,V,Excess)«- Excess>0, V:= Liquid—Excess. legal (jugs(Vl,V2)). capacity (1,8). capacity(2,5). Программа 18.3. Программа для решения задачи о кувшинах. будет содержать весь объем жидкости. В противном случае наполняемый кувшин будет залит полностью, а в опустошаемом кувшине сохранится остаток, равный разности между общим объемом жидкости и емкостью наполненного кувшина. Рассмотренные действия реализуются предикатом adjustl4. Заметим, что проверка допустимости переходов тривиальна, так как все достижимые состояния допустимы. Наиболее интересные задачи имеют слишком большое пространство поиска, которое не под силу такой программе, как программа 18.1. Одна из возможностей усовершенствования процедуры поиска состоит в привлечении больших знаний для выполнения переходов. Решения задачи о кувшинах могут быть найдены посредством наполнения одного из кувшинов, когда это возможно, освобождения другого кувшина, когда это возможно, и в противном случае-переливания содержимого наполненного кувшина в опустошенный кувшин. Тем самым вместо шести переходов потребуется специфицировать только три, а поиск будет более направленным, поскольку только один переход будет применим в любом данном состоянии. Правда, этот путь может не привести к оптимальному решению, если ошибочно выбирается постоянно наполненный кувшин. Развивая это соображение, можно эти три перехода объединить в один переход более высокого уровня fill_and_transfer. В обсуждаемой тактике предполагается наполнение одного кувшина и переливание всего его содержимого в другой кувшин с освобождением последнего по мере необходимости. Следующий фрагмент программы соответствует переливанию жидкости из большего по емкости кувшина в кувшин меньшей емкости. move(jugs(Vl,V2), fill..and. transfer(l)). update (jugs (VI,V2), fill and_.transfer(l), jugs(0,V)) <-
Методы поиска 229 capacity(l,Cl), capacity(2,C2), CI > C2, V:=(Cl+V2)modC2. Для решения задачи, представленной на рис. 18.1, в этом случае необходимо выполнить только три операции наполнения и переливания из одного кувшина в другой. Привлечение знаний проблемной области приводит к полному изменению описания задачи и программированию, возможно, на другом уровне. Другая возможность улучшения эффективности процедуры поиска, исследованная в ранних работах по искусственному интеллекту, связана с эвристическими методами. Общая схема основана на явном выборе следующего состояния при поиске на графе пространства состояний. Этот выбор зависит от численных меток, присвоенных позициям. Метка, вычисляемая с помощью оценочной функции, является мерой качества позиции. Поиск в глубину можно рассматривать как частный случай процедуры поиска, в которой значение оценочной функции равно расстоянию от текущего состояния до начального состояния, в то время как в процедуре поиска в ширину оценочная функция имеет значение, обратное указанному расстоянию. Рассмотрим два метода поиска, в которых явно используется оценочная функция: «подъем на холм», и «сначала-лучший». Для оценочной функции введем предикат value (State, Value). Методы будут описаны абстрактно. Метод «подъем на холм» является обобщением поиска в глубину, в котором следующей выбирается позиция с наивысшей оценкой, а не самая левая позиция, как предопределено Прологом. Верхний уровень процедуры поиска, реализованной программой 18.1, сохраняется. В методе «подъем на холм» предикат move генерирует все состояния, которые могут быть достигнуты из текущего состояния за один переход с помощью предиката set_of. Затем эти состояния выстраиваются в Порядке убывания вычисленных значений оценочной функции. Предикат evaluate_and_order(Move, State, MVs) устанавливает связь между упорядоченным списком MVs пар «переход- значение оценочной функции» и списком переходов Moves из состояния State. Описанный метод поиска реализуется программой 18.4. solve, hili climb (State, History, Moves) <- Moves последовательность переходов для достижения искомого конечного состояния из текущего состояния State. History -ранее пройденные состояния. solve _hilLclimb(State, History, [ ]) <- finaLstate(State). solve._hilLclimb(State,History,Move | Moves]) «- hill_climb(State,Move), update(State, Move, State 1), legal(Statel), not member(Statel,History), solve_hilLclimb(Statel, [State 1 | History], Moves). hill. climb(State,Move) <- set ,of(M, move(State,M),Moves), evaluate_and_order(Moves,State,[ ],MVs), member((Move, value), MVs). evaluate _ and jorder (Moves,State, SoFar, OrderedM Vs) <- Все переходы Moves из текущего состояния State оцениваются и упорядочиваются, результат в OrderedMoves. SoFar накопитель для частичных вычислений.
230 Часть III. Г.шва IS evaluate_and_order([Move | Moves],State,MVs,OrderedMVs) <- update(State, Move, State 1), value(State 1, Value), insert ((Move, Value), M Vs, M Vs 1), evaluate_and_order(Moves, State, MVsl,OrderedMVs). evaluate_and_order([ ],State,MVs,MVs). insert(Mv,[ ],[MV]). insert((MV),[(Ml,Vl)|MVs],[(M,V),(Ml,Vl)|MVs])4- V^Vl. insert((M,V),[(Ml,Vl)|MVs],[(Ml,Vl)|MVsl] <- V<Vl,insert((M,V),MVs,MVsl). Предложение для тестирования базовой программы test_hill_climb(Problem,Moves) <- initial_state(Problem, State), Solve(State, [State], Moves). Прг::-;\!\:\!,1 is 4. Базовая программа для решения задачи поиска методом «подъем на холм». Чтобы познакомиться с работой программы, воспользуемся примером дерева из программы 14.8, добавив к нему факты, определяющие значения оценочной функции для каждого перехода. Необходимые для тестирования данные собраны в программе 18.5. Объединение программ 18.4 и 18.5 вместе с необходимыми определениями предикатов update и legal обеспечивает поиск по дереву, в процессе которого были выбраны вершины d и j. Эту программу легко протестировать на задаче о волке, козе и капусте, используя в качестве оценочной функции число «обитателей» на правом берегу реки. Программе 18.4 присущ недостаток, состоящий в повторном вычислении оценочной функции: при достижении после перехода Move некоторого состояния она вычисляется для выбора нового перехода, а затем повторно-при выполнении предиката update. Повторного вычисления можно избежать введением дополнительного аргумента в отношение move и сохранением состояния и перехода вместе с вычисленным значением до упорядочения переходов. Другая возможность оптимизации вычислений для одного и того же перехода состоит в использовании функции запоминания. Выбор эффективного метода зависит от конкретной задачи. Для задач с простой процедурой update представленная здесь программа будет наилучшей. «Подъем на холм» полезен, когда в пространстве состояний есть только один «холм» и оценочная функция является действительным указателем направления поиска. Существенно, что метод осуществляет локальный просмотр графа пространства состояний, принимая решение о направлении перехода лишь на основе текущего состояния. Альтернативный метод поиска «сначала-лучший» основан на глобальном рассмотрении полного пространства состояний. Лучшее состояние выбирается из всех не пройденных до сих пор состояний. Программа 18.6 для поиска «сначала-лучший» является обобщением алгоритма поиска в ширину, рассмотренного в разд. 17.2. На каждом этапе поиска выполняется очередной наилучший из доступных переходов. Для удобства сравнения программа 18.6 написана, насколько это оказалось возможным, в стиле программы 18.4 для поиска методом «подъем на холм». На каждом этапе поиска рассматривается не один, а множество возможных переходов. Об этом свидетельствует множественное число имен предикатов updates и legals. Так, с помощью предиката legals (States, States!) производится фильтрация
Методы поиска 23 1 initial_state(tree,a). value(a,0). final_state(j). move(a,b). value(b, 1). move(c,g). value(g,6). move (a, c). value (c, 5). move(dj). value (j, 9). move(a,d). value(d.7). move(e,k). value(k, 1). move(a,e). value(e,2). move(f,h). value(h,3). move(c,f). value (f, 4). move(f,i). value (i, 2). ilpoipiiviM.j is.5.Тестовые данные. solve, best (Frontier, History,Moves) <- Moves-последовательность переходов для достижения искомого конечного состояния из начального состояния. Frontier содержит подлежащие рассмотрению текущие состояния. History содержит ранее пройденные состояния. solve_best([state(State,Path,Value) | Frontier],History,Moves) <- finaLstate(State), reverse(Path, Moves). solve_best([state(State,Path, Value) | Frontier], History, Final Path) <- set_of(M, move (State, M), Moves), updates (Moves, Path, State, States), legals(States,Statesl), news(States 1, History, States2), evaluates(States2, Values), inserts(Values, Frontier, Frontier 1), solve. best(Frontier 1,) [State | History], Final Path). updates(Moves, Path,State,States) <- States список возможных состояний, достижимых из текущего состояния State, согласованный со списком возможных переходов Moves. Path-путь из начального состояния к состоянию State. updates([M | Ms],Path,S,[(Sl,[M | Path]) | Ss]) <- update(S, M, S1), updates (Ms, Path, S, Ss). updates([ ],Path,State,[ ]). legalsf States, States!) <- Statesl подмножество списка допустимых состояний States. legals([(S, P) | States], [(S, P) | States 1 ]) <- legal(S), legals(States,States 1). legals([(S,P) | States], Statesl) <- not legal(S), legals(States,Statesl). legals([ ],[ ]). news (States, History, States!) <- States -список состояний из States, не входящих в список History. news([(S,P)| States],History,Statesl) <- member(S,History),news(States,History,Statesl). news([(S,P) | States], History, [(S,P) | Statesl]) <- not member(S,History), news(States,History,Statesl). news([ ], History, [ ]). evaluates (States, Value) <- Values - список наборов из States, расширенных оценками состояний. evaluates([(S,P) | States], [state(S,P,V) | Values]) <- value(S, V), evaluates(States, Values). evaluates([ ],[ ]). JIpoi njiMMii IS.»). Базовая программа для решения задачи поиска методом «сначала- лучший».
232 Часть HI. Глава 18 inserts (States,Frontier,Frontier 1) <- Frontier 1 - результат включения состояний States в текущую границу Frontier. inserts([Value | Values],Frontier,Frontier 1) <- insert (Value, Frontier, FrontierO), inserts( Values, FrontierO, Frontier 1). inserts([ ],Frontier,Frontier). insert (State, [ ], State]). insert(State,[State 1 | States],State,[State 1 | States]) <- less_.than(Statel, State). insert(State,[State 11 States], [State | States]) <- equals(State, State 1). insert(State,[State 11 States],[State 1 | Statesl]) <- less _than(State,State 1 ),insert(State,States,States 1). equals(state(S,P,V),state(S,Pl,V)). less_than(state(Sl,Pl,Vl),state(S2,P2,V2))^ SI Ф S2,V1<V2. Программа 18.6. (Продолжение). solve_best(Frontier,History,Moves) «- Moves- последовательность переходов для достижения искомого конечного состояния из начального состояния. Frontier содержит текущие состояния, подлежащие рассмотрению. History содержит ранее пройденные состояния. solve..best([state(State,Path,Value) | Frontier],History,Moves) «- final .state (State), reverse (Path, [ ], Moves). solve_best([state(State,Path, Value) | Frontier], History, Final Path) <- set._of(M,move(State,M), Moves), update frontier(Moves,State,Path,History, Frontier,Frontier 1), soIve„_best(Frontier 1, [State | History], Final Path). update.frontier([M | Ms],State,Path,History,F,F1) <- update(State,M, State 1), legal(Statel), value(Statel,Value), not member (State 1, History), insert ((State 1,[M | Path],Value),F,FO), update .frontier (Ms, State, Path, History, FO, F1). update_frontier([ ],S,P,H,F,F). insert(State, Frontier, Frontier 1) <- См. программу 18.6. Программа 18.7. Более эффективная базовая программа для решения задачи поиска методом «сначала-лучший». множества последующих состояний путем их проверки на соответствие ограничениям задачи. Один из недостатков алгоритма поиска в ширину (а следовательно, и поиска «сначала-лучший») заключается в неудобстве вычисления пролагае- мого пути. Каждое состояние должно быть явно запомнено вместе с пройденным путем. Эта особенность отражена в программе. Программа 18.6 оттестирована на данных, содержащихся в программе 18.5; порядок прохождения вершин здесь тот же, что и в случае применения метода «подъем на холм». В программе 18.6 каждый шаг процесса выполняется явно. На практике программу можно сделать более эффективной, объединяя некоторые шаги. Например, при фильтрации сгенерированных состояний можно производить одновременную проверку новизны и допустимости состояния, что позволяет обходиться без обра-
Методы поиска 233 зования промежуточных структур данных. Программа 18.7 иллюстрирует идею объединения всех проверок в одной процедуре update.„frontier. Упражнения к разд. 18.1 1. Переделайте программу для задачи о кувшинах, основываясь на двух операциях «наполнить _и перелить». 2. Напишите программу для решения задачи о миссионерах и каннибалах, которая формулируется следующим образом: Три миссионера и три каннибала находятся на левом берегу реки. Здесь же- небольшая лодка, вмещающая не более двух человек. Все хотят перебраться на другой берег. Если на каком-либо берегу миссионеров окажется больше, чем каннибалов, то миссионеры обратят каннибалов в свою веру. Найти последовательность ездок, гарантирующую безопасность миссионерам и свободу вероисповедания каннибалам. 3. Пять ревнивых мужей (Э. Дьюдени, 1917). Во время наводнения пять супружеских пар оказались отрезанными от суши водой. В их распоряжении была одна лодка, которая могла одновременно вместить только трех человек. Каждый супруг был настолько ревнив, что не мог позволить своей супруге находиться в лодке или на любом берегу с другим мужчиной (или мужчинами) в его отсутствие. Найти способ переправить на сушу этих мужчин и их жен в целости и сохранности. 4. По аналогии с программой 18.1 разработайте базовую программу для решения задач методом поиска в ширину, основываясь на программах разд. 17.2. 5. Используйте разработанную вами базовую программу для решения головоломки о восьми ферзях. Найдите подходящую оценочную функцию. 18.2. Игровые деревья поиска Что происходит, когда мы играем в какую-нибудь игру? Игра начинается, например, с расстановки шахматных фигур, сдачи карт, распределения спичек и т. п. Решив, кто начнет игру, игроки делают ходы по очереди. После каждого хода позиция игры соответственно обновляется. Превратим это туманное описание в простую базовую программу игры. Предложением верхнего уровня будет: играть (Игра, Результат) <- инициализировать (Игра, Позиция, Игрок), отобразить (Позиция, Игрок), играть (Позиция, Игрок, Результат). Предикат инициализировать (Игра, Позиция, Игрок) определяет начальную позицию Позиция игры Игра и игрока Игрок, начинающего игру. Игра представляется последовательностью шагов, каждый из которых состоит в выборе игроком хода, выполнении хода и определении игрока, который должен выполнять следующий ход. Поэтому более точное описание дает процедура играть, имеющая три аргумента: позиция игры, игрок, выполняющий ход, и конечный результат. При этом удобно отделить выбор хода выбрать_ход/3 от его выполнения ходить/3. Остальные предикаты в представленном ниже предложении служат для отображения состояния игры и определения игрока, выполняющего следующий ход: играть ( Позиция, Игрок, Результат) <- выбрать..ход (Позиция, Игрок,Ход), ходить (Ход, Позиция, Позиция 1), отобразить игру (Позиция 1, Игрок), следующий игрок (Игрок, Игрок 1), !, играть (Позиция 1, Игрок 1, Результат). Программа 18.8 определяет логическую основу игровых программ. Используя ее для написания программ конкретных игр, следует сосредоточить внимание на
234 Часть III. Глава 18 играть (Игра) <- Играть игру с именем Игра. играть(игра)«- инициализировать (Игра, Позиция, Игрок), отобразить_игру (Позиция, И грок), играть (Позиция, Игрок, Результат). играть (Позиция, Игрок, Результат) <- игра_окончена(Позиция, Игрок,Результат),!, объявить (Результат). играть (Позиция, Игрок, Результат) <- выбрать-ход (Позиция, Игрок, Ход), ходить (Ход, Позиция, Позиция 1), отобразить_игру (Позиция 1, Игрок), другой_игрок (Игрок, Игрок 1), !, играть (Позиция 1, Игрок 1, Результат). Программа 18.8. Базовая программа игры. важных игровых вопросах: какие структуры данных должны быть использованы для представления позиции игры и какие должны быть выражены стратегии игры. Этот процесс будет продемонстрирован в гл. 20 на примерах написания программ для игр в Ним и Калах. Базовые программы решения задач, представленные в предыдущих разделах, легко адаптируются для игр. Для заданного определенного состояния игры задача состоит в поиске последовательности ходов, ведущих к выигрышной позиции. Дерево игры подобно графу пространства состояний. Оно получается идентификацией состояний с вершинами и ходов игроков с ребрами. Однако мы не будем идентифицировать вершины, полученные при различных последовательностях ходов, даже если они повторяют одно и то же состояние. Каждый уровень в дереве игры называется слоем. Деревья игры часто оказываются слишком большими для выполнения исчерпывающего поиска. В этом разделе обсуждаются методы, разработанные с целью «покорения» большого пространства поиска в играх двух лиц. В частности, мы остановимся на минимаксном алгоритме, расширенном альфа-бета-отсечением. Эта стратегия используется в гл. 20 в качестве базовой в программе для игры в калах. Опишем основной подход к организации поиска на игровых деревьях с применением оценочной функции. Оценочная функция, обозначаемая, как и прежде, оценка (Позиция, Оценка), используется для вычисления оценки Оценка позиции Позиция -текущего состояния игры. Для выбора следующего хода используется простой алгоритм: Найти все возможные состояния игры, которые могут быть достигнуты за один ход. Используя оценочную функцию, вычислить оценки состояний. Выбрать ход, ведущий к позиции с наивысшей оценкой. Этот алгоритм представлен программой 18.9. В ней предусматривается использование предиката ходить (Ход,Позиция,Позиция!), который для достижения состояния Позиция! применяет к состоянию Позиция ход Ход. Связь с базовой программой 18.8 обеспечивается предложением выбрать_ход (Позиция, компьютер, Ход) установить (М, ход (Позиция, М), Ходы), оценить_и_выбрать (Ходы, Позиция, (nil, — 1000), Ход).
Методы поиска 235 оценить _и_ выбрать ( Xоды, Позиция, Запись, Л у чшийХод) <- Выбирает ЛучшийХод из множества ходов Ходы исходя из текущей позиции Позиция и записывает текущий лучший ход. оценить_и_выбрать([Ход | Ходы], Позиция,Запись, ЛучшийХод) <- ходить(Ход, Позиция, Позиция 1), оценка(Позиция1,Оценка), изменить(Ход,Оценка,Запись,Запись1), оценить_и_выбрать (Ходы, Позиция, Запись 1, ЛучшийХод). оценить_.и_выбрать([ ], Позиция,(Ход,Оценка),Ход). изменить (Ход, Оценка, (Ход 1, Оценка 1), (Ход 1, Оценка 1)) <- Оценка ^ Оценка 1. изменить (Ход, Оценка, (Ход 1, Оценка 1), Ход, Оценка)) <- Оценка > Оценка 1. Профаммл IS.9. Выбор лучшего хода. Предикат ход (Позиция, Ход) истинен, если ход Ход - возможный ход из текущей позиции. Базовое отношение программы - оценитъ_и_выбратъ (Ходы, Позиция, Запись,Ход) обеспечивает выбор лучшего хода Ход из возможных ходов Ходы, исходя из данной позиции Позиция. Для каждого возможного хода определяется соответствующая позиция, вычисляется ее оценка и выполняется ход с наивысшей оценкой. Переменная Запись используется для записи наилучшего текущего хода. В программе 18.9 она представлена парой {Ход, Оценка). Структура Запись должна быть частично абстрактной в процедуре изменение/4. Степень абстракции данных-вопрос стиля программирования и обеспечения противоречивых требований наглядности, компактности и эффективности программы. Если оценочная функция была бы совершенной, т.е. ее значение отражало бы, какие позиции ведут к победе, а какие-к поражению, то в программе 18.9 достаточно было бы просмотра вперед на один шаг. Игры становятся интереснее, когда совершенная оценочная функция неизвестна. Выбор хода на основе просмотра вперед на один шаг в общем случае не является хорошей стратегией. Лучше просматривать на несколько ходов вперед и на основании результатов просмотра выбирать лучший ход. Стандартный метод определения оценки позиции, основанный на просмотре вперед нескольких слоев дерева игры, называется минимаксным алгоритмом. Его идея состоит в следующем. В алгоритме предполагается, что наш противник из нескольких возможностей сделает выбор, лучший для себя, т.е. худший для нас. Поэтому наша цель-сделать ход, максимизирующий для нас оценку позиции, возможной после лучшего хода противника, т.е. минимизирующий оценку для него. Отсюда название-минимаксный алгоритм. Число слоев дерева игры, просматриваемых при поиске, зависит от доступных ресурсов. На последнем слое используется оценочная функция. В предположении, что оценочная функция выбрана разумно, алгоритм будет давать тем лучшие результаты, чем больше слоев просматривается при поиске. Наилучший ход будет получен при просмотре всего дерева игры. Минимаксный алгоритм основан на предположении о нулевой сумме выигрышей игроков, которое неформально означает: что хорошо для нас должно быть плохо для противника, и наоборот. На рис. 18.2 изображено простое дерево игры глубиной в 2 слоя. В текущей позиции игрок имеет два хода и его противник-два ответных хода. Оценки на листьях деревьев являются оценками для игрока. Противник хочет минимизировать
236 Часть III. Глава 18 оценку, поэтому он будет выбирать минимальные значения, оценивая позиции на первом уровне дерева как + 7 и —У. Игрок, желая максимизировать оценку, будет выбирать вершину со значением + 1. оценить.м ^выбрать (Ходы,Позиция,Глубина,Признак,Запись,ЛучшийХод) «- Выбирает лучший ход ЛучшийХод из множества ходов Ходы в текущей позиции Позиция с использованием минимаксного алгоритма поиска с числом просматриваемых вперед слоев, определяемых параметром. Признак Признак указывает, что производится в текущий момент минимизация или максимизация. Переменная Запись используется для регистрации текущего лучшего хода. оценить _и_выбрать([Ход | Ходы],Позиция,0,МаксМин,Запись, ЛучшийХод) <- ходить (Ход,Позиция,Позиция 1), минимакс(0,Позиция I ,МаксМин,ХодХ,Оценка), изменить(Ход,Оценка,Запись,Запись 1), оценить „. и _.выбрать(Ходы,Позиция,0,МаксМин,Запись1,ЛучшийХод). оценить _ и _ выбрать([ ],Позиция,0,МаксМин,Запись,Запись). минимакс(0,Позиция,МаксМин,Ход,Оценка) <- оценка(Позиция,У), оценка: = V * МаксМин. минимакс(0,Позиция,МаксМин,Ход,Оценка) <- D>0, set_ оГ(М,ход(Позиция,М),Ходы), D1: = D- l, МинМакс: = — МаксМин, оценить..и _выбрать(Ходы,Позиция,01,МинМакс,(пП,— 1000),(Ход,Оценка)). изменить(Ход,Оценка,Запись,Запись1) <- См. программу 18.9. Программа 18.10. Выбор лучшего хода с использованием минимаксного алгоритма. Минимаксный алгоритм реализуется программой 18.10. Основное отношение программы минимакс (D, Позиция, Макс Мин, Ход, Оценка) истинно, если Xod-xojx с наивысшей оценкой Оценка позиции Позиция, полученной в результате поиска на слое D дерева игры. Признак МаксМин указывает, что производится - максимизация (7) или минимизация ( — 7). Обобщение программы 18.9 используется для выбора хода из множества ходов. В предикат оценить_и_выбрать должны быть введены два дополнительных аргумента: число слоев D и признак МаксМин. Последний аргумент {ЛучшийХод) предиката используется для записи не только хода, но и оценки. Процедура минимакс производит необходимые вычисления, а также изменение числа просмотренных вперед ходов и признака. Исходное значение записи Запись устанавливается равным {nil, —1000), где nil представляет произвольный ход, а — 1000-это оценка, меньшая любого возможного значения оценочной функции. Выводы о повышении эффективности программы поиска на графе пространства состояний в результате объединения процедур генерации перехода и изменения состояния по аналогии распространяются и на случай поиска на игровых деревьях. В зависимости от решаемой задачи и при соответствующем изменении алгоритма может оказаться целесообразным вычислять множество позиций, а не множество ходов. Используя известный метод альфа-бета-отсечения, можно усовершенствовать минимаксный алгоритм, сохраняя след поиска. Идея состоит в сохранении для каждой вершины, найденной к текущему моменту, минимальной оценки позиции (альфа-оценки) и максимальной оценки позиции (бета-оценки). Если при рассмотрении некоторой вершины бета-оценка будет превышена, то дальнейший поиск по эти ветви не имеет смысла. В удачных случаях отпадает необходимость оценивания более половины позиций в дереве игры.
Методы поиска 237 В программе 18.11, представляющей собой модификацию программы 18.10, реализована альфа-бета-процедура. В программе появилась новая реляционная схема алъфа_бета (Глубина,Позиция, Альфа, Бета,Ход, Оценка), в которой в отличие от реляционной схемы минимакс вместо признака МаксМин помещены переменные Альфа и Бета. Аналогичное изменение претерпела и схема отношения оценить _и_выбратъ. оцепить _и. выбрать (Ходы,Позиция,Глубина,Альфа,Бета,Ход!,ЛучшийХод) «- Выбирает лучший ход ЛучшийХод из множества ходов Ходы в текущей позиции Позиция, используя минимаксный алгоритм поиска с альфа-бета-отсечением и с числом просматриваемых вперед слоев, определяемых параметром Глубина. Альфа и Бета параметры алгоритма. Параметр Ход! используется для регистрации текущего лучшего хода. оценить и выбрать([Ход | Ходы],Позиция,Э,Альфа,Бета,Ход1,ЛучшийХод) «- ходить(Ход,Позиция,Позиция 1), альфа.. бета(0,Позиция I ,Альфа,Бета,ХодХ,Оценка), Оценка 1: = — Оценка, отсечение(Ход,Оценка 1 ,D, Альфа,Бета,Ходы,Позиция,Ход 1 ,ЛучшийХод). оценить и выбрать([ ],Позиция,Альфа,Бета,Ход,(Ход,Альфа)). альфа_ бета(0,Позиция,Альфа,Бета,Ход,Оценка) «- оценка(Позиция,Оценка). альфа бета(0,Позиция,Альфа,Бета,Ход,Оценка) «- set _ оГ(М,ход(Позиция,М),Ходы), Альфа! : = — Бета, Бета! : = — Альфа. D1:=D- 1, оценить и.. выбрать(Ходы,Позиция,D1 ,Альфа 1 ,Бета 1 ,т!,(Ход,Оценка)). отсечение(Ход,Оценка,0,Альфа,Бета,Ходы,Позиция,Ход 1 ,(Ход,Оценка)) «- Оценка ^ Бета. отсеченис(Ход,Оценка,0,Альфа,Бета,Позиция,Ход 1 ,ЛучшийХод) «- Альфа < Оценка,Оценка < Бета, оценить _и _выбрать(Ходы,Позиция,Э,Оценка,Бета,ХодЛучшийХод). отсечение(Ход,Оценка,0,Альфа,Бета,Ходы,Позиция,Ход 1 ,ЛучшийХод) «- Оценка ^ Альфа, оценить и выбрать(Ходы,Позиция,0,Альфа,Бета,Ход1,ЛучшийХод). Программа 18.11. Выбор хода с использованием минимаксного алгоритма с ал ьфа-бста-отсеченисм. В отличие от программы 18.10 процедура оценить_и_выбрать в программе 18.11 не должна просматривать все возможности. Это достигается введением предиката отсечение, который либо останавливает поиск по текущей ветви, либо продолжает поиск, обновляя значение альфа (альфа-оценку) и соответственно изменяя лучший текущий ход. Например, в дереве игры, изображенном на рис. 18.2, не надо рассматривать последнюю вершину. Как только обнаруживается ход с оценкой —1, которая меньше, чем оценка + 1, игрок может быть уверен, что нет других вершин, которые могли бы повлиять на конечную оценку. Программа допускает обобщение путем замены базового предложения аль- фа_бета проверкой терминальности позиции. Это необходимо, например, в программах игры в шахматы для обработки ситуаций неэквивалентного обмена фигурами.
238 Часть III. Глава 19 Игрок Противник ж ♦ I *2 -I 3 Рис. 18.2. Простое дерево игры. 18.3. Дополнительные сведения Методы поиска для проектирования и игр обсуждаются в учебниках по искусственному интеллекту. Более подробные описания стратегий поиска или минимаксного алгоритма и его расширения альфа-бета-отсечением можно найти, например, у Нильсона (Nilsson, 1971) или у Уинстона (Winston, 1977). Уолтер Уилсон первым реализовал на Прологе альфа-бета-алгоритм. Глава 19 Метаинтерпретаторы Метапрограммы обращаются с другими программами как с данными; они выполняют их анализ, преобразование и моделирование. Легкость разработки метапрограмм, или метапрограммирования, на Прологе обусловлена эквивалентностью программ и данных-и те и другие являются термами Пролога. Действительно, метапрограммирование не является для Пролога чем-то необычным. Несколько примеров программ в предыдущих главах являются метапрограммами: редактор в программе 12.5, действие оболочки в программе 12.6, модели автомата в разд. 14.3 и трансляция грамматических правил в предложения Пролога, реализованная в программе 16.2. В данной главе рассматриваются метапрограммы особого класса - метаинтерпретаторы. В первом разделе обсуждаются принципы построения метаинтерпретаторов. В других разделах главы показано, насколько широка область их применения. Применения метаинтерпретаторов для построения оболочек экспертных систем развиваются во втором разделе. В третьем разделе обсуждаются алгоритмы отладки программ и их реализации.
Метаинтерпретаторы 239 19 1. Простые метаинтерпретаторы Нам/интерпретатор для некоторого языка-это интерпретатор для языка, написанный на том же самом языке. Некоторые языки программирования позволяют легко разрабатывать метаинтерпретаторы, что является важным свойством таких языков. Оно делает возможным построение интегрированной среды программирования и обеспечивает доступ к вычислительным средствам языка. Поскольку метаинтерпретатор - это программа на языке Пролог, то приведем реляционную схему. Отношение решить (Цель) истинно, если Цель истинна в отношении программы, подлежащей интерпретации. Везде в этом разделе для обозначения метаинтерпретатора используется предикат решить/1. решить (Цель) Цель выводится из программы на чистом Прологе, определенной предложением предложение/2. решить(истина). решить(А,В))«- решить(А),решить(В). решить(А)«- предложение(А,В),решить(В). Программа 19.1. Метаинтерпретатор для чистого Пролога. В простейшем метаинтерпретаторе, который может быть написан на Прологе, используются возможности метапеременной, а именно правило вида решить (А) <- А. Полезность использования метапеременных демонстрировалась программой 12.6 и программой 12.7, в которых они служили основой для описания оболочки и средств регистрации, реализованных на Прологе. Более интересный метаинтерпретатор имитирует вычислительную модель логических программ. Редукция цели для программы на чистом Прологе может быть описана тремя предложениями, составляющими программу 19.1. В данной главе рассматриваются этот базовый метаинтерпретатор и его расширения. Интерпретатору соответствует следующее декларативное толкование. Константа истина является истинной. Конъюнкция (А, В) истинна, если истинна цель А и истинна цель В. Цель А истинна, если в интерпретируемой программе существует предложение А <- В, такое, что цель В истинна. Теперь дадим процедурное толкование этим трем предложениям программы 19.1. Факт решить устанавливает, что пустая цель, представленная в Прологе атомом истина, достижима. Следующее предложение относится к конъюнктивным целям. Оно читается так: «Для достижения конъюнкции целей (А, В) необходимо достичь цели А и цели В». Общий случай редукции цели покрывается последним предложением программы. Чтобы доказать некоторую цель, из программы выбирается предложение, заголовок которого унифицируется с целью, а затем рекурсивно применяется тело предложения. Процедурное толкование предложений Пролога необходимо, чтобы показать, что метаинтерпретатор, представленный программой 19.1, действительно отражает возможности Пролога при реализации абстрактной вычислительной модели логического программирования. Такими возможностями, например, являются выбор для редукции крайней левой цели и использование последовательного поиска с возвратом при недетерминированном выборе предложения для редукции цели. Порядок целей в теле предложения решить, содержащего конъюнктивные цели, гарантирует, что самая левая цель в конъюнкции решается первой. Последова-
240 Часть III. Глава 19 тельный поиск и возврат осуществляются при доказательстве цели предложение согласно принципам выполнения Пролог-программ. Постоянная работа интерпретатора обеспечивается третьим предложением программы 19.1. При вызове предложения предложение выполняется унификация с заголовками предложений, имеющихся в программе. Это предложение ответственно также за получение различных решений при возвратах. Кроме того, возвраты происходят и в конъюнктивном правиле (возвраты от цели В к цели А). Поучителен разбор протокола работы метаинтерпретатора, представленного программой 19.1, при доказательстве некоторой цели. На рис. 19.1 приведен протокол метаинтерпретатора при ответе на вопрос решить (member{X,[a>b,c])) в отношении программы 3.12. решать(тетЬег(Х,[а,Ь,с])) предложение (member(X, [a, b, с]), В) {X = а, В = true} решить (истина) истина Результат: X = а решить (истина) предложение (истина, Т) f предложение(тешЬег(Х,[а,Ь,с]),В) {В = member(X,[b,c])} решить (member(X, [b,c])) предложение(тетЬег(Х,[Ь,с]),В1) {X = b,Bl = true} решить (истина) истина Выход: X = b решить (истина) предложение (истина, Т) f предложение(тетЬег(Х,[Ь,с]),В1) {Bl = member(X,[c])} решить(тетЬег(Х, [с])) предложение (member(X, [с]), В2) {X = с,В2 = true} решить (истина) истина Выход: X = с решить (истина) предложение (истина, Т) f предложение(member(X, [с], В2)) {В2 = member(X,[ ])} решить (member (X,[ ])) предложение(member(X,[ ],B3) f Других решений нет Рис. 19.1. Протокол работы метаинтерпретатора. Метаинтерпретаторы можно различать по степени их детализации, т.е. по размеру фрагментов вычисления, которые оказываются доступны программисту. Метаинтерпретатор, состоящий из одного предложения, слишком тривиален и поэтому имеет весьма ограниченную область применения. Можно, хотя и нелегко, написать метаинтерпретатор, который будет моделировать процедуру унификации и механизм возврата. Степень детализации такого метаинтерпретатора очень высока, но стремиться к ней, как правило, бессмысленно, поскольку потеря эффективности при этом не окупается дополнительными приложениями. Метаинтерпретатор в программе 19.1 на уровне редукции цели имеет степень детализации, подходящую для широкой области приложений. Простым примером использования метаинтерпретатора является построение дерева доказательства в процессе решения определенной цели. Этот метод схож с методом конструирования дерева разбора для грамматик, рассматриваемым в программе 16.3. Кроме того, здесь используются структурированные аргументы, применение которых в логических программах рассматривалось в разд. 2.2. Дерево доказательства полезно для средств объяснения в экспертных системах. Этот вопрос будет обсуждаться в следующем разделе.
Метаинтерпретаторы 241 solve (Goal, Tree) «- Tree -дерево доказательства цели Goal, задаваемой программой, определенной предикатом clause/2. solve(true,true). solve((A,B),(ProofA,ProofB)) «- solve(A,ProofA), solve(B,ProofB). solve(A,(A <- Proof)) <- clause(A,B), solve(B,Proof). Программа 19.2. Построение дерева доказательства. Основным отношением метаинтерпретатора является отношение solve (Goal, Tree), где Tree-дерево доказательства для решения цели Goal. Дерево доказательства представляется структурой Goal <- Proof где Proof- конъюнкция ветвей (подцелей) доказываемой цели Goal. Программа 19.2, реализующая предикат solve/2, является простым расширением прогрммы 19.1. Три ее предложения точно соответствуют трем предложениям метаинтерпретатора для чистого Пролога. Факт solve утверждает, что пустая цель истинна и имеет тривиальное дерево доказательства, представляемое атомом true. Во втором предложении утверждается, что дерево доказательства конъюнктивной цели (А, В) представляет собой конъюнкцию деревьев доказательства целей А и В. Последнее предложение solve строит дерево доказательства А <- Proof для цели А, в котором Proof строится рекурсивно при решении тела предложения, используемого для редукции цели А. Рассмотрим пример использования программы 19.2 для чисто логической программы 1.2. Вопрос solve(сын(лот,аран),Proof)? имеет решение Proof = (сын (лот,аран) <- ((отец(аран,лот) *- true), (мужчина(лот) +- true))). Вопрос solve(сын(X,аран),Proof)! имеет решение X = лот и то же самое значение для Proof Для того чтобы обрабатывать программы, в которых используются средства, выходящие за рамки чистого Пролога, метаинтерпретатор, представленный программой 19.1, должен быть расширен. Различные системные предикаты не определяются предложениями программы и поэтому требуют отдельной обработки. Простейший способ обращения к этим системным предикатам состоит в непосредственном их вызове с использованием метапеременных. Необходима таблица, устанавливающая, какие предикаты являются системными. Будем считать, что она состоит из фактов вида system (Predicate) для каждого системного предиката. Часть такой таблицы представлена на рис. 19.2. Предложения метаинтерпретатора, оперирующие с системными предикатами, имеют вид solve(A) <- system(A), A. system (А := В). system (А < В). system (read(X)). system (write(X)). system (integer(X)). system (functor(T,F ,N)). system(clause(A,B))- system(system(X)). Рис. 19.2. Фрагмент таблицы системных предикатов.
242 Часть III. Глава 19 Дополнительное предложение solve делает действие системных предикатов невидимым для интерпретатора. Этот способ можно распространить при необходимости на несистемные предикаты. Наоборот, существуют некоторые системные предикаты, которые должны быть видимыми. Примерами являются предикаты для отрицания и программирования второго порядка. Для каждого из них в метаинтерпретаторе целесообразно иметь специальные предложения, как, например: solve(not A) <- not solve(A). solve(set_of(X,Goal,Xs)) <- sef_of(X,solve(Goal),Xs). Проблемой в этом метаинтерпрераторе является корректное, моделирование отсечения. Наивным было бы рассматривать отсечение как системный предикат, поскольку это означает добавление в метаинтерпретатор предложения solve(!) <-!. которое не приводит к требуемому эффекту. Отсечение в таком предложении скорее гарантирует переход к текущему предложению solve, нежели воздействие на дерево поиска. Другими словами, область действия отсечения слишком локальна. Более искусное решение связано с использованием такого системного предиката, как отсечение с учетом предыстории. Такое отсечение реализовано в языках Wisdom-Пролог и Waterloo-Пролог, но отсутствует в языке Edinburgh-Пролог. Синтаксически отсечение с учетом предыстории описывается как \(Предок), где Предок - ссылка на предка текущей цели. Если Предок - положительное целое число, например я, то в отсечении идет речь об n-м предке текущей цели. Счет предков ведется вверх от текущей цели, так что первый предок-это працель, второй предок - прапрацель и т.д. Если Предок не является целочисленным термом, то рассматривается первый предок, унифицируемый с термом Предок. В любом случае все цели, родственные рассматриваемой, отсекаются от дерева поиска, как если бы непосредственно к предковой цели было применено отсечение. Чтобы корректно использовать отсечения, учитывающие предысторию, необходимо ввести предикат, отличный от solve. Здесь используется предикат reduce (Goal). Корректная область действия отсечения обеспечивается использованием в метаинтерпретаторе для отсечения предшествующей редуцированной цели отсечения, учитывающего предысторию. Все рассмотренные выше усовершенствования метаинтерпретатора включены в программу 19.3-метаинтерпретатор для Пролога. solve (Goal) <- Goal-цель, выводимая из Пролог-программы, определенной предложением clause/2. solve(true). solve((A,B)) <- solve(A), solve(B). solve(!)«- !(reduce(A)). solve(notA) <- not solve(A). solve(set _ of(X,Goal,Xs)) <- set _ of(X,solve(Goal),Xs). solve(A) <- system(A), A. solve(A) <- reduce(A). reduce(A) <- clause(A,B), solve(B). llpoipaxivia 19.3. Метаинтерпретатор для полного Пролога. Метаинтерпретатор в программе 19.3 можно сделать еще более эффективным путем добавления отсечений. Выбор соответствующего предложения из набора предложений solve детерминирован. Как только будет идентифицировано пра-
Метаинтерпретаторы 243 вильное предложение, оно может быть использовано в процессе вычислений. Метаинтерпретатор для построения дерева доказательства, представленный на чистом Прологе программой 19.2, также может быть расширен добавлением некоторых предложений. В частности, системные цели могут обрабатываться с помощью предложения solve(A,(A <- true)) <- system(A), A. Деревом доказательства для системной цели А будет А <- true. В качестве примера рассмотрим усовершенствованный метаинтерпретатор, обеспечивающий трассировку вычисления, как в разд. 6.1. Будут приведены две версии метаинтерпретатора. Интерпретатор, представленный программой 19.4, обрабатывает только успешные ветви вычислений в чистом Прологе и не отображает безуспешные вершины в дереве поиска. Он способен, в частности, генерировать протокол вычислений вопроса append(Xs, Ys,[a,b,c\), представленный на рис. 6.2. Второй метаинтерпретатор (программа 19.5) обрабатывает системные предикаты и, что более важно, отображает безуспешные вершины в дереве поиска. Он способен генерировать протоколы, представленные на рис. 6.1 и 6.3, для вопросов сын{Х,аран) и quicksort([2,1,3],Xs) соответственно. trace (Goal) <- Goal -цель, выводимая из программы на чистом Прологе, определенной предложением clause/2. Программа выполняет трассировку доказательства с использованием побочных эффектов. trace(Goal) <- trace(Goal,0). trace(true, Depth). trace((A,B),Depth) +- trace(A,Depth), trace(B,Depth). trace(A,Depth) <- clause(A,B), display(A,Depth), Depth 1: = Depth + 1, trace(B,Depthl). display(A,Depth) ♦- tab(Depth)write(A),nl. Ilpoi pa\i\Ki h\4. Трассировщик для чистого Пролога. Основной предикат рассматриваемой программы -trace (Goal,Depth), где Goal- цель, решаемая на некоторой глубине Depth. Предполагается, что начальная глубина равна 0. Три предложения программы 19.4 соответствуют трем предложениям программы 19.1. Первые два предложения устанавливают, что пустая цель решается на любой глубине, а глубина решения каждого элемента конъюнктивной цели одинакова. Третье предложение сопоставляет цель с заголовком некоторого предложения трассируемой программы, отображает цель, увеличивает глубину и решает тело предложения программы на новой глубине. Предикат display (Goal, Depth), который служит для отображения цели Goal на глубине Depth, является некоторым интерфейсом, обеспечивающим печать трассируемой цели. Глубина в дереве доказательства отражена глубиной отступа при печати. В определении предиката display используется предикат tab (Depth), определяющий размер отступа, который пропорционален глубине в дереве поиска.
244 Часть III. Глава 19 trace (Goal) <- Goal -цель, выводимая из Пролог-программы, определенной предложением clause/2. Программа выполняет трассировку доказательства с использованием побочных эффектов. trace(Goal) <- trace(Goal,0). trace(true,Depth) <-!. trace((A,B),Depth) <- !, trace(A,Depth), trace(B,Depth). trace(A,Depth) <- system(A), A,!, display(A,Depth), nl. trace(A,Depth) <- clause(A,B), display(A,Depth), nl, Depth 1: = Depth + 1, trace(B,Depthl). trace(A,Depth) <- not clause(A,B), display(A,Depth), tab(8), write(0, nl, fail. display(A,Depth) <- Spacing: = 3 * Depth, tab(Spacing), write(A). Программа 19.5. Трассировщик для Пролога. Существует некоторая хитрость в определении порядка целей в предложении trace(Goal, Depth) <- clause(A,B), display (A, Depth), Display 1: = Depth + 1, trace(B,Depthl). Цель display расположена между целями clause и trace. Это гарантирует, что цель display будет выполняться каждый раз, когда происходит возврат к цели clause. Если же поменять местами цели clause и display, то при трассировке будет отображен только начальный вызов цели А. Использование программы 19.4 для вопроса trace{append(Xs,Ys,[^a,b,c:])) по отношению к программе 3.15 для предиката append приводит к протоколу, представленному на рис. 6.1. Выходные сообщения и точки с запятой для указания альтернативных решений обеспечиваются самой Пролог-системой. Существует лишь одно отличие от протокола на рис. 6.2: унификации здесь уже выполнены. Программа 19.4 может быть расширена с целью трассировки безуспешных целей. Для вывода на печать безуспешных целей следует положиться на операционное поведение Пролога, главное, в части порядка предложений. К первым двум предложениям добавим отсечения, а в конце программы поместим дополнительно следующее предложение: trace (A, Depth) <- not clause(A,B),display(A,Depth), tab(10),write(f),nl,fail. Отметим, что предикат display модифицирован для того, чтобы вывод не начинался с новой строки и сообщения о безуспешных целях были согласованы с результатами трассировки, представленными в гл. 6. В предложении, вызывающем предикат display, необходима дополнительная команда nl. Системные цели обрабатываются предложением trace(A, Depth) <- system(A),A,!, display (A, Depth).
Метаинтерпретаторы 245 Для того чтобы последнее правило действовало корректно, когда решение цели оказывается безуспешным, предикат display вызывается после успешного решения цели. Требуемое поведение обеспечивается введением отсечения. С учетом описанных модификаций получена программа 19.5, которая способна выдать протоколы, представленные на рис. 6.3 и 19.1. Упражнения к разд. 19.1 1. Напишите метаинтерпретатор для подсчета числа вызовов процедуры в некоторой программе. 2. Расширьте программу 19.5, чтобы она обрабатывала программы на полном Прологе аналогично программе 19.3. 3. Напишите интерактивный трассировщик, который обращается к пользователю прежде, чем выполнить редукцию очередной цели. 4. Расширьте программу 19.3 так, чтобы она интерпретировала отсечение с учетом предыстории. 19.2. Усовершенствованные метаинтерпретаторы для экспертных систем Общепринятое представление экспертной системы в виде базы знаний и механизма вывода не полностью пригодно для экспертных систем, написанных на Прологе. Многие функции механизма вывода обеспечиваются самим Прологом. Базы знаний, образованные средствами Пролога, являются выполняемыми. Однако Пролог не обеспечивает некоторых важных свойств экспертных систем, обычно встроенных в механизм вывода. Примеры таких свойств - порождение объяснений и рассуждения в условиях неопределенности. Рассмотрим здесь несколько усовершенствованных метаинтерпретаторов, демонстрирующих три свойства экспертных систем: взаимодействие пользователя и программы, средства объяснения и механизм рассуждений в условиях неопределенности. Средства объяснения и интерактивная оболочка будут продемонстрированы на примере модельной экспертной системы, представленной программой 19.6. Эта программа способна решить, где в печи должно быть размещено блюдо для выпекания. Комментарии относительно пригодности Пролога для построения экспертных систем даны в конце главы в разделе «Дополнительные сведения». поместить-выпечь (Блюдо,Полка) <- Блюдо должно быть помещено для выпекания в печь на полку Полка определенного уровня. поместить_в._печь(Блюдо,верх) <- кондитерское . изделие(Блюдо), размер,(Блюдо,малый). поместить.в_ печь(Блюдо,середина) <- кондитерское_изделие(Блюдо),размер(Блюдо,большой). поместить_в _печь(Блюдо,середина) <- главное_блюдо(Блюдо). поместить в печь(Блюдо,низ) <- медленное выпекание(Блюдо). кондитерское_изделие(Блюдо) <- тип(Блюдодорт). кондитерское._ из дел ие( Блюдо) <- тип(Блюдо,хлеб). главное_.блюдо(Блюдо)«- тип(Блюдо,мясо). медленное-выпекание(Блюдо) <- тип(Блюдо,молочный_пудинг). Программа 19.6. Экспертная система для выбора места размещения блюда в печи.
246 Часть III. Глава 19 Программа 19.7 представляет собой интерактивную оболочку, которая может задавать пользователю вопросы в случае нехватки информации. Предполагается, что определена процедура askable/7, которая в случае безуспешного решения цели интерпретатором может направить ее на рассмотрение пользователю. Чтобы реализовать такую возможность, в конце текста метаинтерпретатора (программа 19.1) добавлено следующее предложение: solve(A) <- askable(A), not known(A), ask(A, Answer), respond (Answer, A). ' Предикат askable (Goal) используется для вывода на экран вопроса к пользователю. Например, факт askable (type (Dish, Type)) указывает, что можно спросить о типе блюда. sove(Goal) <- Goal-цепь, выводимая из программы на чистом Прологе, определяемой предложением clause/2. Недостающая информация запрашивается у пользователя. solve(true). solve((A,B)) <- *solve(A),solve(B). solve(A) «- clause(A,B),solve(B). 4>olve(A) «-• askable(A), not known(A), ask(A,Answer), respond(Answer,A). ask(A, Answer) <- display_query(A), read(Answer). respond(yes,A) <- assert(A). respond(no,A) <- assert(untrue(A)), fail. known(A) <- A. known(A) <- untrue(A). display_query(A)<- write(A), writeC?1). Программа 19.7. Интерактивная оболочка. Чтобы избежать повторения одних и тех же вопросов, программа записывает ответы на вопросы, что обеспечивается предикатом respond/2. Если на вопрос А последовал ответ yes, то в программу вводится факт Л. Если же получен ответ по, то в программу вводится факт untrue (Л). Эта информация используется предикатом known/7, чтобы избежать задания вопросов, ответы на которые уже известны программе. Усовершенствованная версия оболочки позволяет так же успешно вести диалоги и с другой стороны. Когда программа задает вопрос, пользователь может ответить своим собственным вопросом. Рассмотрим, как оболочка ответила бы на вопрос пользователя «почему». Естественным ответом оболочки должно быть правило, на основании которого программа пытается сделать вывод. Такую возможность легко ввести в оболочку, расширяя все отношения дополнительным аргументом, используемым текущим правилом. Поскольку в Пролог- программах доступ к глобальному состоянию процесса вычислений невозможен, это правило должно быть явно представлено в дополнительном аргументе. Как будет
Метаинтерпретаторы 247 показано ниже, соответствующим образом должна быть расширена и цель solve. Интерфейс для обеспечения ответа на вопрос «почему?» реализуется следующим правилом: respond (why, Goal, Rule) <- display_rule( Rule), ask (Goal, Answer), respond (Answer, Goal, Rule). Эта версия предиката respond переписывает текущее родительское правило и приглашает пользователя ответить еще раз. Формат, в котором будет представлено это правило, определяется предикатом display _rule - модульным расширением оболочки, позволяющим пользователю представлять правила в удобной для него форме. Повторные ответы на вопрос «почему?», использующие предложение respond, приводят к повторной переформулировке родительского правила. Улучшенное решение состоит в выдаче прародительского правила в ответ на второй вопрос «почему?», прапрародительского правила в ответ на третье «почему?» и так далее вверх по дереву поиска. С этой целью программа модифицируется так, чтобы аргумент вместо правила содержал список правил, учитывающих предысторию. Новый вариант предиката respond будет выглядеть так: respond (why, Goal, [Rule | Rules]) <- write_rule(Rule), ask (Goal, Answer), respond (Answer, Goal, Rules). На повторные вопросы «почему?» будут теперь выданы правила, учитывающие предысторию. Для учета случая, когда все правила для ответа исчерпаны, необходимо дополнительное предложение. Программа 19.8 представляет собой полную интерактивную оболочку, включающую средства объяснения. Протокол работы этой программы показан на рис. 19.3. Ответы пользователя набраны курсивом. Объясним еще не рассмотренные предикаты этой программы. solve (Goal) <- Goal -цель, выводимая из программы на чистом Прологе, определенной предложением clause/2. Недостающая информация запрашивается у пользователя, пользователь может просить объяснения «почему?». solve(Goal) <- solve(Goal,[ ]). solve(true,Rules). solve((A,B),Rules) <- solve(A,Rules), solve(B,Rules). solve(A,Rules) <- clause(A,B),solve(B,[rule(A,B) | Rules]). solvc(A,Rules) «- askable(A), not know(A), ask(A,Answer),respond(Answer,A,Rules). ask(A,Answer) <- display. query(A), read(Answer). respond(yes,A,Rules) <- assert(A). respond(no,A,Rules) «- assert(untruc(A)), fail.
248 Часть III. Глава 19 respond(why,A,[Rule | Rules]) <- display_rule(Rule), ask(A,Answer), respond(Answer,A,Rules). respond(why,A,[ ]) <- \\тке1п(['Возможности объяснения исчерпаны']), ask(A,Answer), respond(Answer,A,Rules). known(A) <- A. known(A)<- untrue(A). display _query(A) <- why^u'ritef?'). display_rule(rule(A,B)) <- write(' IF'),write. conjunction(B), writeln(['THEN \A]). write_conjunction((A,B)) <- !,write('AND'),write_conjunction(B). write_conjunction(A) <- write(A),nl. Программа 19.8. Интерактивная оболочка со средствами объяснения «почему?». 8о1уе(поместить_в_печь (блюдо 1, X))? тип (блюдо 1, торт)? да. размер (блюдо!, малый)? нет. тип (блюдо 1, хлеб)? лет. размер (блюдо 1, большой)? почему. IF кондитерское_изделие (блюдо 1) AND размер (блюдо!, большой) ТН EN поместить_в_печь (блюдо 1,середина) размер (блюдо 1, большой)? да. X = середина Рис. 19.3. Протокол работы с интерактивной оболочкой. Вторым аргументом предиката solve (Goal, Rules) в программе 19.8 является список правил, используемых для редукции предковых вершин цели Goal в текущем дереве доказательства. Список правил обновляется с помощью предиката solve в процессе редукции цели. Для представления правил выбрана структура rule (Л, В). Единственным предикатом, на который влияет выбор представления правила, является предикат display_rule. Ответы на вопрос «почему?» - это простое средство объяснения, описывающее одну локальную цепочку рассуждений. Следующий пример представляет собой более интересное средство объяснения, поясняющее полное доказательство решенного вопроса. Основная идея состоит в интерпретации доказательства цели, где доказательство представлено в метаинтерпретаторе программы 19.2. Вопрос, как доказывается цель Goal- how (Goal)?, обрабатывается метаинтерпретатором, который производит интерпретацию полученного доказательства. Простое средство объяснения «как» представлено в виде программы 19.9. На рис. 19.4 показан протокол, использующий программу 19.9 для объяснения how(поместитъ_в_печъ(блюдо 1 ,верх))? с использованием фактов тип(блюдоI,хлеб) и размер (блюдо 1, малый). Хотя объяснение на рис. 19.4 исчерпывающе понятно, оно имеет скрытые
Метаинтерпретаторы 249 how (Goal) <- Объясняет, как была доказана цель Goal. how(Goal) <- solve(Goal,Proof),interpret(Proof). solve(Goal,ProoO <- См. программу 19.2. interpret((Proofl,Proof2)) <- interpret(Proof 1), interpret(Proof2). interpret(ProoO <- fact(Proof,Fact), nl,writeln([Fact,^aKT в базе данных.']). interpret(ProoO <- rule(Proof,Head,Body,Proofl), п1,\уп1е1п([Нсас1,'доказывается с помощью правила']). display _ rule(rule(Head,Body)), interpret(Proofl). fact((Fact<- true),Fact). rule((Goal <- ProoO,Goal,Body,ProoO <- Proof ф true,extract_body(Proof,Body). extract_body((Proof 1 ,Proof2), (Body 1 ,Body2)) <- !, cxtract_body(Proof 1 ,Body 1 ),extract_body(Proof2,Body2). extract _body((Goal<- Proof),Goal). * display _rule(Rule)<-См. программу 19.8. Программа 19.9. Объяснение доказательства. поместить _в__печь (блюдо 1, верх) доказано с использованием правила IF кондитерское„изделие(блюдо 1) и размер(блюдо1,малый) THEN поместите в печь (блюдо 1, верх) кондитерское_изделие (блюдо 1) доказано с использованием правила IF тип (блюдо 1, хлеб) THEN кондитерское_изделие(блюдо 1) тип (блюдо 1, хлеб) - факт, содержащийся в базе данных размер (блюдо 1, малый) факт, содержащийся в базе данных Рис. 19.4. Формирование объяснения. недостатки. Один из них - чрезмерная полнота объяснения, что приводит, даже для очень маленькой базы знаний, к выводу слишком большого объема информации. Экранный текст, произведенный экспертной системой, имеющей сотни правил, становится невразумительным. Практичная модификация программы 19.9 ограничивает объяснения одним уровнем при одном обращении, но разрешает пользователю запрашивать при необходимости более детальную информацию. На рис. 19.5 представлен текст модифицированного объяснения. поместить_в_печь(блюдо1,середина) доказано с использованием правила IF кондитерское_изделие(блюдо1) и размер(блюдо1,большой) ТНEN поместить_.в _печь (блюдо 1, середина) кондитерское_изделие(блюдо1) может быть объяснено в дальнейшем размер (блюдо 1, большой)-факт, содержащийся в базе данных Рис. 19.5. Объяснение одного правила при одном обращении.
250 Часть III. Глава 19 Объяснение, данное на рис. 19.4, точно отражает процесс вычисления на Прологе, но может не удовлетворять пользователя. Использование метаинтерпретатора позволяет добиться большей гибкости. Объяснения могут не отражать логику самой программы, однако должны содержать обоснование выбора правила. Для иллюстрации этого принципа рассмотрим очень простой пример. Предположим, что объяснение связано с некоторым экспертом-пекарем, которому знакома классификация блюд, т.е. он знает, что является кондитерским изделием и т. п. Хотя программа все же проводит рассуждения, устанавливая, что если блюдо-торт и т.п., то оно должно быть кондитерским изделием, они не представляют интереса для пекаря. Указанная возможность может быть реализована в программе с помощью специального предложения interpret, в теле которого содержится предикат classification (Goal) для целей, связанных с классификацией. Это предложение имеет вид interpret((Goal <- Proof)) «- classification(Goal), writeln([Goal, 'является примером классификации']). Факт classification является примером проблемно-зависимых метазнаний. Использование таких метазнаний позволяет эксперту строить теорию объяснения, которое скорее дополняет, а не повторяет доказательство, выполняемое экспертной системой. Несоответствие между тем, что говорится, и тем, что делается, характерно для экспертов-людей. Специально выделенные объяснения полезны для описания выполнения системных предикатов, например арифметических или предикатов ввода-вывода. Такие цели Пролога имеют статус, отличающийся от точки зрения пользователя экспертной системы. Вообще все подпрограммы Пролога, реализующие алгоритмы, не нуждаются в объяснении: вместо этого может быть предложен более сжатый отчет о том, что делает соответствующий алгоритм. Последний пример использования метаинтерпретаторов для экспертных систем связан с применением механизма рассуждений в условиях неопределенности. Причиной для введения такого механизма является наличие неопределенной информации-правил и фактов. Дедуктивный вывод при неопределенных предположениях должен приводить к неопределенным заключениям. Существует несколько способов представления неопределенности в правилах и способов вычисления неопределенности заключений. Основное требование состоит в том, чтобы в предельном случае, когда все правила определенные, поведение системы повторяло стандартный механизм дедукции. Остановимся на следующем подходе. С каждым фактом или правилом свяжем коэффициент определенности с, 0 < с ^ У. Логическая программа с неопределенностями-это множество пар (ClauscFactor}, где Clause - предложение, a Factor - коэффициент определенности. Для вычисления неопределенностей будем использовать следующие правила: certainty ((А, В)) = min{certainty(A),certainty(B)} certainty(A) = max{certainty(B)-F| <A <- В,F)-некоторая пара в программе}. Простой интерпретатор, работающий в условиях неопределенности (программа 19.10), получен путем непосредственного усовершенствования базового метаинтерпретатора, представленного программой 19.1. Отношение верхнего уровня solve (Goal, Certainty) истинно, когда цель Goal удовлетворена с определенностью Certainty. Метаинтерпретатор вычисляет комбинацию коэффициентов определенности в конъюнкции как минимум из коэффициентов определенности элементов конъюнк-
Метаинтерпретаторы 251 solve (Goal,Certainty) <- Certainty -наша уверенность в том, что цель Goal истинна. solve(true,l). soive((A,B),C) <- solve(A,C 1), solve(B,C2), minimum(C 1 ,C2,C). solve(A,C) <- dause_cf(A,B,Cl),solve(B,C2), C: = CI *C2. minimum(Nl,N2,M) <- См. программу 11.3. Программа 19.10. Метаинтерпретатор для выполнения рассуждений в условиях неопределенности. ции. Также легко могут быть определены и использованы другие правила вычисления коэффициента определенности. В программе 19.10 предполагается, что предложения с коэффициентами определенности представлены с использованием предиката clause_cf(A,В,CF). Метаинтерпретатор, реализуемый программой 19.10, можно расширить так, чтобы отсекались пути вычислений, не приводящие к желаемому порогу определенности. Для этого необходимо добавить дополнительный аргумент - размер порога отсечения. Модифицированный метаинтерпретатор (программа 19.11) будет содержать новое отношение solve (Goal, Certainty, Threshold). Порог определенности используется в третьем предложении программы 19.11. Определенность любой цели должна превышать текущее значение порога. Если это происходит, то вычисления продолжаются с новым значением порога, равным частному от деления предыдущего значения порога на величину определенности этого предложения. solve(Goal,Certainty,Threshold) <- Certainty -наша уверенность в том, что цель Goal истинна. Определенность выше порога Threshold. solve(True,l,T). solve((A,B),C,T) <- solve(A,C 1 ,T), solve(B,C2,T), minimum(C 1 ,C2,C). solve(A,C,T) <- clause_cf(A,B,Cl), CI >T, T1: = T/C1, solve(B,C2,Tl), C: = CUC2. minimum(Nl,N2,M)<- См. программу 11.3. Программа 19.11. Метаинтерпретатор для выполнения рассуждений в условиях неопределенности с отсечением по порогу определенности. Упражнении к разд. 19.2 1. Усовершенствуйте предикат displayjrule таким образом, чтобы с его помощью генерировались не термы Пролога, а английский текст. 2. В программах 19.7 и 19.8 расширьте предикат known так, чтобы он обеспечивал обработку функциональных понятий. 3. Внесите в программу 19.8 такие изменения, которые позволили бы обрабатывать ответы, отличающиеся от да, пет и почему]. 4. Модифицируйте программу 19.9 так, чтобы она давала протокол, представленный на рис. 19.5.
252 Часть III. Глава 19 5. Введите в интерактивную оболочку механизм рассуждений в условиях неопределенности, с тем чтобы запрашивать пользователя о неопределенности фактов и правил. Записывайте значения так, чтобы они могли использоваться повторно. 19.3. Усовершенствованные метаинтерпретаторы для отладки программ Отладка программ даже на Прологе является существенным аспектом программирования. Языки программирования высокого уровня мало что могут дать для написания безошибочных программ, и поэтому для поддержки процесса разработки программ приходится больше полагаться на мощность системных средств. Между тем по многим причинам подобные средства естественно реализовать в самом языке. По существу это программы для обработки, анализа и моделирования других программ, т.е. метапрограммы. В этом разделе рассмотрены метапрограммы для поддержки процесса отладки программ на чистом Прологе. Причина такого ограничения очевидна и объясняется трудностями обработки изменяемых частей языка Пролог. При отладке программ предполагается, что программист имеет некоторое представление о поведении программы и области применения, в которой программа должна демонстрировать заданное поведение. Таким образом, отладка состоит в поиске расхождения между фактическим поведением программы и поведением, предполагаемым программистом. Напомним определения из разд. 5.2 заданного значения и заданной области программы. Заданным значением М программы на чистом Прологе является множество основных целей, на которых выполнение программы оказывается успешным. Заданная область D программы - область целей, на которой выполнение программы должно завершиться. Мы требуем, чтобы заданное значение программы было подмножеством заданной области целей. Будем говорить, что А ^решение для цели А, если программа для цели А находит ее пример ах. Скажем также, что решение А истинно в заданном значении М, если каждый пример А содержится в М. В противном случае решение цели А ложно в М. В программе на чистом Прологе при заданном значении и заданной области целей могут обнаружиться ошибки только трех типов. Вызов программы для цели А из заданной области целей может завершиться одним из трех исходов: а) выполнение программы не завершается. б) вырабатывается некоторое ложное решение Ад в) пропуск некоторого истинного решения AQ Рассмотрим алгоритм обнаружения и идентификации ошибок каждого из указанных типов. В общем случае невозможно определить, завершается Пролог-программа или нет: эта задача неразрешима. Практически можно назначить априорную границу времени выполнения или глубины рекурсии программы и прерывать вычисления, если эта граница будет превышена. При этом желательно частично сохранить информацию о выполнении программы для анализа причин незавершаемости вычисления. Указанные возможности реализованы в усовершенствованном метаинтерпретаторе (программа 19.12). Обращение к нему осуществляется вызовом solve(Goal,D,Overflow), где Goal-исходная цель. D-верхняя граница глубины рекурсии. Этот вызов будет успешным, если решение находится без превышения заданной глубины рекурсии, при этом переменная Overflow конкретизируется как no_overflow. Этот вызов завершается успешно и в случае, если глубина рекурсии превышена, однако при этом Overflow содержит стек целей, т.е. ветвь дерева
Метаинтерпретаторы 253 solve(A,D,Overflow) <- Цель А имеет дерево доказательства глубиной менее чем D и Overflow равно по-overflow, или цель А имеет в дереве вычисления ветвь длиннее D и Overflow содержит список первых D элементов этой ветви. solve(true,D,no overflow). solvc(A,0,overflow([ ])). solve((A,B),D,Overflow) <- D>0, solve(A,D,OvcrflowA), solve __conjunction(OverflowA,B,D,Overflow). solve(A,D,Overflow) <- D>0, clause(A,B), D1: = D- 1, solve(B,Dl,Overflow B), return __overflow(OverflowB,A,Overflow). solve(A,D,no..overflow) <- D>0, system(A),A. solve conjunction(overflow(S),B,D,overflow(S)). solve_..conjunction(no _overflow,B,D,Overflow) <- solvc(B,D,Overflow). return _overflow(no_.overflow,A,no_overflow). return _ovcrflow(overflow(S),A,overflow([A | S])). Программа 19.12. Метаинтерпретатор, обнаруживающий переполнение стека. вычислений, длина которой превысила границу глубины D. Отметим, что, как только обнаруживается переполнение стека, вычисление прекращается без завершения доказательства. Это обеспечивается предложениями solve_conjuction и return_overflow. В качестве примера рассмотрим программу 19.13 для сортировки вставками. Вызов этой программы с целью solve(isort([2,2\Xs),6,Overflow) приводит к следующему решению: Xs = [2,2.2,2,2,2], Overflow = overflow([ isort ([2,2], [2,2,2,2,2,2]), insert (2, [2], [2,2,2,2,2,2]), insert(2,[2],[2,2,2,2,2]), insert(2,[2],[2,2,2,2]), insert(2,[2],[2,2,2]), insert(2,[2],[2,2])]) Для выяснения причин незавершаемости программы переполненный стек после возврата может быть подвергнут дальнейшему анализу. Незавершаемость программы может быть вызвана, например, зацикливанием, т. е. последовательностью целей Gj, G2, ..., G„ в стеке, где Gt и Gn вызываются с одним и тем же входом, или последовательностью целей, вызов которых происходит с увеличением размера входов. Первая ситуация встречается в приведенном выше примере. Ясно, что программа содержит ошибку. Вторая ситуация не обязательно должна быть обусловлена ошибкой, и требуется дополнительная информация о поведении программы, чтобы определить, имеется ли в программе ошибка или для выполнения программы следует воспользоваться более мощной вычислительной машиной.
254 Часть III. Глава 19 isort(Xs,Ys)+- Ys- упорядоченная перестановка списка Xs. Выполнение программы не завершается. isort([X | Xs],Ys) <- isort(Xs,Zs), insert(X,Zs,Ys). isort([ ],[ ]). insert(X,[Y|Ys][X,Y|Ys])<- X<Y. insert(X,[Y | Ys],[Y | Zs]) <- X ^ Y, insert(Y,[X | Ys],Zs). insert(X,[ ],[X]). Программа 19.13. Незавершающаяся программа сортировки вставками. Ошибки второго типа заключаются в получении ложных решений. Получить ложное решение при использовании некоторой программы можно лишь тогда, когда она содержит ложное предложение. Предложение С ложно по отношению к заданному значению М программы, если оно имеет пример, тело которого истинно в М, а заголовок ложен в М. Такой пример называется контрпримером для С. Рассмотрим, например, программу 19.14 для сортировки вставками. Обращение к этой программе с целью isort([3,2,l],Xs) дает решение ш?г/([3,2,/],[3,2,/]), которое, очевидно, ложно. isort(XsJs) <- Ошибочная программа сортировки вставками. isort([X|Xs],Ys) <- isort(Xs,Zs),insert(X,Zs,Ys). isort([ ],[ ]). insert(X,[Y | Ys],[X,Y | Ys]) <- X^Y. insert(X,[Y | Ys],[Y | Zs]) <- X> Y,insert(X,Ys,Zs). insert(X,[ ],[X]). npoipaMNia 14.14. Некорректная и неполная программа сортировки вставками. Эта программа содержит следующее ложное предложение: insert(X,[Y | Ys], [X, Y | Ys]) <- X > Y. для которого можно привести следующий контрпример: insert (2, [1], [2,1]) <-2 ^ 1. Рассматривая основное дерево доказательства, соответствующее ложному решению, можно найти ложный пример предложения, выполняя обход дерева доказательства в обратном порядке. При этом проверяется истинность посещаемых вершин дерева доказательства. Если обнаружится ложная вершина, то предложение, заголовок которого - найденная ложная вершина, а тело-конъюнкция ее вершин- сыновей, является контрпримером для некоторого предложения программы. Это предложение ложно и должно быть модифицировано или удалено из программы. Корректность этого алгоритма следует из простого индуктивного доказательства. Этот алгоритм реализован в метаинтерпретаторе, представленном программой 19.15. В самом алгоритме и его реализации предполагается использование ора,\т./>/. который может отвечать на вопросы относительно заданного значения программы. Оракул-это некоторое «существо», внешнее по отношению к диагностируемому
Метаин герпретаторм 255 falsesolution(A,Clause) <- Если Л-доказуемо ложный пример, то предложение Clause- ложное предложение программы. Алгоритм основан на просмотре дерева снизу вверх. false _solution(A,Clause) <- sol ve( A, Proof), false_clause(Proof,Clause). false _clause(true,ok). false_clause((A,B),Clause) <- false_clause(A,ClauseA), check_conjunction(ClauseA,B,Clause). false_clause((A<- B),Clause) <- false_clause(B,ClauseB), check_clause(ClauseB,A,B,Clause). check_conjunction(ok,B,Clause) <- false_clause(B,Clause). check_conjuction((A <- B1),B,(A <- Bl)). check _clause(ok,A,B,Clause) <- query _goal(A,Answer), check_answer(Answer,A,B,Clause). check_clause((Al <- B1),A,B,(A1 <- Bl)). check _ ans wer(true, A,B,ok). check_answer(false,A,B,(A <- Bl)) «- extract_body(B,Bl). extract _body(true,true). extract_body((A <- B),A). extract_body(((A +- B),Bs),(A,As)) +- extract_body(Bs,As). query _goal(A,true) «- system(A). query „goal(Goal,Answer) «- not system(Goal), \^п1е1п(['Цель,,Соа1,,истинна?']). read(Answer). Программа 19.15. Программа поиска ложного решения при просмотре дерева снизу вверх. алгоритму. Он может быть программистом, который способен отвечать на вопросы относительно заданного значения его программы, или другой программой, для которой известно, что она имеет то же значение, что и заданное значение отлаживаемой программы. Вторая ситуация может встретиться при разработке новой версии программы и использовании старой версии в качестве оракула. Возможен также случай, когда при разработке некоторой эффективной программы, например программы быстрой сортировки, сначала получают для нее некоторую неэффективно исполняемую спецификацию (например, сортировку перестановками) и используют эту спецификацию в качестве оракула. При обращении к программе 19.15 с целью false_solution(isort([3,2,J],X), С)? будет получен следующий протокол интерактивного взаимодействия: false_solution(isort([3,2,l],X),C)? Цель isort([ ],[ ]) истинна? true.
256 Часть III. Глава 19 Цель insert(l,[ ],[1]) истинна? true. Цель isort([l],[l]) истинна? true. Цель insert(2,[l],[2,1]) истинна? false. Х = [3,2,1], C = insert(2,[l],[2,l])<-2^ 1. В результате получен контрпример для ложного предложения. Вопреки предположению алгоритма дерево доказательства, вырабатываемое предикатом solve/2, не обязательно оказывается основным. Однако каждый пример некоторого дерева доказательства является деревом доказательства. Поэтому эта проблема может быть преодолена либо до вызова алгоритма конкретизацией переменных, оставшихся в дереве доказательства произвольными константами, либо обращением к оракулу для конкретизации запрошенной цели, когда она содержит переменные. Различные примеры могут подразумевать различные ответы. Так как смысл этого алгоритма состоит в поиске контрпримера, то оракул, если это возможно, должен конкретизировать цель ложным примером. Одной из основных задач, связанных с алгоритмами диагностики, является уменьшение их сложности, т. е. сокращение числа вопросов, требуемых для диагностики ошибки. При условии, что отвечать на вопросы, возможно, придется программисту, это желание вполне понятно. Сложность приведенного выше алгоритма диагностики линейно зависит от размера дерева доказательства. Существует другой вариант алгоритма с лучшей стратегией, сложность которого линейно зависит не от размера дерева доказательства, а от его глубины. В отличие от предыдущего алгоритма, в котором обход дерева выполнялся снизу вверх, во втором алгоритме дерево доказательства просматривается сверху вниз. При этом в каждой вершине проверяется, не имеет ли она ложных вершин-сыновей. Если такие вершины не обнаруживаются, то текущая вершина представляет собой контрпример, поскольку цель, соответствующая этой вершине, истинна, а все ее вершины-сыновья ложны. Если же такая вершина обнаруживается, то процесс поиска продолжается с этой вершины. Описанный алгоритм реализован в программе 19.16. Отметим, что в первом предложении предиката false_goal/2 использовано отсечение для реализации неявного отрицания, а предикат query_goal/2 использован в программе как тестовый предикат. Сравните поведение алгоритма, в котором дерево просматривается снизу вверх, с поведением программы 19.16 на основании следующего протокола: false_solution(isort([3,2,l],X),C)? Цель isort([2,l],[2,l]) истинна? false. Цель isort([l],[l]) истинна? true. Цель insert(2,[l],[2,1]) истинна? false. Х = [3,2,1], C = insert(2,[l],[2,l])<-2^ 1. Существует диагностический алгоритм поиска ложного решения, имеющий еще меньшую сложность. В этом алгоритме реализуется принцип «разделяй и спрашивай». В процессе выполнения алгоритма производится расщепление дерева доказательства на две примерно равные части и проверка истинности вершины в точке расщепления. Если эта вершина ложна, то алгоритм применяется рекурсивно к поддереву, корнем которого является эта вершина. Если вершина истинна, то ее поддерево удаляется из дерева и замещается вершиной true. Затем находится новая средняя точка. Можно показать, что в этом алгоритме требуемое число вопросов
Метаинтерпретаторы 257 false _ solution ( A,Clause) «- ЕслиЛ - доказуемо ложный пример, то предложение Clause - ложное предложение программы. Алгоритм основан на просмотре дерева сверху вниз. false_solution(A,Clause) <- solve(A,Proof), false_goal(Proof,Clause). false_.goal((A <- B),Clause) <- false...conjunction(B,Clause),!. false _goal((A <- B),(A <- Bl)) <- extract body(B,Bl). false _conjunction(((A <- B),Bs),Clause) <- query goal(A,false),!, false .goal((A <- B),Clause). false._conjunction((A<- B),Clause) <- query goal(A,false),!, false_goal((A <- B),Clause). false.conjunction((A,As),Clause) <- false . conjunction(As,Clause). extract _.body(Trce, Body)<- См. программу 19.15. query goal(A,Answer) <- См. программу 19.15. Программа 19.16. Программа диагностики ложного решения, в которой реализован алгоритм просмотра дерева сверху вниз. логарифмически зависит от размера дерева доказательства. В случае когда дерево доказательства близко к линейному, этот алгоритм дает экспоненциальное уменьшение сложности по сравнению с рассмотренными выше алгоритмами, использующими обходы дерева сверху вниз и снизу вверх. Третий возможный тип ошибок характеризуется пропуском решения. Диагностирование пропущенного решения оказывается более трудным по сравнению с диагностированием ошибок других типов. Будем говорить, что предложение покрывает цель А относительно заданной интерпретации М, если оно имеет пример, заголовок которого является примером цели А, а тело содержится в М. Рассмотрим, например, цель insert{2,\_l,3~\,Xs). Она покрывается предложением insert(X,[Y | Ys],[X, Y | Ys]) <- X ^ Y. программы 19.14 относительно заданной интерпретации М этой программы, поскольку в следующем примере этого предложения insert(2,[l,3],[l,2,3])<-2^ 1. заголовок является примером А и тело содержится в М. Можно показать, что если программа Р имеет пропущенное решение относительно заданного значения М, то в М существует некоторая цель А, которая не покрывается никаким предложением программы Р. Доказательство этого утверждения выходит за рамки нашей книги. Оно было использовано в диагностическом алгоритме, представленном ниже. Диагностирование пропущенных решений ложится тяжелым бременем на оракула. Он не только должен знать, имеет ли некоторая цель решение, но должен также получить решение, если оно существует. Используя такой оракул, некоторую непокрытую цель можно найти следующим образом. Алгоритмом выдано некоторое пропущенное решение, т.е. цель в заданной интерпретации М программы Р, для которой завершение Р было безуспешным. 9 1402
258 Часть III. Глава 19 Выполнение этого алгоритма начинается с исходного пропущенного решения. Каждое предложение, унифицируемое с ним, проверяется с помощью оракула с целью определения, является ли тело этого предложения примером в М. Если такого предложения не существует, то цель не покрывается и работа алгоритма завершается. В противном случае алгоритм находит в теле цель, которая приводит к безуспешному вычислению. По крайней мере одна из целей тела должна быть такой, иначе программа в противоположность нашему предположению может доказать это тело, а следовательно, и эту цель. Алгоритм применяется рекурсивно к этой цели. missing „solution (A,Goal) <- Если А - недоказуемо истинная основная цель, то цель Goal- истинная основная цель, которая не покрывается этой программой. missing_solution((A,B),Goal)«-!, (not A, missing_solution(A,Goal); A, missing_solution(B,Goal)). missing_solution(A,Goal) <- clause(A,B), query_clause((A <- B)),!, missing_solution (B, Goal). missing_solution(A,A) <- not system (A), query_clause(Clause) <- writeln(['Введите истинный основной пример', Clause, 'если такой пример существует, в противном случае введите «нет»']), read (Answer), !, check_answer(Answer, Clause). check_answer(no, Clause) <- !, fail. check_answer(Clause, Clause) <- !. check_answer(Answer,Clause) <- write ('Неправильный ответ'), !, query clause(Clause). Программа iL,17. Программа диагностирования пропущенного решения. Реализация данного алгоритма представлена программой 19.17. Программа пытается проследить безуспешный путь вычисления и найти непокрываемую истинную цель. В ходе выполнения этой программы был получен в качестве примера следующий протокол: missing_solution(isort([2,1,3],[ 1,2,3]),С)? Введите истинный основной пример (isort([2,l,3],[ 1,2,3]) <- isort([l,3],Xs),insert(2,Xs,[ 1,2,3])) если такой пример существует, в противном случае введите 'нет' (isort([2,l,3l[l,2,3]) <- isort([l, 5], [/, 5]) insert(2[l,3]ll,2,3])). Введите истинный основной пример (isort([l,3],[l,3]) <- isort([3],Ys), insert(l,Ys,[l,3])) если такой пример существует, в противном случае введите 'нет' (isort(\_l,5],[/,5]) - МИМ). insert(l,l3Ul,3M
Метаинтерпретаторы 25е) Введите истинный основной пример (insert(l,[3],[l,3])<-l ^3) если такой пример существует, в противном случае введите 'нет' false С = insert( 1,[3],[ 1,3]). Читатель может проверить, что цель insert(l,[3~\,\_1,3~\) не покрывается программой 19.14. Рассмотренные алгоритмы могут быть положены в основу создания эффективных средств поддержки разработки программ на Прологе. 19.4. Дополнительные сведения Понятие метаинтерпретатора, или, вернее, метациклического интерпретатора, было введено Sussman и Steele (1978), которые первыми предложили использовать способность языка специфицировать тот же язык, как фундаментальный критерий разработки языка. Пролог является естественным языком для построения экспертных систем. Хотя программа 19.6 очень проста, она напоминает ранние робкие попытки создания экспертных систем. Сравним эту программу с правилом из системы MYCIN (Shortliffe, 1976)-экспертной системы для диагностирования и лечения бактериологических инфекций. Рассмотрим следующее правило: IF по методу Грама организм-грамотрицателен, морфология организма-микроб, аэробность организма-неаэробен THEN имеется предположительное основание (0,5) считать, что организм относится к бактероидам. Имеются два аспекта приведенного выше правила. Первый состоит в том. что эвристическая идентификация бактерий основана на методе Грама, морфологии и аэробности. Второй заключается в том, что правилу придается коэффициент достоверности. Мы считаем, что они должны быть разделены. В условиях неопределенности лучше работает такой метаинтерпретатор, как программа 19.10. Эвристические знания можно выразить следующим правилом Пролога: тождественность(Организм,бактероиды) критерий..... грама(Организм,грамотрицательный), морфология(Организм,микроб), аэробность(Организм,неаэробный) Отметим, что это правило и аналогичные правила в программе 19.6 по существу являются основными. В «контексте» изучаемого объекта встречается единственная переменная. В представленном выше правиле системы MYCIN контекстом является «организм», а в системе управления печью (программа 19.6) контекстом было «блюдо». Нетрудно написать правила, в которых используется большая мощность языка Пролог. Ниже приведено предложение из модельной медицинской экспертной системы, которое дает некоторое представление о такой возможности. Экспертная система кредитных операций, представленная в гл. 21, также содержит ряд примеров. Правило из медицинской экспертной системы имеет вид предписать(Пациент,Лекарство) болезнь(Пациент,Симптом), запретить(Симптом,Лекарство), подходящее( Лекарство, Пациент). Отношения болезнь, запретить и подходящее должны быть сами описаны с помощью фактов и правил. 9*
260 Часть III. Глава 19 Первые явные доводы относительно использования Пролога для построения экспертных систем дали Clark и McCabe (1982). В их работе обсуждается, как простая экспертная система типа программы 19.6 может быть расширена введением средств объяснения и рассуждения в условиях неопределенности. Предлагаемый метод основан на добавлении дополнительных аргументов к предикатам программы. Средства объяснения и средства опроса пользователя были введены Hammond и Sergot (Hammond, 1984) в оболочку экспертной системы APES. Использовать метаинтерпретатор в качестве базиса средств объяснения в экспертной системе предложил Sterling (1984). Построение интерактивной системы объяснения с использованием усовершенствованных метаинтерпретаторов описано у Sterling и Lalee (1985). Использовать развитые метаинтерпретаторы для работы в условиях неполной определенности предложил Shapiro (1983c). Shapiro (1983a) предположил, что развитые интерпретаторы должны стать основой для построения среды программирования. В этой книге содержатся и обсуждаются алгоритмы отладки программ, использованные в разд. 19.3. Takeuchi и Furukawa (1985) показали, что частичные вычисления могут уменьшить накладные расходы при использовании метаинтерпретаторов. Результатом частичных вычислений является компиляция объектной программы и развитого метаинтерпретатора в новую объектную программу, наследующую функциональные возможности метаинтерпретатора, но свободную от дополнительных расходов. Sterling и Beer (1986) подробно рассматривают работу экспертных систем. В обеих статьях отмечается 40-кратное ускорение вычислений. Метаинтерпретаторы и вообще метапрограммы рассматривались также в связи с задачами управления в Пролог-программах. Отметим здесь следующие работы: Dincbas и LePape (1984), Gallairc и Lasserre (1980) и Pereira (1982).
Часть IV Приложения Язык Пролог имеет широкую сферу применений: экспертные системы, восприятие текстов на естественном языке, символьные вычисления, разработка компиляторов, создание языков для встроенных систем, архитектурное проектирование и др. В этой части книги представлен «букет» прикладных программ, написанных на Прологе. В гл. 20 рассмотрены игровые программы для трех игр: «Выдающийся ум», Ним и Калах. В гл. 21 описана экспертная система для проверки кредитных требований. В гл. 22 представлена программа для решения уравнений, заданых в символьном виде. В гл. 23 показан компилятор для паскалеподобного языка. Основное внимание в этих главах уделено написанию ясных программ. Знания, вложенные в программы, не завуалированы. Незначительный прирост эффективности игнорируется, если он усложняет декларативное понимание программы. Глава 20 Игровые программы Учиться играть-это забава. Освоив правила, необходимо постоянно изучать новые стратегии и тактики, чтобы овладеть игрой. Разработка игровых программ также является развлечением и в то же время хорошим способом показа того, как использовать Пролог для создания неэлементарных программ. 20.1. «Выдающийся ум» Наша первая программа разгадывает секретный код в игре «Выдающийся ум»1). Она является хорошим примером того, что можно без долгих размышлений легко запрограммировать на Прологе. Мы рассматриваем «детский» вариант игры «Выдающийся ум», который отличается от коммерческого варианта минимумом технических средств (только карандаш и бумага). Игрок А выбирает секретный код, представляющий собой последовательность из N различных десятичных цифр (обычно начинающие игроки выбирают N равным 4, опытные-5). Игрок В пытается угадать задуманный код и спрашивает игрока А о числе «быков» (число «быков»-количество совпадающих цифр в одинаковых позициях предполагаемого и задуманного кодов) и числе «коров» (число «коров» - количество совпадающих цифр, входящих в предпола- п Игра имеет второе название-«Быки и коровы».- Прим. ред.
262 Часть IV. Глава 20 гаемый и задуманный код, но находящихся в разных позициях). Код угадан, если число быков равно N. Существует очень простой алгоритм этой игры: вводится некоторый порядок на множестве допустимых правилами предположений; выдвижение очередных предположений учитывает накопленную к этому моменту информацию, и так до тех пор, пока секретный код не будет раскрыт. Вместо формального определения алгоритма игры обратимся к интуиции читателя: предположения считаются удачными, если ответы на вопросы угадывающего совпадают с ответами, которые были бы даны при разгадке кода. Алгоритм «играет» в силу опытных игроков: для раскрытия кода из 4 цифр ему требуется в среднем 4-6 попыток, наблюдавшийся максимум-8 попыток. Однако человеку эту стратегию применить нелегко, поскольку она требует значительной счетной работы. С другой стороны, управляющая структура Пролога - недетерминированный выбор, моделируемый поиск с возвратами, - представляется идеальной для реализации этого алгоритма. Опишем программу, начиная с верхнего уровня. Программа 20.1 - полная программа рассматриваемой игры. Процедура верхнего уровня в игре представлена следующим правилом: выдающийся_ум(Код) <- чистка,предположение(Код),проверка(Код),сообщение. Сердцевиной процедуры верхнего уровня является цикл «образовать и проверить». выдающийся_ум(Код) <- чистка, предположение(Код), проверка (Код),сообщение. предположение(Код) <- Код = [Х1,Х2,ХЗ,Х4], выбор(Код,[1,2,3,4,5,6,7,8,9,0]). Проверка предложенного предположения проверка (Предположение) «- not противоречивое(Предположение), вопрос(Предположение). противоречивое(Предположение) <- запрос (Старое Предположение, Быки, Коровы), not соответствуют_быки_и_коровы (Старое Предположение, Предположение, Быки, Коровы) соответствуют_быки_и_коровы (Старое Предположение, Предположение, Быки, Коровы) <- точное_совпадение (Старое Предположение, Предположение, N1), Быки = : = N1, % Правильное число быков общие_члены (Старое Предположение, Предположение, N2), Коровы = : = N2 — Быки, % Правильное число коров точное_совпадение(Х,У,1Ч[) <- размер (А, одинаковая_позиция (А, X, Y), N). общие_члены(Х,У,К) <- размер(А,(член(А,Х),член(А,У)),"№). одинаковая_позиция (X, [X | Xs], [X | Ys]). одинаковая_позиция(А,[X|Xs],[Y| Ys]) <- одинаковая_позиция(А,Xs, Ys). Запрос предположения вопрос (Предположение) <- repeat, I !poiгрпмхш ":!).! Игра «Выдающийся* ум».
Игровые программы 263 \\тке1п(['Сколько быков и коров в', Предположение, '?']), read ((Быки, Коровы)), sensible(BbiKH, Коровы),!, assert (запрос, (Предположение, Быки, Коровы)), Быки = 4. допустимо(Быки,Коровы) <- integer(BbiKH),integer(KopoBbi),BbiKH + Коровы ^ 4. Счетная работа чистка <- отменить(запрос,3). сообщение <- pa3Mep(X,At(B|(3anpoc(X,A,B))),N), writeln(['OTBeT найден после',N, 'запросов']). pa3Mep(X,G,N)<- См. программу 17.1. Bbi6op(X,Xs,Ys) <- См. программу 7.7. отменить(Р,Ы)<- См. упражнение 12.4(1). Программа 20.1 (Продолжение) Спрашивающая процедура предположение (Код), которая действует как генератор, использует процедуру выбор(Xs,Ys) (см. программу 7.7) для недетерминированного выбора списка Xs из элементов списка Ys. Согласно правилам игры, Xs ограничивается четырьмя различными десятичными цифрами, в то время как список Ys содержит десять десятичных цифр. Таким образом, предположение(Код) <- Код = [Х1,Х2,ХЗ,Х4], выбор(Код,[ 1,2,3,4,5,6,7,8,9,0]). Процедура проверка (Предположение) испытывает предложенный код Предположение. Сначала проверяется, что Предположение не противоречит всем ранее полученным ответам (т. е. непротиворечиво с каждым из них); затем задается вопрос о числе быков и коров в коде Предположение. Кроме того, процедура вопрос (Предположение) управляет циклом «образовать и проверить», который завершается только тогда, когда число быков равно 4, что является признаком отыскания правильного кода. проверка(Предположение) «- not противоречивое(Предположение),вопрос(Предположение) Процедура вопрос запоминает предыдущие ответы на вопросы в отношении запрос (Х,В,С), где X - предположение, В и С-число быков и коров в нем соответственно. Предположение противоречит предыдущему запросу, если число быков и число коров не соответствуют предыдущему запросу: противоречивое(Предположение) <- запрос(СтароеПредположение,Быки,Коровы), not соответствуют, быки _и_коровы (СтароеПредположение,Быки,Коровы). Предыдущее предположение (СтароеПредположение) и высказываемое предположение (Предположение) согласуются по числу быков, если количество цифр на одних и тех же позициях в этих двух предположениях равно числу быков (Быки) в предположении СтароеПредположение. Это соответствие определяется предикатом точное-совпадение ( СтароеПредположение Предположение,Быки). Предыдущее и высказываемое предположения согласуются по числу коров, если количество одинаковых цифр в предположениях без учета порядка расположения цифр соответствует сумме быков (Быки) и коров (Коровы). Данное соответствие проверяется процедурой соответствие-быков _м-коров. Количество совпадающих цифр и общих цифр в двух
264 Часть IV. Глава 20 запросах легко подсчитать, применяя системный предикат size-of/3. Процедура вопрос (Предположение) является функцией запоминания, которая регистрирует ответ на запрос и с помощью процедуры разумный(Ответ) выполняет ограниченный контроль на входе. Она завершается успешно, если только число быков в ответе равно 4. Ответ играющего представляется упорядоченной парой {Быки,Коровы). Остальные предикаты верхнего уровня являются вспомогательными. В частности, предикат чистка удаляет ненужную информацию, сохранившуюся от предыдущих игр. Предикат сообщение извещает о количестве потребовавшихся попыток, используя для подсчета предикат size-of. Более эффективные реализации процедур точное-совпадение и общие-члены могут быть получены посредством записи их итерационных версий: T04Hoe_coBnaACHHe(Xs,Ys,N) <- точное_coenadenue{Xs,Ys,0,N). точное совпадение([Х | Xs], [X | Ys], К,N) <- К1: = К + 1, точное .совпадение^, Ys, К 1,N). точное.совпадение([Х| Xs],[Y| Ys],К,N) <- X Ф Y, точное, совпадение^, Ys, К, N). точное..совпадение ([ ],[ ],N,N). o6mne_^eHbi(Xs,Ys,N) «- общие.. члены(Х5Д5,0,М). o6uwe__^eHbi([X|Xs],Ys,K,N) <- member(X,Ys), Kl: = К + 1, общие .члены (Xs,Ys,Kl,N). общие члены([Х|Х8]ЛЪ,К,1Ч)<- общие_члены(Х5Д5,К^). общие., члены(£ ],Ys,N,N). Использование более эффективных версий процедур точное-совпадение и общие-члены приводит к уменьшению времени выполнения программы на 10-30%. 20.2, Игра Ним От «Выдающегося ума» перейдем к игре Ним, в которую также играют двое. В игре используются спички, разложенные в N кучек. Игроки поочередно берут из любой кучки произвольное число спичек (можно все). Побеждает игрок, который берет последнюю спичку. На рис. 20.1 показана обычная исходная позиция, в которой четыре кучки содержат 1, 3, 5 и 7 спичек соответственно. Программу для игры Ним будем строить, основываясь на структуре игровой программы 18.8. Сначала надо выбрать способ представления игровой позиции и ходов. Позицию естественно представлять списком целых чисел, элементы которого соответствуют кучкам спичек. Ход представим парой (N,M), где М-число спичек, взятых из кучки N. Нетрудно написать процедуру ход](Ход,Позиция,Позиция!'), согласно которой после хода Ход позиция Позиция переходит в позицию Позиция!. Рекурсивное правило служит для перебора кучек с целью достижения требуемой кучки. Остающиеся кучки спичек, представляющие новую позицию игры, вычисляются обычным образом: ход((К,М),ПМ | NS],[N | Nsl]) <- К > 1, Kl : = К - 1, xofl((Kl,M),Ns,Nsl). Существуют две возможности модификации определенной кучки спичек. Если все спички из кучки взяты, то она удаляется из списка. В противном случае вычисляется новое число спичек в кучке и проверяется его допустимость:
Игровые программы 265 xoa((1,N),[N|Ns],Ns). xoa((1,M),[N|Ns],[N1|Ns])<-N > М, N1: = N - М. Техника ходов в играх для двоих специфицируется двумя фактами. Исходные состояния кучек спичек и право на первый ход определяются игроками. В предположении, что компьютер ходит вторым, игра с исходной позицией, представленной на рис. 20.1, описывается предложением инициализировать(ним,[ 1,3,5,7],противник). I III I I I I I IIIIIII Рис. 20.1. Исходная позиция в игре Ним. 1 1 1 101 1 1 1 000 Рис. 20.2.Вычисление ним-суммы. Игра завершается, когда взята последняя спичка. Этому соответствует состояние игры, представляемое пустым списком. Игрок, который не может сделать следующий ход, проигрывает, о чем сообщается с помощью предложения извещение. Программа 20.2 содержит необходимые детали. Осталось описать, как производится выбор ходов. Ходы противника поступают с клавиатуры. Гибкость организации ввода зависит от программиста. Поскольку нас интересует логика игры, будем предполагать, что игрок вводит допустимые ходы: игра(Партйя) <- См. программу 18.8. Выбор ходов выбор. хода(Позиция,противник,Ход) «- \^п1е1п([,пожалуйста, сделайте ход G1]), геас!(Ход). выбор_хода(№,компьютер,Ход) <- опасная (Ns, сумма), безопасный_ход(№, Сумма, Ход). выбор_ хода(Ns,компьютер,(1,1))«- % «Произвольный ход» компьютера безопасная (Ns). ход (Ход, Позиция, Позиция!) «- Позиция! результат выполнения хода Ход в текущей позиции Позиция. ход ((K,M),[N|Ns],[N|Nsl])<- К > 1,КЛ:=К-1,ход((К1,М),№,№1). xofl((l,N),[N|Ns],Ns). xofl((l,M),[N|Ns],[Nl|Ns])<- N > M,N1: = N-M. Программа 20.5.Программа для игры Ним, реализующая выигрышную стратегию игры.
266 Часть IV. Глава 20 отображение_партии(Позиция,X) <- \\тке(Позиция), nl. следующий_игрок (компьютер, противник). следующий._игрок (противник, компьютер). партия_закончена([ ], Игрок, Игрок). сообщение (компьютер) <- write('Bbi победили! Поздравляю/), nl. сообщение (противник) <- writeffl победил.'), nl. Программа 20.2. (Продолжение) инициализировать (ним, [1,3,5,7], противник). опасная (Позиция, Сумма) <- Позиция с ним-суммой Сумма является опасной. опасная (Ns,Сумма) <- ним_сумма(№,[ ],Сумма), not нуль(Сумма). безопасная (Ns) <- not опасная (Ns, Сумма). ним_сумма (Позиция, Накопленная Сумма, Сумма) <- Су/илш-ним-сумма текущей позиции Позиция, НакопленнаяСумма- накопленное значение суммы. HHM_cyMMa([N | Ns],Bs,CyMMa) <- двоичное (N, Ds), ним_ сложение(08, Bs, Bs l), ним_сумма (Ns, Bs 1, Сумма). ним_сумма([ ],Сумма,Сумма). ним_сложение(В8,[ ],Bs). ним_сложение([ ],Bs,Bs). ним_сложение([В | Bs], [С | Cs], [D | Ds]) <- D: = (В + C)mod2, ним_сложение(В8,С8,08). двоичное(1,[1]). двоичное(N,[D | Ds]) <- N > 1, D: = N mod 2, N1: = N/2, двоичное^ 1, Ds). десятичное(08,^ <- десятичное (Ds, 0,1,N). десятичное([ ],N,T,N). десятичное([Т> | Ds],A,T,N) <- Al: = A + D*T,T1 : = T*2, AecflTH4Hoe(Ds,Al,Tl,N). нуль([ ]). нуль([01 Zs])<- Hyjib(Zs). безопасный_ход (Позиция, НимСумма,Ход) <- Ход-ход в текущей позиции Позиция, которой соответствует значение НимСумма, сохраняющий позицию безопасной. безопасный_ход(Кучки, НимСумма,Ход) <- безопасный_ход (Кучки, НимСумма, 1, Ход). безопасный_ход([Кучка | Кучки],НимСумма,К,(К,М)) <- двоичное(Кучка,В8),может_быть...нуль(В8, НимСумма, Ds,0), десятичное (Ds, M). безопасный_код([Кучка | Кучки], НимСумма, К, Ход) «- К1 : = К + 1, безопасный_ход (Кучки, НимСумма, К1, Ход). может_быть_нуль([ ],НимСумма,[ ],0)<- нуль (НимСумма). может_быть_нуль([В | Bs],[01 НимСумма], [С | Ds],C) <- может_быть_нуль (Bs, НимСумма, Ds, С). может_быть_нуль([В| Bs],[l | НимСумма], [D| Ds],C)<- D: = 1-В*С,С1: = 1 - В,может быть_нуль(Bs,НимСумма,Ds,CI).
Игровые программы 267 выбор_хода(Позиция,противник,Ход) <- \^п1е1п([,пожалуйста,ходите1]), геаё(Ход). При выборе хода компьютер должен придерживаться некоторой стратегии. Простая для реализации стратегия состоит в том, чтобы взять из первой кучки все спички. Ее рекомендуется использовать только в игре с крайне слабыми игроками: Bbi6op_xoAa([N | Ns],KOMnbK)Tep,(l,N)). Для игры Ним известна выигрышная стратегия. Она предполагает деление состояний, или позиций, игры на два класса - безопасные и опасные состояния. Для определения принадлежности некоторой позиции к одному из классов вычисляются двоичные представления числа спичек в каждой кучке. Затем подсчитываются ним-суммы этих двоичных чисел следующим образом: независимо в каждом столбце все элементы суммируются по модулю 2. Если суммы по всем столбцам равны нулю, то соответствующая позиция является безопасной. В противном случае позиция опасна. На рис. 20.2 показан процесс вычисления ним-сумм для позиции из четырех кучек спичек, представленной на рис. 20.1. Двоичными представлениями десятичных чисел 7, 3, 5 и 7 будут 1, 11, 101 и 111 соответственно. Вычислим ним-сумму: заметим, что в первом столбце справа 4 единицы, во втором-2, в третьем-2 единицы. В каждом столбце - четное число единиц, поэтому ним-сумма равна нулю, а соответствующая позиция 11,3,5,7] безопасна. С другой стороны, например, позиция 12,6] является опасной. Здесь двоичными представлениями будут 10 и 110. В первом слева столбце сумма по модулю 2 равна /, что делает позицию опасной. Выигрышная стратегия - всегда оставлять позицию безопасной. Любая опасная позиция может быть преобразована в безопасную (хотя и не любым ходом), в то время как любой ход из безопасной позиции создает опасную позицию. Если игра находится в безопасной позиции, лучшей стратегией будет выполнение произвольного хода в надежде, что противник совершит промах. Опасные позиции следует переводить в безопасные. Для реализации этой стратегии необходимо использовать два алгоритма: один для вычисления ним-суммы данной позиции, другой-для определения хода, переводящего опасную позицию в безопасную. Выбор хода определяется опасностью позиции. Если позиция опасна, то ищется ход, делающий позицию безопасной и ведущий к выигрышу. Если позиция безопасна, делается произвольный ход (берется одна спичка из первой кучки) в надежде на промах противника. Итак, выбор хода определяется следующими правилами: выбор_хода(№,компьютер,Ход) <- onacHan(Ns,CyMMa),безопасный. ход(№,Сумма,Ход). выбор ..хода(№,компьютер,( 1,1))+-% «Произвольный ход» компьютера безопасная(№). Предикат опасная (Ns,Сумма) истинен, если представляемая Ns позиция является опасной. Он определяется с помощью вычисления ним-суммы Сумма (сумма вычисляется процедурой ним-сумма/3) и ее проверкой на равенство нулю: опасная(№,Сумма) <-ним _сумма(№,[ ],Сумма), not нуль(Сумма). В предыдущей версии программы определение истинности предиката опасная не сопровождалось выдачей суммы Сумма. При написании правила для определения предиката безопасный-ход обнаружилось, что было бы полезно использовать здесь уже вычисленное значение ним-суммы, а не перевычислять его заново. Предикат безопасная легко определяется с помощью предиката опасная. Ним-сумма вычисляется предложением ним-сумма( N з,НакопленнаяСумма,
268 Часть IV. Глава 20 Сумма). Вычисление этого отношения связано с получением ним-суммы Сумма чисел Ns посредством добавления их к накопленной сумме НакопленнаяСумма. Для выполнения сложений числа предварительно должны быть преобразованы в двоичный вид с помощью процедуры двоичное/2: ним-сумма([Ы | Ns],Bs,CyMMa) <- Abohhhoc(N,Ds),hhm .сложение(Оз,В8,В81), ним. cyMMa(Ns,Bs 1, Сумма). Число в двоичном виде представляется здесь списком цифр. Чтобы преодолеть затруднение со сложением чисел, представленных списками неравной длины, цифры с меньшими весами записаны в списке слева. Так, 2 (в двоичной системе 10) представляется как [0,7], а б-как [0,1,1]. При этом два числа поразрядно складываются, начиная с младших, левых разрядов. Это сложение выполняется при вычислении предиката ним-сложение/3. Оно несколько проще, чем обычное сложение, поскольку не требуется учитывать переносы. Тексты процедур двоичное и ним-сложение содержатся в программе 20.2. Ним-сумма Сумма используется в предикате безопасный-.ход (Ns,Сумма,Ход) для поиска выигрышного хода Ход в позиции, описываемой Ns. Предикат безопасный-ход/4 служит для поиска кучки, удаление из которой некоторого количества спичек создает безопасную позицию. Такой поиск выражается следующим интересным правилом: безопасный хол([Кучка | Кучки],НимСумма,К,(К,М)) ♦- лвоичное(Кучка,Вз), может _бытьнуль(В8,НимСумма, Ds,0), десятичное(Оз,М). Центральной частью этой программы является предикат может быть „нулъ(Bs, HuMCyMMa,Ds,TJepeHOc). Это отношение истинно, если замена двоичного числа Bs на двоичное число Ds приводит к нулевому значению НимСумма. Число Ds вычисляется последовательно цифра за цифрой. Каждая цифра определяется соответствующей цифрой Bs, НимСумма и цифрой переноса Перенос, которая первоначально устанавливается равной нулю. Чтобы сделать корректный ход, это число преобразуется с помощью предиката десятичное/2 в десятичное представление. Программа 20.2 представляет собой полную программу для игры Ним, которая в диалоге реализует выигрышную стратегию. Так же как и существующая программа для этой игры, она является некоторой аксиоматизацией того, что составляет выигрышную стратегию. 20.3. Игра в калах Рассмотрим теперь программу для игры в калах, в которой реализуется альфа-бета-отсечение. Калах-это игра, которая хорошо вписывается в парадигму деревьев игры: она имеет простую, приемлемо надежную оценочную функцию, а ее дерево игры удобно для обработки в отличие от таких игр, как шахматы и го. Известны утверждения, что существующие программы для игры в калах не проигрывали человеку. Несомненно, что и представленная здесь программа выйдет победителем. В калах играют на доске с двумя рядами из шести лунок, расположенных друг против друга. Каждый игрок владеет рядом из шести лунок и еще одной лункой справа, называемой калахом. В исходной позиции в каждой из шести лунок находится по шесть камешков, а лунка калах-пуста. Исходная позиция изображена в верхней половине рис. 20.3.
Игровые программы 269 о о 8 8 ©©о©©о 2 О о о Рис. 20.3. Положение на доске для игры в калах. Игрок начинает свой ход с изъятия всех камешков из одной из своих лунок. Затем, обходя доску против часовой стрелки, он раскладывает камешки по лункам, по одному камешку в лунку, включая собственный калах, но пропуская калах противника, до тех пор, пока не будут разложены все вынутые из лунки камешки. При этом возможны три различных исхода. Если последний из раскладываемых камешков попадает в калах, то игрок делает еще один ход. Если последний камешек попадает в собственную пустую лунку, а находящаяся напротив лунка противника содержит хотя бы один камешек, то игрок забирает все камешки из этой лунки противника и вместе с последним из раскладываемых камешков помещает их в свой калах. В остальных случаях ход игрока завершается, и следующий ход делает противник. Изображенная в нижней половине рис. 20.3 доска калаха представляет позицию, сложившуюся после двух ходов подряд игрока, владеющего верхним рядом лунок. Он взял шесть камешков из самой правой лунки. При их раскладывании последний камешек попал в калах, и поэтому игрок получил право еще на один ход. На этот раз он взял все камешки из четвертой справа лунки и разложил их. Если все лунки игрока становятся пустыми (даже в результате хода противника), камешки, оставшиеся в лунках противника, перекладываются в принадлежащий ему калах и игра завершается. В игре побеждает тот игрок, в калахе которого окажется больше половины камешков. Трудностью программирования этой игры на Прологе является поиск эффективной структуры данных для представления игральной доски, удобной для вычисления ходов. Мы используем четырехаргументную структуру доска(Лунки,Калах,Лун- киПротивника,КалахПротивника), где Лунки -список количеств камешков в ваших шести лунках, Калах- число камешков в вашем калахе, а ЛункиПротивника и Калах Противника это список количеств камешков и число камешков в калахе противника. Выбор списков, а не шестиместных структур был сделан с целью облегчения написания рекурсивных программ для распределения камешков по лункам. Ход состоит в выборе лунки и распределении находящихся в ней камешков. Ход описывается списком целых чисел со значениями от /до 6 включительно, которыми указываются лунки. Лунка 6-ближайшая к калаху игрока, в то время как лунка /-наиболее удаленная от него. Использование списка, а не отдельного числа
270 Часть IV. Глава 20 объясняется тем, что ход может продолжаться. Так, ход, изображенный на рис. 20.3, представляется списком [У,б]. В программе 20.3 представлены все ходы, используемые при организации возвратов. Если N больше 0, то предикат камешки(М,доска,N) обеспечивает получение числа камешков N в лунке М. Вычисление предиката завершается отказом, если в этой лунке нет камешков. Предикат продолжить-ход(М,Доска, N,Ms) обеспечивает продолжение хода Ms. Второе предложение в процедуре ход используется для обработки случая, когда во время выполнения хода все лунки игрока становятся пустыми. Основная часть программы игры в калах игра(Партия) <- См.программу 18.8. Выбор хода с использованием минимаксной стратегии и альфа-бета отсечение выбрать_ход (Позиция, компьютер, Ход) <- просмотр_вперед (Глубина), альфа_бета (Глубина, Позиция, —40,40, Ход, Значение), п1,\\тке(Ход),п1. выбрать_ход (Позиция, противник, Ход) <- nl, writeln(['Делайте, пожалуйста, ход']), геасЦХод), допустимый (Ход). альфа_бета(0, Позиция, Альфа, Бета,Ход,Значение) <- значение(Позиция,Значение), альфа, бета (D, Позиция, Альфа, Бета, Ход, Значение) <- D>0, множество_состояний(М,Ход(Позиция,М),Ходы), Альфа 1: = — Бета, Бета1: = — Альфа, D1:=D-1, оценить_.и_ выбрать(Ходы, Позиция, D1, Альфа 1, Бета 1, гп1,(Ход, Значение)). оценить_и_выбрать([Ход | Ходы],Позиция,0,Альфа, Бета, Запись,ЛучшийХод) <- ход (Ход, Позиция, Позиция 1), альфа_бета(0, Позиция I, Альфа, Бета, ХодХ, Значение) Значение 1:= — Значение, отсечение (Ход, Значение 1,D, Альфа, Бета, Ходы, Позиция, Запись, ЛучшийХод),!. оценить_и_выбрать([ ],Позиция,D,Альфа,Бета,Ход,(Ход,Альфа)). отсечение (Ход, Значение, D, Альфа, Бета, Ходы, Позиция, Запись, (Ход, Значение)) <- Значение ^ Бета,!. отсечение (Ход, Значение, D, Альфа, Бета, Ходы, Позиция, Запись, ЛучшийХод) «- Альфа < Значение,3начение < Бета,!. оценить_и_выбрать(Ходы, Позиция, 0,3начение, Бета, Ход, ЛучшийХод). отсечение(Ход,Значение,0,Альфа,Бета,Ходы,Позиция,Запись, ЛучшийХод) <- Значение ^ Альфа,!, оценить_и_выбрать (Ходы, Позиции, D, Альфа, Бета, Запись, ЛучшийХод). ход(Доска,[М | Ms]) «- member(M,[l,2,3,4,5,6]), камешки_в_лунке(М, Доска, N), продолжить_ход (N, М, Доска, Ms). ход(доска([0,0,0,0,0,0],К,У8,Ь),[ ]). камешки._в_лунке(М, доска (Hs, К, Ys, L), Камешки) <- п_й_член(М,Нз,Камешки),Камешки > 0. продолжить_ход(Камешки,М,Доска,[ ]) <- Программа 20.3. Программа для игры в калах.
Игровые програмхмы 271 Камешки = \ = (7 — М) mod 13,!. продолжить_х од (Камешки, М, Доска, Ms) <- Камешки =: = (7 — М) mod 13,!, распределить.камешки (Камешки, М, Доска, Доска 1), ход (Доска 1, Ms). Выполнение хода ход(|^ | Ns],Доска, ФинальнаяДоска) «- камешки_в_лунке(М, Доска, Камешки), распределить_камешки (Камешки, N, Доска, Доска 1), ход(№, Доска 1, Финальная Доска). ход([ ],Доска1,Доска2)<- обмен (Доска 1, Доска2). распределить_ камешки (Камешки,Лунка, Доска, Доска!) <- Состояние Доска! -результат распределения камешков (Камешки) из лунки (Лупка) при текущем состоянии Доска. Распределение производится в два этапа: распределение по своим лункам (распределить._по_.моим__лункам) и распределение по лункам противника (распределить._по_вашим_лункам). распределить камешки(Камешки, Лунка, Доска, Финальная Доска) «- распределить ло_моим_(гункам (Камешки, Лунка, Доска, Доска 1, Камешки 1), распределить по., вашим, .лункам (Камешки 1, Доска 1, Финальная Доска). распределить покоим лункам(Камешки,Ь1,доска(Hs,K,Ys,L)^ocKa(Hsl,Кl,Ys,L), Камешки) «- Камешки > 7 — N,!, взять_ и ..распределить (N, Камешки, Hs, Hs 1), К1: = К + 1, Камешки 1: = Камешки + N — 7. распределить _по._моим..лункам(Камешки,1Ч,доска(Н8,К, Ys,L), Доска,0) <- взять_и_.распределить(1М,Камешки,Н8,Н81), проверить. захват (N, Камешки, Hs 1, Hs2, Ys, Ys 1, Штуки), модифицировать_калах (Штуки, N, Камешки, К, К1), проверить._на_окончание(доска(Н82,К1Д81,Ь)Доска). проверить .захват(N, Камешки, Hs,Hs 1, Ys, Ys 1,Штуки) <- ФинишнаяЛунка: = N + Камешки, Противоположная Лунка: = 7 — ФинишнаяЛунка, п_й_член (Противоположная Лунка, Ys, Y). Y > 0,!, п подставка(ПротивоположнаяЛунка,Н8,0,Н81), п_подставка (ФинишнаяЛунка, Ys, 0, Ys l), Штуки:-Y+ 1. проверить..захват(N,Камешки,Hs,Hs,Ys,Ys,0)«- !. проверить_на_окончание(доска(Н8,КД8,Ь),доска(Н8,К,Н8,Ы) <- нуль(Н8), !,cnncoKcyMM(Ys,YsSum), LI := L-b YsSum. проверить_на_окончание(доска(Hs,K,Ys,L)^ocKa(Ys,K 1,Ys,L 1)) <- нуль(У8),!, списоксумм(Н8, HsSum), K1:=K + HsSum. проверить.на окончание (Доска, Доска) <- !. модифицировать_калах(0,Камни^,К,К) <- Камни < 7 — N,!. модифицировать..калах(0, Камни,N,К,К1) <- Камни = : = 7 — N,!, К1: = К + 1. модифицировать _калах(Штуки, Камни,N,К, К1) <- Штуки > 0,!, К1: = К 4- Штуки. распределить, по_вашим,лункам(0, Доска, Доска) <- !. распределить_по_вашим_лункам (Камешки, доска (Hs, К, Ys, L), доска(Н8, К, Ys 1, L)) <- 1 ^ Камешки, Камешки ^ 6, не_.нуль(Н8),!, Программа 20.3 (Продолжение)
272 Часть IV. Глава 20 распределить(Камешки,У8Д81). распределить.. .по._.вашим._.лункам (Камешки, доска (Hs, К, Ys,L), доска (Hs, К, Ysl,L))<- Камешки > 6,!, распределить (6, Ys, Ys l), Камешки 1 : = Камешки — 6, распределить. камешки (Камешки 1,1, доска (Hs, К, Ys 1, L), Доска), распределить .по_вашимлункам (Камешки, доска (Hs, К, Ys, L), доска (Hs, К, Hs, L1)) <- нуль(Н8), !,cnncoKcyMM(Ys, YsSum),Ll: = Камешки + YsSum + L. Распределение камешков на нижнем уровне взять и распределить(1, N, [H | Hs],[0|Hsl]) <- !, распределить (N, Hs, Hs 1). взять_.и„распределить(К,М,[Н | Hs],[H|Hsl]) <- К> 1,!, К1:= К— 1,взягь .и.„распределить(К1,М,Н8,Н81). распределить(0,Н8,Н8) <- !. распределить^, [Н | Hs],[Hl | Hsl]) <- N>0,!,N1:=N-1,H1: = H + 1, распределить (Nl,Hs,Hsl). pacпpeдeлить(N,[ ],[ ])<-!. Оценочная функция значение (доска (Н, К, Y,L), Значение) <- Значение: = К — L. Проверка окончания игры игра._закончена(доска(0,1М,0,N),Игрок,ничья) <- LUTyKH(K),N =:=6*K,!. игра закончена(доска(Н,К, Y,L),MrpoK,Игрок) <- uiTyKH(N), К >6*N,!. игра._закончена(доска(Н,КЛ,Ь),Игрок,Противник) <- uiTyKn(N),L > 6*М,следующий_партнер(Игрок,Противник). сообщение(противник) <- writeln (['Поздравляю! Вы победили.']), сообщение (компьютер) <- writeln (['Я победил.']). сообщение(ничья) <- writeln(['Hrpa закончилась вничью.']). Разные вспомогательные средства п й....член(Ч[Н|Н8],К)«- N > 1,!N1: = N - 1,п_й_член(ГМ1,Н8,К). п..й.член(1,[Н|Н8],Н). ^подстановка (1, [X | Xs], Y, [Y | Xs]) <- !. п ..подстановка(1, [X | Xs], Y, [Y | Xs 1 ]) <- N > 1, !,N1: = N - l,n подстановка(Nl,Xs,Y,Xsl). следующий_.игрок (компьютер, противник). следующий_.игрок (противник, компьютер). допустимый ([N | Ns]) <- 0 < N,N < 7, допустимый(Ns). допустимый ([ ]). обмен (доска (Hs, К, Ys, L), доска (Ys, L, Hs, К)). отображение_партии (Позиция, компьютер) <- демонстрация(Позиция). отображение_партии(Позиция, противник) <- обмен (Позиция, Позиция 1), демонстрация (Позиция 1). демонстрация (доска (Н, К, Y,L)) <- реверсировать(Н,НЯ), вывод_камешков(НЯ), вывод_калахов(К, L), вывод. KaMeiiiKOB(Y). Программа 20.3. (Продолжение)
Игровые программы 273 вывод_камешков(Н) «- nl, tab(5), отображение_лунок(Н). отображение_.лунок([Н | Hs]) <- вывод_кучки(Н), отображение „лунок(Hs). отображение, лунок([ ]) <- nl. вывод_кучки(ТЧ) <- N < 10, write(N), tab(4). вывод_кучки (N) <- N ^ 10,write(N), tab(3). вывод_калахов(К,Ь) <- write(K), tab(34), write(4), nl. нуль([0,0,0,0,0,0]). не_нуль(Нз) <- Hs Ф [0,0,0,0,0,0]. Инициализация просмотр_вперед(2). инициализировать(калах, доска([1М,N,N,N,N,N],0,[N,N,N,N,N,1М],0),противник) <- штуки (N). штуки (6). Ппо'-рамми ?.0 \ (Продолжение) Проверка того, продолжается ли ход, не элементарна, поскольку она может потребовать использования всех процедур для выполнения хода. Если последний камешек не попадает в калах, что может быть определено простым арифметическим подсчетом, то ход будет закончен и не потребуется распределять все камешки. В противном случае камешки распределяются по лункам и ход рекурсивно продолжается. Основным предикатом, используемым для выполнения хода, является предикат распределить -камешки(Камешки,,N, Доска, Доска] ), который определяет, что позиция Доска! получена из позиции Доска при распределении камешков Камешки, начиная с лунки с номером N. Распределение выполняется в два этапа: распределение камешков по своим лункам (предложение распределить-по-моим-лункам) и распределение камешков по лункам противника (предложение распределить -по -вашим- лункам). Более простой случай - распределение камешков по лункам противника. Содержимое лунок модифицируется предикатом распределить, и распределение камешков продолжается рекурсивно, пока имеются нераспределенные камешки. Во время выполнения хода проверяется, не опустели ли лунки игрока, и если обнаруживается что все лунки игрока пусты, то камешки из лунок противника перекладываются в его калах. Распределение камешков должно выполняться с учетом двух возможностей: распределение, начиная с любой данной лунки, и продолжение распределения для большого количества камешков. В предикате взять_«-распределить, который является обобщением предиката распределить, учтены эти требования. Предикат проверить-захват проверяет факт захвата камешков противника и соответственно изменяет содержимое лунок, в то время как предикат обновить-калах модифицирует число камешков в калахе игрока. В программу также включены некоторые другие вспомогательные процедуры, например процедура п-подстановка. Оценочная функция определяется как разность между количествами камешков в двух калахах; для вычисления ее значения используется следующее правило: значение(доска(Н,К,У,Ь),Значение) <- Значение: = К — L. 10-1402
274 Часть IV. Глава 21 Таким образом, описаны основные предикаты программы. Теперь пригодную для выполнения программу можно получить, добавив в нее предложения ввода- вывода, средства инициализации и завершения игры и др. Простые примеры такого дополнения можно найти в программе 20.3, представляющей собой полную программу этой игры. Для оптимизации программы в нее могут быть добавлены отсечения. Можно еще посоветовать переписать основной цикл программы, сделав его не рекурсивным, а управляемым отказами. Это иногда необходимо, когда в системе отсутствует оптимизация остатка рекурсии и нет хорошего сборщика мусора. 20.4. Дополнительные сведения Программа «Выдающийся ум» с небольшими отличиями впервые появилась в журнале SIGART Newsletter (Shapiro, 1983d) в ответ на публикацию программы этой игры на Паскале. Статья в SIGART привлекла внимание: появились как теоретические усовершенствования алгоритма игры, так и практические улучшения программы. Наиболее интересные анализ и обсуждения (Powers, 1984) касались оптимизации Пролог-программы (игра «Выдающийся ум» использовалась как иллюстративный пример), которая в конечном счете привела к ускорению выполнения программы в 50 раз. Доказательство корректности алгоритма для игры в Ним можно найти в любом учебнике, в котором обсуждаются игры на графах, например (Berge, 1962). Игра в калах давно используется для изучения игровых программ в исследованиях по искусственному интеллекту (Slagle и Dixon, 1969). Глава 21 Экспертная система для кредитных операций Время написания этой книги совпало с волной активности в промышленном применении результатов исследований по искусственному интеллекту. Особый интерес вызывают экспертные системы - программы для решения задач, ранее возлагавшихся лишь на высокооплачиваемых экспертов. Главное в экспертных системах - явное представление знаний. Вся наша книга имеет прямое отношение к программированию экспертных систем. Содержащиеся в ней примеры являются типичными Пролог-программами. Например, программа для решения уравнений, представленная в следующей главе, может и должна рассматриваться как некоторая экспертная система. Знания в экспертной системе обычно выражаются в форме некоторых правил. Таким образом, Пролог, основными предложениями которого являются правила, представляет собой естественный язык для реализации экспертных систем. В «Дополнительных сведениях» к гл. 19 кратко обсуждалась связь правил Пролога с классическими системами, например такой, как система MYCIN. Настоящая глава содержит описание реализации макетной экспертной системы. Пример взят из банкового дела: он состоит в оценке просьб о кредите от небольших коммерческих предприятий.
Экспертная система для кредитных операций 275 Мы дадим вымышленный отчет о развитии простой экспертной системы для оценки просьб клиентов о кредите в некотором банке. Описание составлено с точки зрения специалистов по Прологу или инженеров знаний, которым банк поручил создать экспертную систему. Разработке предшествовал наиболее трудоемкий и длительный этап - выделение экспертных знаний. Набравшись мудрости, программисты обратились за консультацией к единственному эксперту по банковому делу-Чесу Манхэттену1*. И Чес поведал о трех факторах, предельно важных при рассмотрении просьбы клиента о кредите. Самым важным фактором является обеспечение, которое может предложить клиент в случае свертывания предприятия. Различные типы обеспечения делятся по категориям. К обеспечению первого класса относятся местные или внешние депозиты. Наличные товары - пример обеспечения второго класса, в то время как обеспечение закладными и т.д. относится к категории неликвидов. Кроме того, очень важна финансовая характеристика клиента. Опыт деятельности банков показывает, что двумя наиболее существенными факторами финансовой характеристики клиента являются собственные средства на покрытие и текущая валовая прибыль от продаж. При оценке характеристики клиента должны учитываться краткосрочные обязательства клиента по годовой продаже, несколько меньшее значение имеет рост сбыта за последний год. Инженерам знаний, имеющим представление о деятельности банков, более подробного объяснения таких понятий не требуется. Вообще же инженер знаний должен разбираться в конкретной области настолько, чтобы понимать эксперта в этой области. Последний фактор, который следует рассмотреть, - ожидаемый доход банка. Это проблема, над которой банк работает постоянно. Существуют планы получения дохода в соответствии с профилем клиентов. Инженеры знаний могут рассчитывать, что подобная информация в подходящей форме будет им доступна. Рассказывая об этих трех факторах, Чес использовал качественные оценки: «Клиент имеет превосходную оценку финансового положения или хорошую форму обеспечения. Его предприятие может обеспечивать умеренный доход и т.п.» Как видно, даже те понятия, которые могли бы определяться количественно, обсуждаются на уровне качественных представлений. Финансовый мир слишком сложен, чтобы определяться лишь постоянно вычисляемыми количественными оценками. При высказываниях своих мнений эксперты в области финансов стремятся размышлять в качественных терминах, что позволяет им чувствовать себя более спокойно. Для отражения выводов эксперта и поддержки дальнейшего взаимодействия с Чесом качественные рассуждения должны моделироваться. В беседе с Чесом стало ясно, что значительное количество описанных им экспертных знаний может быть естественно выражено смесью процедур и правил. Под легким нажимом Чес во втором и третьем интервью дал правила для определения оценки обеспечения и финансового положения клиента. Применение этих правил связано со значительными вычислениями, но на самом деле, признался Чес, для экономии времени он выполняет экспресс-анализ, чтобы решить, стоит ли иметь дело с данным клиентом. Этой информации достаточно для построения макетной системы. Покажем, каким образом эти комментарии и наблюдения используются при создании экспертной системы. Отношение кредит (Клиент,Ответ), где Ответ - реакция на просьбу о кредите клиентом Клиент, является отношением верхнего уровня. В программе выделяются три модуля, соответствующие трем факторам, которые эксперт выделил как наиболее важные: обеспечение, финансовое-положение, п Искушенный читатель, конечно, догадался о родословной персонажа "Chas Manhattan Bank" и не будет шокирован последующей «фамильярностью» авторов.-Прим. ред. ю*
276 Часть IV. Глава 21 доход-банка. Первоначальная оценка клиента выполняется предикатом подходя- щий-профилъ (Клиент). Затем предикат оценить(Профиль,Ответ) обеспечивает получение ответа Ответ на основании оценки профиля Профиль, выполненной указанными выше тремя модулями. С гордостью, как инженеры знаний, отметим свойства описания верхнего уровня в предикате кредит/2. Здесь очевидна модульность системы. Каждый из модулей может разрабатываться независимо от остальной части системы. Кроме того, любые частные структуры данных определяются независимо от других, т.е. используется абстракция данных. Например, структура профиль(С,F,Y) представляет срез оценки обеспечения С, финансового положения F и дохода У некоторого клиента. При этом выбор такой структуры не затрагивает главных модулей и позволяет легко вводить изменения. Рассмотрим некоторые модули системы более подробно. Остановимся на существенных свойствах модуля оценки обеспечения. Предикат оценка-обеспечения/ 2 дает оценку обеспечения определенного клиента. Первый шаг состоит в определении соответствующего профиля клиента. Он выполняется предикатом профиль-обеспечения, который классифицирует обеспечение клиента как обеспечение первого-.класса, второго-класса или неликвидное и дает процент покрытия по каждому классу суммы кредита, запрашиваемого клиентом. Это отношение использует факты базы данных, касающиеся как банка, так и клиента. На практике базы данных для банка и для клиента могут быть разделены. Примеры фактов, представленные в программе 21.1, показывают, в частности, что местный денежный депозит является обеспечением первого класса. Вычисленный профиль используется для получения оценки с помощью предиката оценка-обеспечения. Здесь для оценки обеспечения {превосходное, хорошее и т.п.) используются эмпирические правила. Первое правило предложения оценка-обеспечения, например, читается так: «Обеспечение оценивается как превосходное, если покрытие суммы запрашиваемого кредита обеспечением первого класса составляет не менее 100%». Две особенности программы заслуживают комментария. Первая состоит в том, что используемая в программе терминология - это терминология Чеса. Это сделало программу почти самодокументируемой и понятной экспертам по банковскому делу, которые могут модифицировать ее при незначительной помощи инженеров знаний. Использование понятий конкретной предметной области облегчает отладку программы и помогает в использовании независимых от предметной области «средств объяснения», обсуждаемых в разд. 19.2. Вторая особенность программы в том, что кажущаяся безыскусность правил оценивания обманчива. Существует немало знаний и эмпирических фактов, скрытых за этими простыми соотношениями. Выбор неудачных значений для этих параметров может привести к тяжелым потерям. Модуль оценки финансового положения клиента проверяет финансовую стабильность клиента. В нем главным образом используются статьи баланса и ведомости прибыль/убытки. Финансовая оценка дается также и на качественном уровне. С помощью предложения считать осуществляется подсчет взвешенной суммы финансовых факторов, а предложение калибровать используется для определения качественного класса. Следует отметить, что и модуль оценки обеспечения, и модуль оценки финансового положения клиента отражают точку зрения и стиль мышления конкретного эксперта, Чеса Манхэттена, а не абсолютную истину. Даже в пределах одного банка не существует единомыслия. Некоторые специалисты консервативны, другие - готовы действовать с обдуманным риском. Программирование модулей оценки обеспечения и финансового положения клиента не вызывает затруднений. Знания, полученные от эксперта, почти непос-
Экспертная система для кредитных операций 277 редственно транслируются в текст программы. Однако модуль полной оценки клиента более сложен. Главная трудность состоит в адекватной формулировке знаний эксперта. Наш эксперт, например, больше уделял внимания оценке финансового положения, нежели общим правилам полной оценки. Он с удовольствием обсуждал профили отдельных клиентов и реакции на их просьбы о кредите, но неохотно шел на обобщения. Он предпочитал реагировать на предположения, а не на предлагаемые правила. Все это приводит к тщательной переоценке точной задачи, подлежащей решению. Система может дать три возможных ответа: удовлетворить просьбу о кредите, отказать в кредите, запросить дополнительную информацию. Рассматривались три фактора. Каждый фактор имел качественное значение, выбираемое из небольшого числа возможных значений. Например, оценка финансового положения могла принимать любое из следующих значений: плохое, среднее, хорошее или превосходное. Далее возможные значения должны занимать место в этой исходной шкале. Наша система, очевидно, связана с решением одной общей задачи: отыскать результат на некоторой порядковой шкале, основываясь на качественных результатах нескольких порядковых шкал. Таким образом, правила решения задачи должны обеспечивать получение вывода на основе определенных факторов. Мы вновь «прижали» Чеса и были вознаграждены несколькими правилами. Вот одно из них: «Если клиент имеет превосходную (или еще лучшую) оценку обеспечения, хорошую (или еще лучшую) оценку финансового положения и по крайней мере приемлемый доход, то кредит представляется». Некоторое промежуточное представление этого правила может выглядеть следующим образом: оценить(профиль(превосходный,хороший,приемлемый),дать_кредит). Однако в этом правиле не учитывается много возможных вариантов, например когда клиент имеет профиль (превосходный, хороший, превосходный). Можно перечислять все варианты для данного правила. Однако более благоразумным кажется построение более универсального средства для вычисления правил, выраженных в терминах качественных значений из порядковых шкал. Потенциально существует проблема, связанная с использованием порядковых шкал: она обусловлена большим числом отдельных случаев, которые следует специфицировать. Если каждый из N модулей имеет М возможных результатов, то существует NM вариантов, подлежащих рассмотрению. В общем случае невозможно иметь отдельное правило для каждого варианта. При этом возникают трудности не только размещения в памяти такого большого количества правил, но и поиска корректного правила. Поэтому мы определили небольшое множество специальных правил, одновременно покрывающих много вариантов, которых должно быть достаточно для клиентов данного банка. Для этих правил была выбрана структура правило (Условия,Вывод), где Условия - список условий, при выполнении которых применимо это правило, а Вывод-заключение, которое делается в результате применения правила. Условие имеет форму условие (Фактор,Отношение,Оценка), утверждающее, что фактор с именем Фактор связан с оценкой Оценка отношением с именем Отношение. Отношение представляется стандартными операторами отношения <, =, > и т.п. Упомянутое ранее правило можно записать теперь следующим образом: правило([условие(обеспечение,' ^ Превосходное), условие(финансы,1 ^ \xopoiiiee), условие(доход,4 ^ \приемлемый)],дать_кредит).
278 Часть IV. Глава 21 Еще одно правило Чес сформулировал так: «Если оценки и обеспечения, и финансового положения хорошие, а доход по крайней мере приемлемый, то Вы должны проконсультироваться у старшего эксперта». Это правило запишется так: правило([условие(обеспечение,' = \xopoiiiee), условие(финансы,1 = \хорошее), условие(доход,4 = Приемлемый)], рекомендуется .консультация). Факторы могут упоминаться дважды, чтобы указать, что они лежат в определенном диапазоне, или могут вообще не упоминаться. Например, правило правило([условие(обеспечение/ ^ 'умеренное), условие(финансы/ ^ \среднее)], отказать _в_кредите). устанавливает, что клиенту должно быть отказано в кредите, если оценка обеспечения не лучше, чем умеренная, а финансовое положение в лучшем случае среднее. Доход клиента не относится к делу и поэтому не упоминается. Интерпретатор для этих правил представляется недетерминированной программой. Эта процедура, как определяет правило оценить, «находит правило и проверяет условия его применения». Предикат проверить(Условия,Профиль) используется для проверки того, что отношение между соответствующими символами в правиле и символами, связанными с профилем Профиль клиента, специфицируется условиями Условия. Для каждого типа Тип условия, которое может встретиться, необходимо определить шкалу, которая задается порядком значений. Примерами шкал, определяемых фактами в базе данных банка, могут служить шкалы: шкала(обеспечение, [превосходное,хорошее,умеренное~\) и шкала(финансы, [превосходное,хорошее,среднее,плохое]). Предикат выбрать-значение доставляет соответствующий символ фактора при проверке порядка, которая реализуется предикатом сравнить. Он является предикатом доступа и, следовательно, единственным предикатом, зависящим от выбора структуры данных для профиля. К этому этапу макетная программа должна быть протестирована. Для этого необходимы некоторые данные о реальных клиентах и ответы системы по этим клиентам, которые сопоставляются с теми ответами, которые соответствующий банк сообщает официально. Данные для клиента клиент! даны в программе 21.2. Ответом на вопрос кредит (клиент 1 ,Х) будет дать-кредит. Наша макетная экспертная система-это сплав стилей и методов, а не только реализация вывода «от цели к фактам». Для определения оценки обеспечения используются эвристические правила, для оценки финансового положения применен простой алгоритм, для выражения результатов в терминах значений из дискретных порядковых шкал используется язык правил с соответствующим интерпретатором. Интерпретатор правила действует в направлении от условий к заключению, а не наоборот, как в Прологе. Экспертные системы должны быть настолько сложными, чтобы использовать различные существующие в настоящее время формы знаний. Разработка макетной экспертной системы легла на плечи не только инженеров знаний. Параллельно развивались различные другие средства поддержки работы экспертной системы. В частности, в качестве расширения программы 19.9 были разработаны средства объяснения. Был построен имитатор правил, основанный на порядковых шкалах, чтобы разрешить споры инженеров знаний относительно того, какой набор правил достаточен для покрытия диапазона результатов в общем случае. Наконец, была разработана программа контроля согласованности правил. Следующее метаправило демонстрирует очевидный принцип согласованности
Экспертная система для кредитных операций 279 правил: «Если все факторы клиента А не хуже факторов клиента В, то результат клиента А должен быть не хуже результата клиента В». 21.1. Дополнительные сведения Более подробное описание системы кредитования можно найти в (Ben-Devid и Sterling, 1985). кредит (Клиент,Ответ) <- Ответ-ответ на просьбу клиента Клиент о кредите. кредит (Клиент, Ответ) <- подходящий_профиль (Клиент), оценка_обеспечения (Клиент, Оценка Обеспечения), оценка_финансового_положения(Клиент,ОценкаФинансовогоПоложения), доход_банка (Клиент, Доход), оценить (профиль (ОценкаОбеспечения,ОценкаФинансового Положения, Доход), Ответ). Модуль оценки обеспечения оценка_обеспечения (Клиент, Оценка) <- Оценка - качественное описание, определяющее обеспечение, представляемое клиентом Клиент для покрытия запроса кредита. оценка_обеспечения (Клиент, Оценка) <- профиль_обеспечения(Клиент, ПервыйКласс, Второй Класс, Неликвид), оценка_обеспечения(ПервыйКласс, ВторойКласс, Неликвид, Оценка). профиль_обеспечения (Клиент, ПервыйКласс, ВторойКласс, Неликвид) <- запрашиваемый_кредит (Клиент, Кредит), процент_обеспечения(первый_класс, Клиент, Кредит, ПервыйКласс), процент_обеспечения (второй_класс, Клиент, Кредит, ВторойКласс), процент_обеспечения (Неликвид, Клиент, Кредит, Неликвид). процент_обеспечения(Тип, Клиент, Итог, Стоимость) <- set_of(X, Обеспечение | (обеспечение (Обеспечение, Тип), сумма(Обеспечение, Клиент, X)),Xs), sumlist(Xs, Сумма), Стоимость: = Сумма* 100/Итог. Правила вычисления вычисление_обеспечения(ПервыйКласс, ВторойКласс, Неликвид,превосходное) <- ПервыйКласс^ 100. вычисление_обеспечения (ПервыйКласс, ВторойКласс, Неликвид, превосходное) <- ПервыйКласс > 70, ПервыйКласс + ВторойКласс ^ 100. вычисление_обеспечения (ПервыйКласс, ВторойКласс, Неликвид, хорошее) <- ПервыйКласс + ВторойКласс > 60, ПервыйКласс + ВторойКласс < 70, ПервыйКласс + ВторойКласс + Неликвид ^ 100. Данные банка: классификация обеспечения обеспечение(местный_денежный_депозит,первый_класс). обеспечение(внешний_^енежный_^епозит,первый_класс). обеспечение(документ_о_совершении_сделки,второй_класс). обеспечение (закладная, неликвид). Программа 21.1. Экспертная система оценки возможности выдачи кредита.
280 Часть IV. Глава 21 Финансовая оценка финансовая_оценка (Клиент, Оценка) <- Оценка-качественное описание, определяющее финансовую характеристику, представляемую клиентом Клиент для поддержки запроса кредита. финансовая_оценка (Клиент, Оценка) <- финансовые_факторы (Факторы), счет (Факторы, Клиент, 0, Счет), калибровать (Счет, Оценка). Правила оценивания финансового положения калибровать (Счет, плохой) <- Счет ^ — 500. калибровать (Счет, средний) < 500 < Счет, Счет < 150. калибровать (Счет, хороший) <- 150 < Счет, Счет < 1000. калибровать (Счет, превосходный) <- Счет ^ 1000. Данные банка - взвешенные факторы финансовые_факторы ([(чистая_стоимость_имущества, 5), (рост .сбыта_за_последний_ год, 1), (валовая_прибыль_от_сбыта, 5), (краткосрочные_обязательства_годовых_продаж,2)]). счет ([(Фактор, Вес) | Факторы], Клиент, Акк, Счет) <- значение(Фактор, Клиент, Значение), Акк1 is Акк + Вес*Значение, счет (Факторы, Клиент, Акк 1, Счет). счет([ ], Клиент,Счет,Счет). Окончательная оценка оценить (Профиль, Результат) <- Результат - ответ клиенту с профилем Профиль. оценить (Профиль, Ответ) <- правило(Условия,Ответ),проверить(Условия, Профиль). проверить([условие(Тип,Тест, Оценка) | Условия], Профиль) <- шкала (Тип, Шкала), выбрать_значение(Тип, Профиль, Факт), сравнить (Тест, Шкала, Факт, Оценка), проверить (Условия, Профиль). проверить([ ],Профиль). сравнить(' = ', Шкала, Оценка, Оценка). сравнить(' > ',Шкала,Оценка 1,Оценка2) <- превосходит (Шкала, Оценка 1, Оценка2). сравнить(' ^ ',Шкала,Оценка 1,Оценка2) <- превосходит (Шкала, Оценка 1,Оценка2); Оценка 1 = Оценка2. сравнить(' < ',Шкала,Оценка 1,Оценка2) «- превосходит (Шкала, Оценка2, Оценка 1). сравнить(' ^',Шкала,Оценка 1,Оценка2) <- превосходит(Шкала,Оценка2, Оценка 1); Оценка 1 = Оценка2. превосходит([Я1 | Rs],Rl,R2). превосходит([К|Я8],Я1,Я2)^-К Ф Я2,превосходит(Кз,Я1,К2). выбрать_значение (обеспечение, профиль (С, F, Y), С). выбрать_значение (финансы, профиль^С, F, Y), F). выбрать_значение (доход, профиль (С, F, Y), Y). Утилиты sumlist(Xs,Sum) «-См. программу 8.66. Программа 21.1. (Продолжение)
Решатель уравнений 28 1 Данные банка и правила правило ([условие (обеспечение,' ^ ', превосходное), условие(финансы,' ^', хорошее), условие(доход,' ^ \приемлемый)],дать_кредит). правило ([условие (обеспечение,1 = ', хорошее), условие (финансы,' = ', хорошее), условие(доход,' >',приемлемый)],рекомендуется_консультация). правило ([условие (обеспечение,' ^ 'умеренное), условие(финансы,' ^ \среднее)],отказать_в_кредите). шкала (обеспечение, [превосходное, хорошее, умеренное]), шкала (финансы, [превосходное, хорошее, среднее, плохое]), шкала (доход, [превосходное, приемлемое, низкое]). Программа 21.1. (Продолжение) Данные клиента доход _банка (клиент 1, превосходное). запрашиваемый_кредит (клиент 1,50000). сумма(местный денежный_депозит, клиент 1,30000). сумма (внешний_денежный_^епозит, клиент 1,20000). сумма (гарантии .банка, клиент 1,3000). сумма (документ _о_совершении_.сделки, клиент 1,5000). сумма (наличные_товары, клиент 1,9000). сумма (закладная, клиент 1,12000). сумма (документы, клиент 1,14000). значение (чистая стоимость, имущества, клиент 1,40). значение (рост _ сбыта _.за_ .последний год, клиент! ,20). значение (валовая _ прибыль _от_.сбыта, клиент 1,45). значение(краткосрочные обязательства, годовых _продаж,клиент 1,9). подходящий. профиль(клиент1). Программа 21.2. Тестовые данные Для экспертной системы оценивания кредита. Глава 22 Решатель уравнений Естественной областью применения языка Пролог являются символьные вычисления. Например, программа на Прологе для символьного дифференцирования (типичная задача символьных вычислений) представляет собой, как показано в программе 3.29, непосредственную запись в определенном синтаксисе правил дифференцирования. В настоящей главе рассматривается программа для решения уравнений, представленных в символьной форме. Она является упрощенным вариантом комплекса программ PRESS (PRolog Equation Solving System), разработанного группой математических исследований факультета искусственного интеллекта Эдинбург-
282 Часть IV. Глава 22 ского университета. Программный комплекс PRESS решает уравнения на уровне учащихся старших классов школ с математической ориентацией. Глава имеет следующую структуру. В первом разделе дан обзор методов решения уравнений и приведены примеры решения. В следующих разделах рассматриваются четыре основных метода решения уравнений, реализованные в решателе уравнений. 22.1. Обзор методов решения уравнений Задача решения уравнения может быть описана синтаксически. Дано уравнение Ihs = Rhs с неизвестным X. Преобразуем это уравнение в эквивалентное уравнение X = Rhsl, правая часть которого Rhsl не содержит X. Полученное уравнение является решением. Два уравнения эквивалентны, если одно преобразуется в другое посредством конечного числа применений аксиом и правил алгебры. Хорошие учащиеся не должны решать уравнения путем слепого применения аксиом алгебры. Вместо этого их учат развивать и использовать различные методы и стратегии решения. В нашем решателе уравнений, моделирующем такое поведение, соответственно реализован набор методов решения уравнений. Каждый метод преобразует уравнение посредством применения алгебраических тождеств, выраженных в виде правил преобразования. Эти методы принимают разнообразные формы. Они могут быть набором правил для решения уравнения определенного класса или алгоритмами, реализующими процедуру решения. (1) cos(jc)(1 -2-sin(jc)) = 0, (2) х2- 3-х + 2 = 0, (3) 22*-5-2*+1 + 16 = 0. Рис. 22.1. Примеры уравнений. Абстрактно говоря, использование любого метода связано с проверкой условия его применимости и собственно применением. Типы уравнений, которые может решать наша программа, показаны тремя примерами, представленными на рис. 22.1. Они состоят из алгебраических функций одного неизвестного (используются операции +, —, *, / и возведение в целую степень), а также из тригонометрических и показательных функций. Во всех трех уравнениях х-неизвестное. Покажем коротко, как решается каждое из этих уравнений. Первый шаг в решении уравнения (1), представленного на рис. 22.1, состоит в факторизации. Решение задачи сводится к решению двух уравнений: cos (х) = 0 и 1 — 2 sin (x) = 0. Решение любого из этих уравнений является решением исходного уравнения. Оба уравнения, cos (x) = 0 и l — 2sin(x)=0, решаются путем разрешения уравнения относительно х. Это возможно, поскольку х встречается в каждом уравнении по одному разу. Решением уравнения cos (х) = 0 является х = arccos (х). Уравнение / — 2 sin (x) = 0 решается следующим образом: 1 -2sin(x) = 0, 2sin(x) = 1, sin(x) = 1/2, х = arcsin(l/2). Вообще, уравнения с единственным вхождением переменного могут быть решены алгоритмическим методом, называемым методом изоляции. В этом методе к обеим
Решатель уравнений 283 частям уравнения повторно применяется соответствующая обратная функция до тех пор, пока единственное вхождение переменного не «изолируется» как левая часть уравнения. Решение методом изоляции уравнения / — 2 sin (х) = 0 показано в виде представленной выше последовательности уравнений. Уравнение х2 — Зх + 2 = 0 является квадратным уравнением с одним неизвестным х. Все мы учили в школе формулу для решения квадратных уравнений. Сначала вычисляется дискриминант Ъ2 — 4ас. Для данного уравнения он равен 1. Уравнение имеет два решения: х = (- (- 3) + у/\)/2 = 2 и х = ( — ( — 3) — у/\)/2 = 1. Ключом к решению уравнения (3) рис. 22.1 является распознавание того, что уравнение в действительности является квадратным уравнением относительно 2х. Уравнение 22х — 5 • 2х +1 + 16 = 0 может быть переписано как (2х)2 — 5 • 2 • 2х + 16 = 0. Это уравнение, решаемое относительно 2х, дает два решения в форме 2х = Rhs, где Rhs не содержит х. Каждое из этих уравнений решается относительно х, тем самым получаем решения уравнения (3). Система PRESS тестировалась на решении уравнений, предлагаемых на экзаменах уровня А по математике в Великобритании. По-видимому, экзаменаторы любят предлагать для решения уравнения, подобные уравнению (3). Ученик, манипулируя логарифмическими, экспоненциальными и другими трансцендентными функциями, приводит их к форме, допускающей решение уравнения как алгебраического. Для решения уравнений этого типа развит метод, названный методом гомогенизации . Цель гомогенизации состоит в преобразовании уравнения в полином, содержащий неизвестное в некотором терме. (В учебных целях мы упростим более общий метод гомогенизации, реализованный в системе PRESS). Метод состоит из четырех шагов, которые проиллюстрируем на примере уравнения (3). Сначала производятся разбор уравнения и образование множества из экземпляров всех максимальных неполиномиальных термов, содержащих неизвестное. Такое множество называется множеством представителей. В нашем примере-это множество {22х,2х+ 1}. Второй шаг заключается в отыскании приведенного терма. Результатом гомогенизации является полиномиальное уравнение относительно приведенного терма. Приведенным термом в рассматриваемом примере будет 2х. Третий шаг гомогенизации состоит в нахождении правил преобразования, которые выражают каждый элемент множества представителей в виде полинома от приведенного терма. Нахождение такого множества гарантирует, что гомогенизация будет успешной. В нашем примере правила преобразования -это 22х = (2х)2 и 2{х + 1) = 2-2х. Наконец, правила преобразования используются для получения полиномиального уравнения. Завершим этот раздел кратким описанием решателя уравнений. Основным предикатом является solve „.equation (Equation, X, Solution). Это отношение истинно, если Solution -решение уравнения Equation с неизвестным X. Программа 22.1 представляет собой полную программу решателя уравнений. Программа 22.1 содержит четыре предложения с заголовком solve_equation, no одному предложению для каждого из четырех методов, необходимых при решении уравнений рис. 22.1. В общем случае требуется по одному предложению для каждого метода решения уравнения. В полном комплексе PRESS реализовано больше методов решения уравнений. Наш решатель уравнений не обладает рядом желательных свойств. Он не способен упрощать выражения, здесь отсутствует арифметика с плавающей запятой, не регистрируется последнее решенное уравнение, нет сервисных средств и т. д. Многими из этих возможностей обладает комплекс PRESS, о чем будет сказано в конце главы в разделе «Дополнительные сведения».
284 Часть IV. Глава 22 22.2. Факторизация Факторизация-первый метод, применяемый решателем при решении уравнения. Отметим, что проверка применимости факторизации выполняется очень просто, она заключается в унификации с уравнением А * В = 0. Если проверка успешна, то простейшие уравнения решаются рекурсивно. Предложение верхнего уровня, реализующее факторизацию, имеет вид solve__equation(A *В = 0,| Х,| Solution) <- factorize (A * В, X, Factors\[ ]), remove duplicates (Factors, Factors 1), solve. _factors(Factors 1, X, Solution). Первой целью в теле предложения верхнего уровня в программе 22.1 является отсечение. Это-зеленое отсечение: ни один из других методов не зависит от успешности факторизации. Вообще, мы не включаем зеленые отсечения в предложения и даем соответствующие описания в тексте. 22.3. Метод изоляции Для указания места единственного вхождения неизвестного и его обработки полезно ввести понятие по и пит подтерма в терме. Позиция подтерма в терме-это список номеров аргумента, описывающий местоположение подтерма. Рассмотрим уравнение cos (х) = 0. Терм cos (х), содержащий х, является аргументом уравнения, а х-первый (и единственный) аргумент cos(x). Следовательно, позиция х в cos (x) = 0 определяется списком [/,/]. Список [7,2,2,7] определяет позицию х в терме 7 — 2sin (х) = 0. Соответствующие диаграммы представлены на рис. 22.2. Предложение, определяющее метод изоляции, имеет вид solve „equation (Equation, X, Solution) <- single_occurrence(X, Equation), position (X, Equation, [Side | Position]), maneuver__sides(Side, Equation, Equation 1), isolate (Position, Equation 1, Solution). Условие, характеризующее применимость метода изоляции, состоит в проверке единственного вхождения переменной X в уравнение. Такая проверка осуществляется с помощью предиката single^occurrence. Позиция X вычисляется с помощью предиката position. Затем изоляция X проводится в два этапа. Сначала предикат cos х I х /\ 2 sin х Рис. 22.2. Позиция подтермов в термах.
Решатель уравнений 285 maneuver_sides обеспечивает вхождение X в левую часть уравнения, а потом с помощью предиката isolate X становится субъектом формулы. Помимо предиката single-occurrence полезно определить более общий предикат occurrence (Subterm, Term, N), который подсчитывает число N вхождений подтерма Subterm в терм Term. Как предикат occurrence, так и предикат position представляют собой типичные предикаты структурных проверок. Оба предиката предлагалось запрограммировать в качестве упражнений к разд. 9.2. Программные тексты этих предикатов содержатся в разделе «Утилиты» программы 22.1. solve'_ equation (Equation, Unknown,Solution) «- Solution -это решение уравнения Equation с неизвестным Unknown. solve, equation (А* В = О, X, Solution) <- factorize(A* В, X, Factors\[ ]), remove._duplicates(Factors, Factors 1), solve_factors(Factorsl,X, Solution). solve _equation(Equation,X, Solution) <- single_occurrence(X, Equation), !ч position(X, Equation, [Side | Position]), maneuver__sides(Side, Equation, Equation 1), isolate(Position, Equation 1, Solution). solve_equation(Lhs = Rhs, X, Solution) <- polynomial(Lhs,X), polynomial(Rhs, X), ; polynomiaLnormal_form(Lhs - Rhs,X, PolyForm), solve_polynomial_equation(PolyForm,X, Solution). solve ..equation (Equation, X, Solution) <- homogenize(Equation, X, Equation 1 ,X 1;), I solve_equation (Equation 1, X1, Solution 1), solve_.equation (Solution 1, X, Solution). Метод факторизации factorize (Expression, Subterm, Factors) <- расщепление мультипликативного терма Expression в разностный список множителей Factors, содержащий подтерм Subterm. factorize(A* В, X, Factor\ Rest) <- !, factorize(A, X, Factors \ Factors 1), factorize(B, X, Factors 1 \ Rest). factorize(C,X,[C | Factors] \ Factors) <- subterm(X,C),!. factorize(C, X, Factors \ Factors). solve./actors [Factors, Unknown,Solution) <- Solution-это решение уравнения Factor = 0 с неизвестным Unknown для некоторого множителя Factor в списке Factors. solve_factors([Factor | Factors],X,Solution) <- solve_equation (Factor = 0,X, Solution). solve__factors([ Factor | Factors], X, Solution) <- solve__factors(Factor, X, Solution). Программа 22.1. Полная программа решателя уравнений.
Часть IV. Глава 22 Метод изоляции single_occurrence(Subterm,Term) <- occurrence(Subterm,Term, l). maneuver_sides(l,Lhs = Rhs,Lhs = Rhs)«-!. maneuver_sides(2,Lhs = Rhs, Rhs = Lhs) <- !. isolate([N | Position], Equation,IsolatedEquation)« isolax(N, Equation, Equation 1), isolate(Position, Equation 1, IsolatedEquation). isolate([ ], Equation, Equation). Аксиомы, используемые в методе изоляции isolax(l, — Lhs isolax(l,Terml isolax(2,Terml isolax(l,Terml isolax(2,Terml isolax(l,Terml Term2 Ф 0 isolax(2,Terml Terml t^O isolax(l,Terml = Rhs, Lhs = -Rhs). + Term2 = Rhs, Terml + Term2 = Rhs,Term2 — Term2 = Rhs, Terml -Term2 = Rhs,Term2 *Term2 = Rhs, Terml = *Term2 = Rhs,Term2 = |Term2 = Rhs,Terml = = Rhs — Term2). = Rhs —Terml). = Rhs + Term2). = Term 1 — Rhs). Rhs/Term2) <- Rhs/Terml)+- = Rhs|(-Term2)). isolax(2,Term |Term2 = Rhs,Term2 = log(base(Terml),Rhs)). isolax(l, sin (U) = V,U = arcsin (V)). isolax(l, sin (U) = V,U = n — arcsin (V)). isolax(l, cos (U) = V,U = arccos (V)). isolax(l, cos (U) = V,U = -arccos (V)). Полиномиальные методы polynomial (Term, X) <- См. программу 11.4. polynomial_normalJorm (Expression, Term, PolyNormalForm) <- Poly NormalForm -это полиномиальная нормальная форма выражения Expression, которая является полиномом от Term. polynomial_normal_form(Polynomial,X, NormalForm) <- polynomial_form(Polynomial,X,PolyForm), remove_zero terms (Poly Form, NormalForm),!. polynomiaLform (X, X, [(1,1)]). polynomial_form(X | N,X,[(1,N)]). polynomiaLform (Terml + Term2,X, Poly Form) <- polynomiaLform (Term 1, X, Poly Form 1), polynomiaLform (Term2, X, Poly Form2), add_polynomials(PolyForml, Poly Form2, Poly Form). polynomiaLform (Terml — Term2,X, Poly Form) <- polynomiaLform (Term 1, X, Poly Form 1), polynomiaLform (Term2, X, Poly Form2), subtract_poly nomials(Poly Form 1, Poly Form2, Poly Form). polynomiaLform (Terml *Term2,X, Poly Form) <- poly nomiaLform (Term 1, X, Poly Form 1), polynomiaLform (Term2, X, Poly Form2), multiply_polynomials(Poly Form 1, Poly Form2, Poly Form). polynomiaLform (Term j N,X, Poly Form) <- !, Программа 22.1. (Продолжение) % Унарный минус % Сложение % Сложение % Вычитание % Вычитание % Умножение % Умножение % Возведение в % степень % Возведение в % степень % Синус % Синус % Косинус % Косинус
Решатель уравнений 287 polynomial_form(Term,X, Poly Form 1), binomial(PolyForml,N,PolyForm). polynomialJorm(Term,X,[(Term,0)]) <- free _of(X, Term),!. remove__zero_terms([(0,N) | Poly], Poly l) «- !, remove, zero terms(Poly, Polyl). remove_zero..terms([(C,N)| Poly],[(C,N)| Polyl]) <- С Ф 0,!, remove_zero_terms(Poly,Poly 1). remove_zero_.terms([ ],[ ]). Программы действий с полиномами add polynomials ( Polyl, Poly 2, Poly) <- Poly-сумма полиномов Polyl и Poly2, где Polyl, Poly2 и Poly являются полиномиальными формами. add_polynomials([ ], Poly, Poly)«- !. add_polynomials(Poly,[ ],Poly)«- !. add_polynomials([(Ai,Ni) | Polyl],[(Aj,Nj) | Poly2],[(Ai,Ni) | Poly]) <- Ni > Nj,!, add_polynomials(Poly l,[(Aj,Nj) | Poly2],Poly). add„_polynomials([(Ai,Ni) | Polyl],[(Aj,Nj) | Poly2],[(A,Ni) | Poly]) <- Ni =:= Nj,!,A: = Ai + Aj,add_polynomials(Polyl,Poly2,Poly). add_polynomials([(Ai,Ni) | Poly l],[(Aj,Nj) | Poly2],[(Aj,Nj) | Poly]) <- Ni < Nj,!, add_polynomials([(Ai,Ni)| Poly l],Poly2,Poly). subtract jpolinomials (Poly 1, Poly2, Poly) <- Poly - разность многочленов Polyl и Poly 2, где Polyl, Poly 2 и Poly являются полиномиальными формами. subtract_polynomials(Poly 1, Poly2, Poly) <- multiply_single(Poly2,( - 1,0), Poly3), add_polynomials(Poly 1, Poly3, Poly),!. multiply_single (Poly 1, Monomial, Poly) <- Poly - произведение полинома Polyl на одночлен MonomiaL где Polyl и Poly являются полиномиальными формами, а одночлен Monomial представлен в форме (C,N), обозначающей одночлен C*X**N. multiply_single([(C 1,N1) | Poly 1 ],(С, N), [(C2,N2) | Poly]) <- C2: = C1*C,N2:=N1 + N,multiply_single(Polyl,(C,N),Poly). multiply_single([ ], Factor, [ ]). multiply^polynomials (Poly 1, Poly2, Poly) <- Poly-произведение многочленов Polyl и Poly2\ Polyl, Poly2 и Poly являются полиномиальными формами. multiply_polynomials([(C,N) | Poly l],Poly2, Poly) <- multiply _single(Poly2,(C, N), Poly3), multiply_polynomials(Poly 1, Poly2, Poly4), add_.polynomials(Poly3, Poly4, Pol^). multiply_polynomials([ ],P,[ ]). binomial(Poly, l,Poly). Решатель алгебраических уравнений solve polynomial jequation (Equation, Unknown,Solution) <- Solution - решение алгебраического уравнения Equation с неизвестным Unknown. Программа 22.1. (Продолжение)
288 Часть IV. Глава 22 solve_polynomial_equation(PolyEquation,X,X = —В/А) <- linear(PolyEquation),!, pad(PolyEquation,[(A,l),(B,0)]). solve_polynomial_equation(PolyEquation,X,Solution) <- quadratic(PolyEquation),!, pad(PolyEquation,[(A,2),(B,l),(C,0)]), discriminant(A,B,C, Discriminant), root(X,A,B, Discriminant, Solution). discriminant(A,B,C,D) <- D: = B*B - 4*A*C. root(X,A,B,C,0,X = -B/(2*A)). root(X,A,B,C,D,X = (-B + sqrt(D))/(2*A)) <- D > 0. root(X,A,B,C,D,X=(-B-sqrt(D))/(2*A))<-D>0. pad([(C,N)| Poly],[(C,N)| Polyl]) <- !,pad(Poly,Polyl). pad(Poly,[(0,N)| Polyl]) <- pad (Poly, Polyl). pad([ ],[ ]). linear([(Coeff, 1)| Poly]), quadratic ([(Coeff, 2) | Poly]). Метод гомогенизации homogenize (Equation, X, Equation !,X 1) <- Уравнение Equation с неизвестным Х преобразуется в уравнение Equation! с неизвестным XI, где XI -выражение, содержащее X. homogenize(Equation,X, Equation 1, X1) <- offenders(Equation, X, Offenders), reduced..term (X, Offenders, Ту ре, X1), rewrite(Offenders,Type,X ^Substitutions), substitute(Equation, Substitutions, Equation 1). offenders (Equation, Unknown, Offenders) <- Offenders-множество максимальных неполиномиальных термов уравнения с неизвестным Unknown. offendenrs(Equation,X, Offenders) «- parse(Equation,X,Offenders 1 \ [ ]), remo ve_duplicates (Offenders 1, Offenders). multiple(Offenders). reduced_term (X, Offenders,Туpe, X1) <- classify (Offenders, X, Type), candidate(Type, Offenders, X, X1). Эвристики для экспоненциальных уравнений classify (Offenders, X, exponencial) «- exponentiaLoffenders (Offenders, X). exponential .offenders([A | В | Offs],X) <- free_of(X, A), subterm(X,B), exponential „offenders(Offs,X). exponentiaLoffenders([ ],X). candidate(exponential,Offenders,X, A | X) «- base(Offenders, A), polynomial. exponents(Offenders,X). base([A | В | Offs], A) <- base(Offs,~A). base([ ],A). Программа 22.1. (Продолжение)
Решатель уравнений 289 polynomial_exponents([A | В | Offs],X) <- is_polynomial(B,X),polynomial_exponents(Offs,X). polynomial_exponents([ ],X). Разбор уравнения и выполнение подстановок parse (Expression, Term, Ojfenders) <- Производится разбор выражения Expression с целью получения множества Offenders относительно терма Term, т.е. множества неалгебраических подтермов выражения Expression, содержащих терм Term. parse(A + B,X,Ll\L2)<- !, parse (А, X, L1 \ L3), parse (В, X, L3 \ L2). parse(A*B,X,Ll\L2)<- !,parse(A,X,Ll\L3),parse(B,X,L3\L2). parse(A-B,X,Ll\L2)<- !,parse(A,X,Ll\L3),parse(B,X,L3\L2). parse(A = B,X,Ll\L2)<- !,parse(A,X,Ll\L3),parse(B,X,L3\L2). parse(A|B,X,L)^ integer(B),!, parse(A,X,L). parse(A,X,L\L) <- free_of(X,A),!. parse(A,X,[A|L]\L)<- subterm(X,A),!. substitute (Equation, Substitutions, Equation 1) «- Список подстановок Substitutions применяется к уравнению Equation для получения уравнения Equation]. substituted + В, Subs, New A + New В) «- !,substitute(A,Subs,NewA),substitute(B,Subs,NewB). substitute(A * В, Subs, NewA * New В) <- !,substitute^, Subs, NewA), substitute(B, Subs, New B). substitute(A - B,Subs,NewA - NewB) «- ! ,substitute(A, Subs, NewA), substitute(B, Subs, NewB). substitute^ = B, Subs, NewA = NewB) <- ! ,substitute(A, Subs, NewA), substitute(B, Subs, NewB). substitute(A | B,Subs,NewA | B) <- integer(B),!,substitute(A, Subs, NewA). substituted, Subs, B) «- member(A = B, Subs),!. substituted, Subs, A). Нахождение правил преобразования в методе гомогенизации rewrite([Off| Offs],Type,Xl,[Off = Term | Rewrites]) +- homog_axiom (Ту ре, Off, X1, Term), rewrite(Offs, Type, X1, Rewrites). rewrite([ ],Type,X,[ ]). Аксиомы гомогенизации homog_axiom (exponential, A | (N*X),A | X,(A|X) | N). homog_axiom (exponential, A | (— X), A | X, 1 /(A j X)). homog_axiom(exponential,A f (X + B),A | X,A | B*A |X). Триграмма 22.1 (Продолжение)
290 Часть IV. Глава 22 Утилиты subterm (Sub, Term) <- См. программу 9.2. position(Term,Term,[ ]) <- !. position(Sub,Term,Path) <- com pound (Term), functor (Term, F,N), position(N,Sub,Term,Path),!. position(N,Sub,Term,[N | Path]) <- arg (N, Term, Arg), position (Sub, Arg, Path), position(N,Sub,Term,Path) «- N> 1,N1: = N- 1, position (N1, Sub, Term, Path). free_of (Subterm, Term) <- occurrence(Subterm,Term,N), !,N = 0. single_occurrence(Subterm,Term) <- occurrence(Subterm,Term,N),!, N = 1. occurrence (Term, Term, 1)<-!. occurrence(Sub,Term,N) <- compound (Term),!, functor(Term, F, M), occurrence(M, Sub, Term, 0, N). occurrence (Sub, Term, 0). occurrence(M,Sub,Term,Nl,N2) <- M > 0, !,arg(M,Term, Arg),occurrence(Sub, Arg, N), N3: = N + N1,Ml: = M - l,occurrence(Ml,Sub,Term,N3,N2). occurrence(0, Sub, Term, N, N). multiple([Xl,X2|Xs]). Предположения и данные для тестирования test_press(X,Y) <- equation(X,E,U),solve_equation(E,U,Y). equation(l,cos(x)*(l —2* sin(x)) = 0,x). equation (2,x|2-3*x + 2 = 0,x). equation(3,2|(2*x)-5*2|(x+ 1)+ 16 = 0,x). Программа 22.1. (Продолжение) Предикат maneuver_sides (N, Equation, Equation 1) состоит из двух фактов: maneuver_sides(l,Lhs = Rhs, Lhs = Rhs). maneuver_sides (2, Lhs = Rhs, Rhs = Lhs). Этот предикат обеспечивает нахождение неизвестного в левой части Equation!. Первый аргумент N, голова списка позиции, указывает ту часть уравнения, в которой содержится неизвестное. Если этот параметр имеет значение 1, то неизвестное содержится в левой части и сохраняется прежнее состояние уравнения. Значение параметра, равное 2, означает, что неизвестное содержится в правой части уравнения. В этом случае левая и правая части уравнения меняются местами. Преобразование уравнения производится с помощью предиката isolate/З. При этом правила преобразования применяются до тех пор, пока не будет исчерпан список позиции: isolate ([N | Position], Equation, IsolatedEquation) <- isolax (N, Equation, Equation 1), isolate (Position, Equation, IsolatedEquation). isolate ([ ], Equation, Equation).
Решатель уравнений 291 Правила преобразования, или аксиомы изоляции, специфицируются посредством предиката isolax (N, Equation, Equationl). В качестве примера рассмотрим решение уравнения / — 2sin (х) = 0. Эквивалентным преобразованием уравнений является добавление к обеим частям уравнения одной и той же величины. Покажем представление такого преобразования аксиомой изоляции применительно к обработке уравнений вида и — v = \\\ Отметим, что правила необходимы только для упрощения левых частей уравнений, поскольку гарантируется нахождение неизвестного именно в этой части. Необходимы два правила в соответствии с возможностью нахождения неизвестного в первом или во втором аргументе терма и — v. Терм и — v = w можно переписать двумя способами: и = w + v или v = и — w. Первый аргумент предиката isolax определяет, какой аргумент суммы содержит неизвестное. Правила преобразования на Прологе примут вид isolax (I, Term 1 — Term2 = Rhs, Terml = Rhs + Term2). isolax (2, Term 1 — Term2 = Rhs,Term2 = Terml — Rhs). Другие аксиомы изоляции более сложны. Рассмотрим, как упрощается произведение в левой части уравнения. Одним из ожидаемых должно быть следующее правило: isolax (I, Terml *Term2 = Rhs, Terml = Rhs/Tcrm2). Однако если Тегт2 равен нулю, то такое правило преобразования недопустимо. Следовательно, нужно добавить проверку, которая предотвратит применение аксиом для умножения, ^если терм, на который производится деление, равен нулю. Например, можно записать правило isolax (I,Terml *Term2 = Rhs, Terml = Rhs/Term2) <- Term2 Ф 0. Аксиомы изоляции для тригонометрических функций учитывают ситуацию, когда уравнение sin (x) = 1/2, к которому мы приходим в нашем примере, имеет два решения между о и 2п. Варианты решения поддержаны наличием двух отдельных аксиом isolax: isolax (I, sin (U) = V,U = arcsin (V)). isolax(1, sin(U) = V, U = n — arcsin(V)). Общее решение уравнения можно записать, прибавив к каждому решению 2пп, где «-произвольное целое. Решение о том, частное или общее решение требуется найти, зависит от контекста и семантической информации, независимой от решателя уравнений. Другие примеры аксиом изоляции приведены в полном решателе уравнений (программа 22.1). Описанная нами часть программы позволяет решать первое из представленных на рис. 22.1 уравнений, а именно уравнение cos (х) • (1 — 2sin (х)) = 0. Существуют четыре ответа: arccos (0), —arccos (0), arcsin ((I — 0)12), n-arcsin ((I — 0)12). Каждый ответ может быть упрощен, например, arcsin ((1 — 0)12) = п/6, но не будем этого делать, если выражение не вычисляется явно. Полезность решателя уравнений зависит от того, насколько хорошо он сможет выполнять такое упрощение, даже если оно не является непосредственной частью задачи решения уравнения. Однако написать программу для упрощения выражений нелегко. В общем случае задача определения эквивалентности двух выражений неразрешима. Некоторые простые тождества алгебры, например правило преобразования 0 + и в и, могут быть без труда включены в решатель. Выбор предпочтительной формы выражения из возможных вариантов, например (1 + х) 3 или / + Зх + Зх2 + х3, зависит от контекста.
292 Часть IV. Глава 22 22.4. Полиномиальные уравнения Полиномиальные уравнения решаются решателем полиномиальных уравнений, в котором реализуются разные методы решения таких уравнений. Обе части уравнения проверяются, чтобы установить, являются ли они полиномами от переменной X. Если проверка успешна, то уравнение с помощью предиката polynomial_normal_ form преобразуется в полиномиальную нормальную форму и производится обращение к предикату solve_polynomial_equation, входящему в решатель полиномиальных уравнений: solve..equation(Lhs = Rhs,X,Solution) <- polynomial (Lhs, X), polynomial (Rhs, X), polynomial normal _form (Lhs — Rhs, X, Poly), solve _polynomial._equation (Poly, X, Solution). Полиномиальная нормальная форма представляет собой список пар вида (A^NJ, где А{-коэффициент при XNi, который должен быть не равен нулю. Пары сортируются в порядке строгого убывания Nit для каждой степени существует не больше одной пары. Например, список 1(1,2), (— 3,1), (2,0)] представляет собой нормальную форму полинома х2 — Зх + 2. Старший член полинома является головой списка. Классические алгоритмы обработки полиномов применимы для решения уравнений в нормальной форме. Сведение уравнения к полиномиальной нормальной форме производится в два этапа: polynomial, normal form (Polynomial, X, Normal Form) <- polynomiaLform (Polynomial, X, Poly Form), remove _zero_terms (Poly Form, Normal Form). Предикат polynomiaLform (X, Polynomial, PolyForm) используется для декомпозиции полинома. Отсортированный список PolyForm состоит из пар коэффициент- степень, среди которых могут быть и пары с нулевыми коэффициентами. Для многих полиномиальных методов удобно полагать отсутствие в полиномиальной форме термов с нулевыми коэффициентами. Поэтому вторым этапом сведения к полиномиальной нормальной форме должно быть удаление тех термов, коэффициенты которых равны нулю. Удаление осуществляется простой рекурсивной процедурой remove_zer-о_terms. Программа для предиката polynomial Jorm непосредственно отражает программу предиката polynomial. Для каждого предложения, используемого в процессе разбора, есть соответствующее предложение, дающее результирующий полином. Например, полиномиальной формой терма хп будет [(/,«)]. Эта форма выражается предложением polynomial _ form (X | N, X, [(1, N)]). Для сохранения полиномиальной формы используются рекурсивные предложения polynomiaLform, манипулирующие полиномами. Рассмотрим предложение polynomial, form (Poly 1 + Poly2, X, PolyForm) <- polynomiaLform (Poly 1, X, PolyForm 1), polynomiaLform (Poly2, X, PolyForm2), add_.polynomials (PolyForm 1, PolyForm2, PolyForm). Процедура add_polynomials реализует алгоритм сложения полиномов, представленных в нормальной форме. Соответствующая программа - это непосредственный список возможностей, которые могут появиться.
Решатель уравнений 293 add „polynomials ([ ], Poly, Poly). add_polynomials ([P | Poly], [ ], [P | Poly]). add_polynomials([(Ai, Ni) | Poly 1], [(Aj, Nj) | Poly2], [(Ai, Ni) | Poly]) <- Ni > Nj,add_polynomials(Polyl,[(Aj,Nj)|Poly2],Poly). add_polynomials([(Ai,N)| Poly 1 ],[(Aj,N)| Poly2],[(A,N)| Poly]) <- Ni = : = Nj,A: = Ai 4- Aj,add_polynomials(Poly 1,Poly2,Poly), add .polynomials([(Ai, Ni) | Poly 1 ], [(Aj, Nj) | Poly2], [(Aj, Nj) | Poly]) <- Ni < Nj, add_polynomials ([(Ai, Ni) | Poly 1 ], Poly2, Poly). Подобным образом процедуры subtr act-polynomials, multiply-polynomials и binomial представляют алгоритмы вычитания, умножения и биномиального расширения полиномов, заданных в нормальной форме. Получаемые результаты также являются полиномами в нормальной форме. Вспомогательный предикат multiply_ single (Polyl, Monomial, Poly2) умножает полином на одночлен (С, N) и в качестве результата образует новый полином. Как только полином представлен в нормальной форме, вызывается решатель алгебраических уравнений. Структура решателя алгебраических уравнений совпадает со структурой общего решателя уравнений. В решателе уравнений реализован набор методов решения, которые поочередно испытываются с целью определения, какой из них применим для решения данного уравнения. Предикат solve-polynomial _equation аналогичен предикату solve_equation. Второе уравнение на рис. 22.1 является квадратным уравнением и может быть решено с помощью стандартной формулы. Решатель уравнений использует тот же метод, что и человек. Чтобы убедиться, подходит ли данный метод для решения уравнения, с помощью предиката quadratic проверяется, имеет ли старший член полинома вторую степень. Поскольку нулевые члены исключаются при приведении полинома к нормальной форме, посредством предиката pad такие члены, если необходимо, добавляются. Следующие два шага хорошо знакомы: вычисление дискриминанта уравнения и в соответствии со значением дискриминанта определение корней уравнения. Вновь наличие нескольких решений требует учета соответствующих возможностей: solve_polynomial_.equation (Poly, X, Solution) <- quadratic (Poly), pad (Poly, [(A,2),(B,1),(C,0)]), discriminant (А, В, С, Discriminant), root (X, А, В, С, Discriminant, Solution), discriminant (A, B, C, D) <- D: = (B * В - 4 * A * C). root (X, A, B, C, 0, X = - B/(2 * A)). root (X, A, B, C, D, X) = (- В + sqrt (D))/(2 * A)) <- D > 0. root(X,A,B,C,D,X) = (- В - sqrt(D))/(2 *A)) <- D > 0. Другие предложения solve'^polynomial\ equation представляют в программе отдельные методы для решения разных алгебраических уравнений. Так, линейные уравнения решаются с помощью простой формулы. В комплексе PRESS кубические уравнения решаются в предположении знания одного из корней факторизацией и сведением к квадратному уравнению. Другие приемы распознают очевидные множители или замаскированные квадратные уравнения с отсутствующими кубическим и линейным членами.
294 Часть IV. Глава 22 22.5. Гомогенизация Предложение верхнего уровня solve_equation отражает преобразование исходного уравнения в новое уравнение с новым неизвестным. Преобразованное уравнение решается рекурсивно, затем находится решение исходного уравнения. solve_equation (Equation, X, Solution)«-. homogenize (Equation, X, Equation 1, X1), solve_equation (Equation 1, X1, Solution 1), solve_equation (Solution 1, X, Solution). Тело правила для предиката homogenize/4 реализует четыре этапа гомогенизации, описанные в разд. 22.1. Множество представителей вычисляется предикатом offenders/3, который проверяет наличие в этом множестве нескольких элементов. Если в множестве только один элемент, то гомогенизация бесполезна: homogenize (Equation, X, Equation 1, X1) <- offenders (Equation, X, Offenders), reduced_term (X, Offenders, Type, X1), rewrite (Offenders, Type, XI, Substitutions), substitute (Substitute, Equation, Equation 1). Предикат reduced_term/4 находит некоторый приведенный терм, рассматриваемый далее в качестве нового неизвестного. Для организации поиска приведенного терма определяется тип уравнения. Этот тип используется на следующем этапе для поиска правила преобразования, выражающего каждый элемент множества представителей в виде функции приведенного терма. Уравнение, рассматриваемое в качестве примера, имеет экспоненциальный тип. В комплексе PRESS используется много эвристических правил поиска подходящего приведенного терма. Эти эвристики зависят от типа термов, входящих в множество представителей. Для облегчения процесса поиска приведенного терма выделяются два этапа: классификация множества представителей по типу и нахождение приведенного терма данного типа: reduced_term (X, Offenders,Type, X1) <- classify (Offenders, X,Type), candidatefTy ре, Offenders, X, X1). Как видно, множество правил соответствует рассматриваемому нами уравнению. Множество представителей относится к экспоненциальному типу, поскольку все элементы в нем имеют вид Ав, где А не содержит, а В содержит неизвестное. Истинность этого проверяется стандартными рекурсивными процедурами. Эвристика, испоттьзуемая в этом примере для выбора приведенного терма, формулируется так: если все термы рассматриваемого множества имеют одинаковое основание А, а каждый показатель степени представляет собой полином от неизвестного X, то подходящим приведенным термом является терм Ах: candidate(exponential,Offenders, А, А |Х) <- base (Offenders, А), polynomial_exponents(Offenders,X). Простые программы для предикатов base и polynomial^exponents содержатся в полном тексте программы решателя уравнений. В комплексе PRESS реализованы более тонкие эвристики. Например, может быть вычислен наибольший общий делитель всех старших членов полиномов и использован для выбора приведенного терма. Следующий шаг состоит в проверке того, может ли каждый элемент множества представителей быть выражен через предлагаемый приведенный терм. Это требует поиска соответствующего правила. Набор предложений homogenize_axiom содержит
Решатель уравнений 295 возможные правила преобразования. Другими словами, подходящие правила должны быть специфицированы заранее. В рассматриваемом примере применимыми являются следующие правила: homogenize ...axiom (exponential, A | (N * X), А | Х,( А | X) | N). homogenize ,.axiom(exponential,A|(X + В),А|Х,А|В*А|Х). Подстановка терма в уравнение отражает процесс разбора, использующий предикат offenders, так как проверяется каждая часть уравнения с целью определения в ней терма, подлежащего замене. Упражнения к гл. 22 1. Добавьте в программу 22.1 аксиомы изоляции для обработки отношений в левой части уравнения. Решите уравнение х/2 = 5. 2. Измените решатель алгебраических уравнений так, чтобы с его помощью можно было решать замаскированные линейные и квадратные уравнения. Решите уравнения 2х3— 8 = х3 н х4-5.x2+6=0. 3. Уравнение cos (2х) — sin (х) = 0 может быть решено как квадратное уравнение с неизвестным sin (x). Данное уравнение приводится к квадратному с помощью подстановки cos (2х) = 1 — 2sin2(х). Добавьте в программу 22.1 предложения для решения этого уравнения. При этом необходимо добавить правила для идентификации термов типа trigonometric, эвристики для поиска приведенных тригонометрических термов и соответствующие аксиомы гомогенизации. 4. Перепишите предикат free_of (Term.X) так, чтобы он принимал значение «ложь» при обнаружении вхождения X в терм Term. 5. Измените программу 22.1 так, чтобы она решала простые системы уравнений. 22.6 Дополнительные сведения Символьные вычисления были одним из начальных применений Пролога. Примерами первых программ являются программа символьного интегрирования (Bergman и Kanoui, 1973) и программа доказательства теорем геометрии (Welham, 1976). Программный комплекс PRESS, упрощенным вариантом которого является программа 22.1, разрабатывался большой группой специалистов. Многие специалисты из группы математических исследований Эдинбургского университета, работая с Аланом Банди, создавали этот комплекс. Опубликованные описания принадлежат Bundy и Welham (1981), Sterling и др. (1982) и Silver (1986). В последней из указанных публикаций содержится подробное обсуждение гомогенизации. Комплекс PRESS содержит различные модули, не рассматриваемые в этой главе, но по-своему интересные, например: пакет интервальной арифметики Bundy, 1984), пакет арифметики с плавающей запятой произвольной точности, разработанный Ричардом О'Кифом, и разработанная Лоренсом Бердом программа упрощения выражений, основанная на разностных структурах, которые были описаны в разд. 15.2. Успешная интеграция всех этих модулей-убедительное подтверждение возможности реализации средствами Пролога больших программных проектов. Во время разработки комплекса PRESS решались классические вопросы создания программного обеспечения. Например, на одном этапе было произведено усовершенствование программы на основе анализа статистических данных. Было выполнено профилирование программы, которое показало, что наиболее часто вызывается предикат free_of Предикат был изменен, как это предлагалось сделать в упражнении 4 к гл. 22, что привело к ускорению работы комплекса PRESS на 35%. Программа 22.1 является существенно урезанной и упрощенной версией комплекса PRESS. Приведение этой программы в порядок позволило провести дополнительные исследования. Программа 22.1 легко транслировалась на другие языки логического программирования- Concurrent Prolog и FCP(Sterling и Codisn, 1986). В условиях явного использования методов удалось разработать программу для изучения новых методов решения уравнений на основании примеров (Silver, 1986).
296 Часть IV. Глава 23 Глава 23 Компилятор В качестве последнего примера программы на языке Пролог рассмотрим компилятор. Изложение согласовано с нисходящим принципом проектирования программы. В первом разделе главы в общих чертах излагается назначение компилятора и дается его определение. В следующих трех разделах описываются основные компоненты компилятора: синтаксический анализатор, генератор кода и ассемблер. 23.1. Обзор компилятора Исходный язык компилятора PL (Programming Language)-упрощенная версия языка Паскаль-разработан исключительно для целей этой главы. В него входят оператор присваивания, условный оператор, оператор цикла типа while и простые операторы ввода-вывода. Возможности этого языка легко проиллюстрировать с помощью примера. На рис. 23.1 представлена программа на PL для вычисления факториала. Формальное определение синтаксиса языка неявно содержится в программе 23.1, которая является синтаксическим анализатором. program factorial; begin read value; count: = 1; result: = 1; while count < value do begin count: = count + 1; result: = result* count end; write result end Рис. 23.1. PL-программа для вычисления факториала. Объектный язык - это машинный язык, типичный для ЭВМ с одним аккумулятором. Команды машины представлены на рис. 23.2. Каждая команда имеет один явный операнд, который может быть целочисленной константой, адресом ячейки памяти, адресом команды программы или игнорируемой величиной. Кроме того, имеется псевдокоманда Block, с помощью которой резервируется некоторое количество ячеек памяти, специфицируемое ее целочисленным операндом. Назначение компилятора ясно из рассмотрения нашего примера. На рис. 23.3 показан результат трансляции PL-программы для вычисления факториала в программу на машинном языке. Компилятор вырабатывает команды, представленные на рис. 23.3 двумя колонками, которые обозначены словами «команда» и «операнд» соответственно. Процесс компиляции, как показано на рис. 23.4, может быть разбит на пять этапов. На первом этапе исходный текст программы преобразуется в список лексем.
Компилятор 297 Арифметические команды Литеральные addc subc mule dive loadc store С обращением к памяти add sub mul div load Команды jumpeq jumpne jumplt jumpgt jumple jumpge jump управления Команды ввода-вывода и др. read write halt Рис. 23.2. Команды объектного языка. Символ LABEL1 LABEL2 COUNT RESULT VALUE Адрес 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 Команда READ LOADC STORE LOADC STORE LOAD SUB JUMPGE LOAD ADDC STORE LOAD MUL STORE JUMP LOAD WRITE HALT BLOCK Операнд 21 1 19 20 20 19 21 16 19 1 19 20 19 20 6 20 0 0 3 Символ VALUE COUNT RESULT COUNT VALUE LABEL2 COUNT COUNT RESULT COUNT RESULT LABEL1 RESULT Рис. 23.3. Программы на языке ассемблера для вычисления факториала. Лексический, анализ Исходный текст *» Синтаксический анализ Список лексем Исходная структура Генерация кода Ассемблирование Вывод ^ Объектная структура *~ Объектная структура *~ Объектная (перемещаемая) (абсолютная) программа Рис. 23.4. Этапы компиляции. На этапе синтаксического анализа выполняется разбор списка лексем и вырабатывается некоторая исходная структура (внутреннее представление программы). На третьем этапе исходная структура преобразуется в перемещаемую объектную программу, на четвертом-с помощью ассемблера перемещаемая объектная про-
298 Часть IV. Глава 23 грамма преобразуется в абсолютную объектную программу. На последнем этапе осуществляется вывод объектной программы. В нашем компиляторе реализованы второй, третий и четвертый этапы процесса компиляции. Как стадия лексического анализа, так и этап вывода готовой объектной программы сравнительно неинтересны и здесь не рассматриваются. На верхнем уровне компилятора выполняются синтаксический анализ, генерация кода и ассемблирование. Основной предикат compile (Tokens,ObjectCode) связывает список лексем Tokens с объектной программой ObjectCode, соответствующей программе, представленной списком лексем. Компилятор корректно транслирует любую допустимую программу на языке PL, но не обрабатывает ошибки (этот вопрос в данной главе не рассматривается). Предполагается, что список лексем формируется на предыдущей стадии лексического анализа. Синтаксический анализатор, выполняющий синтаксический анализ с помощью предиката parse, вырабатывает по списку лексем Tokens внутреннее дерево разбора Structure. Эта структура используется генератором кода (предикат encode) для формирования перемещаемой программы Code. Для генерации кода необходима таблица символов (словарь), содержащая имена переменных и соответствующие им адреса ячеек памяти. Кроме того, в таблице символов регистрируются метки, встречающиеся в программах. Она является вторым аргументом предиката encode. Наконец, перемещаемая программа преобразуется в абсолютную объектную программу посредством предиката assemble с помощью сконструированной таблицы символов Dictionary. compile (Tokens, ObjectCode) «- Объектная программа ObjectCode является результатом компиляции PL-программы, представленной списком символов Tokens. compile(Tokens, ObjectCode) «- parse(Tokens, Structure), encode(Structure, Dictionary, Code), assemble(Code, Dictionary, ObjectCode). Синтаксический анализатор parse (Tokens,Structure) «- Structure-результат успешного разбора списка Tokens. parse(Source, Structure) «- pLprogram(Structure, Source \[ ]). pLprogram(S) -* [program],identifier(X),[';'],statement(S). statement((S;Ss)) -* [begin], statement(S),rest_statements(Ss). statement(assign(X,V)) -* identifier(X), [':= '],expression(V). statement(if(T,S1,S2))-* [if], test (T),[then], statements 1), [else], statement(S2). statement(while(T,S) -* [while], test(T), [do], statement(S). statement(read(X)) -* [read],identifier(X). statement(write(X)) -* [write], expression (X). rest_statemcnt((S;Ss)) -* [';'], statement(S), rest statements(Ss). rest_statements(void) -* [end]. Программа 23.1. Компилятор для языка PL.
Компилятор 299 expression(X) -* pLconstant(X). expression(expr(Op, X, Y)) -+ pLconstant(X), arithmetic_op(Op), expression(Y). arithmetic_op(' + ')-»[' + ']. arithmetic_op(' — ')-»[' —']. arithmetk^opC*') -*['*']. arithmetic_op(7') -* ['/]• pl_constant(name(X)) -* identifier(X). pl_constant(number(X)) -* pLinteger(X). identifier(X) - [X],{atom(X)}. pLinteger(X) -* [X], {integer(X)}. test(compare(Op,X,Y)) -+expression(X),comparison_op(Op),expression(Y). comparison_op(' = ')-»[' = ']. comparison_op(' ф ')-»[' Ф ']. comparison_op(' >')-*['> ']• comparison_op(' <')-*['<']. comparison_op(' ^ ') ~* Г ^ ']• comparison_op(' ^ ')-»[' ^']. Генератор кода encode (Structure, Dictionary, RelocatableCode) <- Перемещаемая программа RelocatableCode генерируется исходя из промежуточного представления программы Structure; при этом строится словарь Dictionary, в котором содержатся имена переменных с соответствующими адресами. encode((X;Xs),D,(Y;Ys))<- encode(X, D, Y), encode(Xs, D, Ys). encode(void, D, no_op). encode(assign (Name, E), D,(Code; instr(store, Address))) <- lookup (Name, D, Address), encode_expression (E, D, Code). encode(if(Test, Then, Else), D, (TestCode; ThenCode; instr(jump, L2); label(L 1); ElseCode;label(L2))) <- encode_test (Test, L1; D, TestCode), encode (Then, D, ThenCode), encode(Else, D, ElseCode). encode(while(Test, Do), D, (label (L1); TestCode; DoCode; instr(jump, L1); label(L2))) <- encode_test(Test,L2,D,TestCode), encode (Do, D, DoCode). encode(read(X), D, instr(read, Address)) <- lookup(X, D, Address). encode(write(E),D,(Code;instr(write,0)))<- encode_expression (E, D, Code). encode^expression (Expression, Dictionary, Code) <- Программа Code соответствует арифметическому выражению Expression. encode_expression (number(C), D, instr (loadc, C)). encode_expression (name(X), D, instr (load, Address)) <- lookup (X, D, Address). encode_expression(expr(Op, E1, E2), D,(Load; Instruction)) <- single_instruction(Op, E2, D, Instruction), encode_expression(E 1, D, Load). encode_expression (expr (Op, E1, E2), D, Code) <- Программа 23.1. (Продолжение)
Часть IV. Глава 23 not single_instraction(Op, E2,D, Instruction), single_operation (Op, E1, D, E2Code, Code), encode_expression(E2, D, E2Code). single_instruction(Op, number(C), D, instr(OpCode, C)) <- literaLoperation (Op, OpCode). single. instruction(Op,name(X),D,instr(OpCode,A)) «- memory _operation (Op, OpCode)), lookup (X, D, A). single, operation (Op, E,D, Code, (Code; Instruction)) <- commutative(Op), single_instruction(Op, E, D, Instruction). single_operation (Op, E, D, Code, (Code; instr(store, Address); Load; instr(OpCode, Address))) «- not commutative(Op), lookup(4emp\D, Address), encode_expression(E, D, Load), op_code(Op, E, OpCode). op_code(Op,number(C),OpCode)<- literal_operation(Op,OpCode). op_code(Op,name(X),OpCode) <- memory_operation(Op,OpCode). literaLoperation (' + ',addc). memory _ope ration (' + \add). literaLoperationf —', subc). memory _operationf —\ sub), literal_operationf *\ mule), memory operationf *\mul). literaLoperationf/', dive), memory _operation(y'div). commutativef + '). commutativef*'). encode_test (compare (Op, E1, E2), Label, D,(Code;instr(OpCode, Label))) <- comparision_opcode(Op, OpCode), encode_expression(exprf — ',El,E2),D,Code). companson_opcode(' = 'Jumpne). comparison_opcode(' ф 'Jumpeq). comparison_opcode(' > 'Jumple). comparison_opcodef ^ \jumplt). comparison_opcodef < \jumpge). comparison_opcodef ^ 'Jumpgt). lookup(Name,Dictionary, Address)«- См. программу 15.9. Ассемблер assemble (Code,Dictionary, TidyCode) <- TideCode- результат ассемблирования программы Code, в процессе которого производится удаление меток (label) и примитивов по_ор, а также заполнение словаря Dictionary. assemble(Code, Dictionary, TidyCode)<- tidy_and_count(Code, l,TidyCode\(instr(halt,0); block(L))), N1: = N+1, allocate(Dictionary,N 1,N2), L: = N2-N1,!. tidy_and_count((Codel;Code2),M,N,TCodel \Code2) <- tidy_and_count(Codel,M,Ml,TCodel\Rest), tidy _and_count (Code2, M1, N, Rest \ TCode2). tidy_and_count (instr(X, Y), N, N1, (instr(X, Y); Code) \ Code) <- N1: = N+1. Программа 23.1. (Продолжение)
Компилятор 301 tidy_and_count(label(N),N,N,Code\Code). tidy_.and_count(no_op,N,N,Code\Code). allocate(void,N,N). allocate(dict(Name, N1, Before, After), N0, N) <- allocate(Before,NO,Nl), N2: = N1 + 1; allocate(After,N2,N). Программа 23.1. (Продолжение) Для проверки компилятора используется программа 23.2, в которой содержатся необходимые тестовые данные и команды. Программа factorial на языке PL для вычисления факториала (рис. 23.1) представлена здесь списком лексем. Еще две небольшие программы содержат по одному оператору и используются для проверки средств языка, не проверяемых тестовой программой вычисления факториала. Программой testl проверяется компиляция нетривиального арифметического выражения, а программой test2- правильность компиляции условного оператора if-then_else. проверка_компилятора(Х, Y) <- программа(Х, P),compile(P, Y). программа (testl, [program, test 1,';', begin, write, x,' + ',y,' — \z,7\2,end]). программа (test2, [program, test2,';', begin, if, a,' > \b,then,max,': =', a, else, max,': = ',b,end]). программа (factorial, [program, factorial,';' , begin , read, value,';' ,count,': = ', 1,';' , result,': = ', 1,';' , while,count,' < \value,do , begin ,count,': = ',count,' +', 1,';' , result,': =', result,' *', count ,end,';' , write, result ,end]). Программа 23.2. Программа для тестирования компилятора. 23.2. Синтаксический анализатор Синтаксический анализатор представлен в виде DC-грамматики (рассмотрению DC-грамматик посвящена гл. 16). Предикат parse, как показано в программе 23.1, обеспечивает взаимосвязь с представлением синтаксического анализатора в виде DC-грамматики, в котором на верхнем уровне используется предикат pLprogram. Как было отмечено в гл. 16, DC-грамматика имеет единственный аргумент- структуру, соответствующую предложениям языка. Для трансляции DC-грамматики в предложения Пролога предполагается использовать вариант программы 16.2. Одно из требований, связанное с использованием этой программы, состоит в том, чтобы последний аргумент предикатов, определяемых DC-грамматикой, был разностным списком: parse(Source, Structure) <- pi program (Structure, Source\[ ]).
302 Часть IV. Глава 23 Первым оператором в любой PL-программе должен быть оператор program. Оператор program состоит из слова program, за которым следует имя программы. Слова, по которым определяется, какое правило грамматики должно быть применено, будем называть стандартными идентификаторами- Слово program - пример стандартного идентификатора. Имя программы в языке является некоторым идентификатором. Что представляют собой идентификаторы и константы, будет обсуждаться при рассмотрении арифметических выражений. После имени программы ставится точка с запятой (этот знак также считается стандартным идентификатором), а затем начинается собственно текст программы. Тело PL-программы состоит из операторов или, более точно, из единственного оператора, который сам может состоять из нескольких операторов. Все это выражается следующим грамматическим правилом верхнего уровня: pLprogram(S) -* [program],identifier(X),[';'],statement(S). Структура, полученная в результате разбора, является оператором, составляющим тело программы. Для целей генерации кода оператор верхнего уровня program не имеет смысла и не включается в создаваемую структуру. Сначала рассмотрим синтаксис составного оператора. Этот оператор начинается стандартным идентификатором begin, за которым следует первый оператор S составного оператора, а затем-остальные операторы Ss. Структура, получаемая после разбора составного оператора, имеет вид (S;Ss), где точка с запятой используется как двуместный инфиксный функтор. Она рекурсивна, так как ее элементы S и Ss могут быть составными операторами или содержать составные операторы. Точка с запятой выбрана в качестве функтора по аналогии с ее использованием в PL для обозначения последовательности операторов. Грамматическое правило для составного оператора имеет следующий вид: statement((S,Ss)) -* [begin],statement(S),rest.statements(Ss). Операторы в языке PL разделяются точками с запятой. Соответственно конструкция rest_statements (остальные операторы) определяется как точка с запятой, за которой следуют непустой оператор и (рекурсивно) «остальные операторы»: rest_statements((S;Ss)) -* [Y],statement(S),rest__statements(Ss). Конец последовательности операторов обозначается стандартным идентификатором end. Atom void (пусто) используется для отметки конца оператора во внутренней структуре. Таким образом, исходное определение для rest^statements: rest_statements(void) -* [end]. Приведенные выше определения операторов исключают возможность использования в языке пустого оператора: программы и составные операторы в языке PL не могут быть пустыми. Следующим рассмотрим оператор присваивания. Он имеет простое синтаксическое определение: за левой частью оператора идет стандартный идентификатор : =, за которым следует правая часть оператора. Левая часть в языке PL может быть только идентификатором, в то время как правая является произвольным арифметическим выражением. Оператор присваивания имеет следующее определение: statement(assign(X, Е)) -* idcntifier(X),[': = '],expression(E). Структура после успешного распознавания оператора присваивания имеет вид assign (Х,Е). Переменная Е представляет в Прологе структуру арифметического выражения, в то время как X является именем переменной (в программе на PL), которой должно быть присвоено значение арифметического выражения. Неявно предполагается, что X будет идентификатором в PL.
Компилятор 303 Для простоты ограничимся некоторым подклассом арифметических выражений, определяемым двумя правилами. Выражение есть или константа, или константа, за которой следуют арифметический оператор и рекурсивно арифметическое выражение. Примерами выражений в этом подклассе являются х, 3, 2t и x + y-z/2, а также выражение в тестовом предложении testl в программе 23.2. Синтаксис выражений этого подкласса определяется следующими грамматическими правилами: expression(X) -* pLconstant(X). expression(expr(Op,X,Y)) <- pLconstant(X), arithmetic_op(Op), expression(Y). В выражениях рассматриваемого подкласса используется нестандартный приоритет выполнения арифметических операторов. Например, выражение х-2-\-у будет разобрано как выражение х(2 + у). С другой стороны, выражение х + у — z/2 однозначно интерпретируется как х + (у — (z/2)). Ограничения, введенные выше, сделаны с целью упрощения программы и изложения материала данной главы. Ограничимся также и двумя типами констант языка PL: идентификаторами и целыми числами. Спецификация констант задается двумя правилами pLconstant. Каждому правилу соответствует определенная выходная структура. Идентификаторам X соответствует структура пате(Х), а целым числам Х-структура number (X). Необходимые правила записываются следующим образом: pi constant(name(X)) -* identifier(X). pl_constant(number(X)) -* pLinteger(X). Для простоты предположим, что целые и идентификаторы в языке PL являются целыми и атомами языка Пролог соответственно. Это позволяет использовать системные предикаты Пролога для распознавания идентификаторов и целых чисел в PL. Напомним, что фигурные скобки в DC-грамматиках используются для спецификации целей в Прологе: identifier(X) -* [X], {atom(X)}. plJnteger(X) - [X], {integer(X)}. В действительности, чтобы повысить эффективность программы, во всех правилах грамматики, в которых используются идентификаторы и константы языка PL, необходимо непосредственно вызывать соответствующие предикаты языка Пролог. Для завершения определения арифметических выражений необходим список арифметических операторов. Предложение для определения арифметического оператора сложения, представляемого знаком « + », имеет следующий вид: arithmetic_op(' + ') -> [' + ']. Грамматические правила для операторов вычитания, умножения и деления аналогичны; они содержатся в программе 23.2-полной программе синтаксического анализа. Теперь рассмотрим условный оператор, или оператор if-then-else. В языке PL этот оператор имеет обычный синтаксис: за стандартным идентификатором if следует проверка условия, которая будет определена ниже. После проверки записывается стандартный идентификатор then, за которым следуют некоторый оператор (then-частъ), стандартный идентификатор else и, наконец, оператор, являющийся еке-члстъю условного оператора. Синтаксический разбор условного оператора завершается построением структуры if(T,Sl,S2), где Г-условие, S7-/Лея-часть, S2-else-4'dCTb. Синтаксис условного оператора определяется следующим правилом: statement (if (T,Sl,S2))-> [if], test(T), [then], statement(Sl), [else], statement(S2).
304 Часть IV. Глава 23 Проверка условия определяется выражением, за которым следуют оператор сравнения и другое выражение. Структура, формируемая после успешного разбора проверки, имеет вид compare (Op,X, Y), где Ор -оператор сравнения, а!и У-левое и правое выражения в проверке соответственно. Проверка определяется следующими правилами: test(compare(Op,X,Y)) -* expression(X), comparison_op(Op), expression(Y). Определение операторов сравнения с использованием предиката comparison_op аналогично определению арифметических операторов с помощью предиката arithmetic_ор. Программа 23.1 содержит определения для операторов сравнения: =, Ф, >, <, >, < Оператор while состоит из проверки условия и действия, которое выполняется, если проверяемое условие истинно. Успешный разбор оператора while завершается построением структуры while(T, S), где Г-проверка (условие), a S- действие (оператор). Синтаксис оператора while определяется следующим правилом: statement(while(T,S)) -» [while], test(T), [do], statement(S). Для ввода-вывода в языке PL предусмотрены простой оператор ввода (оператор read) и простой оператор вывода (оператор write). Оператор ввода состоит из стандартного идентификатора read, за которым следует идентификатор языка PL. Разбор оператора ввода завершается построением структуры вида read(X), в которой Х-идентификатор. Оператор вывода определяется аналогично. Синтаксис операторов ввода-вывода определяется следующими правилами: statement(read(X)) -> [read],identifier(X). statement(write(X)) -> [write], expression(X). Рассмотренные выше фрагменты DC-грамматики вкупе дают синтаксический анализатор для рассматриваемого языка. Отметим, что, опуская аргументы в полученной DC-грамматике, можно получить формальную БНФ-грамматику для языка PL. Рассмотрим, как ведет себя синтаксический анализатор при обработке тестовых данных, содержащихся в программе 23.2. При разборе двух однооператорных программ формируются структуры, имеющие следующую форму: {structure); void, где {structure} представляет разобранный оператор. Оператор write транслируется в структуру write (expr ( +, name (х), expr ( —, name (у), expr (/, name (z), number (2) ) ) ) ), а оператор if-then-else-в структуру Программа test I: write(expr( + ,name(x),expr( — ,name(y), expr(/,name(z), number (2))))); void Программа test2: if(compare( > ,name(a),name(b)),assign(max, name(aj), assign (max, name(b))); void Программа test3: read(value);assign(count,number(l));assign(result,number(l)); write(compare( < ,name(count),name(value)), (assign(count,expr( + ,name(count),number(l))); assign (result,expr(*, name(result), name(count))); void)); write(name(result));void Рис. 23.5. Результаты разбора для тестовых программ.
Компилятор 305 // (compare ( >, name (a), name (b)), assign (max,name (a) ), assign (max, name (b))). Программа factorial после разбора представляется последовательностью из пяти операторов, за которыми следует void. Результаты разбора всех трех тестовых программ представлены на рис. 23.5. Они являются входом для второй фазы компиляции -генерации кода. 23.3. Генератор кода Основное отношение генератора кода encode (Structure,Dictionary, Code) обеспечивает генерацию кода Code из структуры Structure, произведенной синтаксическим анализатором. Поэтому настоящий раздел является прямым продолжением предыдущего раздела. Здесь описывается генерируемый код для каждой структуры, произведенной синтаксическим анализатором для различных операторов языка PL. Словарь Dictionary связывает PL-переменные с ячейками памяти, а метки-с адресами команд. Ассемблер использует словарь для определения адресов меток и идентификаторов программы. В данном разделе словарь обозначается буквой D. Для реализации словаря используется неполная структура данных-упорядоченное двоичное дерево, описанное в разд. 15.3. Для доступа к неполному двоичному дереву используется предикат lookup (Name,D, Value) (см. программу 15.9). Структура, соответствующая составному оператору, представляет собой последовательность составляющих его структур. Она транслируется в последовательность блоков кода, рекурсивно определяемых отношением encode. Для установления последовательности используется функтор «;». Пустой оператор void транслируется в «пустую» операцию (null), имеющую обозначение по_ор. При ассемблировании перемещаемого кода эта «псевдокоманда» исключается из программы. Структура, вырабатываемая синтаксическим анализатором для оператора присваивания языка PL, имеет вид assign (Name,Expression), где Expression- выражение, значение которого присваивается PL-переменной Name. Выражение вычисляется соответствующей откомпилированной формой, за которой следует команда store, чей аргумент - адрес, соответствующий переменной Name. Отдельные команды в откомпилированном коде представляются структурой вида instr(X,Y), где X- команда, а У операнд. Следовательно, соответствующая трансляция структуры assign даст структуру (Code; instr (store, A ddress) ), где Code - откомпилированная форма выражения, после выполнения которой значение выражения остается в сумматоре. Такая форма генерируется с помощью предиката encode^expression (Expression, Dt Expression Code). Кодирование оператора присваивания выполняется предложением encodc(assign(Namc, Expression), D, (Code;inslr(storc. Address))) «-- lookup(Namc,D, Address), encode expression!Expression,D,Code). Это предложение хороший пример Пролог-программы, которая декларативно воспринимается легко, но скрывает усложненную процедурную интерпретацию. Логически необходимо специфицировать отношения между Name и Address, а также между Expression и Code. С точки зрения программиста, безразлично, когда конструируется окончательная структура, и действительно, порядок двух целей в теле этого предложения может быть изменен, что не приведет к изменению поведения всей программы. Более того, цель lookup, связывающая имя Name с адресом Address, на стадии ассемблирования либо делает новую запись в словарь, 11-1402
306 Часть IV. Глава 23 либо осуществляет поиск существующей записи с соответствующим значением Name. Все эти действия не требуют явного внимания программиста. Они корректно выполняются как фоновые. Существует несколько ситуаций, возникающих при компиляции выражений, которые требуют отдельного рассмотрения. Константы загружаются непосредственно; для этого используется соответствующая машинная команда loadcC, где С-константа. Подобным образом идентификаторы компилируются в команду load А, где А -адрес идентификатора. Соответствующие этим случаям предложения encode-expression имеют вид encode_expression (number (С), D, instr (loadc, С)). encode_expression(name(X),D,instr(load, Address)) <- lookup (X, D, Address). Обычное выражение представляется структурой ехрг(Ор,Е1,Е2), где Op - оператор, El - константа языка PL и Е2-выражение. Вид откомпилированного кода зависит от Е2. Если выражение является PL-константой, то сгенерированный код состоит из двух операторов: соответствующей команды load, рекурсивно определяемой предложением encode-expression, и отдельной команды, соответствующей оператору Ор. Вновь не имеет значения, в каком порядке определяются эти две команды. Предложение encode-expression запишется следующим образом: encode_expression(expr(Op,El,E2),D,(Load; Instruction)) <- single_instruction(Op,E2,D, Instruction), encode_expression (E1, D, Load). Вид отдельной команды зависит от оператора и от того, является ли PL-константа числом или идентификатором. Если константа - число, то используются литеральные операции, в то время как идентификаторы связаны с операциями обращения к памяти. Следующие предложения учитывают эти варианты: single_instruction(Op,number(C),D,instr(Opcode,C)) <- literaLoperation (Op, Opcode). single_instruction(Op,name(X),D,instr(Opcode,A))<- memory_operation (Op, Opcode), lookup (X, D, A). Для каждого вида операций требуется своя таблица фактов. Соответствующие формы фактов иллюстрируются для операции « + »: literaLoperation(+ ,addc). memory_operation( + ,add). Специального рассмотрения требует случай, когда выражение Е2 в структуре ехрг не является константой и не может быть закодировано одной командой. Форма откомпилированного кода будет определяться откомпилированным кодом для вычисления выражения Е2 и отдельной операцией, определяемой оператором Ор и константой Е1. Соответствующие предложения запишутся следующим образом: encode_expression (expr(Op, Е1, Е2), D,Code) <- not single_instruction(Op,E2,D,Instruction), single_operation (Op, E1, D, E2Code, Code), encode_expression (E2, D, E2Code). Вообще, результат вычисления выражения Е2 должен быть записан в некоторую временную ячейку памяти, которую обозначим ниже как "Stemp". Таким образом, последовательность команд будет состоять из кода для выражения Е2, команды store, команды load для константы Е1 и соответствующей операции обращения к памяти, адресующей сохраняемое содержимое. Окончательно с использованием определенных выше предикатов получим фрагмент программы генерации кода для выражения
Компилятор 307 single_operation (Op, E, D, Code, (Code; instr(store, Address); Load; instr(OpCode, Address)) )«- not commutative(Op), lookup('$temp\ D, Address), encode_expression(E,D,Load), op_code(Op, E, OpCode). Если операция коммутативна, например сложение или умножение, то возможна некоторая оптимизация кода, благодаря которой можно избежать использования временной переменной. Предполагая, что результат вычисления Е2 остается в сумматоре, литеральная операция или операция обращения к памяти в этом случае может быть выполнена с использованием Е1: single operation(Op,E,D,Code,(Code;Instruction)) <- commutative(Op),single_instruction(Op,E,D,lnstruction). Рассмотрим теперь компиляцию условного оператора if-then-else после синтаксического разбора структурой if (Test,Then,Else). Для компиляции этой структуры необходимо ввести метки, используемые при реализации команд перехода. Требуются две метки, отмечающие начало и конец ^te-части условного оператора соответственно. Эти метки имеют вид label (N), где N - адрес команды. Значение N определяется на стадии ассемблирования, когда само предложение label удаляется. Схема получаемой программы дается третьим аргументом следующего предложения encode: encode (if (Test, Then, Else), D, (TestCode; ThenCode; instr(jump,L2); label(Ll); ElseCode; label(L2)) )«- encode_test(Test, L1, D, TestCode), encode(Then, D,ThenCode), encode(Else, D, ElseCode). Для сравнения двух арифметических выражений второе выражение вычитается из первого, а затем выполняется операция перехода, соответствующая конкретному оператору сравнения. Например, при проверке двух выражений на равенство программа будет введена в заблуждение, если результат вычитания двух выражений не равен нулю. Поэтому в генератор кода должен быть включен факт compari- son_opcode('=',jumpne). Отметим, что метка, являющаяся вторым аргументом предиката encode_test, является адресом первой команды программы после проверки. encode_ test (compare (Op, E1, E2, Label, D,(Code;instr(OpCode, Label))) <- comparison opcode(Op, OpCode), encode_expression (expr (' — \ E1, E2), D, Code). Следующим рассмотрим оператор цикла while. Синтаксический разбор этого оператора завершается построением структуры while (Test,Statements). В данном случае метка необходима перед проверкой. Код для проверки генерируется так же, как и в случае условного оператора. Затем генерируются тело программы для и*
308 Часть IV. Глава 23 оператора while, соответствующее параметру Statements, и переход на повторное выполнение проверки. После команды перехода jump необходимо разместить метку, на которую будет передаваться управление, когда проверка примет значение «ложно». Предложение 'encode для оператора while примет вид encode(while(Test, Do), D, (label(Ll); TestCode; DoCode; instr(jump,Ll); label(L2)) )«- encode_test (Test, L2,D,TestCode), encode (Do, D, DoCode). Операторы ввода-вывода языка PL компилируются просто. В результате разбора оператора ввода формируется структура read(X), которая компилируется в одну команду read. При этом для получения корректного адреса переменной X используется предикат lookup(X,D,Address). В данном случае предложение encode будет выглядеть так: encode (read (X), D,instr(rcad, Address)) <- lookup(X,D, Address). При компиляции оператора вывода сначала компилируется входящее в оператор выражение, а затем генерируется команда вывода write. Для этого предусматривается следующее предложение encode: encode(write(E), D,(Code;instr(write,0))) <- encode expression(E, D,Code). Для каждого из трех примеров, содержащихся в программе 23.2, на рис. 23.6 представлен перемещаемый код, произведенный генератором кода и готовый к ассемблированию. Для облегчения восприятия здесь использованы мнемонические имена переменных. Программа testl: ((((instr(load,Z);instr(divc,2));instr(store,Temp); instr(Load, Y); instr (sub, Temp)); instr(add, X)); instr(write,0));no. op Программа test2: (((instr(load,A);instr(sub,B));instr(jumple,Ll)); (instr(load,A);instr(storc,Max)):instr(jump,L2); label(L 1 );(instr(load, B); instr(store, Max)); label(L2));no_.op Программа factorial: instr(read, Value);(instr(loadc, l);instr(storc,Count)); (instr(loadc, l);instr(store,Result));(label(Ll); ((instr(load,Count);instr(sub, Value)); instr(jumpge,L2));(((instr(load, Count): instr(addc, 1)): instr(store,Count));((instr(load, Result); instr(mul,Count));instr(storc, Result));no op); instr(jump,Ll);label(L2));(instr(load, Result); instr(wrile,0));no op Рис. 23.6. Сгенерированный код.
Компилятор 309 23.4. Ассемблер Последняя стадия обработки, выполняемой компилятором, состоит в ассемблировании перемещаемой программы в абсолютную объектную программу. Предикат assemble (Code,Dictionary,ObjectCode), используя сформированные на предыдущем этапе компиляции программу Code и словарь Dictionary, вырабатывает объектную программу. Ассемблирование проходит в два этапа. На первом этапе подсчитывает- ся число команд в программе, вычисляются адреса всех образованных во время генерации кода меток и удаляются ненужные пустые операции. Приведенная таким образом в порядок программа далее расширяется добавлением команды останова, обозначаемой как instr(haltS)), блоком из L ячеек памяти для PL-переменных и рабочими ячейками, используемыми в программе. Определенное пространство для ячеек памяти обозначается как block (L). На втором этапе ассемблирования определяются адреса PL-переменных и временных переменных, используемых в программе. Предложение assembler запишется следующим образом: assemble(Code, Dictionary,TidyCode) <-- tidy and count (Code, l,N,TidyCodc\(instr(halt,0);block(L))), N1 := N + 1, allocate(Dictionary, N1, N2), L:= N2-N1. С помощью предиката tidy___and__count (Code,M,N',TidyCode) производится преобразование программы Code в программу TidyCode, в которой определены правильные адреса меток и удалены пустые операции. Процедурно выполнение предиката tidy_Mnd._count соответствует второму проходу по программе. М является адресом начала программы, а N превышает на единицу адрес конца исходной программы. Таким образом, число фактических команд в программе Code составляет N + 1 — М. Программа TidyCode представляется с помощью разностной структуры, основанной на функторе «;». В рекурсивном предложении tidy, and ..count демонстрируются техника использования разностных структур и приемы изменения числовых значений: tidy, and count((Codcl;Codc2),M,N,TCodcl\TCodc2) <- tidy and count(Codcl,M.M l.TCodel\Rest), tidy and count(Codc2, M I,N,Rest \TCode2). В программе встречаются примитивы трех типов: команды, метки и «пустые команды» по__ор. Команды обрабатываются обычным образом. Счетчик адреса увеличивается на единицу, и команда вводится в разностную структуру: tidy and count(instr(X,Y),N,Nl,(instr(X,Y);Code)\Code) ^ NI:=N+ 1. И метки, и команды по.ор удаляются без изменения текущего адреса и добавления команды к обрабатываемой программе: tidy and count(labcl(N),N,N.Code\Code). tidy and count(no op,N,N,Codc\Code). Декларативно эти предложения идентичны, но процедурно унификация номера метки с текущим адресом вызывает большие действия в программе. При этом определяется каждая ссылка на адрес метки. Эта программа является еще одной иллюстрацией мощности логической переменной. Предикат allocate(Dictionary,M,N) имеет главным образом процедурную интерпретацию. В процессе генерации кода при построении словаря ячейки памяти связываются с каждой из PL-переменных программы и с временными переменными, необходимыми для вычисления выражений. Действие предиката allocate состоит в
310 Часть IV. Глава 23 назначении фактических ячеек памяти переменным и занесении ссылок на них в программу. Поиск переменных производится путем обхода словаря Dictionary. M является адресом ячейки памяти для первой переменной, а ЛГ-на единицу превышает адрес последней переменной. Порядок переменных имеет алфавитное соответствие порядку их расположения в словаре. Кроме того, программа превращает словарь в полную структуру данных. Предикат allocate определяется следующими предложениями: allocate(void, N, N). allocate(dict (Name, N1, Before, After), N0, N) <- allocate(Before,NO,Nl), N2:=N1 + 1, allocate(After,N2,N). На рис. 23.7 представлены результаты компиляции тестовых программ, содержащихся в программе 23.2. Программа testl: instr (load, 11); instr(divc, 2);instr(store, 12); instr(load, 10); instr(sub, 12); instr (add, 9); instr(write,0); instr(halt,0);block(4) Программа test2: instr(load, 10); instr(sub, 11); instr(jumple, 7); instr(load,10);instr(store,12);instr(jump,9); instr(load,ll);instr(store,12);instr(halt,0); block(3) Программа factorial: instr (read, 21); instr (loadc, l);instr(store, 19); instr(loadc, 1); instr (store, 20); isntr(load, 19); instr (sub, 21); instr (jumpge, 16);instr(load,19);instr(addc, 1); isntr (store, 19); instr(load, 20); instr(mul, 19); instr(store, 20); instr (jump, 6); instr(load, 20); instr(write,0);instr(halt,0);block(3) Рис. 23.7. Откомпилированный объектный код. Упражнения к гл. 23 1. Расширьте компилятор так, чтобы он мог обрабатывать циклы типа repeat. Оператор repeat имеет следующую синтаксическую форму: repeat (statement) until (test). Необходимо выполнить расширение как синтаксического анализатора, так и генератора кода. Протестируйте компилятор следующей программой: program repeat; begin i:= 1; repeat begin write(i); i: = i+l; end until i = 11 end.
Компилятор 3 1 1 2. Расширьте определение арифметических выражений так, чтобы оно соответствовало выражениям общего вида. При модификации генератора кода вам представится возможность использовать несколько временных переменных. 23.5. Дополнительные сведения Представленное здесь описание компилятора основано на прекрасной статье Уоррена (Warren, 1980).
Приложение А. Использование Пролог-системы Реализации Пролог-систем отличаются как средствами взаимодействия пользователя с системой, так и средствами поддержки разработки программ. Здесь будет в общих чертах описано, какие средства предоставляет пользователю некоторая стандартная Пролог-система. Пролог-система обычно реализуется на основе системы файлов. Это означает, что исходная программа размещается в одном или нескольких файлах. Стандартный цикл разработки программы можно представить следующим образом: Пока не завершена разработка программы, выполнить С использованием текстового редактора ввести программу (часть программы) и поместить се в файл; Войти в Пролог-систему и загрузить файл с программой; Выполнить программу (выполнение программы осуществляется обычно с помощью отладчика, имеющегося в Пролог-системе). Для загрузки набора процедур, находящихся в некотором файле, в Edinburgh- Прологе предусмотрен стандартный системный предикат consult. Обычно в операционных системах, допускающих приостановку процессов, Пролог-система и текстовый редактор работают параллельно. В этом случае необходимо загружать только тот файл, в котором производились изменения. В других случаях необходимо каждый раз загружать всю программу заново. Если Пролог-система обеспечивает сохранение своего состояния в некотором файле, а операционная система не поддерживает выполнение параллельных процессов, то при отладке могут создаваться контрольные точки. С каждой контрольной точкой связано сохранение состояния Пролог-системы, и в процессе отладки запуск системы может осуществляться с нужной контрольной точки. В некоторых Пролог-системах, например в Quintus-Пролог, взаимодействие системы с текстовым редактором реализуется еще более эффективно. Что касается отладки, то тут каждая Пролог-система имеет свои особенности. Однако наиболее современные отладчики основаны на модели Байда (Warren, 1981). В этой модели можно выполнять анализ цели при ее вызове, успешном решении и попытке повторного решения. Отладчик, обладающий такими возможностями, представлен в гл. 19. Отметим также, что он реализован в Wisdom-Прологе. В. Системные предикаты В этом разделе описаны все вычисляемые системные предикаты, реализованные в Wisdom-Прологе. Будучи системными, они не могут переопределяться пользователем. Любая попытка добавить предложение в системный предикат или исключить предложение из системного предиката завершается отказом, сопровождаемым выдачей сообщения об ошибке. Соответствующий системный предикат остается при
Приложение 31 3 этом неизменным. Вычислимые системные предикаты предназначены для выполнения следующих задач: Ввод/вывод: Считывание программ Открытие и закрытие файлов Считывание и запись термов Пролога Ввод и вывод символов Арифметика Воздействие на ход выполнения программы Классификация и действия над термами Пролога (металогические средства) Сравнение термов Средства отладки Средства операционной среды Обозначение Смысл Типы: atom(Atom) integer(I) atomic(A) constant(X) functor(Sl,F,A) arg(N,S,Sn) var(V) nonvar(C) Atom является атомом. I - целое число. А атом или целое число. Совпадает с определением предиката atomic. F является главным функтором St арности А. Sn является N-м аргументом S. V переменная. С не является переменной. Предикаты преобразования программы: assert (Clause) asserta(Clause) assertz(Clause) retract (Clause) abolish(F,A) consult (File) reconsult(File) clause(G,B) listing listing(Name) Включение предложения Clause в конец программы. Включение предложения Clause в начало программы. Совпадает с определением предиката assert. Извлечение некоторого предложения, унифицируемого с предложением Clause. Извлечение (стирание) всех предложений с функтором F арности А. Обеспечивает ввод программы, содержащейся в файле File. Если вводится некоторая директива, то она исполняется. Если вводится предложение, то оно помещается после любого предложения, уже введенного в соответствующей процедуре. Имеет тот же смысл, что и предикат consult, однако старые предложения процедур, определенных в файле, удаляются из программы. В является телом предложения, заголовок которого унифицируется с G. Распечатка текущей программы. Распечатка содержащихся в программе предикатов с именем Name.
314 Приложение Предикаты ввода-вывода: read(T) sread(T,Vs) write (T) writeq(T) display (T) displayq(T) displayl(L) print (T) see (File) seeing(File) seen tell(File) telling(File) told Hush get(C) getO(C) put(C) tab(N) nl skip(C) ttyget(C) ttyput(C) ttynl ttyskip(C) ttytab(N) op(P,Type,Op) save (File) log T является следующим термом, отделенным в текущем входном файле специальным ограничителем (либо точкой ".", за которой следует возврат каретки <CR), либо пробелом). Предикат не выполняется, если следующий элемент в текущем выходном потоке не унифицируется с термом Т или если пропущен признак конца файла (EOF). Считывается терм Т и Vs, Vs является списком пар: имя переменной и сама переменная (значение переменной). Запись терма Т в текущий выходной файл. Запись терма Т, если необходимо, с кавычками. Вывод терма Т на экран дисплея. Вывод терма Т на экран дисплея, если необходимо, с кавычками. Вывод на экран дисплея списка объектов. Имеется возможность улучшения качества печати с помощью собственного предиката portray (T). Файл File является новым текущим входным файлом. Файл File унифицируется с именем текущего входного файла Закрыть текущий входной поток. Определение файла File в качестве текущего выходного потока (файла). Файл File унифицируется с именем текущего выходного файла. Закрыть текущий выходной поток. Выдача текущего выходного файла (система использует буфер ввода/вывода). Чтение печатаемого (выводимого) символа из текущего входного файла. Чтение символа из текущего входного файла. Вывод символа С в текущий выходной файл. Печать N пробелов в текущем выходном файле. Запись символа новой строки в текущий выходной файл. Пропуск в текущем выходном файле всех символов вплоть до символа С. Чтение символа С с терминала. Возврат символа С, представленного в коде ASCII, на терминал. Возврат символа новой строки на терминал. Пропуск всех символов, поступающих с терминала, вплоть до символа С. Печать N пробелов на терминале. Определение оператора Ор с приоритетом Р и типом Туре. В качестве Ор может использоваться и список имен. Сохранение текущего состояния системы в файле File. Вывести оглавление базы данных в файл "log". Средства отладки программ: trace trace(G) Назначить цель для трассировки. Выполнить трассировку для цели G.
Приложение 315 Универсальные предикаты: true fail t exit abort call(G) not(G) name(A,L) repeat X = Y \ = Tl = = T2 Tl\= =T2 system (Comm) systemp(F,A) save _ term (T) unsave_term(T) member (X,Xs) append (Xs,Ys,Zs) Всегда имеет значение «истинно». Всегда имеет значение «ложь». Отсечение. Завершить работу с Пролог-системой. Прекратить выполнение программы. Вызов G. Если G выполняется, то not(G) не выполняется. L-список символов в А. Повторить произвольное число раз. Конъюнкция. Дизъюнкция. Предикат univ. X унифицируемо с Y. Отрицание =. Т1 равно Т2. Отрицание = = . Выполнение команды Comm операционной системы. F-функтор, А-арность системного предиката. Помещение в стек. Считывание из стека. Х-элемент списка Xs. Zs-результат присоединения Ys к Xs. Специальные предикаты: iterate (G) fork _ exec(file,Comm) ancestor(G,N) cutg(G) retry (g) Выполнение G до тех пор, пока ее решение не завершится отказом (эффективно). С-подобная команда. Цель G — N-й предок текущей цели (используется при отладке). Отсечение с учетом предыстории. Повторение G. Арифметические предикаты: • Арифметические системные вызовы: Tl <t2 Tl =\ = T2 Tl >T2 Tl ^T2 Tl ^T2 Tl = : = Т2 R: = Exp R is Exp Значение арифметического выражения Т1 меньше, чем значение арифметического выражения Т2. Значение выражения Т1 не равно значению выражения Т2. Значение выражения Т1 больше, чем значение выражения Т2 Значение выражения Т1 больше или равно значению выражения Т2. Значение выражения Т1 меньше или равно значению выражения Т2. Значение выражения Т1 не равно значению выражения Т2. R-результат вычисления арифметического выражения Ехр. Возможная форма записи оператора : =. • Арифметические операции: X + Y Сложение. X — Y Вычитание.
316 Приложение X * Y Умножение. X/Y Деление. X mod Y Нахождение остатка от деления X на Y. — X Унарный минус. + X Унарный плюс. Загрузка и повторная загрузка программы: Повторная загрузка выполняется посредством [filel,file2, ]. или - reconsult(file I ),reconsult(file2),... тогда как загрузка выполняется посредством [+nlel, + file2,...,...]. или eonsuluTile 1 ),consult(filc2),... Отметим, что это может быть сделано только на верхнем уровне. Для ввода процедур с терминала необходимо воспользоваться файлом пользователя: [user] или [ + user] и затем вводить свои предложения. Операционная оболочка: Пользователь может определить свою собственную операционную оболочку с помощью предиката shell/О. Системная оболочка (shell) по умолчанию определяется как $shell: — shelKabort. Sshell: - ttynl, displayf | ? — ^sprcadfGoalKVs), s„expand goaI(Goalb,Goal), user _call(Goal,Vs),abort. expand goal: этот предикат вызывается оболочкой, преобразующей цель верхнего уровня. Применяя этот предикат, пользователь может определять свои собственные преобразования для цели верхнего уровня. expand clause: определяется так же, как и предикат expand_goal, но применяется к вызванным предложениям. Оболочка: s _ expand _ goal Системная расширенная цель. user _ call Выполнить вызов пользователя. display _ result(Vs) Вывести на экран дисплея значения переменных get _ reply Чтение ответа пользователя с терминала. cons _ list Для загрузки ряда файлов, использующих списки. С. Предварительно определяемые операторы Для ввода-вывода составных термов обычно используются унарные или бинарные операторы. Унарные операторы могут быть префиксными (оператор
Приложение 317 предшествует аргументу), постфиксными (оператор следует за аргументом) или инфиксными (оператор записывается между двумя аргументами). Чтобы избежать двусмысленности, каждый оператор получает приоритет, а также тип и обозначение, что позволяет различать операторы одинакового приоритета. Тип оператора и число аргументов описываются с использованием следующих соглашений: • для префиксных операторов- fx означает, что приоритет аргумента меньше, чем приоритет оператора. fy означает, что приоритет аргумента может быть равен приоритету оператора. • для постфиксных операторов - xf и у/ определяются аналогично префиксным операторам. • для инфиксных операторов - xfx - означает, что оба подвыражения, являющиеся аргументами оператора /, должны иметь приоритет, меньший чем приоритет оператора; xfy- левый аргумент должен иметь приоритет, меньший чем приоритет оператора; yfx- правый аргумент должен иметь приоритет, меньший чем приоритет оператора. Для определения типа оператора используется конструкция |? — ор(Приоритет, Тип, Оператор). где Приоритет может принимать значения от 1 до 1200, Тип - определен выше, Оператор -имя оператора или список имен. Напомним, чю приоритет аргумента должен быть меньше 1000 (этот приоритет соответствует оператору 7)- Таким образом, следует писать assert((A: - В)) а не assert(A: - В) Определения операторов: :-(op(1200.fx,[: -.?-])). :-op(1200.xfx,[(: -),<-,-]). : -ор( 11 OO.xfy,';'). : - op(1000,xfy,7). : - op(900,fx,[not]). : - op(700,xfx,[ = ,\ =, is,: =,= ..,= =,\= = ,= : = , = \ = ,<,>,<,:>]). :-op(500,yfx,[+ ,-]). : - op(500,fx,[( + ),(-)]). : - op(400,yfx,[*,/,//]). : - op(300,xfx,[mod]).
Литература Apt, K.R. and van Emden, M.H., Contributions to the Theory of Logic Programming, J. ACM 29, pp. 841-862, 1982. Ben-David, A. and Sterling, L., A Prototype Expert System for Credit Evaluation,. Proc. International Workshop on Artificial Intelligence in Economics and Management, Zurich, Switzerland, 1985. Berge, C, The Theory of Graphs and its Applications, Methuen & Co., London, 1962. Bergman, M. and Kanoui, H., Application of Mechanical Theorem Proving to Symbolic Calculus, Proc. Third International Symposium on Advanced Computing Methods in Theoretical Physics, CNRS, Marseille, 1973. Bloch, C, Source-to-Source Transformations Of Logic Programs, Tech. Report CS-84-22, Department of Applied Mathematics, Weizmann Institute Of Science, Rehovot, Israel, 1984. Bowen, K. and Kowalski, R., Amalgamating Language and Meta-Language, in (Clark and Tarnlund, 1982), pp. 153-172. Boyer, R.S. and Moore, J.S., A Computational Logic, Academic Press, ACM Monograph Series, 1979. Bowen, D.L., Byrd, L., Pereira, L.M., Pereira, F.C.N, and Warren, D.H.D., Prolog on the DECSystem-10 user's manual, University of Edinburgh, 1981. Breuer, G. and Carter, H.W., VLSI Routing in Hardware and Software Concepts. in VLSI, G. Rabbat (ed.), Van Nostrand Reinhold, 1983. Bruynooghe, M., The Memory Management of Prolog Implementation, in (Tarnlund, 1980). Bruynooghe, M. and Pereira, L.M., Deductive Revision by Intelligent Backtracking, in (Campbell, 1984). Bundy, A., A Computer Model Of Mathematical Reasoning, Academic Press, 1983. Bundy, A., A Generalized Interval Package and its Use for Semantic Checking,, ACM Transactions on Mathematical Software 10, pp. 392-407, 1984. Bundy, A. and Welham, R., Using Meta-level Inference for Selective Application of Multiple Rewrite Rules in Algebraic Manipulation, Artificial Intelligence 16, pp. 189-212, 1981. Burstall, R.M. and Darlington, J., A Transformation System for Developing Recursive Programs, J. ACM 24, pp. 46-67, 1977. Byrd, L., Understanding the Control Flow of Prolog programs, in (Tarnlund, 1980).
Литература 319 Campbell, J.A. (ed.), Implementations Of Prolog, Ellis Horwood Publication, Distributed By John-Wiley & Sons, New York, 1984. van Caneghem, M. (ed.), Prolog-II user's Manual, 1982. van Caneghem, M. and Warren, D.H.D. (eds.), Logic Programming and its Applications, Ablex Publishing Co., 1986. Chikayama, Т., Unique Features of ESP, Proc. International Conference on Fifth Generation Computer Systems, pp. 292-298, Tokyo, Japan, 1984. Clark, K.L., Negation as Failure, in (Gallaire and Minker, 1978). Clark, K.L. and Gregory, S., A Relational Language for Parallel Programming, Proc. ACM Conference on Functional Languages and Computer Architecture, 1981. Clark, K.L. and Gregory, S., PARLOG: Parallel Programming in Logic, Research Report 84/4, Department of Computing, Imperial College of Science and Technology, England, 1984. Clark, K.L. and McCabe, F.G., The Control Facilities of IC-Prolog in Expert Systems in the Microelectronic Age, D. Michie (ed.), pp. 153-167, University of Edinburgh, Scotland, 1979. Clark, K.L. and McCabe, F.G., PROLOG: A Language for Implementing Expert Systems, in Machine Intelligence 10, Hayes, Michie and Pao (eds.), pp. 455- 470, Ellis Horwood, 1982. Clark, K.L. and Tarnlund, S.-A., A First Order Theory of Data and Programs, Information Processing 77, pp. 939-944, North Holland, 1977. Clark, K.L. and Tarnlund, S.-A. (eds.), Logic Programming, Academic Press. London, 1982. Clark, K.L, McCabe, F.G. and Gregory, S., IC-Prolog Language Features, in (Clark and Tarnlund, 1982). Clocksin, W.F. and Mellish, C.S., Programming in Prolog, 2nd Edition, Springer- Verlag, New York, 1984. Coelho, H., Cotta, J.C. and Pereira, L.M., How to Solve it in Prolog, 2nd Edition, Laboratorio Nacional de Engenharia Civil, Lisbon, Portugal, 1980. Cohen, J., Describing Prolog by Its Interpretation and Compilation, J. ACM 28, pp. 1311-1324, 1985. Colmerauer, A., Les systemes-Q ou un Formalisme pour Analyser et Synthesizer des Phrases sur Ordinateur, Publication Interne No. 43, Dept. d'Informatique, Universite de Montreal, Canada, 1973. Colmerauer, A., Kanoui, H., Pasero, R. and Roussel, P., Un Systeme de Communication Homme-machine en Francais, Research Report, Groupe Intelligence Artificielle, Universite Aix-Marseille II, France, 1973. Colmerauer, A., Prolog-II, Manuel de reference et modele theorique, Groupe d'Intelligence Artificielle Universite d'Aix-Marseille II, 1982.
320 Литература Colmerauer, A., Prolog And Infinite Trees, in (Clark and Tarnlund, 1982). Deo, N., Graph Theory with Applications to Engineering and Computer Science, Prentice-Hall, 1974. Dincbas,M. and Le Pape,J.P., Metacontrol of Logic Programs in METALOG, International Conference on Fifth Generation Computer Systems, November 1984. Dudeney, H.E., Amusements in Mathematics, Thomas Nelson and Sons, London, 1917. Dwork, C, Kanellakis, P.C. and Mitchell, J.C., On the Sequential Nature of Unification, J. Logic Programming 1, pp. 35-50, 1984. Eggert, P.R. and Chow, K.P., Logic Programming Graphics with Infinite Terms, Tech. Report University of California, Santa Barbara 83-02, 1983. Elcock, E.W., The Pragmatics of Prolog: Some Comments, Proc. Logic programming Workshop '83, pp. 94-106, Algarve, Portugal, 1983. van Emden, M., Warren's Doctrine on the Slash, Logic Programming Newsletter, December, 1982. van Emden, M. and Kowalski, R., The Semantics Of Predicate Logic as a Programming Language, J. ACM 23, pp. 733-742, 1976. Even, S., Graph Algorithms, Computer Science Press, 1979. Gallaire, H. and Lasserre, C, A Control Metalanguage for Logic Programming, in (Clark and Tarnlund, 1982). Gallaire, H. and Minker, J., Logic And Databases, Plenum Publishing Co., New York, 1978. Gallaire, H., Minker, J. and Nicolas, J.M., Logic and Databases: A Deductive Approach, Computing Surveys 16, pp. 153-185, 1984. Hammond,P., Micro-Prolog for Expert Systems, Chapter 11 in Micro-Prolog: Programming in Logic, Clark and McCabe (eds.), Prentice Hall 1984. Hill, R., LUSH-Resolution and its Completeness, DCL Memo 78, Department of Artificial Intelligence, University of Edinburgh, 1974. Hopcroft, J.E. and Ullman, J.D., Introduction To Automata Theory, Languages, Computation, Addison-Wesley, Reading, Massachussets, 1979. Jaffar, J., Lassez, J.L. and Lloyd* J.W., Completeness of the Negation as Failure Rule, Proc. of the International Joint Conference on Artificial Intelligence pp. 500-506, Karlsruhe, Germany, 1983. Kahn, K.M., A Primitive for the Control of Logic Programs, Proc. International IEEE Symposium on Logic programming, Atlantic City, 1984. Knuth, D., The Art Of Computer Programming, Volume 1, Fundamental Algorithms, Addison-Wesley, Reading, Massachussets, 1968.
Литература 321 Knuth, D., The Art Of Computer Programming, Volume 3, Sorting and Searching, Addison-Wesley, Reading, Massachussets, 1973. Kowalski, R., Predicate Logic as a Programming Language, Proc. IFIP Congress, North-Holland, Stockholm 1974. Kowalski, R., Logic For Problem Solving, North-Holland, 1979. Kowalski, R., Algorithm = Logic + Control, Communications of the ACM 22, pp. 424-436, 1979. Li, D., A Prolog Database System, Research Studies Press, Ltd., England, 1984. Lloyd, J.W., Foundations Of Logic Programming, Springer Verlag, 1984. Martelli, A. and Montanari, U., An Efficient Unification Algorithm, A CM Trans-, actions on Programming Languages and Systems 4(1), 1982. Matsumoto, Y., Tanaka, H. and Kiyono, M., BUP: A Bottom-Up Parsing System for Natural Languages, in (van Caneghem and Warren, 1986). Mellish, C.S., Some Global Optimizations for a Prolog Compiler, J. Logic Programming 2, pp. 43-66, 1985. Minsky, M., Semantic Information Processing, MIT Press, Cambridge, Massachusetts, 1968. Naish, L., Negation and Control in Prolog, Tech. Report 85/12, University of Melbourne, Australia, 1985. Naish, L., All Solutions Predicate in PROLOG, Proc. of IEEE Symposium on, Logic Programming, Boston, 1985. Naish, L., Automating Control For Logic Programs, J. Logic Programming 2, pp. 167-184, 1985. Nakashima, H., Tomura S. and Ueda, K., What is a Variable in Prolog? Proc. of the International Conference on Fifth Generation Computer Systems, pp. 327-332, ICOT, Tokyo, 1984. Nilsson, N.J., Problem Solving Methods In Artificial Intelligence, Mcgraw-Hill Publications, New York, 1971 Nilsson, N.J., Principles Of Artificial Intelligence, Tioga Publishing Co., Palo Alto, California, 1981. O'Keefe, R.A., Programming Meta-Logical Operations in Prolog, DAI Working Paper No. 142, University of Edinburgh, 1983. O'Keefe, R.A., On the Treatment of Cuts in Prolog Source-Level Tools, Proc. 1985 Symposium on Logic Programming, IEEE Computer Society Press, Boston, 1985. Paterson, M.S. and Wegman, M.N., Linear Unification, J. Computer and Systems Sciences 16, pp. 158-167, 1978.
322 Литература Pereira, L.M., Logic Control with Logic, Proc. First International Logic Programming Conference, pp. 9-18, Marseille, 1982. Pereira, F.C.N, and Warren, D.H.D., Definite Clause Grammars for Language Analysis - A Survey of the Formalism and a Comparison with Augmented Transition Networks, Artificial Intelligence 13, pp. 231-278, 1980. Peter, R., Recursive Functions, Academic Press, New York, 1967. Plaisted, D.A., The Occur-Check Problem in Prolog, New Generation Computing 2, pp. 309-322, 1984. Powers, D., Playing Mastermind More Logically, SIGART Newsletter 89, pp. 28-32, 1984. Quintus Prolog Reference Manual, Quintus Computer Systems Ltd., 1985. Reiter, R., On Closed World Databases, in Logic and Databases, Gallaire, H. and Minker, J. (eds.), Plenum Press, 1978, pp. 55-76. Also in Readings in Artificial Intelligence, webber and Nilsson (eds.), Published by Tioga, 1981. Robinson, J.A., A Machine-Oriented Logic Based On the Resolution Principle, J. ACM 12, pp. 23-41, January 1965. Robinson, J.A. and Sibert, E.E., LOGLISP: Motivation, Design and Implementation, in (Clark and Tarnlund, 1982). Schank, R.C. and Riesbeck, C, Inside Computer Understanding: Five Programs Plus Miniatures, Lawrence Erlbaum, Hillsdale, N.J., 1981. Schank, R.C. and Abelson,R.P., Scripts, Plans, Goals, and Understanding, Lawrence Erlbaum, Hillsdale, N.J., 1977. Sedgewick, R., Algorithms, Addison-Wesley, 1983. Shapiro, E., Algorithmic Program Debugging, MIT Press, Cambridge, Massachus- sets, 1983. Shapiro, E., A Subset of Concurrent Prolog and its Interpreter, Tech. Report TR-003, ICOT-Institute for New Generation Computer Technology, Tokyo, Japan, January 1983. Shapiro, E., Logic Programs with Uncertainties: A Tool for Implementing Rule- Based Systems, Proc. 8th International Joint Conference on Artificial Intelligence, pp. 529-532, Karlsruhe, Germany, 1983. Shapiro, E., Playing Mastermind Logically, SIGART Newsletter 85, pp. 28-29, 1983. Shapiro, E., Alternation and the Computational Complexity of Logic Programs, J. Logic Programming 1, pp. 19-33, 1984. Shapiro, E., Systems Programming in Concurrent Prolog, in (van Caneghem and Warren, 1986). Shapiro, E. and Takeuchi, A., Object Oriented Programming in Concurrent Prolog, New Generation Computing 1, 1983.
Литература 323 Shortliffe, E.H., Computer Based Medical Consultation, MYCIN, North-Holland, New York, 1976. Silver, В., Meta-level Inference, Elsevier Science, Amsterdam, Netherlands, 1986. Silverman, W., Hirsch, M., Houri, A. and Shapiro, E., The Logix System User Manual, Weizmann Institute of Science, Rehovot, Israel, 1986. Slagle, J. and Dixon, J., Experiments with Some Programs that Search Game Trees, J. A CM 16, pp. 189-207, 1969. Steele, G.L., Jr. and Sussman, G.J., The Art of the Interpreter or, the Modularity, Complex, Technical memorandum AIM-453, MIT AI-Lab, May 1978. Sterling, L., Expert System = Knowledge -f Meta-Interpreter, Tech. Report CS84- 17, Weizmann Institute Of Science, Rehovot, Israel, 1984. Sterling, L. and Codish, M., PRESSing for Parallelism: A Prolog Program Made Concurrent, J. Logic Programming 3, pp. 75-92, 1986. Sterling, L. and Bundy, A., Meta-Level Inference and Program Verification, Proc. of the Sixth Conference on Automated Deduction, pp. 144-150, Springer Ver- lag LNCS 138, 1982. Sterling, L. and Lalee, M., An Explanation Shell for Expert Systems, Tech. Report TR-125-85, Center for Automation and Intelligent Systems Research,, CWRU, Cleveland, USA, 1985. Sterling, L., Bundy, A., Byrd, L., O'Keefe, R. and Silver, В., Solving symbolic equations with PRESS, in Computer Algebra, pp. 109-116, Springer-Verlag LNCS 144, Takeuchi, A. and Furukawa, K., Partial Evaluation of Prolog Programs and its Application to Meta Programming, Tech. Report, ICOT, Tokyo, Japan, 1985. Tarnlund, S.-A. (ed.), Proc. of the Logic Programming Workshop, Debrecen, Hungary, 1980. Ueda, K., Guarded Horn Clauses, ICOT Tech. Report 103, ICOT, Tokyo, Japan, 1985. Ullman, J.D., Principles Of Database Systems, 2nd edition, Computer Science Press, MD, 1982. Warren, D.H.D., Generating Conditional Plans and Programs, Proc. AISB Summer Conference, pp. 344-354, Edinburgh, 1976. Warren, D.H.D., Implementing Prolog - Compiling Logic Programs 1 and 2, DAI Research Reports 39 and 40, University of Edinburgh, 1977. Warren, D.H.D., Logic Programming and Compiler Writing, Software-Practice and Experience 10, Number II, 1980. Warren, D.H.D., Higher-Order Extensions to Prolog: Are They Needed?, Machine Intelligence 10, pp. 441-454, Hayes, Michie and Pao (eds.), Ellis Horwood, 1982.
Предметный указатель Абстракция данных 30-32, 171 Автомат 179-182 - конечный недетерминированный 179— 181 - магазинный 179-181 Алгебра реляционная 34, 35 Алгоритм Евклида 42, 43, 102 - Ли 217-219, 224 - минимаксный 235-237 - унификации 63-65, 121, 122 Альфа-бета-отсечение 236-238 Анализ лексический 298 Аргумент 15, 24 Арность 15, 24, 28 Ассемблер 309, 310 Атом 13 База данных логическая 26 -- простая 26-29 -- реляционная 34, 35 Базис Эрбрана 71, 72 Вариант алфавитный 62, 63 Ввод-вывод литеры 142, 143 - слова 143 - списка 143 Верификация 161 Вершина успешная 76 - безуспешная 76, 243 Внелогические предикаты 141-154 --ввода-вывода 141-144 Возведение в степень 40 Возврат 81, 84, 128-130, 152, 167, 188, 240 - внешний 81 - глубокий 81 Вопрос 14-18 - второго порядка 211-214 - конъюнктивный 17-18 - основной 15 - простой 14, 18 - экзистенциальный 16, 23, 212 Выбор недетерминированный 23, 81 Выводимость логическая 19, 25 Вызов процедуры 84 Выражение арифметическое 101, 102, 303-304 - множественное 211-221 - символьное 57 Вычисление арифметического выражения 101, 102 - детерминированное 68, 77 - избыточное 92, 93 - логической программы 21, 25, 65 - недетерминированное 77 - незавершающееся 65, 77, 88-90, 103, 252 - параллельное 70 - программы на Прологе 81-84 - частичное 260 Вычислительная модель Пролога 80-84 Генератор кода 305-309 Глубина вложений рекурсии 155 Голова списка 43 Головоломка «зебра» 188 - логическая 170-173 Гомогенизация 283, 294, 295 Грамматика, задаваемая определительными предложениями 203, 210 Граф 33, 34, 175-177, 215 - бесконечный 177, 216 - ориентированный 33, 34, 175-177 -- ациклический 175-177, 215 - циклический 215 Данные структурированные 30-32 Декартово произведение 35 Декларативное понимание 19, 50, 51, 193, 239 Деление 101 Дерево бинарное 54-57, 199 - вывода (доказательства) 22, 48, 49, 75, 241, 243, 248, 253, 255-257 - игры 233-238 - конечно-безуспешное 78 - поиска 68, 75-78, 87, 88 - бесконечное 68, 77, 81, 214 - разбора 206 Детализация метаинтерпретатора 240- 242 Дизъюнкция в булевой формуле 60, 61 - логическая 126, 127 Дифференцирование 58-60, 117 - сложной функции 59 Заголовок правила 18
Предметный указатель 325 Задача об N ферзях 167-169 Запись терма 142 Значение декларативное 72 - логической программы 23-25 Игра «выдающийся ум» 261-264, 274 - «калах» 268-274 - ним 264-268 - с нулевой суммой 235 Идентификатор в PL 302 - стандартный 302 Идентичность 123 Изоморфизм бинарных деревьев 55, 56, 174 Индексирование 132, 133, 155, 156 Интерпретатор абстрактный 21-23, 65- 70, 80 Интерпретация логической программы 72 Итерация 104-109 Квантор общности 16, 17, 19 - существования 19 Компилятор 296-311 Компиляция арифметического выражения 302, 303, 306-307 - оператора ввода/вывода 304, 308 - оператора присваивания 305, 306 - оператора цикла 304, 307-308 - условного оператора 309, 307 Константа 24, 74, ПО Контекст 219-221 Контекстно-зависимая информация 84, 90 Контекстно-свободная грамматика 203- 206 Конфликт имен 66 Концептуальная зависимость 185, 190 Копирование терма 126, 145 Корректность программы 24, 25, 37-39, 103 Коэффициент определенности 250, 251 Лисп 43, 44, 80 Логика второго порядка 221, 222 - первого порядка 221 Ложное решение 252, 256, 257 Лямбда-выражение 223-224 Лямбда-исчисление 80 Машина Тьюринга 79, 182 Метазнание 250 Метаинтерпретатор 238-260 - для экспертных систем 245-251 - усовершенствованный 242, 245-260 Метапеременная 126, 172, 241 Метапрограммирование 126, 238, 252 Метка 309, 310 Метод «подъем на холм» 229, 230 - «разделяй и властвуй» 52 - «разделяй и спрашивай» 256 - изоляции 284, 290, 291 - расписаний 66, 70, 81 Множество представителей 283, 294 Модель вычислений 179-182 -- недетерминированная 179-181 - минимальная 72 Наибольший общий делитель 42, 43, 102 -- унификатор (н.о.у.) 63, 67 Накопитель 99, 104-106, 112, 176, 193- 195, 212, 218 Недетерминизм 23, 68, 77, 164-189, 203, 217 - с неизвестным выбором 174 - с произвольным выбором 174 Неопределенность переменной 101 Неподвижная точка 73 Область вычислений логической программы 73, 74 - заданная 252 - остановки 73 Оболочка 149 152, 245-247, 259 - интерактивная 247-248 Обработка данных 85 символьных выражений 57-62, 281, 282, 295 - строк 142 Обход графа в глубину 175 - дерева 56, 57, 255 -- сверху вниз 56, 57, 256 -- снизу вверх 56, 57 Объединение 35 Объяснение «как» 249 Оператор бинарный 100-102 - постфиксный 101 - префиксный 101 - пустой 302 - составной 302 - сравнения 102 Операция коммутативная 307 кредитная 274, 275 - сравнения 102
326 Предметный указатель Оптимизация остатка рекурсии 130, 132, 133, 141, 153, 156, 274 Остаток рекурсии 132, 133, 156 Отладка программы 252-259 Отношение 14, 26, 39 - дизъюнктивное 35 - коммутативное 89 Отображение монотонное 72 - списка 96, 222 Отождествление термов 15 Отрицание в логической программе 78, 133-136 - как безуспешное выполнение 78, 133— 136, 213 Отсечение 127-141, 212, 242, 284 - зеленое 127-132, 140, 284 - красное 136-140 - с учетом предыстории 242 - слабое 140 Оценочная функция 229, 230, 235-237 Очередь 200-202 - пустая 200 Ошибка вычислений 101-103 Ошибочная ситуация 85, 86, 101 Палиндром 181, 182 Пара «ключ-значение» 198 Параллельный Пролог 70, 81, 121, 127 Переключатель 158 Переменная 15, 63-65, 122, 124-126 - анонимная 160 - глобальная 158 - логическая 63-65, 85, 105, 193, 309 - общая 17, 18, 35, 85, 170 Пересечение 35 Подстановка 15, 24, 56, 63, 114-116, 124 Поиск «сначала-лучший» 229-232 - в глубину 77, 81, 87, 88, 176, 224, 229 - в ширину 176, 215, 216, 229 - конечно-безуспешный 78 - последовательный 164, 239, 240 - пути между вершинами 175, 216 Полнота программы 24, 25, 36-38 - рекурсивной структуры данных 73, 74 Порог определенности 251 Порождение и проверка 52, 165, 166 в игре 263 Порочный круг в определении 89, 90 Порядок обхода двоичного дерева 57 - правил 86, 87 - целей 68, 90-92, 120, 156, 239, 244 Построение структуры нисходящим методом 97, 98, 162, 193, 197, 207 -- снизу вверх 98, 99, 162, 193, 198 Правила по умолчанию 139, 140 Правило 18-21 - modus ponens обобщенное 19, 71 - грамматики 203, 206 - конкретизации 17 - левое рекурсивное 89 - обобщения 16 - рекурсивное 32-34, 88-90 Предикат 112-118 - второго порядка 221-223 - металогический 119-122, 136 - множественный 211-214, 223-224 -- примитивный 212 - системный 100-102 - типовый 100-112 -- металогический 119-122 Предложение 18, 25, 81, 144 - единичное 18, 25 - итерационное 18, 25, 104 - хорново 18 Преобразование рекурсии в итерацию 104-109 Пример общий 17, 63 - основной 20, 23 Присваивание в языке PL 302, 305 - деструктивное 85, 115 Проблема голландского флага 195, 196 Проверка на вхождение 63, 64, 70, 75, 121 Программа арифметическая логическая 36-43 - на Прологе 100-103 - интерактивная 147-151 - итерационная 104 - логическая 20, 25 - минимальная рекурсивная 37, 89, 94 - объектная 297, 298 - перемещаемая 297 - рекурсивная 50-54, 104-109 Программирование «сверху вниз» 162 - второго порядка 210-224, 242 - интерактивное 147-152 Проекция 35 Произведение скалярное 106, 107 Пролог II 81 Пропуск решения 257, 258 Протокол вычисления 21, 66, 68, 81-83 --на Прологе 81-83 - сеанса 150-152 Прототип 162 Процедура 21
Предметный указатель 327 - итерационная 105 Равенство 63 Размораживание терма 125, 200 Разностная структура 196-198, 295, 309 - сумма 196, 197 Разностный список 190-196, 202, 204, 206, 216 Разность симметрическая 34 Разрез 140 Раскрашивание плоской карты 169-172 Раскрытие целей 181, 189 Распознавание многочленов 57, 58, 130 Редактор 148, 149 Редукция 21, 22, 65, 66, 68, 69 - основная 22 Резольвента 21, 65-67 Рекурсия 104-109, 132, 133 Решатель задач 225-233 - уравнений 281-295 Решение избыточное 92-94 Решения многочисленные 39 Сборка мусора 133, 153, 155, 274 Связность графа 33, 34, 177 Семантика 71-73 - декларативная 71 - денотационная 72 - операционная 70 Символ нетерминальный 203 Синтаксический анализатор языка PL 301 Следствие логическое 14-16, 19 Слияние упорядоченных списков 93, 127-130 Словарь 305, 309 Сложение 38, 39 Сложность 74, 75 - временная 75 - глубины вывода 75 - длины вывода 75 - емкости памяти 75 - размера цели 74, 75 Совпадение 14 Сортировка быстрая 53, 54, 83, 84, 91, 194 - вставкой 53, 254 - перестановкой 52, 131 - слиянием 53 Спецификация 161, 162 Список 43-49, 82, 85, 89, 91, 93-99, 108 - лексем 297-298 - неполный 74, 89, 91, 95, 190 - полный 74, 89, 91, 95 - пустой 43, 95, 191 Справочник 198-200, 203 Стек 63-65, 81, 111, 112, 180, 181, 253, 254 Строка литер 142 Структура данных 85, 97, 99, 116, 155, 185 - на Прологе 97-99 - неполная 185, 190-202 - неполная 99 - рекурсивная 97 Схема логическая 28-29 - реляционная 26 Сценарий 185-188 Текст программы 296, 297 Тело правила 18 Терм 15, 30, 62, 112, 113, 124, 145 - замороженный 125 - неосновной 122 - общий 63 - основной 24 - приведенный 283, 294 - составной 15, 24, 63, 74, 112-118 Тест металогический 120-124 Тип 36 - неполный 74 - полный 73, 74 - рекурсивный 36 Традиционный язык программирования 84-86 Транзитивное замыкание 33-34 Трассировка метаинтерпретатора 243- 245 Умножение 40, 101 Универсум Эрбрана 71, 72 Унификатор 63, 67 Унификация 62, 71, 75, 85, 97, 121, 122, 155, 195 - с проверкой на вхождение 121, 122 Управление последовательностью действия 84, 90 Уравнение экспоненциального типа 29 Условие типовое 40 Условия неопределенности 250, 251 Факт 13, 14, 144 - универсальный 16 Факториал 40, 41, 103 Факторизация 284
328 Предметный указатель Формула булева 60, 61 Функтор 15, 112, 113, 116 Функция Лккермана 42 - алгебраическая 282 - запоминающая 146, 147 - следования 36 Ханойская башня 60, 69, 146, 147 Хвост списка 43 Цель дизъюнктивная 126, 127 - конъюнктивная 65, 144 - основная 24, 25, 66 - порожденная 65 - предшественник 66 - родственная 65 - снятая 22 Цикл интерактивный 147 - управляемый отказом 152, 153, 274 Число натуральное 36-43, 72 -- полное 74 Чистый Пролог 80-100 Чтение терма 142 Шаблон 184, 185, 188 Экспертная система 245-251 -- для кредитных операций 274-281 Эффект побочный 145, 153, 191 Язык параллельного программирования 70, 153, 154 - реляционный 86 ANALOGY 182, 189 APES 260 append 47, 48, 63, 67, 72, 74, 75, 82, 88, 89, 91, 133, 193, 204, 220 apply (команда редактора) 148, 149 apply (предикат второго порядка) 222, 223 arg 113, 117 assert 145, 146 atom 110, 111 call 126 car 44 cdr 44 clause 144 DC 203-210 DCG 203-210 ELIZA 184, 185, 189 Edinburgh-Пролог 81, 100, 101, 120, 154, 160, 242 FCP 295 functor 112, 113 get 143 GHC 70, 81 IC-Пролог 81, 100, 188 KWIC 220, 221 LIPS 155, 163 LOGLISP 100 McSAM 185-187, 190 member 44-47, 93, 138, 139, 166 MU-Пролог 81, 100 MYCIN 259, 274 name 142 nonvar 119 not 134 PARLOG 70, 81 PL 296 PRESS 281, 282, 294-295 put 143 Quintus Prolog 154 read 142-144 rplacd 191 S-выражение 191 size_of 213 SLD-резолюция 79 univ 116, 117 var 119 Wisdom Пролог 102, 242 write 141
Оглавление Предисловие редактора перевода 5 Предисловие 6 Введение 9 Часть I. ЛОГИЧЕСКИЕ ПРОГРАММЫ Глава I. Основные конструкции 13 1.1. Факты 13 1.2. Вопросы 14 1.3. Логические переменные, подстановки и примеры 15 1.4. Экзистенциальные вопросы 16 1.5. Универсальные факты 16 1.6. Конъюнктивные вопросы и общие переменные 17 1.7. Правила 18 1.8. Простой абстрактный интерпретатор 21 1.9. Значение логической программы 23 1.10. Резюме 24 Глава 2. Программирование баз данных 27 2.1. Простые базы данных 27 Упражнения к разд. 2.1 30 2.2. Структурированные и абстрактные данные 30 Упражнения к разд. 2.2 32 2.3. Рекурсивные правила 32 Упражнения к разд. 2.3 34 2.4. Логические программы и модель реляционной базы данных 34 2.5. Дополнительные сведения 35 Глава 3. Рекурсивное программирование 36 3.1. Арифметика 36 Упражнения к разд. 3.1 43 3.2. Списки 43 Упражнения к разд. 3.2 49 3.3. Построение рекурсивных программ 50 Упражнения к разд. 3.3 54 3.4. Бинарные деревья 54 Упражнения к разд. 3.4 57 3.5. Работа с символьными выражениями 57 Упражнения к разд. 3.5 61 3.6. Дополнительные сведения 62 Глава 4. Вычислительная модель логических программ 62 4.1. Унификация 62 Упражнения к разд. 4.1 65 4.2. Абстрактный интерпретатор логических программ 65 Упражнения к разд. 4.2 70 4.3. Дополнительные сведения 70
330 Оглавление Глава 5. Теория логических программ 71 5.1. Семантика 71 5.2. Корректность программы 73 Упражнения к разд. 5.2 74 5.3. Сложность 74 Упражнения к разд. 5.3 75 5.4. Деревья поиска 75 Упражнения к разд. 5.4 77 5.5. Отрицание в логическом программировании 78 5.6. Дополнительные сведения 78 Часть П. ЯЗЫК ПРОЛОГ Глава 6. Чистый Пролог 80 6.1. Вычислительная модель Пролога 80 Упражнения к разд. 6.1 84 6.2. Сравнение с традиционными языками программирования 84 6.3. Дополнительные сведения 86 Глава 7. Программирование на чистом Прологе 86 7.1. Порядок правил 87 Упражнения к разд. 7.1 88 7.2. Проблема завершения программ 88 Упражнения к разд. 7.2 90 7.3. Порядок целей 90 Упражнения к разд. 7.3 92 7.4. Избыточные решения 92 7.5. Рекурсивное программирование в чистом Прологе 94 Упражнения к разд. 7.5 99 7.6. Дополнительные сведения 100 Глава 8. Арифметика 100 8.1. Системные арифметические предикаты 100 8.2. Повторное рассмотрение арифметических логических программ 102 Упражнения к разд. 8.2 103 8.3. Замена рекурсии итерацией 104 Упражнения к разд. 8.3 109 8.4. Дополнительные сведения 109 Глава 9. Анализ структуры термов ПО 9.1. Типовые предикаты ПО Упражнения к разд. 9.1 112 9.2. Составные термы 112 Упражнения к разд. 9.2 118 9.3. Дополнительные сведения 118 Глава 10. Металогические предикаты 118 10.1 Типовые металогические предикаты 119 Упражнения к разд. 10.1 122 10.2. Сравнение неосновных термов 122 10.3. Использование переменных в качестве объектов 124 10.4. Доступность метапеременных 126 10.5. Дополнительные сведения 127
Оглавление 331 Глава 11. Отсечения и отрицание 127 11.1. Зеленые отсечения: выражение детерминизма 127 Упражнения к разд. 11.1 132 11.2. Оптимизация остатка рекурсии 132 11.3. Отрицание 133 Упражнения к разд. 11.3 136 11.4. Красные отсечения: устранение явных условий 136 Упражнения к разд. 11.4 139 11.5. Правила по умолчанию 139 11.6. Дополнительные сведения 140 Глава 12. Внелогические предикаты 141 12.1. Ввод-вывод • 141 Упражнения к разд. 12.1 144 12.2. Доступ к программам и обработка программ 144 12.3. Запоминающие функции 146 Упражнения к разд. 12.3 147 12.4. Интерактивные программы 147 Упражнения к разд. 12.4 152 12.5. Циклы, управляемые отказом 152 Упражнения к разд. 12.5 153 12.6. Дополнительные сведения 153 Глава 13. Практические рекомендации 154 13.1. Эффективность программ на Прологе 155 13.2. Программистские трюки 156 13.3. Стиль программирования и запись программ 160 13.4. Разработка программ 161 13.5. Дополнительные сведения 163 Часть III. СОВРЕМЕННЫЕ МЕТОДЫ ПРОГРАММИРОВАНИЯ НА ПРОЛОГЕ Глава 14. Недетерминированное программирование 164 14.1. Метод «образовать и проверить» 165 Упражнения к разд. 14.1 173 14.2. Недетерминизм с произвольным выбором альтернативы и недерминизм с неизвестным выбором альтернативы 174 14.3. Моделирование недетерминированных вычислений 179 Упражнение к разд. 14.3 182 14.4. Классические интеллектуальные программы: ANALOGY, ELIZA и McSAM 182 Упражнения к разд. 14.4 188 14.5. Дополнительные сведения 188 Глава 15. Неполные структуры данных 190 15.1. Разностные списки 190 Упражнения к разд. 15.1 196 15.2. Разностные структуры 196 Упражнения к разд. 15.2 197 15.3. Справочники 198 15.4. Очереди 200 15.5. Дополнительные сведения 202
332 Оглавление Глава 16. Синтаксический анализ для грамматик, задаваемых определительными предложениями 203 Упражнения к гл. 16 210 16.1. Дополнительные сведения 210 Глава 17. Программирование второго порядка 210 17.1. Множественные выражения 211 Упражнения к разд. 17.1 215 17.2. Применения множественных выражений 215 Упражнения к разд. 17.2 221 17.3. Другие предикаты второго порядка 221 17.4. Дополнительные сведения 223 Глава 18. Методы поиска 224 18.1. Поиск на графах пространства состояний 224 Упражнения к разд. 18.1 233 18.2. Игровые деревья поиска 233 18.3. Дополнительные сведения 238 Глава 19. Метаинтерпретаторы 238 19.1. Простые метаинтерпретаторы 239 Упражнения к разд. 19.1 245 19.2. Усовершенствованные метаинтерпретаторы для экспертных систем . . . . 245 Упражнения к разд. 19.2 251 19.3. Усовершенствованные метаинтерпретаторы для отладки программ .... 252 19.4. Дополнительные сведения 259 Часть IV. ПРИЛОЖЕНИЯ ?61 Глава 20. Игровые программы 20.1. «Выдающийся ум» 261 20.2. Игра Ним 264 20.3. Игра в калах 268 20.4. Дополнительные сведения 274 Глава 21. Экспертная система для кредитных операций 274 21.1. Дополнительные сведения 279 Глава 22. Решатель уравнений 281 22.1. Обзор методов решения уравнений 282 22.2. Факторизация 284 22.3. Метод изоляции 284 22.4. Полиномиальные уравнения 292 22.5. Гомогенизация 294 Упражнения к гл. 22 295 22.6. Дополнительные сведения 295 Глава 23. Компилятор 296 23.1. Обзор компилятора v. 296 23.2. Синтаксический анализатор 301
Оглавление 333 23.3. Генератор кода 305 23.4. Ассемблер 309 Упражнения к гл. 23 310 23.5. Дополнительные сведения 311 Приложение A. Использование Пролог-системы 312 B. Системные предикаты 312 C. Предварительно определяемые операторы 316 Литература 318 Предметный указатель 324
УВАЖАЕМЫЙ ЧИТАТЕЛЬ! Ваши замечания о содержании книги, ее оформлении, качестве перевода и другие просим присылать по адресу: 129820, Москва, И-110, 1-й Рижский пер., д. 2, изд-во «Мир».
Научное издание Леон Стерлинг, Эгуд Шапиро Искусство программирования на языке Пролог Зам. заведующего редакцией Э. Н. Бадиков Ст. научный редактор Т. Н. Шестакова Младший научный редактор Ю.Л. Евдокимова Художник Е. Д. Дроздов Художественный редактор Н. М. Иванов Технический редактор Т. А. Мирошина Корректор В. И. Киселева ИБ № 6911 Сдано в набор 2.12.88. Подписано к печати 12.09.89. Формат 70 х 100Vi6- Бумага офсетная №2. Печать офсетная. Гарнитура тайме. Объем 10,50 бум. л. Усл. печ.л. 27,30. Усл. кр.-отт. 54,60. Уч.-изд.л. 26,75. Изд. №6/6145. Тираж 50000 экз. Зак. 1402. Цена 2 р. 10 к. Издательство «Мир» В/О «Совэкспорткнига» Государственного комитета СССР по делам издательств, полиграфии и книжной торговли, 129820, ГСП, Москва, И-ПО, 1-й Рижский пер., 2. Можайский полиграфкомбинат В/О «Совэкспорткнига» Государственного комитета СССР по делам издательств, полиграфии и книжной торговли, г. Можайск, ул. Мира, 93.