- •У.Клоксин, к.Меллиш программирование на языке пролог Для программистов и пользователей эвм. Предисловие редакторов перевода
- •Предисловие ко второму изданию
- •Предисловие к первому изданию
- •Глава 1 введение
- •1.1. Факты
- •1.2. Вопросы
- •1.3. Переменные
- •1.4. Конъюнкции
- •1.5. Правила
- •1.6. Заключение и упражнения
- •Глава 2 более детальное описание
- •2.1. Синтаксические правила
- •2.1.1. Константы
- •2.1.2. Переменные
- •2.1.3. Структуры
- •2.2. Литеры
- •2.3. Операторы
- •2.4. Равенство и установление соответствия
- •2.5. Арифметика
- •2.6. Общая схема согласования целевых утверждений
- •2.6.1. Успешное доказательство конъюнкции целевых утверждений
- •2.6.2. Рассмотрение целевых утверждений при использовании механизма возврата
- •2.6.3. Установление соответствия
- •Глава 3. Использование структур данных
- •3.1. Структуры и деревья
- •3.2. Списки
- •3.3. Принадлежность элементов списку
- •3.4. Пример: преобразование предложений
- •3.5. Пример: упорядочение по алфавиту
- •3.6. Использование предиката присоединить и спецификация деталей
- •Глава 4. Возврат и отсечение
- •4.1. Порождение множественных решений
- •4.2. Отсечение
- •4.3. Общие случаи использования отсечения
- •4.3.1. Подтверждение правильности выбора правила
- •4.3.2. Комбинация «отсечение-fail»
- •4.4. Проблемы, связанные с использованием отсечения
- •Глава 5 ввод и вывод
- •5.1. Ввод и вывод термов
- •5.1.1. Вывод термов
- •5.1.2. Ввод термов
- •5.2. Ввод и вывод литер
- •5.2.1. Вывод литер
- •5.2.2. Ввод литер
- •5.3. Ввод предложений
- •5.4. Чтение файлов и запись в файлы
- •5.4.1. Запись в файлы
- •5.4.2. Чтение файлов
- •5.4.3. Ввод программ
- •5.5. Объявление операторов
- •Глава 6. Встроенные предикаты
- •6.1. Ввод новых утверждений
- •Списковая форма записи
- •6.2. Выполнение и невыполнение целевого утверждения
- •6.3. Классификация термов
- •6.4. Работа с утверждениями как с термами
- •6.5. Создание структур и работа с компонентами структур
- •6.6. Воздействие на процесс возврата
- •Отсечение
- •6.7. Формирование составных целевых утверждений
- •Конъюнкция целей
- •Дизъюнкция целей
- •6.8. Равенство
- •6.9. Ввод и вывод данных
- •6.10. Обработка файлов
- •6.11. Вычисление арифметических выражений
- •6.12. Сравнение чисел
- •6.13. Наблюдение за выполнением программы на Прологе
- •Глава 7. Еще несколько примеров программ
- •7.1. Словарь в виде упорядоченного дерева
- •7.2. Поиск в лабиринте
- •7.3. Ханойские башни
- •7.4. Справочник комплектующих деталей
- •7.5. Обработка списков
- •7.6. Представление и обработка множеств
- •7.7. Сортировка
- •7.8. Использование базы данных: random, генатом, найтивсе
- •Генератор случайных чисел (random)
- •Генератор имен (генатом)
- •Генератор списков структур (найтивсе)
- •7.9. Поиск по графу
- •7.10. Просеивай Двойки, Просеивай Тройки
- •7.11. Символьное дифференцирование
- •7.12. Отображение структур и преобразование деревьев
- •7.13. Применение предикатов clause и retract
- •Глава 8. Отладка пролог-программ
- •8.1. Расположение текстов программ
- •8.2. Типичные ошибки
- •8.3. Модель трассировки
- •8.4. Трассировка и контрольные точки
- •Выдача информации о цели
- •Выдача информации о предшественниках
- •Изменение уровня трассировки
- •Вмешательство в процесс согласования цели
- •Другие команды
- •Заключение
- •8.5. Фиксация ошибок
- •Глава 9. Использование грамматических правил в прологе
- •9.1. Проблема синтаксического анализа
- •9.2. Описание синтаксического анализа на языке Пролог
- •9.3. Запись грамматических правил в Прологе
- •9.4. Присоединение дополнительных аргументов
- •9.5. Введение дополнительных условий
- •9.6. Заключение
- •Глава 10. Пролог и математическая логика
- •10.1. Краткое введение в исчисление предикатов
- •10.2. Приведение формул к стандартной форме
- •Этап 1 - исключение импликаций и зквивалентностей
- •Этап 2 - перенос отрицания внутрь формулы
- •Этап 3 - сколемизация
- •Этап 4 - вынесение кванторов общности в начало формулы
- •Этап 5 - использование дистрибутивных законов для & и #
- •Этап 6 - выделение множества дизъюнктов
- •10.3. Форма записи дизъюнктов
- •10.4. Принцип резолюций и доказательство теорем
- •10.5. Хорновские дизъюнкты
- •10.6. Пролог
- •10.7. Пролог и логическое программирование
- •Глава 11. Программные проекты на прологе
- •11.1. Простые проекты
- •11.2. Более сложные проекты
- •Приложение а. Ответы к некоторым упражнениям
- •Приложение в. Программа приведения формул исчисления предикатов к стандартной форме
- •Этап 1 - исключение импликаций
- •Этап 2 - перенос отрицания внутрь формулы
- •Этап 3 - сколемизация
- •Этап 4 - вынесение кванторов общности в начало формулы
- •Этап 5 - использование дистрибутивных законов для. & и #
- •Этап 6 - выделение множества дизъюнктов
- •Печать утверждений
- •Приложение с. Различные версии языка пролог
- •Синтаксис
- •Различные ограничения
- •Возможности окружения
- •Компиляция
- •Специальные встроенные предикаты
- •Средства отладки
- •Приложение d. Пролог для эвм dec system-10
- •Пример сеанса работы
- •Синтаксис
- •Различные ограничения
- •Возможности окружения
- •Компиляция
- •Различия во встроенных предикатах
- •Дополнительные встроенные предикаты
- •Средства отладки
- •Литература
- •Приложение е. Микро-пролог
- •Пример сеанса работы
- •Синтаксис
- •Различные ограничения
- •Возможности окружения
- •Специальные встроенные предикаты
- •Средства отладки
- •Литература
- •Приложение f. Система мпролог[19]
- •Пример сеанса работы
- •Синтаксис
- •Модульность
- •Компоненты системы мПролог
- •Различные ограничения
- •Дополнительные встроенные предикаты
- •Средства отладки
- •Литература
- •Примечания
3.3. Принадлежность элементов списку
Предположим, что имеется некоторый список, в котором X обозначает его голову, a Y – хвост списка. Напомним, что такой список мы можем записать так: [X|Y]. Этот список мог бы содержать, например, клички тех лошадей потомков жеребца Coriander, которые все выиграли скачки в Великобритании в 1927 году:
[curragh_tip, music_star, park_mill, portland]
Теперь предположим, что мы хотим определить, содержится ли некоторая кличка в указанном списке. В Прологе это можно сделать, определив, совпадает ли данная кличка с головой списка.
Если совпадает, то наш список завершается успехом. Если нет, то мы проверяем, есть ли кличка в хвосте исходного списка. Это значит, что снова проверяется голова, но уже хвоста списка. Затем проверяется голова очередного хвоста списка. Если мы доходим до конца списка, который будет пустым списком, то наш поиск завершается неудачей: указанной клички в исходном списке нет.
Для того чтобы записать все это на Прологе, сначала надо установить, что между объектом и списком, в который этот объект может входить, существует отношение. Это отношение, называемое отношением принадлежности, представляет достаточно распространенное в повседневной жизни понятие. Так, мы говорим о людях, являющихся членами клубов, и о других тому подобных вещах. Для записи этого отношения мы будем использовать предикат принадлежит: целевое утверждение принадлежит(X, Y) является истинным («выполняется»), если терм, связанный с X, является элементом списка, связанного с Y. Имеются два условия, которые надо проверить для определения истинности предиката. Первое условие говорит, что X будет элементом списка Y, если X совпадает с головой списка Y. На Прологе этот факт записывается следующим образом:
принадлежит(X,[X |_]).
Эта запись констатирует, что X является элементом списка, который имеет X в качестве головы. Заметим, что мы использовали анонимную переменную '_' для обозначения хвоста списка. Это сделано потому, что мы никак не используем хвост списка в этом частном факте. Заметим, что данное правило могло бы быть записано и по-другому:
принадлежит(X,[Y|_]:- X = Y.
К этому моменту вы должны уже понимать, почему можно использовать X сразу в двух местах в первой, более короткой, версии этого правила.
Второе, и последнее, правило говорит о том, что X принадлежит списку при условии, что он входит в хвост этого списка, обозначаемый через Y. И нет лучшего пути, чем использовать тот же самый предикат принадлежит для того, чтобы определить, принадлежит ли X хвосту списка! В этом и состоит суть рекурсии. На Прологе это выглядит так:
принадлежит(X,[_ |Y]):- принадлежит(X,Y).
и констатирует, что X является элементом списка, если X является элементом хвоста этого списка. Заметим, что мы использовали анонимную переменную '_', так как нас не интересует имя переменной, обозначающей голову списка. Два этих правила в совокупности определяют предикат для отношения принадлежности и указывают Прологу, каким образом просматривать список от начала до конца при поиске некоторого элемента в списке. Наиболее важный момент, о котором следует помнить, встретившись с рекурсивно определенным предикатом, заключается в том, что прежде всего надо найти граничные условия и способ использования рекурсии.
Для предиката принадлежит в действительности имеются два типа граничных условий. Либо объект, который мы ищем, содержится в списке, либо он не содержится в нем. Первое граничное условие для предиката принадлежитраспознается первым утверждением, которое приведет к прекращению поиска в списке, если первый аргумент предиката принадлежит совпадает с головой списка, соответствующего второму аргументу. Второе граничное условие встречается, когда второй аргумент предиката принадлежит является пустым списком.
Как мы можем убедиться в том, что граничные условия будут когда-либо удовлетворены? Для этого необходимо обратить внимание на то, как используется рекурсия во втором правиле для предиката принадлежит. Заметим, что каждый раз, когда при поиске соответствия для целевого предиката принадлежит происходит рекурсивное обращение к тому же предикату, новая цель формируется для более короткого списка. Хвост списка всегда является более коротким списком, чем исходный список. Очевидно, что рано или поздно произойдет одно из двух событий: либо произойдет сопоставление с первым правилом для принадлежит, либо в качестве второго аргумента принадлежит будет задан список длины 0, т. е. пустой список. Как только возникнет одна из этих ситуаций, прекратится рекуррентное порождение целей для предиката принадлежит. Первое граничное условие распознается фактом, который не вызывает порождения новых подцелей. Второе граничное условие не распознается ни одним из утверждений для принадлежит, так что процесс поиска сопоставимого элемента списка для целевого утверждения принадлежит закончится неудачей. Это демонстрирует следующий пример на Прологе:
принадлежит(Х, [X | _]).
принадлежит(Х,[_|Y]):- принадлежит (X,Y).
?- принадлежит(d,[a,b,с,d,e,f,g]).
да
?- принадлежит(2,[3,a,4,f]).
нет
Предположим, мы введем вопрос
?- принадлежит(clugatе,[curraugh_tiр,music_star,раrk_mill, ortland]).
Так как clygate не сопоставимо с curragh_tip, то происходит сопоставление со вторым правилом для принадлежит. Переменная Y получает значение [music_star, park_mill, portland], и порождается следующая цель: определить, принадлежит ли clygate этому списку. Опять происходит сопоставление со вторым правилом, и снова выделяется хвост списка. Текущей целью становится принадлежит (clugate,[park_mill,portland]) Этот процесс рекурсивно повторяется до тех пор, пока мы не доберемся до цели, у которой X есть clygate, a Y есть [portland]. Происходит еще одно сопоставление со вторым правилом, и теперь Y конкретизируется хвостом списка [portland], который является пустым списком. Следующей целью становится принадлежит(clygate,[]). Ни одно из правил в базе данных не сопоставимо с этой целью, так что цель оказывается ложной и ответ на вопрос будет отрицательным.
Важно помнить, что каждый раз, когда при согласовании принадлежит с базой данных выбирается второе утверждение этого предиката, Пролог рассматривает рекурсивное обращение к предикату принадлежит как попытку найти соответствие для некоторой новой его «копии». Это предотвращает путаницу переменных, соответствующих одному употреблению утверждения, с переменными, соответствующими другому употреблению этого же утверждения.
Предикат отношения принадлежности настолько полезен, что мы еще неоднократно будем использовать его в оставшейся части этой книги. Предикат принадлежит важен еще и потому, что он представляет практически наименьший полезный пример рекурсивного предиката – определение предиката принадлежит содержит утверждения, которые могут быть проверены с помощью только того же самого предиката принадлежит. Рекурсивные определения часто встречаются в программах на Прологе, и они полностью равноправны с другими определениями. Однако надо быть осторожным, чтобы не допускать «закольцованные» определения, как, например, следующее:
родитель(X,Y):- ребенок(Y,X).
ребенок(A,B):- родитель(B,A).
В этом примере, чтобы согласовать с базой данных целевое утверждение родитель, необходимо согласовать подцель ребенок. Однако определение для ребенок приведет к появлению единственной подцели – родитель. Вы должны понимать, почему вопрос, содержащий в качестве целей родитель или ребенок, приводит к циклу, находясь в котором Пролог не сможет найти какие-либо новые факты, и этот цикл никогда не завершится.
Одна важная проблема, на которую следует обращать внимание в рекурсивных определениях, - это левосторонняя рекурсия. Она возникает в случае, когда правило порождает подцель, по существу эквивалентную исходной цели, которая явилась причиной использования этого правила. Так, если бы мы определили предикат
человек(X):- человек(Y), мать(Х,Y).
человек(адам).
и ввели вопрос
?- человек(X).
то Пролог сначала использовал бы правило и рекурсивно породил подцель человек (Y). Попытка найти соответствие этой цели вновь привела бы к выбору первого правила и породила бы еще одну новую эквивалентную подцель. И так далее, до тех пор, пока не исчерпались бы вычислительные ресурсы. Конечно, если бы была возможность использовать механизм возврата, то был бы найден сообщенный в определении факт об Адаме и началось бы порождение решений[7]. Ошибка заключается в том, что для того, чтобы начался возврат, Пролог должен потерпеть неудачу при проверке первого утверждения. В данном же случае поиск решения оказывается неопределенно длинным, и нет никакой возможности завершить этот поиск с успехом либо с неудачей. Из всего сказанного выше можно извлечь следующую мораль:
Не следует предполагать, что только потому, что вы предоставили все относящиеся к делу факты и правила, Пролог всегда найдет их. Создавая программу на Прологе, вы все время должны представлять, каким образом Пролог осуществляет поиск в базе данных и какие переменные будут конкретизированы, когда будет использовано одно из ваших правил.
Для приведенного примера имеется простой способ устранения ошибки – поместить факт перед правилом, а не после него. В действительности существует хороший эвристический принцип: помещать, где это возможно, факты перед правилами. Иногда при размещении правил в некотором конкретном порядке может возникнуть ситуация, когда они будут правильно работать для целей одного вида и не будут работать для целей другого вида. Рассмотрим следующее определение предиката список (X), при котором предикат список является истинным, если X – список, последний элемент которого имеет в качестве хвоста пустой список:
список([A|B]):- список(B).
список([]).
Если мы используем эти правила для получения ответов на вопросы, подобные следующим:
?- список ([a,b,c,d]).
?- список([]).
?- список(f(1,2,3))
то данное определение будет работать хорошо. Но если мы сделаем запрос
?- список(X).
то программа зациклится. Предикат, аналогичный предикату список, но неподверженный зацикливанию, задается следующими двумя фактами:
обобщенный_список([]).
обобщенный_список([_ |_]).
В этом варианте просто проверяется начало списка, а не тот факт, что последний хвост списка является пустым списком ([]). Последнее определение не является таким же строгим тестом правильности списка, как определение предикатасписок, но оно не приведет к зацикливанию, если аргументом является переменная.
