ПРОЛОГ-ЛЕКЦИИ

advertisement
http://it.kgsu.ru/Prolog/pro005.html
Шаг 1.
Основы логического программирования.
Краткая характеристика языка программирования Пролог
Шаг 1. Краткая характеристика языка программирования Пролог
Шаг 2. Представление объектов в логике предикатов первого порядка
Шаг 3. Кванторы
Шаг 4. Формулы логики предикатов первого порядка
Шаг 5. Предваренная нормальная форма формул
Шаг 6. Проверка общезначимости формулы. Метод резолюций
Шаг 7. Этапы приведения формулы к стандартной форме записи
Шаг 8. Хорновские дизъюнкты
Шаг 9. Основы языка Пролог
Шаг 10. Факты и правила
Шаг 11. Размещение фактов, правил и запросов
Шаг 12. Общие представления о переменных
Шаг 13. Предложения
Шаг 14. Предикаты
Шаг 15. Переменные
Шаг 16. Цели (запросы)
Шаг 17. Комментарии
Шаг 18. Сопоставление
Шаг 19. Основные разделы Пролог-программ. Раздел предложений
Шаг 20. Раздел предикатов
Шаг 21. Раздел доменов
Шаг 22. Раздел цели
Шаг 23. Описание доменов
Шаг 24. Задание типов аргументов при декларации предикатов
Шаг 25. Арность (размерность)
Шаг 26. Синтаксис правил
Шаг 27. Автоматическое преобразование типов
Шаг 28. Сопоставление и унификация
Шаг 29. Поиск с возвратом
Шаг 30. Управление поиском решений
Шаг 31. Прерывание поиска с возвратом: отсечение
Шаг 32. Детерминизм и отсечение
Шаг 33. Факты и правила в качестве процедур
Шаг 34. Простые объекты данных
Шаг 35. Составные объекты данных и функторы
Шаг 36. Объявление составных доменов
Шаг 37. Определение составных смешанных доменов
Шаг 38. Повтор
Шаг 39. Использование отката с петлями
Шаг 40. Оптимизация хвостовой рекурсии
Шаг 41. Использование аргументов в качестве переменных цикла
Шаг 42. Списки и рекурсия
Шаг 43. Работа со списками
Шаг 44. Использование списков
Шаг 45. Хвостовая рекурсия
Шаг 46. Принадлежность к списку
Шаг 47. Поиск всех решений для цели сразу
Шаг 48. Составные списки
Шаг 49. Арифметические вычисления и сравнения
Шаг 50. Целочисленная и вещественная арифметика
Шаг 51. Сравнение
Шаг 52. Сравнение символов, строк и идентификаторов
Шаг 53. Запись и чтение
Шаг 54. Предикат writef
Шаг 55. Чтение
Шаг 56. Файловая система в Прологе
Шаг 57. Работа с файлами
Шаг 58. Основные предикаты управления строкой
Шаг 59. Преобразования типов
Шаг 60. Внутренняя база фактов
Шаг 61. Использование внутренних баз фактов
Шаг 62. Обновление внутренней базы фактов
Шаг 63. Создание базы данных, располагающейся в оперативной
памяти
Шаг 64. Примеры использования внутренней базы фактов
Шаг 65. Создание и работа с окнами
Шаг 66. Использование других предикатов для работы с окнами
Шаг 67. Использование окон для ввода и вывода
Шаг 68. Создание перекрывающихся окон
Шаг 69. Создание меню при помощи окон
Шаг 70. Графика
Шаг 71. Графика (продолжение)
Шаг 72. Среда программирования Turbo Prolog 2.0
Шаг 73. Текстовый редактор Turbo Prolog 2.0
Шаг 74. Средства отладки в Turbo Prolog 2.0
На этом шаге мы рассмотрим краткую характеристику языка
программирования Пролог.
В начале 70-х годов группа специалистов Марсельского
университета
во
главе
с
А.Колмероэ
разработала
специализированную систему для доказательства теорем. Она была
написана на языке Фортран и использовалась для обработки
естественного языка. Система доказательства теорем, названная
Пролог, воплощала процедурную семантику Р.Ковальского. Позже
Ван Энден и Р.Ковальский разработали формальную семантику для
языка логических программ.
Сегодня Пролог - язык, предназначенный
приложений, использующих средства и методы
интеллекта и создания экспертных систем.
для создания
искусственного
Пролог применяется при создании приложений в следующих
областях:








разработка быстрых прототипов прикладных программ;
управление производственными процессами;
создание динамических реляционных баз данных;
перевод с одного языка на другой;
создание
естественно-языковых
интерфейсов
для
существующих систем;
реализация экспертных систем и оболочек экспертных систем;
создание пакетов символьных вычислений для решения
уравнений, интегрирования и дифференцирования;
доказательство теорем и пакеты искусственного интеллекта, в
которых возможности Пролога по обеспечению дедуктивного
вывода применяются для проверки различных теорий.
Математическая модель, лежащая в основе Пролога, шире модели
реляционных баз данных. По этой причине Пролог может
использоваться для приложений обработки информации.
Формальная точность и возможность интерпретации Пролога
делают его чрезвычайно подходящим языком для представления
знаний.
В области экспертных систем Пролог применяется для
моделирования экспертизы в различных предметных областях.
Коммерческие системы, написанные на Прологе, ориентированы на
такие приложения, как медицина, законодательство, автоматизация
производства.
На Прологе удобно описывать различные структуры данных, такие
как списки и деревья.
В наших шагах рассматриваются структура и методы Пролога,
необходимые для разработки программ. Излагаются вопросы,
связанные с использованием списков, файлов, графики, обработки
строковой информации.
Пролог существенно отличается от языков, традиционно
используемых в программировании. В языках Бейсик, Паскаль
основным методом программирования является разбиение задачи на
отдельные
шаги
и
их
последовательное
описание.
Последовательность шагов отображается в машинные команды,
выполняемые компьютером. Отменить ранее выполненные команды
невозможно, поскольку содержимое памяти постоянно обновляется.
Языки
программирования
такого
вида
называются
алгоритмическими.
Свое название Пролог получил от слов "ПРОграммирование на
языке ЛОГики". На самом деле Пролог не является чистым языком
логического программирования, но его создание - важный этап в этом
направлении.
При программировании на Прологе значительно упрощается
описание решения, и программист имеет возможность заниматься
непосредственно задачей, а не поиском способа разбиения ее
решения на небольшие шаги, которые можно запрограммировать.
Теоретической основой Пролога является раздел символьной
логики, называемый исчислением предикатов. Прологу присущ ряд
свойств,
которыми
не
обладают
традиционные
языки
программирования, что делает его мощным средством в области
логического программирования. К таким свойствам относятся
механизм вывода с поиском и возвратом, встроенный механизм
сопоставления с образцом, и простая структура данных с
возможностью ее изменения. Пролог отличает единообразие
программ и данных. Программа и данные - это две различные точки
зрения на объекты Пролога. В единой базе данных можно свободно
создавать и уничтожать отдельные элементы. Поскольку не
существует различия между программами и данными, можно менять
программу во время ее работы. В Прологе отсутствуют указатели,
операторы присваивания и перехода. Естественным методом
программирования является рекурсия.
Со следующего шага мы начнем рассматривать некоторые
теоретические положения, лежащие в основе Пролога.
Шаг 2.
Основы логического программирования.
Представление объектов в логике предикатов первого
порядка
На этом шаге мы рассмотрим представление объектов в логике
предикатов первого порядка.
Большинство предметных областей могут быть описаны с помощью
логики предикатов первого порядка. В этом случае решение задачи
означает запись ее с помощью формул логики предикатов первого
порядка,
а
затем
доказательство
общезначимости
или
противоречивости полученной формулы.
Для представления объектов предметной области в логике
предикатов первого порядка используют атомы, константы,
переменные, термы, предикаты, кванторы. Определим эти понятия.
Атом - это повествовательное предложение, которое может быть
истинно или ложно, но не то и другое одновременно. Атом
рассматривают, как единое целое, его структуру и состав не
анализируют. Например, "Иванов работает на заводе", "Петров
является студентом" и т.п.
Константа - это символ, обозначающее индивидуальный объект
или понятие.
Переменная - это символ, используемый в разное время для
обозначения различных объектов. Для определения области действия
переменных используются кванторы.
Термы рекурсивно определяются следующим образом:
1. константа есть терм;
2. переменная есть терм;
3. если f есть n-местный функциональный символ и t1, t2, ..., tn термы, то составной терм f(t1, t2,…, tn) - тоже терм;
4. никаких других термов, кроме составленных с помощью правил
(1) - (3), нет.
Поясним третье правило. Составной терм f(t1, t2,..., tn) состоит из
функционального символа и упорядоченного множества термов,
являющихся его аргументами. Составной терм обозначает некоторый
объект, зависящих от других объектов, представленных его
аргументами.
Например, высказывание "сын Ивана" можно представить с
помощью составного терма "сын(Иван,x)". Здесь слово "сын"
является функциональным символом, слово "Иван" - константой, а
переменная x используется для обозначения сыновей Ивана. Причем
константа может рассматриваться как функциональный символ без
аргументов.
Предикатный символ P (предикатная буква) используется для
представления отношений между объектами в некоторой области.
Если предикатный символ P имеет n аргументов, то P называется nместным предикатным символом.
Предикатные символы применяются для построения высказываний
в отличие от функциональных символов, используемых для
построения термов.
Предикат состоит из предикатного символа и соответствующего ему
упорядоченного множества термов, являющихся его аргументами.
Если P есть n-местный предикатный символ и t1, t2, ..., tn - термы, то
P(t1, t2,…, tn) - предикат. Таким образом, предикатные символы
синтаксически
используются
для
образования
формул,
а
семантически обозначают предикаты.
Предикат - это логическая функция, принимающая только два
значения - "истина" или "ложь". Предикаты являются атомарными
формулами или атомами логики предикатов первого порядка.
Определение.
Если P есть n-местный предикатный символ языка (
) и t1,
t2,…, tn - термы, то P(t1, t2, ..., tn) есть атомарная (элементарная)
формула языка.
Запись P(t1, t2, ..., tn) означает, что истинно высказывание,
гласящее, что объекты t1, t2, ..., tn связаны отношением P.
Поясним сказанное на конкретных примерах. Высказывание "x
является отцом y" можно представить с помощью предиката
отец(x,y), где отец - предикатный символ; x, y - переменные.
Высказывание "Сын Ивана является братом Степана" можно
представить с помощью предиката брат(сын(Иван,x),Степан), где
брат - предикатный символ, сын - функциональный символ, Иван и
Степан - константы, выражение сын(Иван,x) - терм.
Для построения атомов используются предикатные и
функциональные символы, переменные и константы. Из атомарных
формул с помощью логических связок
(конъюнкции),
(дизъюнкции), ~ (отрицания),
(импликации),
(эквиваленции) и
кванторов можно строить сложные формулы языка.
На следующем шаге мы начнем рассматривать кванторы.
Шаг 3.
Основы логического программирования.
Кванторы
На этом шаге мы рассмотрим кванторы.
Квантор - логическая операция, дающая количественную
характеристику предметной области, к которой относится выражение,
получаемое в результате использования квантора.
Квантор - это общее название для логических операций, которые по
предикату P(x) строят высказывание, характеризующее область
истинности предиката P(x). В логике предикатов первого порядка
применяются квантор всеобщности и квантор существования .
Кванторы всеобщности
и существования
используются для
определения области действия переменных. Так, если x - переменная,
то запись
читается "для любого x ", "для каждого x " и т.п., а
запись
- "существует x ", "хотя бы для одного x " и т.п.
Кванторы позволяют строить высказывания о множествах объектов
и формулировать утверждения, истинные для этих множеств.
Например, высказывание
означает, что область истинности
предиката P(x) совпадает с областью значений переменной x , а
xP(x) - что область истинности предиката P(x) не пуста.
Часть формулы, на которую распространяется действие квантора,
называется областью действия этого квантора.
Вхождение переменной в формулу непосредственно после знака
квантора или в область действия квантора, после которого стоит эта
переменная, называется связанным. Все остальные вхождения
переменных называются свободными.
Например, вхождение переменной x в формулу F будет связанным,
если x входит в часть формулы F вида
или
; остальные
вхождения x в формулу F будут свободными.
Формула, содержащая свободные вхождения переменных, зависит
от них (является их функцией); связанные же вхождения переменных
можно
переименовывать,
например,
заменив
попадающие под квантор существования, константами.
переменные,
Переменная x называется параметром F, если найдено хотя бы
одно свободное вхождение x в F. Переменная является связанной,
если она попадает в поле действия квантора. В противном случае
переменная является свободной.
В одной и той же формуле переменная может быть и свободной (до
попадания в поле действия квантора), и связанной (после попадания в
поле действия квантора).
Примеры.
1. В формуле
переменная x является связанной, а
переменные y и z - свободными.
2. В формуле
переменная x будет связанной, а
переменная y - свободной.
3. В формуле
переменная x является
связанной, переменная y - и свободной (до попадания в поле
действия квантора), и связанной (после попадания в поле
действия квантора), а переменная z - свободной.
На следующем шаге мы начнем рассматривать формулы логики
предикатов первого порядка .
Шаг 4.
Основы логического программирования.
Формулы логики предикатов первого порядка
На этом шаге мы рассмотрим формулы логики предикатов
первого порядка.
Формулы логики предикатов первого порядка рекурсивно
определяются следующим образом:
1. атом есть формула;
2. если A и B - формулы, то
- тоже
формулы;
3. если A(x) - есть формула, а x - свободная переменная в A(x) , то
и
- тоже формулы;
4. других формул, кроме составленных с помощью правил (1) - (3),
нет.
Язык изучают с помощью языка исследователя - метаязыка. В нем
могут использоваться различные знаки, не входящие в алфавит
изучаемого языка - метасимволы. Мы будем использовать
различные метасимволы, в том числе A, B, C, ..., E, A1, A2, A3, ...
(большие буквы латинского алфавита с индексами или без них) - для
обозначения произвольных формул, x,y,z - для обозначения
произвольных предметных переменных, t, t0, t1,. .. - произвольных
термов и так далее.
Для упрощения записи формул в метаязыке используют те или
иные соглашения. Будем считать, что можно опустить все те скобки,
которые будут восстановлены при выполнении следующей
процедуры: запись просматриваем слева направо и вокруг каждого
очередного вхождения знака
расставляем пару скобок так, чтобы
подслово, в котором эти скобки служат началом и концом
соответственно, являлось формулой, причем возможно более
длинной; затем ту же операцию повторяем для связок
(именно в
этом порядке!).
Под интерпретацией формул в логике высказываний
понимают операцию приписывания входящим в формулу атомам
значений И ("истина") и Л ("ложь").
Интерпретация формулы в логике предикатов первого
порядка - это задание области значений переменных и присвоение
значений всех констант, функциональных и предикатных символов,
встречающихся в этой формуле, в соответствии со следующими
правилами:
1. каждой константе ставится в соответствие некоторый элемент из
области значений переменных D;
2. каждому n-местному функциональному символу ставится в
соответствие отображение элемента из Dn в D, где Dn ={(x1, x2,
..., xn) | x1
n
3. каждому n-местному предикатному символу ставится в
соответствие отображение элемента из Dn в {И,Л}.
Когда мы определяем истинностное значение формулы на области
D, то
и
интерпретируются как "для всех элементов x из Dn" и
"существует элемент x из Dn" соответственно.
Формула, интерпретируемая на области D, принимает значение И
или Л согласно следующим правилам:
1. если заданы значения формул A и B, то истинностные значения
формул
получают по таблице
истинности, которая справедлива так же и для логики предикатов
первого порядка;
2. формула
получает значение "истина", если A(x) "истина" для каждого x из D; в противном случае
получает значение Л;
3. формула
получает значение И, если A(x) - И хотя бы
для одного x из D; в противном случае
получает
значение Л.
Приписывая формуле истинностные значения, предполагают, что
она либо не содержит свободных переменных, либо свободные
переменные рассматриваются как константы.
Семантическая классификация формул. В логике предикатов
первого порядка, как и в логике высказываний, используются понятия
общезначимости, противоречивости, выполнимости формул, а
так же логического следствия, поэтому ее можно рассматривать как
расширение логики высказываний, а формулу в логике предикатов
первого порядка, не содержащую переменных и кванторов - как
формулу в логике высказываний.
В логике предикатов первого порядка формула является:



общезначимой тогда и только тогда, когда не существует
никакой интерпретации, при которой формула принимает
значение "ложь";
противоречивой тогда и только тогда, когда не существует
никакой интерпретации, при которой формула принимает
значение "истина";
выполнимой тогда и только тогда, когда существует хотя бы
одна такая интерпретация, что формула принимает значение
"истина".
Формула A есть логическое следствие формул B1, B2, ..., Bn тогда и
только тогда, когда для любой интерпретации, если B1 2
n
истинна при этой интерпретации, то A так же истинна.
На следующем шаге мы рассмотрим предваренную нормальную
форму формулы.
Шаг 5.
Основы логического программирования.
Предваренная нормальная форма формулы
На этом шаге мы рассмотрим предваренную нормальную форму.
Формула находится в предваренной нормальной форме тогда и
только тогда, когда она состоит из цепочки кванторов, называемой
префиксом, и следующей за ней бескванторной формулы,
называемой матрицей:
(Q1x1)(
Q2x2)
…
где Qi - квантор всеобщности или квантор существования
(Qixi) обозначает
i) или
i), i=1, 2, ..., n;
(Qnxn)M,
, то есть
(Q1x1)( Q2x2) ... (Qnxn) - префикс формулы; M - матрица.
Например, следующие формулы находятся в предваренной
нормальной форме:
;
.
Для записи формул в предваренной нормальной форме можно
использовать приведенные ниже эквивалентные формулы, в которых
кванторы всеобщности и существования обозначены буквой Q:
(Qx)A(x) B=(Qx)(A(x) B);
(Qx)A(x) B=(Qx)(A(x) B);
~(( x)A(x))=( x)(~A(x));
~(( x)A(x))=( x)(~A(x));
( x)A(x) ( x)B(x)= ( x)(A(x) B(x));
( x)A(x)
( x)B(x)= ( x)(A(x) B(x));
(Q1x)A(x) (Q2x)B(x)= (Q1x)(Q2y)(A(x) B(y));
(Q3x)A(x) (Q4x)B(x) =(Q3x)(Q4y)(A(x) B(y)).
Здесь A(x) - форма, содержащая наряду с другими переменными и
свободную переменную x; B - формула, содержащая любые
переменные, кроме x.
На следующем шаге мы рассмотрим проверку общезначимости
формул.
Шаг 6.
Основы логического программирования.
Проверка общезначимости формулы. Метод резолюций
На этом шаге мы рассмотрим доказательство общезначимости
и противоречивости формул в логике предикатов первого
порядка.
В отличие от логики высказываний в логике предикатов первого
порядка имеется очень большое число интерпретаций формулы,
поэтому общезначимость или противоречивость этой формулы
невозможно доказать проверкой ее истинности при всех возможных
интерпретациях.
В логике предикатов первого порядка не существует никаких
алгоритмов, проверяющих общезначимость формулы, но существуют
алгоритмы, которые могут ее подтвердить.
По определению общезначимой называется формула, которая
истинна при всех интерпретациях. Существуют алгоритмы нахождения
такой интерпретации, при которой формула ложна. Однако, если
данная формула
общезначима, то
не
существует
такой
интерпретации, при которой формула была бы ложна.
Поскольку формула общезначима тогда и только тогда, когда ее
отрицание противоречиво, то можно установить противоречивость
отрицания данной формулы. Это служит основой для большинства
современных автоматических алгоритмов поиска доказательства.
Наиболее эффективно поиск доказательств общезначимости
формул осуществляется методом резолюций. Процедура поиска
доказательства методом резолюций фактически является процедурой
поиска опровержения, то есть вместо доказательства общезначимости
формулы доказывается, что отрицание формулы противоречиво.
Суть метода резолюций состоит в следующем. Пусть S - множество
дизъюнктов, которые представляют стандартную форму формулы F.
Тогда F противоречива в том и только в том случае, когда
противоречиво S. Если S содержит пустой дизъюнкт, то S
невыполнимо. Если S не содержит пустой дизъюнкт, то проверяется,
может ли пустой дизъюнкт быть получен из S.
Процедура опровержения применима, если формула находится в
стандартной форме, введенной Дэвисом и Патнемом. Для
преобразования формулы к стандартной форме ими были
использованы следующие положения:
1. в логике предикатов первого порядка формула может быть
приведена к предваренной нормальной форме , когда матрица
не
содержит
никаких
кванторов,
а
префикс
есть
последовательность кванторов;
2. поскольку матрица не содержит кванторов, она не может быть
представлена в конъюнктивной нормальной форме;
3. сохраняя противоречивость формулы, в ней можно уничтожить
кванторы существования, заменив переменные, попадающие в
область действия кванторов существования, константами.
Дизъюнктом называется дизъюнкция литер. Множество литер
можно рассматривать как синоним дизъюнкта.
Дизъюнкт, содержащий
дизъюнктом.
n
литер,
называется
n-литерным
Однолитерный дизъюнкт называется единичным дизъюнктом.
Дизъюнкт называется пустым, если он не содержит никаких литер.
Пустой дизъюнкт всегда ложен, так как в нем нет литер, которые
могли бы быть истинными при любых интерпретациях.
Метод резолюций для логики высказываний
Метод резолюций можно применять к любому множеству
дизъюнктов
с
целью
проверки
их
невыполнимости
(противоречивости). Рассмотрим сначала метод резолюций для
логики высказываний.
Литеры A и ~A называются контрарными, а множество {A, ~A} –
контрарной парой.
Допустим, что в дизъюнкте C1 существует литера L1 , контрарная
литере L2 в дизъюнкте C2. Вычеркнем литеры L1 и L2 из дизъюнктов C1
и C2 соответственно и построим дизъюнкцию оставшихся дизъюнктов.
Построенный дизъюнкт называется резольвентой дизъюнктов C1 и
C2. Резольвента двух дизъюнктов является их логическим следствием.
Резольвента двух единичных дизъюнктов (если она существует) –
пустой дизъюнкт.
Резолютивный вывод C из множества дизъюнктов S есть такая
конечная последовательность C1, C2, ..., Ck дизъюнктов, в которой
каждый Ci (i=1, ..., k) или принадлежит S, или является резольвентой
дизъюнктов, предшествующих Ci и Ck=C.
Дизъюнкт C может быть выведен или получен из S, если существует
вывод C из S. Для невыполнимого множества дизъюнктов в
результате последовательного применения правила резолюций
получается пустой дизъюнкт.
Вывод из множества S пустого дизъюнкта
опровержением (доказательством невыполнимости) S.
называется
Метод резолюций для логики предикатов первого порядка
Нахождение в двух дизъюнктах контрарных литер выполняется
довольно просто, если дизъюнкты не содержат переменных.
Дизъюнкты, содержащие переменные, необходимо унифицировать, то
есть найти такую подстановку, в результате которой исходные
дизъюнкты будут содержать контрарные литеры.
Рассмотрим несколько примеров.
1. Пусть формулы A(x) и A(b) не тождественны. В формуле A(x)
встречается переменная x, а в A(b) – константа b. Чтобы
отождествить A(x) и A(b), заменим переменную x на константу b,
то есть присвоим переменной x значение b. В результате
получаем A(x)=A(b).
2. Рассмотрим
дизъюнкты:
D1:
A(x)
B(x),
D2:
~A(f(y))
C(y),
которые не содержат контрарных литер. Если в D1 подставить
f(a) вместо x, а в D2 – значение a вместо y, то дизъюнкты
преобразуются к следующему виду:
D1:
D2: ~A(f(a)) C(a).
A(f(a))
B(f(a)),
Так как A(f(a)) и ~A(f(a)) контрарны, то получим резольвенту
дизъюнктов D1 и D2 в виде:
R: B(f(a)) C(a).
Дизъюнкты имеют одну или несколько резольвент, так как возможны
разные подстановки. Любую подстановку можно представить с
помощью множества упорядоченных пар ti/xi:
S={t1/x1, t2/x2 , ..., tn/xn},
где ti – терм, xi – переменная, i=1, ..., n.
Пара ti/xi означает, что переменная xi повсюду заменяется на терм
ti. Переменная не может быть заменена на терм, содержащий ту же
самую переменную, например переменную xi нельзя заменить на терм
ti=f(xi).
Если подстановка S применяется к каждому члену некоторого
множества {Ei}, то множество подстановочных выражений
обозначается {Ei}S. Множество выражений {Ei} унифицируемо, если
существует подстановка S такая, что E1S= E2S= E3S=... . В таком
случае S называется унификатором для множества выражений {Ei}.
Например, подстановка S={A/x,B/y} унифицирует выражения
P(x,f(y),B) и P(x,f(B),B). После замены переменной x на терм A и
переменной у на терм В выражение P(x,f(y),B) преобразуется в
P(A,f(B),B), a P(x,f(B),B) в P(A,f(B),B). Полученные выражения
унифицируемы.
Обычно унификацию используют для того, чтобы показать, можно
ли литеры привести в соответствие между собой. Для этого в литерах
вместо переменных надо подставить термы.
Процесс сопоставления некоторого выражения с эталонным
называется сопоставлением с образцом. Он играет важную роль в
системах искусственного интеллекта.
Одна и та же задача с помощью формул логики предикатов первого
порядка может быть записана в различной форме. Представляется
целесообразным,
особенно
для
выполнения
формальных
преобразований, использование стандартной формы
записи
выражений.
На следующем шаге мы рассмотрим семь этапов приведения
формул к стандартной форме записи.
Шаг 7.
Основы логического программирования.
Этапы приведения формулы к стандартной форме записи
На этом шаге мы рассмотрим этапы приведения формулы к
стандартной форме записи.
В логике предикатов первого порядка процесс приведения формул к
стандартной форме записи включает в себя семь этапов.
1. Исключение символов импликации, то есть следствия "если
..., то ...", с помощью формулы эквивалентности
.
Например,
.
2. Перенос символа отрицания внутрь формулы (только для
формул, не являющихся атомарными). При этом область
действия символа отрицания сужается. Каждый символ
отрицания "~" может быть применен только к одной атомарной
формуле. Например,
.
3. Разделение переменных. Переменная, связанная некоторым
квантором, в пределах области действия этого квантора может
быть заменена любой другой переменной, не встречающейся
ранее в этой формуле. Значение истинности формулы при этом
не меняется. В итоге каждому квантору будет соответствовать
свойственная только ему переменная.
Например, формула
в результате
разделения переменных, то есть замены переменной x после
попадания ее под действие квантора существо
переменную y, преобразуется к виду
.
4. Исключение кванторов существования из формулы.
Переменная, связанная квантором существования, может быть
заменена функцией, называемой сколемовской, а в частном
случае константой (то есть сколемовской функцией без
аргументов).
Например, формула
может быть заменена формулой
A(b) при x=b, где b - константа.
Если формула содержит квантор общности, то надо вводить
не константы, а составные термы (функциональные символы с
множеством
переменных
аргументов),
чтобы
показать
зависимость объекта от переменных.
Например,
если
в
формуле:
,
обозначающей, что каждый человек имеет мать, удалить квантор
существования
и поставить y=Мария, то получим, что все
люди имеют одну и ту же мать по имени Мария, а это неверно.
константу, а составной терм, то есть функциональный символ
f(x), который каждому конкретному человеку ставит в
соответствие
его
мать:
.
5. Преобразование в предваренную нормальную форму. Все
кванторы общности переносят в начало формулы и считают, что
область действия каждого из них включает всю формулу,
следовательно, все переменные связаны кванторами общности.
Формула в предваренной нормальной форме состоит из
цепочки кванторов и следующей за ней бескванторной формулы.
Так как область действия каждого квантора включает всю
формулу, то кванторы сами по себе не несут больше какой-либо
дополнительной информации и, следовательно, их можно
опустить.
Так, после удаления кванторов всеобщности рассмотренная
выше
формула
будет
иметь
следующий
вид:
.
6. Приведение формулы к конъюнктивной нормальной
форме. Формулу записывают в виде последовательности
соединенных между собой символами конъюнкции элементов,
каждый из которых является литерой или состоит из нескольких
литер, соединенных между собой символами дизъюнкции.
Например,
формулы
и
(A(x)
приведены к конъюнктивной
нормальной форме.
7. Исключение символов конъюнкции. Так как формула
является конъюнкцией составляющих ее частей, то знак
конъюнкции указывать необязательно. Для сокращения записи
его опускают, но при этом подразумевают.
Следовательно, каждая формула исчисления предикатов
эквивалентна совокупности дизъюнктов, состоящих из литералов
и соединенных символами дизъюнкции. Слово "совокупность"
подчеркивает, что порядок дизъюнктов не имеет значения.
Например,
формулу:
можно представить в виде совокупности дизъюнктов (знак
дизъюнкции
опущен):
{A(x), B(x), C(x)}.
Таким образом, любая исходная формула может быть записана в
стандартной форме, то есть в виде совокупности дизъюнктов.
Стандартная форма записи формул достаточно лаконична и
выразительна, так как в ней опущены символы логических связок
дизъюнкции и конъюнкции, а также кванторы всеобщности и
существования.
Преобразование формулы к стандартному виду может быть
выполнено вне зависимости от того, существует или нет
интерпретация, при которой формула истинна.
Метод резолюций является полным. Если некоторый факт следует
из исходных гипотез, то всегда можно доказать его истинность
методом резолюций.
При рассмотрении логики высказываний было показано, что
формула A является логическим следствием формул B1, B2, ..., Bn
тогда и только тогда, когда формула B1 2
n
противоречива, то есть доказательство того, что отдельная формула
есть
логическое
следствие
конечного
множества
формул,
эквивалентно доказательству того, что некоторая связанная с ними
формула общезначима или противоречива.
Так же и в логике предикатов, если множество формул {B1, B2,…,
Bn} непротиворечиво, то формула А является следствием формул {B1,
B2, ..., Bn} тогда и только тогда, когда множество формул {B1, B2,…, Bn,
~А} противоречиво.
Если множество исходных гипотез непротиворечиво, то надо
добавить к нему дизъюнкты, отрицающие высказывание, которое
следует доказать. Если доказываемое высказывание следует из
заданных гипотез, то получим пустой дизъюнкт. Добавляемые к
множеству гипотез дизъюнкты называются целевыми.
При доказательстве по методу резолюций возможно образование
большого количества лишних дизъюнктов. Многие из таких
дизъюнктов представляют собой общезначимые формулы, которые
истинны при всех интерпретациях и не влияют на процедуру
доказательств, а следовательно, их можно вычеркнуть.
Используя метод резолюций, можно автоматически доказывать
теоремы, выводя их из аксиом. Необходимо лишь задать исходные
высказывания, а следствие из них будет получено автоматически с
помощью правил вывода.
На следующем шаге мы рассмотрим хорновские дизъюнкты.
Шаг 8.
Основы логического программирования.
Хорновские дизънкты
На этом шаге мы рассмотрим хорновские дизънкты.
Дизъюнкт - это совокупность литер, одни из которых не содержат,
а другие содержат знак отрицания. Если первые записывать через
точку с запятой слева от знака ":-", а вторые - без знака отрицания
через запятую справа от знака ":-" и в конце каждого дизъюнкта
ставить
точку,
то
получим
выражение
вида:
A1;A2;…;An:- B1,B2,…,Bm
Из каждых двух дизъюнктов, представленных в таком виде,
используя метод резолюций, можно получить новый дизъюнкт,
являющийся следствием двух первых. Если одна и та же атомарная
формула появляется в левой части одного дизъюнкта (литера без
отрицания) и в правой части другого (литера с отрицанием), то новый
дизъюнкт, полученный в результате слияния исходных дизъюнктов и
вычеркивания повторяющейся рассматриваемой атомарной формулы,
является следствием исходных дизъюнктов. При форме записи это
означает вычеркивание контрарных пар, находящихся по разные
стороны от знака ":-". Например, даны два дизъюнкта:
A;B;C:-D,E,F.
G;H:-A,M,N.
Полученный в результате их слияния и вычеркивания повторяются
литеры
A
новый
дизъюнкт:
B;C;G;H:-D,E,F,M,N
является следствием исходных. Вычеркивание повторяющейся слева
и справа от знака ":-" литеры А аналогично рассмотренномy ранее
вычеркиванию контрарной пары А и ~А.
Метод резолюций допускает сопоставление нескольких литер в
левой части одного дизъюнкта с несколькими литерами в правой
части другого.
Если дизъюнкты содержат переменные, то для получения
резольвенты атомарные формулы не обязательно должны быть
идентичными,
они
должны
быть
лишь
сопоставимыми
(унифицируемыми).
При
сопоставлении
формул
происходит
конкретизация значений переменных (конкретизация переменных), в
результате которой формулы становятся идентичными. Например,
при сопоставлении формул A(x) и A(b) происходит конкретизация
переменной x, которая принимает значение, равное b.
Хорновский дизъюнкт - это дизъюнкт, содержащий не более
одной
литеры
без
отрицания.
Например:
A:-B,C,D;
A:-B1,B2,Bn.
Приведенным хорновским дизъюнктам эквивалентны следующие
формулы
логики
предикатов
первого
порядка:
B1
2
n
(переменные в этих формулах не указаны).
Существует два вида хорновских дизъюнктов.
Хорновский дизъюнкт с заголовком - это дизъюнкт, содержащий
одну литеру без отрицания. Он может содержать одну или несколько
литер с отрицанием или не содержать их вообще, например:
A:-B,C,
...,D;
A:-B;
A:-.
Последний из приведенных дизъюнктов можно записывать без
знака ":-", то есть в виде А.
Хорновский дизъюнкт без заголовка - это дизъюнкт, не
содержащий
литер
без
отрицания,
например:
:-B,C,
...,D;
:- A.
В результате применения метода резолюций к двум хорновским
дизъюнктам с заголовками вновь получается хорновский дизъюнкт с
заголовком. Известно, что пустой дизъюнкт не имеет заголовка.
Следовательно, если все дизъюнкты имеют заголовки, то из них могут
быть получены только дизъюнкты с заголовками.
Таким образом, для того чтобы из исходных дизъюнктов, отрицая
доказываемое высказывание, можно было бы вывести пустой
дизъюнкт, необходимо наличие, по крайней мере, одного дизъюнкта
без заголовка.
Если среди исходных имеется несколько дизъюнктов без заголовка,
то доказательство каждого нового дизъюнкта методом резолюций
может быть преобразовано в доказательство, в котором используется
не более чем один дизъюнкт без заголовка. Если пустой дизъюнкт
следует из заданного множества исходных, то он следует и из его
подмножества, содержащего дизъюнкты с заголовками и не более
одного дизъюнкта без заголовка.
Любая задача, которая может быть выражена с помощью
хорновских дизъюнктов, должна иметь только один дизъюнкт без
заголовка (целевой); все остальные дизъюнкты должны быть с
заголовками (гипотезы).
Хорновские дизъюнкты были выбраны в качестве основы для
создания программных систем автоматического доказательства
теорем.
Метод резолюций для хорновских дизъюнктов более прост. Поэтому
в Прологе используется ограниченный вариант метода резолюций,
рассчитанный на работу с одной литерой в левой части.
Со следующего шага мы начнем рассматривать основы языка
Пролог.
Шаг 9.
Основы логического программирования.
Основы языка Пролог
На этом шаге мы
программирование.
рассмотрим
введение
в
логическое
В Прологе вы получаете решение задачи логическим выводом из
ранее известных положении. Обычно программа на Прологе не
является последовательностью действий, - она представляет собой
набор фактов с правилами, обеспечивающими получение заключений
на основе этих фактов. Поэтому Пролог известен как
декларативный язык.
Пролог использует упрощенную версию синтаксиса логики
предикатов, он прост для понимания и очень близок к естественному
языку.
Пролог включает механизм вывода, который основан на
сопоставлении образцов. С помощью подбора ответов на запросы он
извлекает хранящуюся (известную) информацию. Пролог пытается
проверить истинность гипотезы (другими словами - ответить на
вопрос), запрашивая для этого информацию, о которой уже
известно,что она истинна. Прологовское знание о мире - это
ограниченный набор фактов (и правил), заданных в программе.
Одной из важнейших особенностей Пролога является то, что, в
дополнение к логическому поиску ответов на поставленные вами
вопросы, он может иметь дело с альтернативами и находить все
возможные решения. Вместо обычной работы от начала программы
до ее конца, Пролог может возвращаться назад и просматривать
более одного "пути" при решении всех составляющих задачу частей.
Логика предикатов была разработана дли наиболее простого
преобразования принципов логического мышления в записываемую
форму. использует преимущества синтаксиса логики для разработки
программного языка. В логике предикатов вы, прежде всего,
исключаете из своих предложений все несущественные слова. Затем
вы преобразуете эти предложения, ставя в них на первое место
отношение, а после него - сгруппированные объекты. В дальнейшем
объекты становятся аргументами, между которыми устанавливается
это отношение. В качестве примера в таблице 1 представлены
предложения, преобразованные в соответствии с синтаксисом логики
предикатов.
Таблица 1. Синтаксис логики предикатов
Предложения на естественном языке
Синтаксис логики
предикатов
Машина красивая
fun (car)
Роза красная
red (rose)
Билл любит машину, если машина
красивая
likes(bill, car) if fun(car)
На следующем шаге мы рассмотрим факты и правила.
Шаг 10.
Основы логического программирования.
Факты и правила
На этом шаге мы рассмотрим факты и правила.
Программист на Прологе описывает объекты (objects) и
отношения (relations), а затем описывает правила (rules), при
которых эти отношения являются истинными. Например, предложение
Билл
любит
собак.
(Bill
likes
dogs.)
устанавливает отношение между объектами Bill и dogs (Билл и
собаки); этим отношением является likes (любит). Ниже
представлено правило, определяющее, когда предложение "Билл
любит собак" является истинным:
Билл любит собак, если собаки хорошие. (Bill likes dogs if the
dogs are nice.)
Факты
В Прологе отношение между объектами называется фактом
(fact). В естественном языке отношение устанавливается в
предложении. В логике предикатов, используемой Прологом,
отношение соответствует простой фразе (факту), состоящей из имени
отношения и объекта или объектов, заключенных в круглые скобки.
Как и предложение, факт завершается точкой (.).
Ниже представлено несколько предложений на естественном языке
с отношением "любит" (likes):
Билл
любит
Синди.
Синди
любит
Билла.
Билл любит собак. (Bill likes dogs)
(Bill
(Cindy
likes
likes
Cindy)
Bill)
А теперь перепишем ли же факты, используя синтаксис Пролога:
likes (bill,cindy).
likes(cindy,bill).
likes(bill,dogs).
Факты, помимо отношений, могут выражать и свойства. Так,
например, предложения естественного языка "Kermit is green"
(Кермит зеленый) и "Caitlin is girl" (Кейтлин - девочка) на
Прологе, выражая те же свойства, выглядят следующим образом:
green(kermit).
girl(caitlin).
Правила
Правила позволяют вам вывести один факт из других фактов.
Другими словами, можно сказать, что правило - это заключение, для
которого известно, что оно истинно, если одно или несколько других
найденных заключений или фактов являются истинными. Ниже
представлены правила, соответствующие связи "любить" (likes):
Синди любит все, что любит Билл. (Cindy likes everything that
Bill
likes)
Кейтлин любит все зеленое. (Caitlin likes everything that is
green)
Используя эти правила, вы можете из предыдущих фактов найти
некоторые вещи, которые любят Синди и Кейтлин:
Синди
любит
Синди.
(Cindy
Кейтлин любит Кермит. (Caitlin likes Kermit)
likes
Cindy)
Чтобы перевести эти правила на Пролог, вам нужно немного
изменить синтаксис, подобно этому:
likes(cindy,Something):likes(bill,Something).
likes(caitlin,Something):green(Something).
Символ :- имеет смысл "если", и служит для разделения двух
частей правила: заголовка и тела.
Вы можете рассматривать правило и как процедуру. Другими
словами, эти правила
likes(cindy,Something):likes(bill,Something)
likes(caitlin,Something):green(Something).
также означают: "Чтобы доказать, что Синди любит что-то, докажите,
что Билл любит это" и "Чтобы доказать, что Кейтлин любит что-то,
докажите, что это что-то зеленое". С такой "процедурной" точки зрения
правила могут "попросить" Пролог выполнить другие действия,
отличные от доказательств фактов, - такие как напечатать что-нибудь
или создать файл.
Запросы
Однократно дав языку Пролог несколько фактов, мы можем
задавать вопросы, касающиеся отношений между ними. Это
называется запросом (query) системы языка Пролог. Мы можем
задавать Прологу такие же вопросы, которые мы могли бы дать вам
об этих отношениях. Основываясь на известных, заданных ранее
правилах, вы можете ответить на вопросы об этих отношениях, в
точности это может сделать Пролог.
На естественном языке мы спрашиваем:
Does Bill like Cindy? (Билл любит Синди?)
По правилам Пролога мы спрашиваем:
likes(bill,cindy).
Получив такой запрос, Пролог мог бы ответить:
yes (да)
потому что Пролог имеет факт, подтверждающий, что это так.
Немного усложнив вопрос, мы могли бы спросить вас на естественном
языке:
What does Bill like? (Что любит Билл?)
По правилам Пролога мы спрашиваем:
likes(bill,What).
Заметим, что синтаксис Пролога не изменяется, когда вы задаете
вопрос: этот запрос очень похож на факт. Впрочем, важно отметить,
что второй объект начинается с большой буквы, тогда как первый
объект - bill - нет. Это происходит потому, что bill - фиксированный,
постоянный объект - известная величина, a What - переменная.
Переменные всегда начинаются с заглавной буквы или символа
подчеркивания.
Пролог всегда ищет ответ на запрос, начиная с первого факта, и
перебирает факты, пока они не закончатся. Получив запрос о том, что
Билл любит, Пролог ответит:
What=cindy
What=dogs
Так как ему известно, что
likes(bill,
cindy).
и
likes(bill, dogs).
Надеемся, что вы пришли к такому же выводу.
Если бы мы спросили вас (и Пролог):
What does Cindy like? (Что любит Синди?)
likes(cindy, What).
то Пролог ответил бы:
What = bill
What = cindy
What = dogs
поскольку Пролог знает, что Синди любит Билла, и что Синди любит
то же, что и Билл, и что Билл любит Синди и собак.
Мы могли бы задать Прологу и другие вопросы, которые можно
задать человеку. Но вопросы типа "Какую девушку любит Билл?" не
получат решения, т.к. Прологу в данном случае не известны факты о
девушке, а он не может вывести заключение, основанное на
неизвестных данных: в этом примере мы не дали Прологу какогонибудь отношения или свойства, чтобы определить, являются ли
какие-либо объекты девушками.
На следующем шаге мы рассмотрим размещение фактов, правил
и запросов.
Шаг 11.
Основы логического программирования.
Размещение фактов, правил и запросов
На этом шаге мы рассмотрим размещение фактов, правил и
запросов.
Предположим, у вас есть следующие факты и правила:
Быстрая
Большая
машина
машина
-
приятная. (A fast car is fun).
красивая. (A big car is nice).
Маленькая машина - практичная. (A little car is practical).
Биллу нравится машина, если она приятная. (Bill likes a car if
the car is fun).
Исследуя эти факты, вы можете сделать вывод, что Биллу нравится
быстрый автомобиль. В большинстве случаев Пролог придет к
подобному решению. Если бы не было фактов о быстрых
автомобилях, вы не смогли бы логически вывести, какие автомобили
нравятся Биллу. Вы можете делать предположения о том, какой тип
машин может быть крепким, но Пролог знает только то, что вы ему
скажете; Пролог не строит предположений.
Вот пример, демонстрирующий, как Пролог использует правила
для ответа на запросы.
likes(ellen,tennis).
likes(john,football).
likes(tom,baseball).
likes(eric,swimming).
likes(mark,tennis).
likes(bill,Activity):likes(tom,Activity).
Последняя строка в программе является правилом:
likes(bill,Activity):likes(torn,Activity).
Это правило соответствует предложению естественного языка:
Биллу нравится занятие, если Тому нравится это занятие.
(Bill likes an activity if Tom likes that activity)
В данном правиле заголовок - это likes (bill, Activity), а тело - likes
(torn, Activity). Заметим, что в этом примере нет фактов о том, что
Билл любит бейсбол. Чтобы выяснить, любит ли Билл бейсбол, можно
дать Прологу такой запрос:
likes(bill,baseball).
Пытаясь отыскать решение по этому запросу, Пролог будет
использовать правило:
likes (bill, Activity):likes (tom, Activity).
Рассмотрим программу:
predicates
likes(symbol,symbol)
clauses
likes(ellen,tennis).
likes(john,football).
likes(tom,baseball).
likes(eric,swimming).
likes(mark, tennis).
likes(bill,Activity):likes (tom, Activity).
Текст этой программы можно взять здесь.
Запустите Пролог, загрузите программу pro11_1.pro с помощью
пункта меню File | Load или клавиши F3, запустите программу с
помощью пункта Run или сочетания клавиш Alt+R и после запроса
цель наберите: likes(bill,baseball).
Пролог ответит:
yes (да)
Система использовала комбинированное правило
likes(bill,Activity):likes(tom,Activity).
с фактом
likes(tom,baseball).
для решения, что
likes(bill,baseball).
Попробуйте также следующий запрос:
likes(bill,tennis).
Пролог ответит:
no (нет)
Пролог ответит по на последний запрос "Does Bill like tennis?"
(Любит ли Билл теннис), поскольку:


нет фактов, которые говорят, что Билл любит теннис;
отношение Билла к теннису не может быть логически выведено с
использованием данного правила и имеющихся в распоряжении
фактов.
Вполне возможно, что Билл любит теннис в реальной жизни, но
ответ Пролога основан только на фактах и правилах, которые вы
дали ему в тексте программы.
На следующем шаге мы рассмотрим общие представления о
переменных.
Шаг 12.
Основы логического программирования.
Общие представления о переменных
На этом шаге мы рассмотрим переменные.
В Прологе переменные позволяют нам записывать общие факты и
правила и задавать общие вопросы. В естественном языке вы
пользуетесь переменными в предложениях постоянно. Обычное
предложение на английском языке может быть таким:
Bill likes the same thing as Kim. (Билл любит то же, что и Ким)
Как мы говорили, при задании переменной в Прологе первый
символ имени должен быть заглавной буквой или символом
подчеркивания. Например, в следующей строке Thing - это
переменная.
likes(bill,Thing):likes(kim,Thing).
В предшествующем примере:
likes(cindy,Something):likes(bill,Something).
объект Something начинается с заглавной буквы, т.к. это переменная;
он определяет что-то, что Билл любит. С таким же успехом этот
объект мог бы называться x или Zorro.
Объекты bill и cindy начинаются со строчной буквы, т.к. они не
являются переменными - это идентификаторы, имеющие постоянное
значение. Пролог может обрабатывать произвольные текстовые
строки подобно тому, как мы оперировали символами, упомянутыми
выше, если текст заключен в двойные кавычки. Следовательно,
вместо bill вы могли бы также успешно написать "Bill".
На следующем шаге мы рассмотрим предложения.
Шаг 13.
Основы логического программирования.
Предложения
На этом шаге мы рассмотрим предложения.
Ранее мы говорили о фактах и правилах, отношениях, основных
конструкциях и запросах. Все эти термины являются частью логики и
естественного языка. Сейчас мы будем обсуждать те же понятия, но
используя термины Пролога, - предложения, предикаты, переменные
и цели.
По сути, есть только два типа фраз, составляющих язык Пролог:
фраза может быть либо фактом, либо правилом. Эти фразы в
Прологе известны под термином предложения (clause). Сердце
программ на Прологе состоит из предложений.
Подробнее о фактах. Факт представляет либо свойство объекта,
либо отношение между объектами. Факт самодостаточен. Прологу не
требуется дополнительных сведений для подтверждения факта, и
факт может быть использован как основа для логического вывода.
Подробнее о правилах. В Прологе, как и в обычной жизни, можно
судить о достоверности чего-либо, логически выведя это из других
фактов. Правило - это конструкция Пролога, которая описывает, что
можно логически вывести из других данных. Правило - это свойство
или отношение, которое достоверно, когда известно, что ряд других
отношений достоверен. Синтаксически эти отношения разделены
запятыми.
Рассмотрим несколько примеров работы с правилами.
1. Пример, иллюстрирующий правило, которое может быть
использовано для того, чтобы сделать вывод, подходит ли некоторое
блюдо из меню Диане:
Диана - вегетарианка и ест только то, что говорит ей ее
доктор. (Diane is a vegetarian and eats only what her doctor tells her
to eat)
Зная меню и предыдущее правило, вы можете сделать вывод о том,
выберет ли Диана данное блюдо. Чтобы выполнить это, вы должны
проверить, соответствует ли блюдо заданному ограничению:


является ли Food_on_menu овощем?
находится ли Food_on_menu в списке доктора?

заключение: если оба ответа положительны, Диана может
заказать Food_on_menu.
В Прологе подобное отношение должно быть выражено правилом,
т. к. вывод основан на фактах. Вот один вариант записи правила:
diane_can_eat(Food_on_menu):vegetable(Food_on_menu),
on_doctor_list(Food_on_menu).
Обратите внимание, что после vegetable(Food_on_menu) стоит
запятая. Она обозначает конъюнкцию нескольких целей и читается как
"и"; оба правила - vegetable (Food_on_menu) и on_doctor_list
(Food_on_menu) - должны быть истинны для истинности
diane_can_eat (Food_on_menu) .
2. Предположим, что вам хотелось бы записать факт, который
истинен, если Person1 является родителем Person2. Нужный факт
выглядит так:
parent(paul,samantha).
Этот факт обозначает, что Пол - родитель Саманты. Но,
предположим, что в базе данных фактов Пролога есть факты,
формулирующие отношение отцовства. Например, "Пол - отец
Саманты":
father(paul,samantha).
Пусть у вас также есть факты, формулирующие отношение
материнства, например, "Джулия - мать Саманты":
mother(julie,samantha).
Если у вас уже есть несколько фактов, формулирующих отношения
отцовства/материнства, то не стоит тратить время на запись факта
родства в базу данных фактов для каждого отношения родства.
Так как вы знаете, что Person1 - родитель Person2, если Personl отец Person2 или Person1 - мать Person2, то почему бы не записать
правило, объединяющее эти ограничения? После формулирования
этих условий на естественном языке достаточно просто записать их в
правило Пролога.
parent(Person1,Person2):father(Person1,Person2).
parent(Person1,Person2):-
mother(Person1,Person2).
Эти правила Пролога говорят, что
Person1 является родителем Person2, если Person1 является
отцом
Person2.
и
Person1 является родителем Person2, если Person1 является
матерью Person2.
3. Вот другой пример:
Человек может купить машину, если машина ому нравится (likes), и
если машина продается (for sale).
Это отношение может быть переведено на язык Пролог
следующим правилом:
can_buy(Name,Model):person(Name),
car(Model),
likes(Name,Model),
for_sale(Model).
Это правило выражает следующие отношения:
Name
может
купить
(can_buy)
если
Name
является
человеком
и
Model
является
машиной
и
Name
нравится
(likes)
и Model продается (for_sale).
Model,
(person),
(саr),
Model,
Это правило будет истинным, если истинны все 4 условия в теле
правила.
На следующем шаге мы рассмотрим предикаты.
Шаг 14.
Основы логического программирования.
Предикаты
На этом шаге мы рассмотрим предикаты.
Отношение в Прологе называется предикатом. Аргументы - это
объекты, которые взываются этим отношением; в факте likes (bill,
cindy) отношение likes - это предикат, а объекты bill и cindy аргументы.
Вот несколько
аргументов:
примеров
предикатов
с
различным
числом
pred(integer,symbol)
person(last,first,gender)
run()
birthday(firstName,lastName, date)
В вышеприведенном примере показано, что предикаты могут вовсе
не иметь аргументов, но использование таких предикатов ограничено.
Чтобы выяснить имя Rosemont, можно применить запрос person
(rosemont,Name,male). Но что делать с запросом без аргументов run?
Выясним, есть ли в программе предложение run, и если run - это
заголовок правила, то можно вычислить данное правило. В некоторых
случаях это оказывается полезным - например, если бы вы захотели
создать программу, работающую по-разному в зависимости от того,
имеется ли предложение run.
На следующем шаге мы рассмотрим переменные.
Шаг 15.
Основы логического программирования.
Переменные
На этом шаге мы рассмотрим переменные.
В простом запросе, чтобы найти того, кто любит теннис, можно
использовать переменные. Например:
likes(X,tennis).
В этом запросе буква X используется как переменная для
нахождения неизвестного человека. Имена переменных в Прологе
должны начинаться с заглавной буквы (или с символа подчеркивания),
после которой может стоять любое количество букв (заглавных или
строчных), цифр или символов подчеркивания. Ниже приведены
правильные имена переменных:
My_first_correct_variable_name
Sales_10_ll_86
следующие три - неправильные:
lstattempt
second_attempt
"disaster"
Удобно использовать в названии переменной буквы разного
регистра:
IncomeAndExpenditureAccount
Осмысленный выбор имен переменных делает программу более
удобной для чтения. Например:
likes(Person,tennis).
лучше, чем
likes(X,tennis).
потому что Person имеет больше смысла, чем х. Теперь испытайте
цель в программе pro11_1.pro из шага 11:
Цель: likes(Person,tennis).
Пролог ответит:
Person=ellen
Person=mark
2 решения
т.к. цель может получить два решения, а именно, сопоставляя
переменную Person последовательно со значениями ellen и mark.
Инициализация переменных
Вы уже могли заметить, что Пролог не имеет оператора
присваивания. Это важное отличие Пролога от других языков
программирования.
Замечание: Переменные в Прологе инициализируются при
сопоставлении с константами в фактах или правилах.
До инициализации переменная свободна; после присвоения ей
значения она станонится связанной. Переменная остается связанной
только то время, которое необходимо для получения решения по
запросу, затем Пролог освобождает ее и ищет другое решение.
Замечание: Нельзя сохранить информацию, присвоив значение
переменной. Переменные используются как часть процесса поиска
решения, а не как хранилище информации.
В примере pro15_1.pro, приведенном ниже, рассмотрено как и когда
переменные получают свои значения.
predicates
likes(symbol,symbol)
clauses
likes (ellen, reading).
likes(John,computers).
likes(John,badminton).
likes(leonard,badminton).
likes(eric,swimming).
likes(eric,reading).
Текст этой программы можно взять здесь.
Рассмотрим запрос: есть ли человек, который любит и чтение, и
плавание?
likes(Person,reading),
likes(Person,swimming).
Пролог будет решать обе части запроса посредством поиска
фактов с начала и до конца программы. В первой части запроса
likes(Person,reading)
переменная Person свободна; ее значение неизвестно перед тем, как
Пролог попытается найти решение. С другой стороны, второй
аргумент, reading, известен. Пролог ищет факт, который
соответствует первой части запроса. Первый факт в программе
likes(ellen,reading)
удовлетворяет первой части запроса (reading в факте соответствует
reading в запросе), значит Пролог связывает свободную переменную
Person со значением ellen, соответствующим значению в факте. В
тоже время Пролог помещает указатель и список фактов,
показывающий, как далеко продвинулась процедура поиска.
Далее, для полного разрешения запроса (поиск человека, которому
нравится и чтение, и плавание) должна быть решена вторая часть
запроса. Так как Person сейчас связана со значением ellen, Пролог
должен искать факт
likes(ellen,swimming)
Пролог ищет этот факт от начала программы, но совпадений нет
(потому что в программе нет такого факта). Вторая часть запроса
ложна, если Person имеет значение ellen.
Теперь Пролог освободит переменную Person и попытается найти
иное решение первой части запроса. Поиск другого факта,
удовлетворяющего первой части запроса, начинается с указателя в
списке фактов(такое возвращение к отмеченной позиции называется
поиском с возвратом).
Пролог ищет следующего человека, кто любит чтение и находит
факт likes (eric, reading). Переменная Person сейчас связана со
значением eric, и Пролог пытается вновь найти соответствие со
второй частью запроса посредством поиска в программе факта:
likes(eric,swimming)
Пролог
программе),
возвращает
находит совпадение (последнее предложение в
и запрос полностью удовлетворяется. Пролог
Person=eric.
1 решение.
Анонимные переменные
Анонимные переменные позволяют "привести в порядок" наши
программы. Если вам нужна только определенная информация
запроса, можно использовать анонимные переменные для
игнорирования ненужных значений. В Прологе анонимные
переменные обозначаются символом подчеркивания (_).
Следующий "семейный" пример pro15_2.pro
использование анонимных переменных.
демонстрирует
predicates
male(symbol)
female(symbol)
parent(symbol, symbol)
clauses
male(bill).
male (joe).
female(sue).
female(tammy).
parent(bill,joe).
parent (sue, joe).
parent(joe,tammy).
Текст этой программы можно взять здесь.
Замечание: Анонимная переменная может быть использована на
месте любой другой переменной и ей никогда не присваивается
значение.
Например, в следующем запросе нам понадобится узнать, какие
люди являются родителями, но вам неинтересно, кто их дети. Пролог
знает, что каждый раз, когда вы используете символ подчеркивания в
запросе, вам не нужна информация о значении, представленном на
месте переменной.
parent(Parent, _ ).
Получив такой запрос, Пролог отвечает:
Parent=bi11
Parent=sue
Parent=joe
3 решения
В этом случае Пролог находит и выдает трех родителей, но он не
выдает значения, связанные со вторым аргументом в предложении
parent.
Анонимные переменные также можно использовать в фактах.
Следующие факты Пролога:
owns(_,shoes).
eats(_).
могли быть использованы
естественном языке:
для
У
каждого
есть
туфли.
Каждый ест. (Everyone eats)
выражения
(Everyone
утверждений
owns
на
shoes)
Анонимные переменные сопоставляются с любыми данными.
Со следующего шага мы начнем рассматривать цели.
Шаг 16.
Основы логического программирования.
Цели (запросы)
На этом шаге мы рассмотрим цели.
До сих пор мы, говоря о вопросах, задаваемых Прологу,
употребляли слово "запрос". Далее мы будем использовать более
общее слово "цель". Трактовка запросов как целей такова: когда вы
даете Прологу запрос, в действительности вы даете ему цель для
выполнения. ("Найди ответ на вопрос, если он существует: ...")
Цели могут быть или простыми:
likes(ellen,swimming).
likes(bill,What).
или более сложными. Например цель из двух частей:
likes (Person,reading),
likes (Person,swimming).
Цель, состоящая из двух и более частей, называется сложной
целью, а каждая часть сложной цели называется подцелью.
Часто бывает нужно найти общее решение двух целей. Например, в
предыдущем примере о родителях вы могли бы поинтересоваться,
которые из родителей являются родителями мужского пола. Искать
решение для такого запроса можно, задав сложную цель.
Составные цели: конъюнкция и дизъюнкция
Как вы уже видели, составные цели можно использовать для поиска
решения, в котором обе подцели А и В истинны (конъюнкция),
разделяя подцели запятой. Вы также можете искать решения в том
случае, если истинна либо подцель А, либо подцель В (дизъюнкция),
разделяя подцели точкой с запятой. Ниже представлен пример
программы pro16_1.pro, иллюстрирующей эту идею.
predicates
car(symbol,real,integer,symbol,real)
truck(symbol,real,integer,symbol,real)
vehicle(symbol,real,integer,symbol,real)
clauses
car(Chrysler,130000,3,red,12000).
car(ford,90000,4,gray,25000).
car(datsun,8000,1,red,30000).
truck(ford,80000,6,blue,0000).
truck(datsun,50000,5,orange,20000).
truck(toyota,25000,2,black,25000).
vehicle(Make,Odometer,Age,Color,Price):car(Make,Odometer,Age,Color,Price);
truck(Make,Odometer,Age,Color,Price).
Текст этой программы можно взять здесь.
Введите цель:
car(Make,Odometer,Years_on_road,Body,25000).
Данная цель попытается найти описанную в предложениях машину
(саr), котора стоит ровно $25 000. Пролог ответит:
Make=ford,
Odometer=90000,
Years_on_road=4,
Body=gray
1 решение
Однако данная цель несколько неестественна, т.к. скорее всего
будет задан вопрос типа:
Есть ли в списке машина, стоящая меньше, чем $25000? (Is
there a car listed that costs less than $25000?)
Для поиска такого решения вы можете задать следующую
составную цель:
car(Make, Odometer,Years_on_road,Body,Cost), % подцель А
и
Cost < 25000. % подцель В
Это и является конъюнкцией. Для разрешения этой составной цели
Пролог будет пытаться по очереди решать подцели. Вначале он
попытается решить:
car(Make,Odometer,Years_on_road,Body,Cost).
а затем
Cost < 25000.
с переменной Cost, имеющей идентичное значение в обеих подцелях.
Замечание: Подцель cost < 25 000 соответствует отношению
"меньше чем", которое встроено в систему Пролога. Отношение
"меньше чем" ничем не отличается от любого другого отношения,
использующего два числовых объекта, но правильнее применять
для его обозначения символ (<), помещая его между двумя
объектами.
Посмотрим, является ли истенным следующее выражение, на
естественном языке оно звучит так:
Есть ли в списке автомобиль, стоимостью меньше $25000,
или грузовик стоимостью меньше $20000?
При задании следующей составной цели Пролог выполнит поиск
требуемого решения:
car(Make,Odometer,Years_on_road,Body,Cost),
Cost < 25000 % подцель А
или
truck(Make,Odometer,Years_on_road,Body,Cost),
Cost < 20000. % подцель В
Этот тип составной цели является дизъюнкцией. Данная цель
установила две иль тернативные подцели так же, как если бы это
были два предложения одного правила. Пролог будет искать все
решения, удовлетворяющие обеим подцелям. При разрешении такой
составной цели Пролог вначале попытается решить первую подцель
("найти автомобиль..."), состоящую из следующих подцелей:
car(Make,Odometer,Years_on_road,Body,Cost)
и
Cost < 25000.
Если автомобиль найдется - цель истинна; если нет - Пролог
попытается разрешить вторую составную цель ("найти грузовик..."),
состоящую из подцелей:
truck(Make,Odometer,Years_on_road,Body,Cost),
и
Cost < 20000.
На следующем шаге мы начнем рассматривать комментарии.
Шаг 17.
Основы логического программирования.
Комментарии
На этом шаге мы рассмотрим комментарии.
Хорошим стилем программирования является включение в
программу комментариев, объясняющих все то, что может быть
непонятно кому-то другому (или даже вам,спустя полгода). Если вы
подберете подходящие имена для переменных, предикатов и
доменов, то вам понадобится меньше комментариев, т. к. программа
будет объяснять себя "сама".
Многострочные комментарии должны начинаться с символов /*
(косая черта, звездочка) и завершаться символами */ (звездочка, косая
черта). Для установки однострочных комментариев можно
использовать либо эти же символы, либо начинать комментарий
символом процента (%).
/*
%
Это
Это
первый
пример
комментария
*/
второй
пример
комментария
/**************************************
А эти три строчки - пример многострочного комментария
**************************************/
/*Вы также можете поместить комментарий Пролога /*внутри
комментария */ как здесь*/
В Прологе можно использовать комментарий после каждого
субдомена в объявлении доменов:
domains
articles=book(string Title,string Author);
horse(string Name)
и в объявлениях предикатов:
predicates
conv(string Uppercase,string Lowercase)
Слова Title, Author, Name, Uppercase и Lowercase будут
проигнорированы компилятором, нo сделают программу более
читабельной.
На следующем шаге мы рассмотрим сопоставление.
Шаг 18.
Основы логического программирования.
Сопоставление
На этом шаге мы рассмотрим сопоставление.
В предыдущих шагах вы познакомились с тем, как Пролог
"сопоставляет
вопросы
и
ответы",
"ищет
сопоставление",
"сопоставляет условия с фактами", "сопоставляет переменные с
константами" и т. д. Ниже рассмотрим, что же понимается под
термином сопоставление (matching).
В Прологе имеется несколько примеров сопоставления одной вещи
с другой. Ясно, что идентичные структуры сопоставимы
(сравнимы) друг с другом: parent (joe,tammy) сопоставимо с parent
(joe,tammy). Однако сопоставление (сравнение) обычно использует
одну или несколько свободных переменных. Например, если X
свободна, то parent (joe, X) сопоставимо с parent (joe,tammy) и X
принимает значение (связывается с) tammy.
Если же X уже связана, то она действует так же, как обычная
константа. Таким образом, если X связана со значением tammy, то
parent (joe,X) сопоставимо с parent (joe,tammy), но parent (joe,X) не
сопоставимо с parent (joe,millie).
В предыдущем примере сопоставление не выполняется, т. к. если
переменная становится связанной, то ее значение не может
изменяться.
Как может переменная оказаться связанной при попытке Пролога
сопоставить ее с чем-либо? Вспомним, что переменные не могут
хранить значения, т. к. они становятся связанными только на
промежуток времени, необходимый для отыскания (или попытки
отыскания) одного решения цели. Поэтому имеется только одна
возможность для переменной оказаться связанной - перед попыткой
сопоставления, если цель требует больше одного шага, и переменная
стала связанной на предыдущем шаге. Например: parent (joe,X),
parent (X, jenny) является корректной целью. Она означает: "Найти
кого-либо, являющегося ребенком Joe и родителем Jenny".
Здесь при достижении подцели parent(X, jenny) переменная X уже
будет связана. Если для подцели parent (X, jenny) нет решений,
Пролог "развяжет" переменную X и вернется назад, пытаясь найти
новое решение для parent (joe,X), а затем проверит, будет ли
"работать" parent (X, jenny) с новым значением X.
Две свободные переменные могут сопоставляться друг с другом.
Например, parent (joe,X)сопоставляется с parent (joe,Y), связывая при
этом переменные X и Y между собой. С момента "связывания" X и Y
трактуруются одна переменная и любое изменение значения одной из
них приводит к немедленному соответствующему изменению другой.
И случае подобного "связывания" между собой нескольких свободных
переменных все они называются совмещенными переменными.
Некоторые методы программирования специально используют
"взаимосвязывание" свободных переменных, являющихся, на самом
деле, различными.
В Прологе связывание переменных (со значениями) производится
двумя способами: на входе и выходе. Направление, в котором
передаются значения, указывается в шаблоне потока параметров
(flow pattern). В дальнейшем (для краткости) будем опускать слово
"шаблон" и говорить просто "поток параметров". Когда переменная
передается в предложение, она считается входным аргументом и
обозначается символом (i). Когда же переменная возвращается из
предложения, она является выходным аргументом и обозначается
символом (о).
На следующем шаге мы рассмотрим раздел предложений.
Шаг 19.
Основы логического программирования.
Основные разделы Пролог-программ. Раздел предложений
На этом шаге мы рассмотрим раздел предложений.
Обычно программа на Прологе состоит из четырех основных
программных разделов. К ним относятся:




раздел clauses (предложений);
раздел predicates (предикатов);
раздел domains (доменов);
раздел goal (целей).
Раздел clauses - это сердце Пролог-программы; именно в этот
раздел записываются факты и правила, которыми будет оперировать
Пролог, пытаясь разрешить цель программы.
Раздел predicates - это тот, в котором объявляются предикаты и
домены (типы) их аргументов (вам не нужно объявлять предикаты,
встроенные в Прологе).
Раздел domains служит для обьявления всех используемых нами
доменов, не являющихся стандартными доменами Пролога
(стандартные домены объявлять не нужно).
Раздел goal - это тот, в который вы помещаете цель Прологпрограммы.
Раздел предложений
В раздел clauses (предложений) вы помещаете все факты и
правила, составляющие вашу программу. Основное внимание на этом
шаге было уделено рассмотрению предложений (фактов и правил)
программы: что они означают, как их писать и т. д.
Если вы поняли, что собой представляют факты и правила и как их
записывать в Прологе, то вы знаете, что все предложения для
каждого конкретного предиката в разделе clauses должны
располагаться
вместе.
Последовательность
предложений,
описывающих один предикат, называется процедурой.
Пытаясь разрешить цель, Пролог (начиная с первого предложения
раздела clauses) будет просматривать каждый факт и каждое
правило, стремясь найти сопоставление. По мере продвижения вниз
по разделу clauses, он устанавливает внутренний указатель на
первое предложение, являющееся частью пути, ведущего к решению.
Если следующее предложение не является частью этого логического
пути, то Пролог возвращается к установленному указателю и ищет
очередное подходящее сопоставление, перемещая указатель на него
(этот процесс называется поиск с возвратом).
На следующем шаге мы рассмотрим раздел предикатов.
Шаг 20.
Основы логического программирования.
Раздел предикатов
На этом шаге мы рассмотрим раздел предикатов.
Если в разделе clauses программы на Прологе вы описали
собственный предикат, то вы обязаны объявить его в разделе
predicates (предикатов); в противном случае Пролог не поймет, о
чем вы ему "говорите". В результате объявления предиката вы
сообщаете, к каким доменам (типам) принадлежат аргументы этого
предиката.
Предикаты задают факты и правила. В разделе же predicates все
предикаты просто перечисляются с указанием типов (доменов) их
аргументов. Эффективность работы Пролога значительно возрастает
именно из-за того, что вы объявляете типы объектов (аргументов), с
которыми работают ваши факты и правила.
Как объявить пользовательский предикат
Объявление предиката начинается с имени этого предиката, за
которым идет открывающая (левая) круглая скобка, после чего
следует ноль или больше доменов (типов) аргументов предиката:
predicateName(argument_type1 OptionalName1,
argument_type2 OptionalName2,..,
argument_typeN OptionalNameN)
После каждого домена (типа) аргумента следует запятая, а после
последнего типа аргумента - закрывающая (правая) скобка. Отметим,
что, в отличие от предложений в разделе clauses, декларация
предиката не завершается точкой. Доменами (типами) аргументов
предиката могут быть либо стандартные домены, либо домены
объявленные вами в разделе domains. Можно указывать имена
аргументов OptionalNameK - это улучшает читаемость программы, и
не сказывается на скорости ее исполнения, т. к. компилятор их
игнорирует.
Имена предикатов
Имя предиката должно начинаться с буквы, за которой может
располагаться последовательность букв, цифр и символов
подчеркивания. Регистр букв не имеет значения, однако мы не
советуем вам использовать заглавные буквы в качестве первой буквы
имени предиката. Имя предиката может иметь длину до 250 символов.
В именах предикатов запрещается использовать пробел, символ
минус, звездочку и другие алфавитно-цифровые символы. Корректные
имена Пролога могут включать символы, перечисленные в таблице 1.
Таблица 1. Символы, используемые в именах предикатов в
Прологе
Название символов
Примеры символов
Заглавные буквы
А, B, ..., Z
Строчные буквы
a, b ..., z
Цифры
0, 1, ..., 9
Символ подчеркивания
_
Имена предикатов и аргументов могут состоять из любых
комбинаций этих символов при условии, что вы подчиняетесь
правилам построения соответствующих имен. В таблице 2 приведены
корректные и некорректные имена предикатов.
Таблица 2. Имена предикатов в Прологе
Корректные имена предикатов Некорректные имена предикатов
Fact
[fact]
is_a
*is_a*
has_a
has/a
PatternCheckList
Pattern-Check-List
choose_Menu_Item
Choose Menu Item
PredicateName
Predicate<Name>
first_in_10
>first_in_10
Агументы предикатов
Аргументы предикатов должны принадлежать доменам, известным
Прологу. Эти домены могут быть либо стандартными доменами, либо
некоторыми из тех, что вы объявляли в разделе доменов.
Рассмотрим несколько примеров.
1. Если предикат my_predicate(symbol, integer) объявлен в
разделе predicates следующим образом:
predicates
my_predicate(symbol,integer)
то вам не нужно в разделе domains декларировать домены его
аргументов, т.к. simbol и integer - стандартные домены. Однако если
этот же предикат вы объявите так:
predicates
my_predicate(name,number),
то необходимо объявить, что name (символический тип) и number
(целый тип) принадлежат к стандартным доменам symbol и integer:
domains
name=symbol
number=integer
predicates
my_predicate(name,number)
2. Следующий фрагмент программы показывает
различных объявлений доменов и предикатов:
несколько
domains
person,activity=symbol
car,make,color=symbol
mileage,years_on_road,cost=integer predicates
likes(person,activity)
parent(person,person)
can_buy(person,car)
car(make, mileage,years_on_road,color,cost)
green(symbol)
ranking(symbol,integer)
Этот фрагмент сообщает следующую информацию об этих
предикатах и их аргументах:

предикат likes имеет два аргумента (person и activity), причем
они оба принадлежат домену symbol (это означает, что их
значениями являются идентификаторы, а не числа);





предикат parent имеет дна аргумент (person), причем каждый из
person относится к домену symbol;
предикат can_buy имеет дна аргумента (person и car), которые
относятся к типу symbol;
предикат саr имеет 5 аргументов: make и color относятся к
домену symbol, a mileage, years_on_road и cost - к домену
integer;
предикат green имеет один аргумент типа symbol; нет
необходимости декларировать этот тип аргумента, т. к. он
относится к стандартному домену symbol;
предикат ranking имеет два аргумента, каждый из которых
принадлежит к стандартному домену (symbol и integer), поэтому
декларировать типы этих аргументов не требуется.
На следующем шаге мы рассмотрим раздел доменов.
Шаг 21.
Основы логического программирования.
Раздел доменов
На этом шаге мы рассмотрим раздел доменов.
Домены позволяют задавать разные имена различным видам
данных, которые, в противном случае, будут выглядеть абсолютно
одинаково. В программах Пролога объекты в отношениях (аргументы
предикатов) принадлежат доменам, причем это могут быть как
стандартные, так и описанные вами специальные домены.
Раздел domains служит двум полезным целям. Во-первых, вы
можете задать доменам осмысленные имена, даже если внутренне
эти домены аналогичны уже имеющимся стандартным. Во-вторых,
объявление специальных доменов используется для описания
структур данных, отсутствующих в стандартных доменах. Иногда
очень полезно описать новый домен - особенно, когда вы хотите
прояснить отдельные части раздела predicates. Объявление
собственных доменов, благодаря присваиванию осмысленных имен
типам аргументов, помогает документировать описываемые вами
предикаты.
Рассмотрим несколько примеров.
1. Данный пример показывает, как объявление доменов помогает
документировать предикаты:
Франк - мужчина, которому 45 лет.
Используя стандартные
соответствующий предикат:
домены,
вы
можете
так
объявить
person(symbol,symbol,integer).
В большинстве случаев такое объявление будет работать очень
хорошо. Однако предположим, что несколько месяцев спустя после
написания программы, вы решили скорректировать ее. Есть опасение,
что подобное объявление этого предиката абсолютно ничего вам не
скажет. И напротив, декларация этого же предиката, представленная
ниже, поможет вам разобраться в том, что же представляют собой
аргументы данного предиката:
domains
names,sex=symbol
age=integer
predicates
person (name,sex,age)
Одним из главных преимуществ объявления собственных доменов
является то,что Пролог может отслеживать ошибки типов, например,
такие:
same_sex(Х,Y):person(X,Sex,_),
person(Sex,Y,_).
Несмотря на то, что и name и sex описываются как symbol, они не
эквивалентны друг другу. Это и позволяет Прологу определить
ошибку, если вы перепутаете их. Это полезно в тех случаях, когда
ваши программы очень велики и сложны.
Почему же мы не можем использовать специальные домены для
объявления всех аргументов, если они привносят больше смысла в
обозначение аргументов? Ответ заключается в том, что аргументы с
типами из специальных доменов не могут смешиваться между собой,
даже если эти домены одинаковы. Именно поэтому, несмотря на то,
что name и sex принадлежат одному домену symbol, они не могут
смешиваться. Однако все собственные домены пользователя могут
быть сопоставлены стандартным доменам.
2. Следующий пример программы pro21_1.pro при его загрузке
приведет к ошибке типа.
domains
product,sum = integer
predicates
add_em_up(sum,sum,sum)
multiply_em(product,product,product)
clauses
add_em_up(X,Y,Sum):Sum=X+Y.
multiply_em(X,Y,Product):Product=X*Y.
Текст этой программы можно взять здесь.
Эта программа выполняет две операции: складывает и умножает.
Зададим ей следующую цель:
add_em_up(32,54,Sum).
Пролог ответит:
sum=86
1 Решение
что является суммой двух целых чисел, которые вы передали в
программу. С другой стороны, эта же программа с помощью предиката
multiply_em умножает два аргумента. Поэкспериментируем. Если вам
нужно узнать произведение чисел 13 и 31, введите цель:
multiply_em(31,13,Product).
Пролог вернет вам корректный результат:
Product=403
1 Решение
Предположим, что вам понадобилась сумма чисел 42 и 17; цель
выглядит так:
add_em_up(42,17,Sum).
А теперь, допустим, вы хотите удвоить произведение 31 на 17.
Задаете следующую цель:
multiply_em(31,17,Sum),
add_em_up(Sum,Sum,Answer)
и ждете, что Пролог ответит:
Sum=527,
Answer=1054
1 Решение
Однако вместо этого вы получите ошибку типа. Это случилось из-за
того, что имела место попытка передать результирующее значение
предиката multiply_em, которое относится к домену product, в
качестве первого и второго аргументов (которые должны относится к
домену sum) в предикат add_em_up. Это, собственно, и привело к
ошибке, т.к. домен product отличается от домена sum. И хотя оба эти
домена соответствуют типу integer - это различные домены.
Поэтому, если переменная в предложении используется более
чем в одном предикате, она должна быть одинаково объявлена
в каждом из них. Очень важно, чтобы вы поняли концепцию
описанной здесь ошибки типа, что позволит избегать сообщений об
ошибках компиляции. Различные автоматические и явные
преобразования типов, предлагаемые Прологом, будут описаны
ниже.
На следующем шаге мы рассмотрим раздел цели.
Шаг 22.
Основы логического программирования.
Раздел цели
На этом шаге мы рассмотрим раздел цели.
Во существу, раздел goal (цели) аналогичен телу правила: это
просто список подцелей. Цель отличается от правила лишь
следующим:


за ключевым словом goal не следует ":-";
при запуске программы Пролог автоматически выполняет цель.
Это происходит так, как будто Пролог вызывает goal, запуская тем
самым программу, которая пытается разрешить тело правила goal.
Если все подцели в разделе goal истинны, - программа завершается
успешно. Если же какая-то подцель из дела goal ложна, то считается,
что программа завершается неуспешно (хотя чисто внешне никакой
разницы в этих случаях нет, - программа просто завершит свою
работу).
На следующем шаге мы рассмотрим описание доменов.
Шаг 23.
Основы логического программирования.
Описание доменов
На этом шаге мы рассмотрим описание доменов.
Как в любом другом языке программирования, в Прологе все
используемые конструкции должны быть предварительно описаны.
Поэтому в описании предиката мы должны указать типы его
аргументов.
В Прологе имеется 6 встроенных типов доменов, решающих эту
задачу. Кроме того, существует возможность создания новых типов
доменов на базе стандартных.
Основные стандартные домены перечислены в таблице 1.
Таблица 1. Основные стандартные домены
Тип данных
Ключев
ое
слово
Диапазон значений
Примеры
использования
Символы
char
Все возможные символы
a', 'b',
'\13','%'
Целые числа
integer
От -32768 до 32767
-63,
23,
32763
Действительн
real
ые числа
От +1Е-307 до +1Е308
42769, 8324, 360,
093,
1.25Е23,
5.15Е-9
Строки
"today",
Последовательность
"123",
символов (не более 250) "пример
строки"
Символьная
константа
string
symbol
1. Последовательност
ь букв, цифр и
подчеркиваний,
начинающаяся
с
маленькой
буквы
(латинской
или
русской)
или
большой
русской
буквы.
2. Последовательност
ь любых символов,
заключенная
в
кавычки.
Используется тогда,
когда имя должно
начинаться
с
'#',
'B',
2349,
"телефонный
номер",
alfa_beta_gamma
,
"Alfa_beta_gamm
a",
Сидоров_П_П
большой латинской
буквы
или
содержать
пробелы.
Длина
символьной
константы
не
превышает
250
символов.
Файлы
file
Допустимое в DOS имя
файла. При операциях с
mail.txt,
файлами связывается с
BIRDS.DBA
конкретными
файлами
или устройствами.
Помимо использования стандартных типов доменов в Прологе
можно создавать свои типы доменов в разделе domains. Перечислим
основные способы создания новых доменов.
1. Создание
псевдонимов
(альтернативных
имен)
стандартных доменов. Эта операция осуществляется по
следующей схеме:
<новое имя домена> = <стандартное имя домена>.
Этот формат служит для объявления нового имени домена,
состоящего из элементов (доменов) стандартных типов, к
которым относятся типы, перечисленные в таблице 1. Этот
способ применяется для объявления типов объектов, которые
подобны синтаксически, но отличаются семантически (по
смыслу), и поэтому не должны в программе смешиваться друг с
другом.
Отнесение
их
к
различным,
определенным
программистом типам, позволяет компилятору осуществлять
тщательный контроль их использования в программе.
Следующий
фрагмент
программы
использование рассмотренных конструкций:
. . . . . . .
domains
name = string
predicates
men (name)
иллюстрирует
woman (name)
brother (name,name)
. . . . . . .
Этот же фрагмент программы можно было записать так:
. . . . . . .
predicates
men (string)
woman (string)
brother (string, string)
. . . . . . .
Понятно, что в первом случае, глядя на описания предикатов,
ясно, что в качестве аргументов предикатов нужно использовать
имена людей. При использовании второй формы описания
предикатов ничего конкретного сказать нельзя. Таким образом,
использование альтернативных имен доменов делает программу
более понятной.
2. Создание домена типа "список". Этот формат применяется
при описании предикатов, осуществляющих обработку списков.
Общий вид создания такого домена следующий:
<домен типа "список"> = <тип элементов списка>*.
Символ "*" (звездочка) "говорит" о том, что создаваемый
домен является списком. Тип элементов списка может
относиться как к стандартному типу, так и к доменам,
определенным программистом. Например:
list_int = integer* /*Домен типа списка целых чисел.*/
list_char = char* /*Домен типа списка символов.*/
Более детально мы рассмотрим описание доменов типа
"список" в соответствующем разделе.
3. Создание домена типа "структура". Чаще всего этот формат
применяется при организации баз данных. Его общий вид
следующий:
4. <структура> =
5.
<функтор1> (<домен11>,<домен12>, ...,<домен1N>);
6.
<функтор2> (<домен21>,<домен22>, ...,<домен2N>)
Объявление структуры (домена, состоящего из сложных и
перекрывающихся объектов) состоит из имени структуры -
функтора и доменов всех используемых компонент и
подкомпонент данной структуры. Например, можно объявить
домен "владелец" так:
владелец = имеет(фамилия,книга)
и затем задавать его элементы в программе, например, так:
имеет(Иванов,книга(Стругацкие,"Жук в муравейнике"))
Правая часть объявления структуры может содержать
описание альтернативных вариантов, разделенных служебным
словом or или знаком ";". Каждая альтернатива должна
содержать уникальный функтор и список действительно
используемых доменов. Например, объявление:
водитель=имеет(имя,автомобиль);документы(имя,
автомобиль)
определяет для
альтернативы.
структуры
водитель
две
возможные
Проиллюстрируем использование простейшей структуры на
конкретном примере
/* Программа Б и б л и о т е к а */
/* Назначение. Демонстрация одноуровневого составного
объекта. */
domains
personal_library = book(title,author,publisher,year)
/* персональная библиотека = книга(название,автор,
издательство,год издания) */
collector,title,author,publisher = symbol
year = integer
predicates
collection(collector,personal_library)
/* коллекция (имя коллекционера, библиотека) */
clauses
collection("В.В.Иванов",book("Программирование на языке
ПРОЛОГ для
искусственного интеллекта","Братко И.", "Мир",1990)).
collection("В.В.Иванов",book("Использование
Турбо_Пролога",
"Ин Ц., Соломон Д","Мир",1993)).
collection("С.С.Сидоров",book("Реляционный язык Пролог
и его применение",
"Малпас Дж.","Наука",1990)).
collection("С.С.Сидоров",book("Программирование
экспертных систем на
Турбо-Прологе",
"Марселиус
Д.","Финансы
и
статистика", 1994)).
collection("П.П.Петров",book("Искусство
программирования на языке Пролог",
"Стерлинг Р., Шапиро Э.","Мир",1990)).
collection("П.П.Петров",book("Турбо-Пролог
в
сжатом
изложении",
"Янсон А.","Мир",1991)).
/* К о н е ц программы. */
Функтор структуры personal_library имеет имя book. Его
описание таково:
personal_library=book(title,author,publisher,year)
collector,title,author,publisher=symbol
year=integer
Предикат, использующий эту структуру, определяется так:
collection(collector,personal_library). Описание содержит два
имени объектов. Первое имя относится к обычному объекту,
второе - к структуре из нескольких объектов.
Использование доменной структуры упрощает структуру
предиката. Если не использовать конструкцию доменной
структуры, то программа требовала бы такого описания
предиката collection:
collection(collector,title,author,publisher,year)
В этом описании 4 последних объекта обозначают атрибуты
книги. Правило, которое оперирует с персональными
библиотеками рассматривало бы эти 4 последних объекта как
независимые сущности, что сделало бы код программы более
сложным.
Данная программа использует внешнюю цель. Для того чтобы
узнать, какие книги принадлежат В.В.Иванову, необходимо
ввести такое целевое утверждение:
collection("В.В.Иванов",B).
Объект "В.В.Иванов" является частным значением из домена
collector, а B - свободной переменной. Цель заключается в
отыскании всех книг, принадлежащих В.В.Иванову.
Предположим теперь, что требуется узнать имена владельцев
и названия книг, напечатанных в 1990 году. Цель для поиска этой
информации выглядит следующим образом:
collection(Collector,book(Title,_,_,1990)).
Здесь свободными переменными являются уже Сollector и
Title. Подчерки (_) указывают на то, что нас не интересуют
объекты с родовыми именами author и publisher. (Подчерк
замещает собой анонимную переменную)
7. Создание домена типа "файл". Этот домен применяется в том
случае, когда в программе необходимо ссылаться на файлы с
помощью файловых переменных (логических имен файлов).
Формат создания такого домена следующий:
8.
file = <имя1>;< имя1>; ...; < имяN> .
Как видно из приведенного формата можно указывать
несколько логических имен файлов, но само описание должно
быть единственным. Например, объявление:
file = datafile1;datafile2
определяет два логических имени (для разных файлов).
Существует несколько предопределенных (встроенных в язык)
логических имен файлов. К ним относятся логические имена:
o
o
o
o
printer - для устройства печати;
screen - для экрана;
keyboard - для клавиатуры;
com1 - для дополнительного устройства связи.
Замечание. Количество имен, объявляемых в разделе domains, не
должно превышать 250.
На следующем шаге мы рассмотрим задание типов аргументов.
Шаг 24.
Основы логического программирования.
Задание типов аргументов при декларации предикатов
На этом шаге мы рассмотрим задание типов аргументов.
Объявление доменов аргументов в разделе predicates называется
заданием типов аргументов. Предположим, имеется следующая
связь объектов:
Франк - мужчина, которому 45 лет.
Факт
Пролога,
соответствующий
этому
естественного языка, может быть следующим:
предложению
person(frank,male,45).
Для того чтобы объявить person (человек), как предикат с этими
тремя аргументами, вы можете разместить в разделе predicates
следующую строку:
person(symbol,symbol,unsigned).
Здесь для всех трех аргументов использованы стандартные
домены. Отныне всякий раз при работе с предикатом person, вы
должны передавать ему три аргумента, причем первые два должны
быть типа symbol, а третий - типа integer.
Если в программе используются только стандартные домены, то нет
необходимости использовать раздел domain; вы уже видели
несколько программ такого типа.
Или, предположим, что вы хотите описать предикат, который
сообщал бы позицию буквы в алфавите, т. е. цель
alphabet_position(Letter,Position)
Должна вернуть вам Position = 1, если Letter = a, Position = 2, если
Letter = b и т. д. Предложения этого предиката могут выглядеть
следующим образом:
alphabet_position(A_character,N).
Если при объявлении предиката используются только стандартные
домены, то программе не нужен раздел domains. Предположим, что
вы хотите описать предикат так, что цель будет истинна, если
Acharacter является N-м символом алфавита. Предложения этого
предиката будут такими:
alphabet_position('а',1).
alphabet_position('b',2).
alphabet_position('c',3).
...
alphabet_position('z',26).
Вы можете объявить данный предикат следующим образом:
predicates
alphabet_position(char,unsigned)
и тогда вам не будет нужен раздел domains. Если разместить все
фрагменты программы pro24_1.pro вместе, получим:
predicates
alphabet_position(char,integer)
clauses
alphabet_position('a',1).
alphabet_position('b',2).
alphabet_position('с',3).
% здесь находятся остальные буквы
alphabet_position('z',26).
Текст этой программы можно взять здесь.
Ниже представлено несколько простых целей, которые вы можете
использовать:
alphabet_position('a',1).
alphabet_position(X,3).
alphabet_position('z',What).
На следующем шаге мы рассмотрим арность предиката.
Шаг 25.
Основы логического программирования.
Арность (размерность)
На этом шаге мы рассмотрим арность предиката.
Арность предиката - это количество аргументов, которые он
принимает. Вы можете иметь два предиката с одним и тем же именем,
но отличающейся арностью. В разделах predicates и clauses версии
предикатов с одним именем и разной арностью должны собираться
вместе; за исключением этого ограничения, различная арность
всегда понимается как полное различие предикатов.
Проиллюстрируем это примером pro25_1.pro.
domains
person = symbol
predicates
father(person) % этот person - отец
father(person, person) % первый person является отцом
другого
clauses
father(Man):father(Man,_).
father(adam,seth).
father(abraham,isaac).
Текст этой программы можно взять здесь.
На следующем шаге мы рассмотрим синтаксис правил.
Шаг 26.
Основы логического программирования.
Синтаксис правил
На этом шаге мы рассмотрим cинтаксис правил.
Правила используются в Прологе в случае, когда какой-либо факт
зависит от истинности другого факта или группы фактов. Как мы
объясняли ранее в правиле Пролога есть две части: заголовок и
тело. Ниже представлен обобщенный синтаксис правила в Прологе:
HEAD:-<Subgoaд>,<Subgoal>,...,<Subgoal>.
Заголовок:- <Подцель>,<Подцель>,...,<Подцель>.
Тело правила состоит из одной или более подцелей. Подцели
разделяются запятыми, определяя конъюнкцию, а за последней
подцелью правила следует точка. Каждая подцель выполняет вызов
другого предиката Пролога, который может быть истинным или
ложным. После того, как программа осуществила этот вызов, Пролог
проверяет истинность вызванного предиката, и если это так, то работа
продолжается, но уже со следующей подцелью. Если же в процессе
такой работы была достигнута точка, то все правило считается
истинным; если хоть одна из подцелей ложна, то все правило ложно.
Для успешного разрешения правила Пролог должен разрешить все
его подцели и создать последовательный список переменных,
должным образом связав их. Если же одна из подцелей ложна.
Пролог вернется назад для поиска альтернативы предыдущей
подцели, а затем вновь двинется вперед, но уже с другими
значениями переменных. Этот процесс называется поиском с
возвратом. Подробное изложение данного процесса и того, как
Пролог ищет решения, приводится на шаге 29.
Как упоминалось выше, к качестве разделителя заголовка и тела
правила Пролог использует знак :-, который читается как "если" (if).
Однако if Пролога отличается от if в других языках, например в
Pascal, где условие, содержащееся в оператре if, должно быть
указано перед телом оператора if, который может быть выполнен.
Другими словами:
"if HEAD is true, then BODY is true (or then do BODY)"
(если ЗАГОЛОВОК истинен, тогда ТЕЛО истинно (или: тогда
выполнить ТЕЛО)
Данный тип оператора известен как условный оператор
если/тогда (if/then). Пролог же использует другую форму логики в
таких правилах. Вывод об истинности заголовка правила Пролога
делается, если (после того, как) тело этого правила истинно
например, так:
"HEAD is true if BODY is true (or: if BODY can be done)"
ЗАГОЛОВОК истинен, если ТЕЛО - истинно (или: если ТЕЛО
может быть выполнено)
Учитывая вышесказанное, правило
условной форме тогда/если, (then/if).
На следующем шаге
преобразование типов.
мы
Пролога
рассмотрим
соответствует
автоматическое
Шаг 27.
Основы логического программирования.
Автоматическое преобразование типов
На этом шаге мы рассмотрим автоматическое преобразование
типов.
Совсем не обязательно, чтобы при сопоставлении двух Прологпеременных они принадлежали одному и тому же домену.
Переменные могут быть связаны с константами из различных
доменов. Такое (избирательное) смешение допускается, т.к. Пролог
автоматически выполняет преобразование типов (из одного домена в
другой), но только в следующих случаях:


между строками (string) и идентификаторами (symbol);
между целыми, действительными и символами (char). При
преобразовании символа в числовое значение этим значением
является величина символа в коде ASCII.
Аргумент из домена my_dom, который объявлен следующим
образом:
domains
my_dom=<base
domain>
стандартный домен
%
<base
domain>-
это
может свободно смешиваться с аргументами из этого основного
домена и с аргументами всех совместимых с ним стандартных
доменов. Если основной домен - string, то с ним совместимы
аргументы из домена symbol; если же основной домен integer, то с
ним совместимы домены real, char, word и др.
Такое преобразование типов означает, например, что вы можете:




вызвать предикат с аргументами типа string, задавая
аргументы типа symbol, и наоборот;
передавать предикату с аргументами типа real параметры
integer;
передавать предикату с аргументами типа char параметры
integer;
использовать в выражениях и сравнениях символы
необходимости получения их кодов в ASCII.
ему
типа
типа
без
Существует набор правил, определяющих, к какому домену
принадлежит результат смешивания разных доменов.
На следующем
унификацию.
шаге
мы
рассмотрим
сопоставление
и
Шаг 28.
Основы логического программирования.
Сопоставление и унификация
На этом шаге мы рассмотрим сопоставление и унификацию.
Рассмотрим программу с точки зрения того, как Пролог будет
отыскивать все решения следующей цели:
written_by(X,Y).
Пытаясь выполнить целевое утверждение written_by(X,Y), Пролог
должен проверить каждое предложение written_by в программе.
Сопоставляя аргументы X и Y с аргументами каждого предложения
written_by, Пролог выполняет поиск от начала программы до ее
конца. Обнаружим предложение, соответствующее целевому
утверждению, Пролог присваивает значения свободным переменным
таким образом, что целевое утверждение и предложение становятся
идентичными; говорят, что целевое утверждение унифицируется с
предложением.
Такая
операция
сопоставления
унификацией. Рассмотрим пример pro28_1.pro:
называется
domains
title,author=symbol
pages=integer
predicates
book(title,pages)
written_by(author, title)
long_novel(title)
clauses
written_by(fleming, "DR NO").
written_by(melville, "MOBY DICK"),.
book("MOBY DICK", 250).
book("DR NO", 310).
long_novel(Title) :written_by(_,Title),
book(Title,Length),
Length >300.
Текст этой программы можно взять здесь.
Поскольку X и Y являются свободными переменными в целевом
утверждении, а свободная переменная может быть унифицирована с
любым другим аргументом (и даже с другой свободной переменной),
то целевое утверждение может быть унифицировано первым
предложением written_by в программе, как показано ниже:
written_by(X,Y).
written_by(fleming,"DR NO").
Пролог устанавливает соответствие, X становится связанным с
fleming, a Y - с DR NO. В этот момент Пролог напечатает:
X=fleming, Y="DR NO"
Поскольку Пролог ищет все решения для заданной цели, целевое
утверждение также будет унифицировано и со вторым предложением
written_by:
written_by(melville,"MOBY DICK").
Пролог печатает второе решение:
X=melville, Y="MOBY DICK"
Теперь предположим,
утверждение
что
вы
задали
программе
целевое
written_by(X,"MOBY DICK").
Пролог произведет сопоставление с первым предложением
written_by:
written_by(X,"MOBY DICK").
written_by(fleming,"DR NO").
Так как "MOBY DICK" И "DR NO" не соответствуют друг другу,
попытка унификации завершается неудачно. Затем Пролог проверит
следующий факт в программе:
written_by(melville,"MOBY DICK").
Этот факт действительно унифицируется, и X становится
связанным с melville. Рассмотрим, как Пролог выполнит следующее
целевое утверждение:
long_novel(X).
Когда Пролог пытается выполнить целевое утверждение, он
проверяет, действительно ли обращение может соответствовать
факту или заголовку правила. В нашем случае устанавливается
соответствие с long_novel(Title).
Пролог проверяет предложение для long_novel, пытаясь
завершить сопоставление унификацией аргументов. Поскольку в
целевом утверждении X - свободная переменная, то она может быть
унифицирована с любым другим аргументом. Title также не является
связанным в заголовке предложения long_novel. Целевое
утверждение соответствует заголовку правила, и унификация
выполняется. Впоследствии Пролог будет пытаться согласовывать
подцели с правилом.
long_novel(Title):written_by(_, Title),
book(Title, Length),
Length>300.
Пытаясь выполнить согласование тела правила, Пролог обратится
к первой подцели в теле правила - written_by(_,Title). Поскольку
авторство книги является несущественным, на месте аргумента
author появляется анонимная переменная (_). Обращение
written_by(_,Title) становится текущей подцелью, и Пролог ищет
решение для этого обращения. Пролог ищет соответствие с данной
подцелью от вершины и до конца программы.
В результате достигается унификация с первым фактом для
written_by, а именно:
written_by(_,Title),
written_by(fleming,"DR NO").
Переменная Title связывается с "DR NO", и к следующей подцели
book(Title, Length) обращение выполняется уже с этим значением
переменной.
Далее Пролог начинает очередной процесс поиски, пытаясь найти
соответствие с обращением к book. Так как Title связан с "DR NO",
фактическое обращение выглядит как book("DR NO",Length).
Процесс поиска опять начинается с вершины программы. Заметим,
что первая попытка сопоставления с предложением book("MOBY
DICK",250) завершится неудачно, и Пролог перейдет ко второму
предложению book в поиске соответствия. Здесь заголовок книги
соответствует подцели, и Пролог связывает переменную Length с
величиной 310. Теперь третье предложение в теле long_novel
становится текущей подцелью:
Length > 300.
Пролог выполняет сравнение, завершающееся успешно: 310
больше, чем 300. В этот момент все подцели в теле правила
выполнены, и, следовательно, обращение long_novel (X) успешно.
Так как X в обращении был унифицирован с переменной Title в
правиле, то значение, с которым связывается Title при подтверждении
правила, возвращается и унифицируется с переменной X.
Переменная Title в случае подтверждения правила имеет значение
"DR NO", поэтому Пролог выведет:
X="DR NO"
B следующих шагах будет показано несколько прикладных
примеров унификации. Однако прежде ознакомимся с рядом
основополагающих понятий.
На следующем шаге мы рассмотрим поиск с возвратом.
Шаг 29.
Основы логического программирования.
Поиск с возвратом
На этом шаге мы рассмотрим поиск с возвратом.
Часто при решении реальной задачи мы придерживаемся
определенного пути для ее логического завершения. Если полученный
результат не дает искомого ответа, мы должны выбрать другой путь.
Так, вам, возможно, приходилось играть в лабиринт. Один из верных
способов найти конец лабиринта - это поворачивать налево на каждой
развилке лабиринта до тех пор, пока вы не попадете в тупик. Тогда
следует вернуться к последней развилке и попробовать свернуть
вправо, после чего опять поворачивать налево на каждом
встречающемся распутье. Путем методичного перебора всех
возможных путей вы, в конце концов, найдете выход.
Пролог при поиске решения задачи использует именно такой метод
проб и возвращений назад; этот метод называется поиск с
возвратом. Если, начиная поиск решения задачи (или целевого
утверждения), Пролог должен выбрать между альтернативными
путями, то он ставит маркер у места ветвления (называемого точкой
отката) и выбирает первую подцель, которую и станет проверять.
Если данная подцель не выполнится (что эквивалентно достижению
тупика в лабиринте), Пролог вернется к точке отката и попробует
проверить другую подцель.
Рассмотрим простой пример pro29_1.pro.
predicates
likes(symbol,symbol)
tastes(symbol,symboI)
food(symbol)
clauses
likes (bill,X):food(X),
tastes(X,good).
tastes(pizza,good).
tastes(brussels_sprouts,bad).
food(brussels_sprouts).
food(pizza).
Текст этой программы можно взять здесь.
Эта маленькая программа составлена из двух множеств фактов и
одного правила. Правило, представленное отношением likes,
утверждает, что Билл любит вкусную пищу.
Чтобы увидеть, как работает поиск с возвратом, дадим программе
для решения следующее целевое утверждение:
likes(bill,What).
Замечание. Когда Пролог пытается произвести согласование
целевого утверждения, он начинает поиск с вершины программы.
В данном случае Пролог будет искать решение, производя с
вершины программы поиск соответствия с подцелью
likes (bill,What).
Он обнаруживает соответствие с первым предложением в
программе и переменная what унифицируется с переменной X.
Сопоставление с заголовком правила заставляет Пролог попытаться
удовлетворить это правило. Производя это, он двигается по телу
правила и обращается к первой находящейся здесь подцели: food(X).
Замечание. Если выполняется новое обращение, поиск
соответствия для этого обращения вновь начинается с вершины
программы.
Пытаясь согласовать первую подцель, Пролог (начиная с вершины)
производит сопоставление с каждым фактом или заголовком правила,
встреченным в программе.
Он обнаруживает соответствие с запросом у первого же факта,
представляющего отношение food. Таким образом, переменная X
связывается со значением brussels_sprouts. Поскольку существует
более чем один возможный ответ на обращение food(X), Пролог
ставит точку возврата (маркер) возле факта food(brussels_sprouts).
Эта точка поиска с возвратом указывает на то место, откуда Пролог
начнет поиск следующего возможного соответствия для food(X).
Замечание. Когда установление соответствия обращения
завершается успешно, говорят, что обращение возвращается, и
может быть испытана очередная подцель.
Поскольку переменная X связана с brussels_sprouts, следующее
обращение будет выполняться так:
tastes: (brussels_sprouts,good)
и Пролог вновь начнет поиск с вершины программы, пытаясь
согласовать
это
обращение.
Поскольку
соответствующих
предложений не обнаруживается, обращение завершается неудачно,
и теперь Пролог запускает механизм возврата. Начиная поиск с
возвратом, Пролог отступает к последней позиции, где была
поставлена точка отката. В данном случае Пролог возвращается к
факту food(brussels_sprouts).
Замечание. Единственным способом освободить переменную,
однажды связанную в предложении, является откат при поиске с
возвратом.
Когда Пролог отступает к точке поиска с возвратом, он
освобождает все переменные, связанные после этой точки, и будет
искать другое решение для исходного обращения.
Обращение было food(X), так что связанность brussels_sprouts с X
отменена. Теперь Пролог пытается заново произвести решение для
этого обращения. Он обнаруживает соответствие с фактом food
(pizza); на этот раз переменная X связывается со значением pizza.
Пролог переходит к следующей подцели в правиле, имея при этом
новую связанную переменную. Производится новое обращение, tastes
(pizza, good), и начинается поиск (опять от вершины программы). На
этот раз соответствие найдено, и целевое утверждение успешно
выполняется. Поскольку переменная What в целевом утверждении
унифицирована с переменной X в правиле likes, а переменная X
связана со значением pizza, переменная What отныне связана со
значением pizza и Пролог сообщает решение:
What=pizza
На следующем шаге мы рассмотрим управление поиском
решений.
Шаг 30.
Основы логического программирования.
Управление поиском решений
На этом шаге мы рассмотрим управление поиском решений.
Встроенным механизм поиска с возвратом в Прологе может
принести к поиску не нужных решений, в результате чего теряется
эффективность, например, когда желательно найти только одно
решение. В других случаях может оказаться необходимым
продолжать поиск дополнительных решений, даже если целевое
утверждение уже согласовано. В этом шаге показаны некоторые
методы, которые можно использовать для управления поиском
решений ваших целевых утверждений.
Пролог обеспечивает два инструментальных средства, которые
дают возможность управлять механизмом поиска с возвратом:
предикат fail, который используется для инициализации поиска с
возвратом, и cut или отсечение (обозначается !) - для запрета
возможности возврата.
Использование предиката fail
Пролог начинает поиск с возвратом, когда вызов завершается
неудачно. В определенных ситуациях бывает необходимо
инициализировать выполнение поиска с возвратом, чтобы найти
другие решения. Пролог поддерживает специальный предикат fail,
вызывающий
неуспешное
завершение,
и,
следовательно,
инициализирует возврат. Действие предиката fail равносильно
эффекту от сравнения 2 = 3 или другой невозможной подцели.
Программа
pro30_1.pro
иллюстрирует
использование
этого
специального предиката.
domains
name=symbol
predicates
father(name, name)
everybody
clauses
father(leonard,katherine).
father(carl,jason).
father(carl,marilyn).
everybody:father(X,Y),
write(X," is ",Y,"'s father\n"),
fail.
Текст этой программы можно взять здесь.
Пусть необходимо найти все решения цели father (X,Y). Цель можно
записать как
father (X,Y).
Пролог найдет все решения цели father (X,Y) и отобразит значения
всех переменных следующим образом:
X=leonard,Y=katherine
X=carl,Y=jason
X=carl,Y=marilyn
Нo если вы скомпилируете эту программу и запустите ее (командой
меню Run), то Пролог найдет только первое подходящее решение
для father (X,Y). После того как целевое утверждение, определенное в
разделе goal, выполнено впервые, ничто не говорит Прологу о
необходимости продолжения поиска с возвратом. Поэтому обращение
к father приведет только к одному решению. Как же найти все
возможные решения? Предикат everybody в программе использует
fail для поддержки поиска с возвратом.
Задачa предиката everybody - найти все решения для father и
выдать полный ответ. Сравните предыдущие ответы Пролога с
целью father (X,Y) и ответы на выполнение следующей цели:
everybody.
отображенные приведенной программой:
leonard
carl
carl is marilyn's father
is
is
katherine's
jason's
father
father
Предикат everybody использует поиск с возвратом с тем, чтобы
получить все решения для father (X,Y), заставляя Пролог выполнять
поиск с возвратом сквозь тело правила everybody:
father(X,Y),
write(X, " is ",Y, "'s father\n"),
fail.
fail не может быть согласован (он всегда неуспешен), поэтому
Пролог вынужден повторять поиск с возвратом. При поиске с
возвратом он возвращается к последнему обращению, которое может
произвести множественные решения. Такое обращение называют
недетерминированным.
Недетерминированное
обращение
является противоположностью детерминированному обращению,
которое может произвести только одно решение.
Предикат write не может быть вновь согласован (он не может
предложить новых решений), поэтому Пролог должен выполнить
откат дальше, на этот раз к первой подцели в правиле.
Обратите внимание, что помещать подцель после fail в теле
правила бесполезно. Предикат fail все время завершается неудачно,
нет возможности для достижения подцели, расположенной после fail.
На следующем шаге мы рассмотрим отсечения.
Шаг 31.
Основы логического программирования.
Прерывание поиска с возвратом: отсечение
На этом шаге мы рассмотрим прерывание поиска с возвратом:
отсечение.
Пролог предусматривает возможность отсечения, которая
используется для прерывания поиска с возвратом; отсечение
обозначается восклицательным знаком (!). Действует отсечение
просто: через него невозможно совершить откат (поиск с возвратом).
Отсечение помещается в программу таким же образом, как и
подцель в теле правила. Когда процесс проходит через отсечение,
немедленно удовлетворяется обращение к cut и выполняется
обращение к очередной подцели (если таковая имеется). Однажды
пройдя через отсечение, уже невозможно произвести откат к
подцелям, расположенным в обрабатываемом предложении перед
отсечением, и также невозможно возвратиться к другим
предложениям, определяющим обрабатывающий предикат (предикат,
содержащий отсечение).
Существуют два основных случая применения отсечения.


Если вы заранее знаете, что определенные посылки никогда не
приведут к осмысленным решениям (поиск решений в этом
случае будет лишней тратой времени), - примените отсечение, программа станет быстрее и экономичнее. Такой прием
называют зеленым отсечением.
Если отсечения требует сама логика программы для исключения
из рассмотрения альтернативных подцелей. Это - красное
отсечение.
Использование отсечений
На этом шаге даются примеры, показывающие, как следует
использовать отсечение, рассматриваются несколько условных
правил (r1, r2 и r3), которые определяют условный предикат r, а также
несколько подцелей - а, b, с и т. д.
Предотвращение поиска с возвратом к предыдущей подцели в
правиле
а,
rl:b,
!,
c.
Такая запись является способом сообщить Прологу о том, что вас
удовлетворит первое решение, найденное им для подцелей a и b.
Имея возможность найти множественные решения при обращении к с
путем поиска с возвратом, Пролог при этом не может произнести
откат (поиск с возвратом) через отсечение и найти альтернативное
решение для обращений а и b. Он также не может возвратиться к
другому предложению, определяющему предикат r1.
В качестве конкретного примера рассмотрим следующую программу
pro31_1.pro:
predicates
by_саr (symbol, symbol)
car(symbol,symbol,integer)
colors(symbol, symbol)
clauses
buy_car (Model,Color) :car(Model,Color, Price),
colors(Color,sexy),
!,
Price < 25000.
car(maserati,green, 25000).
car(corvette,black, 24000).
car(corvette, red, 26000).
car(porsche,red, 24000).
colors(red,sexy).
colors(black,mean).
colors(green,preppy).
goal
buy_car(corvette,Y).
Текст этой программы можно взять здесь.
И данном примере поставлена цель: найти Corvette (Корвет)
приятного цвета, подходящий по стоимости. Отсечение в правиле
buy_car означает, что поскольку в базе данных содержится только
один "Корвет" приятного цвета, хоть и со слишком высокой ценой, то
нет нужды искать другую машину.
Получив целевое утверждение
buy_car(corvette, Y)
программа отработает следующие шаги:
1. Пролог обращается к саr, первой подцели для предиката
buy_car.
2. Выполняет проверку для первой машины, Maserati, которая
завершается неудачно.
3. Затем проверяет следующее предложение саr и находит
соответствие, связывая переменную Color со значением black.
4. Переходит к следующему обращению и проверяет, имеет ли
выбранная машина прияный цвет. Черный цвет не является
приятным в данной программе, таким образом проверка
завершается неудачно.
5. Выполняет поиск с возвратом к обращению car и снова ищет
Corvette, удовлеряющий этому критерию.
6. Находит соответствие и снова проверяет цвет. На этот раз цвет
оказывается приятным, и Пролог переходит к следующей
подцели в правиле: к отсечению. Отсечение немедленно
выполняется, "замораживая" все переменные, ранее связанные
в этом предложении.
7. Переходит к следующей (и последней) подцели в правиле, к
сравнению
Price < 25000.
8. Проверка завершается неудачно, и Пролог пытается совершить
поиск с возвратом с целью найти другую машину для проверки.
Отсечение предотвращает попытку решить последнюю подцель,
и наше целевое утверждение завершается неудачно.
Предотвращение поиска с возвратом к следующему
предложению
Отсечение может быть использовано, как способ сообщить
Прологу, что он выбрал верное предложение для определенного
предиката. Например, рассмотрим следующий фрагмент:
r(1):!,
а,b,с.
:!,
d.
:!,
с.
:-
r(2)
r(3)
r(_)
write("This is a catchall clause.").
Использование отсечения делает предикат r детерминированным.
В данном случае Пролог выполняет обращение к r с единственным
целым аргументом. Предположим, что произведено обращение r(l).
Пролог просматривает программу в поисках соответствия для
обращения; он находит его с первым предложением, определяющим r.
Поскольку имеется более чем одно возможное решение для данного
обращения, Пролог проставляет точку возврата около этого
предложения.
Теперь Пролог начинает обработку тела правила, проходит через
отсечение и исключает возможность возвращения к другому
предложению r. Это отменяет точки поиска с возвратом, повышая
эффективность выполнения программы, а также гарантирует, что
отлавливающее ошибки предложение будет выполнено лишь в том
случае, если ни одно из условий не будет соответствовать обращению
к r.
Обратите внимание, что конструкция такого типа весьма похожа на
конструкции case в других языках программирования; ycловие
проверки записывается в заголовке правил. Вы могли бы написать
такие предложения:
r(Х)
r(X)
:
r(X)
:X=1,
!,
a,b,с.
X=2,
!,
d.
:X=3,
!,
r(_)
write("This is a catchall clause.").
c.
:-
Однако следует, по возможности, помещать проверочное условие
именно в заголовок правила, - это повышает эффективность
программы и упрощает ее чтение.
В качестве другого примера рассмотрим программу pro31_2.pro.
predicates
friend(symbol,symbol)
girl(symbol)
likes(symbol,symbol)
clauses
friend(bill,jane):girl (jane),
likes(bill,jane),
!.
friend(bill,jim):likes(jim,baseball),
!.
friend(bill,sue):girl(sue).
girl(marу).
girl (jane).
girl(sue) .
likes(jim,baseball).
likes(bill,sue).
goal
friend(bill,Who).
Текст этой программы можно взять здесь.
Если бы и программе не было отсечения, то Пролог предложил бы
два решения: Билл является другом как Джейн, так и Сью. Однако
отсечение в первом преложении, определяющем friend, говорит о
том, что если это предложение согласовано, то друг Билла уже
найден, и нет нужды продолжать поиск других кандидатур. Поиск с
возвратом может иметь место внутри предложений в попытке
согласовать обращение, но, однажды обнаружив решение, Пролог
проходит через отсечение. Предложения friend, записанные так, как
показано выше, возвратят одного и только одного друга Билла (если
друг вообще может быть найден).
На следующем шаге мы рассмотрим детерминизм и отсечения.
Шаг 32.
Основы логического программирования.
Детерминизм и отсечение
На этом шаге мы рассмотрим детерминизм и отсечение.
Если бы предикат friend, определенный в предыдущей программе,
не содержал отсечений, то это был бы недетерминированный
предикат (способный производить множественные решения при
помощи поиска с возвратом). В предыдущих реализациях Пролога
программисты должны были обращать особое внимание на
недетерминированные предложения из-за сопутствующих им
дополнительных требований к ресурсам памяти. Теперь Пролог сам
выполняет проверку на недетерминированные предложения, облегчая
вашу работу.
В Прологе существует директива компилятора check_determ. Если
вставить эту директиву в самое начало программы, то Пролог будет
выдавать
предупреждение
в
случае
обнаружения
недетерминированных предложений в процессе компиляции.
Вы можете превратить недетерминированные предложения в
детерминированные,
вставляя
отсечения
в
тело
правил,
определяющих данный предикат. Например, помещение отсечений в
предложения, определяющие предикат friend, делает этот предикат
детерминированным, поскольку в данном случае обращение к friend
может возвратить одно и только одно решение.
Предикат not
Следующая программа pro32_1.pro демонстрирует, как вы можете
использовать предикат not для того, чтобы выявить успевающего
студента: студента, у которого средний балл (GPA) не менее 3.5 и у
которого в настоящее время не продолжается испытательный срок.
domains
name=symbol
gpa = real
predicates
honor_student(name)
student(name,gpa)
probation(name)
clauses
honor_student(Name):-
student(Name,GPA),
GPA>=3.5,
not(probation(Name)).
student("Betty Blue",3.5).
student("David Smith",2.0).
student("John Johnson",3.7).
probation("Betty Blue"),
probation("David Smith").
goal
honor_student (X).
Текст этой программы можно взять здесь.
Замечание. При использовании предиката not необходимо иметь
в виду следующее: предикат not будет успешным, если не может
быть доказана истинность данной подцели.
Этo приводит к предотвращению связывания внутри not
несвязанных переменных. При вызове изнутри not подцели со
свободными переменными, Пролог возвратит сообщение об ошибке:
"Free variables not allowed in not or retractall" (Свободные
переменные не разрешены в not или retract). Это происходит
вследствие того, что для связывания свободных переменных в
подцели, подцель должна унифицироваться с каким-либо другим
предложением и выполняться. Правильным способом управления
несвязанными переменными подцели внутри not является
использование анонимных переменных.
Вот несколько примеров корректных и некорректных предложений:
likes(bill,Anyone):- % Anyone - выходной аргумент
likes(sue,Anyone),
not(hates(bill,Anyone).
В
этом
примере
Anyone
связывается
посредством
likes(sue,Anyone) до того, как Пролог делает вывод, что hates (bill,
Anyone) не является истиной. Данное предложение работает
корректно.
Если вы измените его таким образом, что обращение к not будет
выполняться первым, то получите сообщение об ошибке: "Free
variable are not allowed in not" (Свободные переменные в not не
разрешены).
likes(bill,Anyone):- % Это не будет работать правильно
not(hates(bill,Anyone)),
likes(sue,Anyone).
Даже если вы замените в not(hates (bill, Anyone)) Anyone на
анонимную переменяю, и предложение, таким образом, не будет
возвращать ошибку, все равно получите неправильный результат.
likes(bill,Anyone):- % Это не будет работать правильно
not(hates(bill,_)),
likes(sue,Anyone).
Это предложение утверждает, что Биллу нравится кто угодно, если
неизвестно ничего о том, кого Билл ненавидит, и если этот "кто-то"
правится Сью. Подлинное предложение утверждало, что Биллу
нравится тот, кто нравится Сью, и при этом Билл не испытывает к
этому человеку ненависти.
Неверное использование предиката not приведет к сообщению об
ошибке или к ошибкам в логике вашей программы. Следующая
программа
pro32_2.pro
является
примером
правильного
использования предиката not.
predicates
likes_shopping(symbol)
has_credit_card (symbol, symbol)
bottomed_out(symbol,symbol)
clauses
likes_shopping(Who):has_credit_card(Who,Card),
not(bottomed_out(Who, Card)),
write(Who," can shop with the ",Card, " credit card.\n").
has_credit_card(chris,visa).
has_credit_card(chris,diners).
has_credit_card(joe,shell).
has_credit_card(sam,mastercard).
has_credit_card(sam,citibank).
bottomed_out(chris,diners).
bottomed_out(sam,mastercard).
bottomed_out(chris, visa) .
goal
likes_shopping(Who).
Текст этой программы можно взять здесь.
Со следующего шага мы начнем рассматривать факты и правила
в качестве процедур.
Шаг 33.
Основы логического программиравания.
Факты и правила в качестве процедур
На этом шаге мы рассмотрим факты и правила в качестве
процедур.
Можно рассматривать правила Пролога как определения процедур.
Например, правило:
likes(bill,Something):likes(cindy,Something)
означает:
"Для того чтобы доказать, что Билл любит что-то, необходимо
доказать, что Синди любит это".
Таким образом, видим, что предикаты типа:
say_hello:write("Hello"),nl.
и
greet:write("Hello,Earthlings!"),
nl.
соответствуют подпрограммам
программировании.
и
функциям
в
других
языках
Вы можете рассматривать даже факты Пролога, как процедуры;
например, факт
likes(bill,pasta)
означает:
"Для того чтобы доказать, что Билл любит pasta, не нужно ничего
делать, и если аргументы Who и What в вашем запросе
likes(Who,What) - свободные переменные, то вы можете присвоить им
значения bill и pasta, соответственно". Далее мы покажем, как
известные процедуры программирования (условное ветвление,
булевы выражения, безусловные переходы и возвращение результата
вычисления) могут быть реализованы в Прологе.
Использование правил для условного ветвления
Одно из основных различий между правилами в Прологе и
процедурами в других языках программирования заключается в том,
что Пролог позволяет задавать множество альтернативных
определений одной и той же процедуры. Это видно по "семейной"
программе pro15_2.pro на шаге 15. Человек может быть предком,
будучи отцом или матерью, поэтому определение предка состоит из
двух правил.
Вы можете использовать множество определений так же, как вы
применяете предложение case в Pascal, задавая множество
альтернативных определений для каждого значения аргумента (или
множества значений аргумента). Пролог же будет перебирать одно
правило за другим, пока не найдет то, которое подходит, и затем
выполнит действие, заданное правилом (как в следующей программе
pro33_1.pro).
predicates
action(integer)
clauses
action (1):nl,
write("You typed 1."),nl.
action(2):nl,
write("You typed two."),nl.
action(3):nl,
write("Three was what you typed."),nl.
action(N):nl,
N<>1,N<>2,N<>3,
write("I don't know that number!"),nl.
goal
write("Type a number from 1 to 3: "),
readint(Choiсe),
action(Choiсe).
Текст этой программы можно взять здесь.
Результат работы программы можно посмотреть на рис.1
Рис.1. Результат работы программы pro33_1.pro
Если пользователь нажмет клавиши <1>, <2> или <3>, action будет
вызвана с coответствующим значением аргумента и будет вызвано
одно из первых трех правил этого примера.
Выполнение проверки в правиле
Посмотрите более внимательно на четвертое правило для action.
Оно будет сопоставлено для любого аргумента, переданного правилу.
Если вы хотите быть уверенными, что оно не напечатает I don't know
that number (Я не знаю такого числа) когда число попадает в
правильный диапазон, - это задача для подцелей
X<>1,
X<>2,
X<>3
где <> обозначает "не равно". Теперь, для того чтобы напечатать "Я
не знаю такого числа", Пролог должен сначала доказать, что X не
равен 1, 2 или 3. Если какая-либо из этих подцелей неуспешна, то
Пролог попытается сделать откат и найти новые альтернативы. Но
так как таких альтернатив нет, то остаток предложения никогда будет
выполнен.
Замечание: Предикат action подразумевает, что Choice уже
связана. Если вы вызывае action со свободной переменной в
качестве аргумента, то компилятор сгенеририрует ошибку.
Отсечение как GoTo
Программа pro33_1.pro не совсем корректна из-за того, что после
выбора и выполнения нужного правила Пролог продолжает поиск
альтернатив.
Вы могли бы сэкономить ресурсы и время, если бы указали, где
нужно прекратить поиск альтернатив, используя отсечение. Это
означает:
"Если вы дошли до этого места, то не нужно производить откаты
внутри этого правила и не нужно проверять остальные альтернативы
этого правила".
Возвращение все еще возможно, но только на более высоком
уровне. Если текущее правило вызывается другими правилами, и
высшие правила имеют альтернативы, они могут быть испробованы.
Но отсечение отбрасывает альтернативы внутри правила и
альтернативы данного правила (предиката).
Используя отсечение (cut), программа pro33_1.pro может быть
переписана следующим образом:
predicates
action(integer)
clauses
action(1):-!,
nl,
write("You typed 1.").
action(2):-!,
nl,
write("You typed two.").
action(3) :-!,
nl,
write("Three was what you typed.").
action(_):write("I don't know that number!").
goal
write("Type a number from 1 to 3: ") ,
readint(Num),
action(Num), nl.
Текст этой программы можно взять здесь.
Отсечение не имеет никакого эффекта, пока оно не будет
выполнено реально. В приведенном выше примере, для того чтобы
выполнить отсечение, Пролог должен войти в правило, содержащее
отсечение и достичь точки, где расположено отсечение.
Отсечение может бьпъ представлено другими примерами:
action(X):Х>3,
!,
write("Too high.").
В этом правиле отсечение не произведет никакого действия, пока
не будет достигнута первая подцель X>3.
Заметьте, что порядок правил здесь имеет значение. В программе
pro33_1.pro вы могли написать правила в любом порядке; только
одно из них сопоставлялось с конкретным числом. Но в примере
pro33_2.pro вы должны быть уверены, что компьютер не сделает
попытки выполнить правило, печатающее "Я не знаю такого числа",
раньше, чем будут испробованы (и не выполнят своих отсечений) все
предыдущие правила.
Отсечения в программе pro33_2.pro иногда называют красными
отсечениями, т. к. они меняют логику программы. Если вы сохраните
проверки X<>1, Х<>2 и Х<>3, изменив программу только вставкой
отсечений в каждом предложении, то вы сделаете зеленые
отсечения. Они экономят время и, тем не менее, оставляют
программу такой же правильной, как и без отсечений. Выигрыш при
этом не так велик, но риск внести ошибку в программу уменьшается.
Отсечение - это мощный, но и опасный оператор Пролога. В этом
отношении oн соответствует предложению GoTo, и остальных языках
программирования он многое позволяет, но делает нашу программу
более трудной для понимания.
Возврат вычисленного значения
Как мы уже видели, правила и факты Пролога могут возвращать
информацию в цель, которая их вызывает. Это делается путем
связывания переменных, которые были ранее не связанными.
Факт:
likes(bill,cindy).
Возвращает информацию в цель
likes(bill,Who).
путем присваивания переменной Who значения cindy.
Правило может возвращать тем же способом и результат
вычислений. В программе pro33_3.pro приведен пример.
predicates
classify(integer,symbol)
clauses
classify(0,zero).
classify(X,negative):X < 0.
classify(X,positive):X > 0.
Текст этой программы можно взять здесь.
Первый аргумент classify должен всегда получать константу или
связанную переменную. Второй аргумент может быть связанной или
свободной переменной, oн сопоставляется с символами zero,
negative, pozitive в зависимости от значения первого аргумента.
Здесь приведены несколько примеров правил, которые могут
возвращать значения.
1. Вы можете узнать, положительно ли число:
classify(45,positive).
да
Так как 45 больше 0, только третье предложение classify может
быть успешным. Оно сопоставляет второй аргумент с positive. Но
второй аргумент уже равен positive, поэтому сопоставление успешно,
и вы получаете ответ yes (да).
2. Если cопоставление неуспешно, вы получаете ответ no (нет):
classilу(45,negative).
нет
Что происходит при этом:



Пролог проверяет первое предложение, но первый аргумент не
равен 0 (а также второй не равен zero);
затем он проверяет второе предложение, связав X с 45, но
проверка X<0 неуспешна;
после этого он проверяет третье предложение, но на этот раз
второй аргумент не совпадает.
3. Для получения правильного ответа, а не yes или nо, вы должны
вызвать classify со свободным вторым аргументом.
classify(45,What).
What=positive
1 решение
Что происходит в этом случае:



цель classify(45,What) не сопоставляется с заголовком первого
предложения, т. к. 45 не сопоставляется с 0. Первый класс
использовать нельзя;
цель classify(45,What) снова сопоставляется с заголовком
следующего предложения, classify(X,negative), связывая X с 45
и negative с What. Но подцель X<0 неуспешна, т. к. X равен 45 и
неверно, что 45<0, поэтому Пролог возвращается из этого
предложения, освобождая созданные связи;
наконец, classify (45,What) сопоставляется с classify
(X,positive), связывая X и 45, а также What и positive, подцель
X>0 правильна. Так как это успешное решение, Пролог не
выполняет поиск с возвратом; он возвращается в вызывающую
процедуру (которая в данном случае цель, которую вы задали).
И поскольку переменная X принадлежит к вызывающей
процедуре, эта процедура может использовать ее значение - в
данном случае автоматически напечатать его.
На следующем шаге мы рассмотрим простые объекты данных.
Шаг 34.
Основы логического программирования.
Простые объекты данных
На этом шаге мы рассмотрим простые объекты данных.
Простой объект данных - это переменная или константа. Не
путайте это значение слова "константа" с символьными константами,
которые вы определяете в разделе constants программы. То, что мы
здесь называем константой, это нечто, идентифицирующее объект,
который нельзя изменять: символ (char), число (integer или real) или
атом (symbol или string).
Переменные как объекты данных
Названия переменных должны начинаться с заглавной буквы (A-Z)
или с символа подчеркивания (_). Символ подчеркивания
представляет анонимную переменную, которая используется в
ситуации "неважно что". В Прологе переменная может связываться с
любым допустимым аргументом Пролога или объектом данных.
Переменные Пролога локальны, а не глобальны. Так, если два
предложения содержат переменную, названную X, то это две
различные переменные. Они могут быть связаны друг с другом, если
совпадут во время унификации, но обычно они не оказывают влияния
друг на друга.
Константы как объекты данных
Константы включают символы, числа и атомы. Опять же, не путайте
константы в данном контексте с символьными константами,
определенными в разделе constants программы. Значение
константы - это ее имя. Так константа 2 может соответствовать
только числу 2, а константа abracadabra - только идентификатору
abracadabra.
Символы
Символы имеют тип char. Печатные символы (ASCII 32-127) - это
цифры (0-9), прописные буквы A-Z, строчные буквы a-z, символы
пунктуации и специальные символы.
Символ-константа записывается в простых кавычках:
'a' '3'
'*' '{'
'W' 'А'
Если же вы хотите записать обратную косую черту или простую
кавычку, как литерную константу, вы должны поставить перед ней
символ обратный слэш \ (управляющий escape-символ):
'\\' backslash '\'' single quote.
Существует набор символьных констант, которые представляют
специальные функции в том случае, если им предшествует
управляющий символ (таблица 1).
Таблица 1. Символьные константы, представляющие
специальные функции
Константа
Описание
' \n'
Новая строка (перевод строки)
' \t'
Табуляция (горизонтальная)
Символьные константы могут также быть записаны своим
десятичным ASCII-кодом после управляющего символа, например:
'\225'
В
'\134' Ж
Числа
Числа могут быть целыми (integer) или вещественными (real).
Вещественные имеют значения от 1е-308 до 1е308 (от 10-308 до 10+308).
Примеры целых и вещественных чисел приведены в таблице 2.
Таблица 2. Целые и вещественные числа
Целые
Вещественные
3
3.
-77
34.96
32034
-32769
-10
4е27
0
-7.4е-296
Атомы
Атомы имеют тип идентификатор (symbol) или строка (string).
Отличие между ними - главным образом вопрос машинного
представления и реализации, и, в основном, оно синтаксически не
заметно. Когда атом передается в качестве аргумента при вызове
предиката, то к какому домену принадлежит атом - symbol или string определяется по тому, как описан этот аргумент в декларации
предиката.
Пролог автоматически преобразует типы между доменами string и
symbol, поэтому вы можете использовать атомы symbol в доменах
string и наоборот. Однако принято считать, что объект в двойных
кавычках принадлежит домену string, а объект, не нуждающийся в
кавычках, домену symbol. Атомы типа symbol - это имена,
начинающиеся со строчной буквы и содержащие только буквы, цифры
и знак подчеркивания.
Атомы типа string выделяются двойными кавычками и могут
содержать любую комбинацию литер, кроме ASCII-нуля (0, бинарный
нуль), который обозначает конец строки атома.
Примеры строк и идентификаторов приведены в таблице 3.
Таблица 3. Строки и идентификаторы
Атомы-идентификаторы
food
Атомы-строки
"Jesse James"
rick_Jones_2nd
"123 Pike street"
fred_Flintstone_1000_Bc_Bedrock "jon"
a
"a"
new_york
"New York"
Так как string/symbol взаимозаменяемы, их отличие не
существенно. Однако имена предикатов и функторы для составных
объектов должны соответствовать синтаксическим соглашениям
домена symbol.
На следующем шаге мы рассмотрим составные объекты
данных и функторы.
Шаг 35.
Основы логического программирования.
Составные объекты данных и функторы
На этом шаге мы рассмотрим составные объекты данных и
функторы.
Составные объекты данных позволяют интерпретировать
некоторые части информации как единое целое таким образом, чтобы
затем можно было легко разделить их вновь. Возьмем, например, дату
"октябрь 15, 1991". Она состоит из трех частей информации - месяц,
день и год. Представим ее на рис. 1, как древовидную структypy.
Рис.1. Древовидная структура даты
Вы можете сделать это, объявив домен, содержащий составной
объект date:
domains
date_cmp = date(string,integer,integer)
а затем просто записать:
D=date("October",15,1991).
Такая запись выглядит как факт Пролога, но это не так - это объект
данных, который вы можете обрабатывать наряду с символами и
числами. Он начинается с имени, называемого функтором (в данном
случае date), за которым следуют три аргумента.
Обратите внимание, что функтор в Прологе - не то же самое, что
функция и других языках программирования; это просто имя, которое
определяет вид составного объекта данных и объединяет вместе его
аргументы.
Замечание. Функтор не обозначает, что будут выполнены какиелибо вычисления.
Аргументы составного объекта данных могут сами быть составными
объектами. Например, вы можете рассматривать чей-нибудь день
рождения (рис. 2), как информацию со следующей структурой:
Рис.2. Древовидная структура даты рождения
На языке Пролог это выглядит следующим образом:
birthday(person("Leo", "Jensen"),date("Apr ", 14 , 1991))
У составного объекта birthday в этом примере есть две части:
объект person ("Leo","Jensen") и объект date("Apr",14,1900).
Функторами для этих объектов будут person и date.
Унификация составных объектов
Составной объект может быть унифицирован с простой переменной
или с составным объектом (возможно, содержащим переменные в
качестве частей во внутренней структуре), который ему соответствует.
Это означает, что составной объект можно использовать для того,
чтобы передавать целый набор значений как единый объект, и затем
применять унификацию для их разделения. Например:
date("April",14,1960)
сопоставляется с X и присваивает X значение date("April ",14, 1960).
Также
date("April",14,1960)
сопоставляется с date(Mo,Da,Yr)
Mo="April", Da=14 и Yr = 1960.
и
присваивает
переменным
Использование знака равенства для унификации составных
объектов
Пролог осуществляет унификацию в двух случаях. Во-первых,
когда цель сопоставляется с заголовком предложений. Во-вторых,
через знак равенства (=), который является инфиксным предикатом
(предикатом, который расположен между своими аргументами, а не
перед ними).
Фактически, Пролог выполняет операцию присваивания для
унификации объектов по разные стороны знака равенства. Это
свойство полезно для нахождения значений аргументов составного
объекта. Например, программа pro35_1.pro проверяет, совпадают ли
фамилии у двух людей, и затем дает второму человеку тот же адрес,
что и у первого.
domains
person=person(name,address)
name=name(first,last)
address=addr(street,city,state)
street=street(number,street_name)
city,state,street_name=string
first,last=string
number=integer
goal
P1=person(name(jim,mos), addr(street(5,"1st st"), igo, "CA")),
P1=person(name(_,mos), Address),
P2=person(name(jane,mos), Address),
write("P1=",P1) ,nl,
write("P2=",P2),nl.
Текст этой программы можно взять здесь.
Использование нескольких значений как единого целого
Составные объекты могут рассматриваться в предложениях
Пролога как единые объекты, что сильно упрощает написание
программ. Рассмотрим, например, факт:
owns(john,book("From Here to Eternity","James Jones")).
в котором утверждается, что у Джона есть книга "From Here to
Eternity" (Отсюда в вечность), написанная James Jones (Джеймсом
Джонсом). Аналогично можно записать:
owns(john,horse(blacky)).
что означает:
John owns a horse named blacky.(У Джона есть лошадь Блеки.)
Составными объектами в этих двух примерах являются:
book("From Here to Eternity","James Jones")
и
horse(blacky)
Если вместо этого описать только два факта:
owns(john,"From Here to Eternity"),
owns(john,blacky).
то нельзя было бы определить, является ли blacky названием книги
или именем лошади. С другой стороны, можно использовать первый
компонент составного объекта - функтор для распознавания
различных объектов. Этот пример использует функторы book и horse
для указания разницы между объектами.
Замечание. Составные объекты состоят из функтора и
объектов,
принадлежащих
этому
функтору,
например:
functor(objectl,object2,..., objectN).
Пример использования составных объектов
Важная особенность составных объектов состоит в том, что они
позволяют легко передавать группы величин, как один аргумент.
Рассмотрим в качестве примера ведение телефонной базы данных.
Допустим, вы хотите включить и базу дни рождения ваших друзей и
родственников. Для чтого пришлось бы написать программу, часть
которой приведена ниже:
predicates
phone_list(symbol First, symbol Last, symbol Phone,
symbol Month, integer Day, integer Year)
clauses
phone_list(ed,willis,422_02_08,aug,3,1955).
phone_list(chris,grahm,433_99_06,may,12,1962).
Обратите внимание, что в факте phone_list шесть аргументов, пять
из них могут быть разбиты (рис. 3) на два составных объекта.
Рис.3. Разбивка составных объектов
Может оказаться более полезным представлять факты так, чтобы
они отражали эти составные объекты данных. Вернувшись на шаг
назад, видим, что person (лицо) - это отношение, а имя и фамилия объекты. Также, birthday (день рождения) - это отношение между
тремя аргументами: месяцем, днем и годом. В представлении
Пролога они могут быть записаны следующим образом:
person(First_name,Last_name)
birthday(Month,Day,Year)
Вы можете теперь переписать свою маленькую базу данных с
включением этих составных объектов как части вашей базы данных.
domains
name=person(symbol First,symbol Last)
birthday = b_date(symbol Month, integer Day, integer Year)
ph_num = symbol % Phone_number
predicates
phone_list(name, ph_num, birthday)
clauses
phone_list(person(ed,willis),"422-02-08",b_date(aug,3,1955)).
phone_list(person(chris,grahm),"433-99-06",b_date(may,12,1962)).
В эту программу введены два определения составных доменов. Мы
рассмотрим некоторые подробности этих составных структур данных
далее, а сейчас остановимся на применении таких составных
объектов.
Предикат phone_list теперь содержит три аргумента, что
отличается от шести предыдущем примере. Иногда разбиение данных
и составном объекте делает более ясной логику программы и может
помочь в обработке данных. А теперь добавим несколько правил в
нашу маленькую программу. Допустим, вы хотите создать список
людей, у которых день рождения в этом месяце. Ниже приведена
программа pro35_2.pro, которая решает эту задачу, используя
встроенный предикат date для получения даты из внутреннего
календаря компьютера. Предикат date возвращает текущие год, месяц
и день из календаря компьютера.
domains
name=person(symbol,symbol) % (первый,последний)
birthday=b_date(symbol,integer,integer) % (месяц,день,год)
ph_num=symbol % телефонный номер
predicates
phone_list(name,ph_num,birthday)
get_months_birthdays()
convert_month(symbol,integer)
check_birthday_month(integer,birthday)
write_person(name)
clauses
get_months_birthdays:write("************ This Month's Birthday List ***********+*"),nl,
write(" First name\t\t Last Name\n"),
write("*****************************************************"),nl,
date(_, This_month, _), % получить месяц из системных
часов
phone_list(Person, _, Date),
check_birthday_month(This_month, Date),
write_person(Person) ,
fail.
get_months_birthdays:write("\n\n Press any key to continue: "),nl,
readchar(_).
write_person(person(First_name,Last_name)):write(" ",First_name,"\t\t
",Last_name),nl.
check_birthday_month(Mon,b_date(Month,_,_)):convert_month(Month,Monthl),
Mon = Monthl.
phone_list(person(ed,willis),"7 67-84 63",b_date(jan, 3,1955)).
phone_list(person(benjamin,thomas),"4388400",b_date(feb,5,1985)).
phone_list(person(ray,william),"555-5653",b_date(mar,3,1935)).
phone_list(person(thomas,alfred),"767-2223",b_date(apr,29,1951)).
phone_list(person(chris,grahm), "555-1212",b_date(may,12,1962)).
phone_list(person(dustin,robert),"438-8400",b_date(jun,17,1980)).
phone_list(person(anna,friend),"767-8463",b_date(jun, 20,1986)).
phone_list(person(brandy,rae),"555-5653",b_date(jul,16,1981)).
phone_list(person(naomi,friend),"767-2223",b_date(aug,10,1981)).
phone_list(person(christina,lynn),"438-8400",b_date(sep,25,1981)).
phone_list(person(kathy,ann),"438-8400",b_date(oct,20,1952)).
phone_list(person(elizabeth,ann),"555-1212",b_date(nov,9,1984)).
phone_list(person(aaron,friend) ,"767-2223",b_date(nov,15,1987)).
phone_list(person(jennifer,caitlin),
"4388400",b_date(dec,31,1981)).
convert_month(jan,1).
convert_month(feb,2).
convert_month(mar,3).
convert_month(apr,4).
convert_month(may,5).
convert_month(jun,6).
convert_month(jul,7).
convert_month(aug,8).
convert_month(sep,9).
convert_month(oct,10).
convert_month(nov,11).
convert_month(dec,12).
goal
get_months_birthdays().
Текст этой программы можно взять здесь.
Результат работы программы можно посмотреть на рис.4
Рис.4. Результат работы программы pro35_2.pro
Загрузите и запустите эту программу.
1. Программа использует окно для вывода результата.
2. Помещает в окне заголовок, помогающий понять результат.
3. Использует в предикате get_month__birthdays встроенный
предикат date.
4. Кроме того, программа производит поиск в базе данных и
печатает список людей, родившихся в текущем месяце. Сначала
ищется первый человек в базе данных. Вызов phone_list
(Person,_,Date) помещает имя и фамилию человека в
переменную Person, помещая функтор person целиком в
Person, а день рождения - в переменную Date.
Заметим, что необходимы две переменные: одна для
хранения полного имени человека, а другая - для хранения дня
рождения. Это достигается за счет использования составных
объектов.
5. Ваша программа может теперь передавать день рождения
человека просто путем передачи переменной Date. Это
происходит в следующей подцели, где программа передает
текущий месяц (представленный целым числом) и день
рождения в предикат check_birthday_month.
6. Пролог вызывает предикат check_birthday_month с двумя
переменными: первая переменная связана с целым, а вторая - с
термом birthday. В заголовке правила, которое определяет
check_birthday_month,
This_month,
сравнивается
с
переменной Mon. Второй аргумент, Date, сопоставляется с
b_date(Month,_,_).
Так как нас интересует месяц рождения человека,
используются анонимные переменные для даты и года
рождения.
7. Предикат check_birtday_month сначала превращает символ
месяца в целое число. После того, как это сделано, Пролог
может сравнить значение текущего месяца с месяцем рождения
человека.
Если
это
сравнение
удачно,
подцель
check_birthday_month завершается успешно и обработка
продолжается. Если сравнение неуспешно (человек родился в
другом месяце), Пролог откатывается для поиска другого
решения задачи.
8. Следующая подцель, которую нужно обработать, - write_person.
Лицо, данные которого нужно обработать, имеет день рождения
в этом месяце, и поэтому в отчет попадают только правильные
данные. После распечатки информации предпожение терпит
неуспех, что вызывает поиск с возвратом.
9. Поиск с возвратом всегда возвращается к последнему
неудовлетворенному вызову и пытается его удовлетворить. В
данной программе последний неудовлетворенный вызов phone_list. Поэтому программа будет искать другое лицо,
которое может быть обработано. Если в базе данных нет больше
людей, текущее предложение терпит неуспех, Пролог пытается
доказать этот вызов, просматривая базу данных дальше, но т. к.
есть еще предложение get_month_birthdays, Пролог пытается
доказать его, доказав подцели этого предложения.
На следующем шаге мы рассмотрим объявления составных
доменов.
Шаг 36.
Основы логического программирования.
Объявление составных доменов
На этом шаге мы рассмотрим объявление составных доменов.
Рассмотрим, как определяются составные домены. После
компиляции программы, которая содержит следующие отношения:
owns(john,book("From Here to Eternity","James Jones")).
и
owns(john,horse(blacky)).
вы должны послать системе запрос в следующем виде:
owns(john,X).
Переменная X может быть связана с различными типами объектов:
книга, лошадь и, возможно, другими объектами, которые вы
определите. Отметим, что теперь вы не можете более использовать
старое определение предиката owns:
owns(symbol,symbol)
Второй элемент более не является объектом типа symbol. Вместо
этого вы можете дать новое определение этого предиката:
owns(name,articles)
Домен articles в разделе domains можно описать так:
domains
articles=book(title,author);
horse(name)% Articles - это books или horses
title,author,name=symbol
Точка с запятой читается как "или". В этом случае возможны два
варианта: книга будет определяться своим заглавием и автором, а
лошадь будет распознаваться своим именем. Домены title, author и
name имеют стандартный тип symbol.
К определению домена легко могут быть добавлены другие
варианты. Например, articles может также включать лодку, дом,
чековую книжку. Лодку можно определить функтором без
присоединенных к нему аргументов. С другой стороны, вы можете
включить платежный баланс, как часть чековой книжки. Определение
домена articles расширится до:
Articles=book(title,author);
horse(name);
boat;
bankbook(balance)
title,author,name=symbol
balance=real
Ниже приведена полная программа pro36_1.pro, которая
показывает, как составные объекты из домена articles могут
использоваться в фактах, которые определяют предикат owns.
domains
articles=book(title,author);
horse(name);
boat;
bankbook(balance)
title,author,name=symbol
balance=real
predicates
owns(name,articles)
clauses
owns(john,book("A friend of the family","Irwin Shaw")).
owns(john,horse(blacky)).
owns(john,boat).
owns(john,bankbook(1000)).
Текст этой программы можно взять здесь.
А теперь загрузите программу в Пролог и запустите ее, задав цель:
owns(john,Thing).
Пролог ответит:
Thing=book("A friend ot the family","Irwin Shaw")
Thing=horse("blacky")
Thing=boat
Thing=bankbook(1000)
4 решения
Ниже дано общее представление о том, как декларировать домены
для составных объектов:
domain=alternative1 (D Name,D Name, ...); alternative2(D Name,D
Name, ...); ...
Здесь alternative1 и aiternative2 - допустимые (но различные)
функторы. Запись (D, D, ...) представляет список имен доменов,
которые объявлены где-то в программе или являются стандартными
типами доменов (такими как symbol, integer, real и др.).
Необязательные параметры Name могут использоваться для
комментария
имен
аргументов;
они
будут
игнорироваться
компилятором.
Замечания:



Альтернативы разделяются точкой с запятой.
Каждая альтернатива состоит из функтора и, возможно,
списка доменов соответствующих аргументов.
Если функтор не имеет аргументов, вы можете записать в
вашей программе alternativeN или alternativeN (). В наших
шагах используется первый вариант синтаксиса.
Многоуровневые составные объекты
Пролог позволяет конструировать
нескольких уровнях. Например, в:
составные
объекты
на
book("The Ugly Duckling","Andersen")
вместо фамилии автора вы можете использовать новую структуру,
которая описывает автора более детально, включая имя и фамилию.
Вызывая функтор для нового объекта author (автор), вы можете
изменить описание книги на:
book("The Ugly Duckling", author("Hans Christian", "Andersen"))
В старом определении домена
book(title,author)
вторым аргументом функтора book был author. Но старое
определение
author=symbol
может включать только одно имя, а этого уже недостаточно.
Определим теперь author, как составной объект, состоящий из имени
и фамилии автора.
Это достигается с помощью декларации следующего домена:
author=author(first_name,last_name)
что приводит к следующим определениям:
domains
articles=book(title,author);
% Первый уровень
author=author(first_name,last_name) % Второй уровень
title,first_name,last_name=symbol
% Третий уровень
При использовании составных объектов со многими уровнями часто
помогает такое "дерево" (рис.1):
Рис.1. Дерево многоуровневого составного объекта
Декларация домена объявляет только один уровень дерева, а не
целое дерево. Например, book не может быть описана таким
предложением:
domains
book=book(title,author(first_name,last_name)) % Неправильно
Рассмотрим в качестве еще одного примера составных объектов
грамматический разбор структуры предложения:
У Элен есть книга.
Наиболее простая структура английского предложения состоит из
существительного и группы сказуемого:
предложение
=предложение(существительное,группа_сказуемого)
Существительное - это просто слово:
существительное=существительное(слово)
А группа сказуемого состоит из глагола и группы существительного
или из одного глагола.
группа_сказуемого=группа_сказуемого(глагол,существительное);
глагол = глагол(слово)
Используя
эти
определения
доменов
(предложение,
существительное, сказуемое и глагол), предложение у Элен есть книга
будет записано:
предложение(существительное(Элен),группа_сказуемого(глагол(и
меет),существительное(книга)))
Рис.2. Дерево предложения
Соотнетствующее дерево представлено на рис. 2.
На следующем шаге мы рассмотрим определение составных
смешанных доменов.
Шаг 37.
Основы логического программирования.
Определение составных смешанных доменов
На этом шаге мы
смешанных доменов.
рассмотрим
определение
составных
В этом разделе мы обсудим различные типы определений доменов,
которые можно добавлять к программам. Эти объявления позволят
использовать предикаты, которые имеют возможность:



получать аргумент более чем одного типа;
получать различное количество аргументов, все разных
указанных типов;
получать различное количество аргументов, некоторые из
которых могут быть более чем одного из возможных типов.
Аргументы множественных типов
Для того чтобы позволить предикатам получать аргументы, которые
содержат информацию различных типов, вам нужно добавить
описание функтора.
В следующем примере предложение уоur_age (ваш возраст)
получает аргумент типа age (возраст), который может иметь тип
string, real или integer.
domains
age = i(integer);r(real);s(string)
predicates
your_age(age)
clauses
your_age(i(Age)):write(Age).
your_age(r(Age)):write(Age).
your_age(s(Age)):write(Age).
Пролог не допустит следующего описания домена:
domains
age=integer;real;string% He разрешено
Списки
Предположим, вы хотите заполнить расписание занятий по
различным
предметам,
которые
могут
проводить
разные
преподаватели. Вы можете написать следующую программу:
predicates
teacher(symbol First_name,symbol Last_name,symbol Class)
% учитель(имя,фамилия,класс)
clauses
teacher(ed,willis,englishl).
teacher(ed,willis,mathl).
teacher(ed,willis,historyl).
teacher(marу,maker,history2).
teacher(marу,maker,math2).
teacher(chris,grahm,geometry).
Здесь вы должны повторять имя учителя для каждого предмета,
который он или она ведет. Для каждого предмета вам приходится
добавлять факт к базе данных. Хотя это и совершенно правильно в
такой ситуации, но можно найти школу, где преподают сотни
предметов; такой тип данных становится слишком сложным. Здесь
было бы удобно создать аргумент для предиката, который содержит
одно или несколько значений.
Список в Прологе - это как раз то, что нужно. В следующей
программе аргумент class (класс) имеет тип "список". Мы покажем
здесь, как список представляется в Прологе,
работающие со списками, опишем на шаге 42.
а
предикаты,
domains
classes=symbol* % объявляем домен-список
predicates
teacher(symbol First,symbol Last,symbol Classes)
% имя,фамилия,предметы
clauses
teacher(ed,willis,[englishl,mathl,historyl]).
teacher(marу,maker,[history2,math2]).
teacher(chris,grahm,[geometry]).
В этом примере текст программы более краток и понятен, чем в
предыдущем. Обратите внимание на определение домена:
domains
classes=symbol*
Звездочка (*) обозначает, что classes - это
идентификаторов. Так же просто объявить список целых:
domains
integer_list=integer*
список
После того, как домен определен, его очень просто использовать;
поместите его в аргумента в предикат в разделе predicates. Ниже
приведен пример использования списка целых:
domains
integer_list=integer*
predicates
test_scores(symbol First,symbol Last,integer_list Test_Scores)
/*список_результатов(имя,фамилия,список_результатов */
clauses
test_scores(lisa,lavender,[86,91,75]).
test_scores(libby,dazzner,[79,75]).
test_scores (jeff,zheutlin,[]).
В случае jeff zheutlin обратите внимание, что список может вообще
не иметь элементов.
На следующем шаге мы рассмотрим повтор.
Шаг 38.
Основы логического программирования.
Повтор
На этом шаге мы рассмотрим процесс повторения.
Компьютеры способны повторять одно и то же действие снова и
снова, Пролог может выражать повторение как в процедурах, так и в
структурах данных. Идея повторяющихся структур данных может
показаться странной, но Пролог позволяет создвать структуры
данных, размер которых не известен во время создания.
Процесс повторения
Программисты на языках Pascal или С, которые начинают
использовать Пролог, часто испытывают разочарование, обнаружив,
что язык не имеет конструкций For, While или Repeat. He существует
прямого способа выражения повтора. Пролог обеспечивает только
два вида повторения - откат, с помощью которого осуществляется
поиск многих решений в одном запросе, и рекурсию, в которой
процедура вызывает сама себя.
Как будет показано, этот недостаток не снижает мощи Пролога.
Фактически, Пролог распознает специальный случай рекурсии хвостовую рекурсию - и компилирует ее в оптимизированную
итерационную петлю. Это означает, что хотя программная логика и
выражается рекурсивно, скомпилированный код так же эффективен,
как если бы программа была написана на Pascal.
Снова поиск с возвратом
Когда выполняется процедура поиска с возвратом (откат),
происходит поиск другого решения целевого утверждения. Это
осуществляется путем возврата к последней из проверенных
подцелей, имеющей альтернативное решение, использования
следующей альтернативы этой подцели и новой попытки движения
вперед. Программа pro38_1.pro показывает, как иcпольpзуется поиск
с возвратом для выполнения повторяющихся операций. Эта
программа выводит на печатающее устройство все возможные
решения запроса.
predicates
country(symbol)
print_countries
clauses
country("England").
country("France").
country("Germany").
country("Denmark").
print_countries:country(X),
write(X), % записать значение Х
nl,% начать новую строку
fail.
print_countries.
goal
print_countries.
Текст этой программы можно взять здесь.
Предикат country составляет список названий различных стран,
которые являются решениями для поставленного целевого
утверждения:
country(X)
Затем предикат print_countries отпечатывает все эти решения. Он
определен следующим образом:
print_countries:country(X),
write(X),
nl,
fail.
print_countries.
Первое предложение гласит: "Чтобы отпечатать страны, надо найти
решения country(X), написать значение X, начать новую строку и
инициировать неудачное завершение".
В этом случае "неудачное завершение" (fail) означает:
"Принять, что решение поставленного целевого утверждения не
было достигнуто, поэтому вернуться назад и искать альтернативное
решение".
Встроенный предикат fail всегда вызывает "неудачное завершение",
но можно было бы вызвать поиск с возвратом и обратившись к таким
целевым
утверждениям,
как,
например:
5=2+2
или
country(shangri_la).
Первый проход выполняется до конца, X присваивается значение
England, которое выводится на печать. Затем, наткнувшись на fail,
компьютер возвращается в начало. Так как nl или write(X) не имеют
альтернативных решений, то компьютер
следующего решения предиката country(X).
перходит
к
поиску
При последнем выполнений country(X), ранее свободная
переменная X стала связанной. Поэтому, снова выполняя country(X),
компьютер освобождает переменную X находит альтернативное
решение предиката country(X) и присваивает X новое значение.
После этого обработка продолжается, и название другой страны
выводится на печать.
В конце концов, первое предложение будет проверено для всех
альтернатив. Останется выполнить только второе предложение того
же предиката, что происходит без каких-либо дополнительных
сложностей. И наконец, целевое утверждение print_countries
успешно завершится следующим результатом:
England
France
Germany
Denmark
yes
Если бы второго предложения не было, то целевое утверждение
print_countries не исполнилось бы, и последнее сообщение было бы
nо, при этом результат был бы таким же.
Предварительные и последующие операции
Отметим, что программа, которая находит решения для целевого
утверждения, может выполнять какие-либо предварительные или
завершающие операции. Например, и нашем примере программа
могла бы:
1. Напечатать Some delightful places to live are... (Некоторые
восхитительные места для проживания...).
2. Напечатать все решения для country(X).
3. Завершить печать фразой And maybe others (Могут быть и
другие).
Заметьте, что print_countries, определенное в предыдущем
примере, уже содержит , предложение вывести на печать все решения
country(X) и отпечатать завершающее сообщение.
Первое предложение для print_countries соответствует шагу 2 и
выводит на печать все решения. Его второе предложение
соответствует шагу 3 и просто успешно завершает целевое
утверждение (потому что первое предложение всегда в режиме fail "неудачное завершение"). Можно было бы изменить первое
предложение в программе pro38_1.pro.
print_countries:write("And maybe others."),nl.
которое выполнило бы шаг 3, как указано. А что можно сказать о шаге
1? В нем нет смысла, когда print_countries содержит только 2
предложения. Но в предикате может быть и три предложения:
print_countries:write("Some delightful places to live are"),nl,
fail.
print_countries:country(X),
write(X),nl,
fail.
print_countries:write("And maybe others."),nl.
Наличие fail в первом предложении важно, поскольку он
обеспечивает после выполнения первого предложения возврат и
переход ко второму предложению. Кроме того, это важно, потому что
предикаты write и nl не образуют альтернатив. Строго говоря, первое
предложение проверяет все возможные решения перед тем, как
завершиться неуспехом.
Такая структура из трех предложений более удобна по сравнению с
общепринятым подходом. Однако вы можете захотеть еще упростить
код и применить такой метод для составления программы:
print_countries_with_captions:write("Some delightful places to live are"),nl,
print_countries,
write("And maybe others."),nl.
print_countries:country(X),
write(X),nl,
fail.
Но такая программа работать не будет. Проблема в том, что, как
написано в последнем примере, print_countries всегда будет
"неудачно завершаться", и print_ countries_with_captions никогда не
вызовет выполнения подцелей, следующих за print_countries. В
результате фраза And maybe others никогда не напечатается.
Все, что необходимо сделать для исправления ошибки, - это
восстановить второе предложение print_countries для предиката
print_countries в его первоначальное состояние. Если вы хотите,
чтобы целевое утверждение print_countries_with_captions было
выполнимым, то print_countries должно иметь, по крайней мере, одно
предложение, не содержащее fail.
На следующем шаге мы рассмотрим использование отката с
петлями.
Шаг 39.
Основы логического программирования.
Ипользование отката с петлями
На этом шаге мы рассмотрим ипользование отката с петлями.
Поиск с возвратом является хорошим способом определить все
возможные решения целевого утверждения. Но, даже если ваша
задача не имеет множества решений, можно испопользовать поиск с
возвратом для выполнения итераций. Просто определите предикат с
двумя предложениями:
repeat.
repeat:repeat.
Этот прием демонстрирует создание структуры управления
Пролога (программа pro39_1.pro), которая порождает бесконечное
множество решений, а каким образом, - поймете позже, прочитав
раздел о хвостовой рекурсии. Цель предиката repeat - допустить
бесконечность поиска с возвратом (бесконечное количество откатов).
/*Ипользование repeat для сохранения введенных символов и
печатать их до тех пор, пока пользователь не нажмет Tab
(Табуляция).*/
predicates
repeat
typewriter
clauses
repeat.
repeat:repeat.
typewriter:repeat,
readchar(C),% Читать символ, его значение присвоить С
write(С),
С = '\t',% Символ табуляция (Tab)? или неуспех
!.
goal
typewriter () ,nl.
Текст этой программы можно взять здесь.
Программа pro39_1.pro показывает, как работает repeat. Правило
typewriter:- описывает процесс приема символов с клавиатуры и
отображения их на экране, пока пользователь не нажмет клавишу Tab.
Правило typewriter работает следующим образом:
1. Выполняет repeat (который ничего не делает, но ставит точку
отката).
2. Присваивает переменной C значение символа.
3. Отображает C.
4. Проверяет, соответствует ли C коду табуляции.
5. Если соответствует, то - завершение. Если нет - возвращается к
точке отката и ищет альтернативы. Так как ни write, ни readchar
не являются альтернативами, постоянно происходит возврат к
repeat, который всегда имеет альтернативные решения.
6. Теперь обработка снова продвигается вперед: считывает
следующий символ, отображает его и проверяет на соответствие
коду табуляции.
Заметьте, кстати, что C теряет свое значение после отката в
позицию перед вызовом предиката readchar(С), который связывает
переменную C. Такой тип освобождения переменной существенен,
когда
поиск с возвратом применяется для определения
альтернативных решений целевого утверждения, но не эффективен
при использовании поиска с возвратом в других целях. Смысл в том,
что хотя поиск с возвратом и может повторять операции сколько
угодно раз, он не способен "запомнить" что-либо из одного повторения
для другого.
Замечание: Все переменные теряют свои значения, когда
обработка откатывается в позицию, предшествующую тем
вызовам предикатов, которые эти значения устанавливали.
Не существует простого способа сохранения значений счетчика
числа повторений петли или любых других значений, вычисляемых
при выполнении циклов повторения.
Рекурсивные процедуры
Другой способ организации повторений - рекурсия. Рекурсивная
процедура - это процедура, которая вызывает сама себя. В
рекурсивной процедуре нет проблемы запоминания результатов ее
выполнения, потому что любые вычисленные значения можно
передавать из одного вызова в другой как аргументы рекурсивно
вызываемого предиката.
Логика рекурсии проста для осуществления. Забудьте на
мгновение, что компьютер последовательно проходит ячейки памяти
одну за другой, и представьте себе ЭВМ, способную "понять":
Найти факториал числа N:
Если N равно 1,то факториал равен 1
Иначе найти факториал N-1 и умножить его на N.
Этот подход означает следующее: чтобы найти факториал 3, вы
должны найти факториал 2, а чтобы найти факториал 2, вы должны
вычислить факториал 1. Вы можете найти факториал 1 без
обращения к другим факториалам, поэтому повторения не начнутся.
Если у вас есть факториал 1, то умножаете его на 2, чтобы получить
факториал 2, а затем умножаете полученное на 3, чтобы получить
факториал 3.
В Прологе это выглядит так:
factorial(1,1):-!.
factorial(X,FactX):factorial(Y,FactY),
Y=X-1,
factorial(Y,FactY),
FactX=X*FactY.
Полностью осуществление такого подхода к поставленной задаче
представлено в программе pro39_2.pro.
/*Рурсивная программа вычисления факториалов.
Рекурсия обычная, не хвостовая.*/
predicates
factorial(integer,real)
clauses
factorial(1,1):-!.
factorial(X, FactX) :Y=X-1,
factorial(Y,FactY),
FactX = X*FactY.
goal
factorial(X,FactX),
write(FactX).
Текст этой программы вы можите взять здесь.
Как компьютер выполняет предикат factorial в середине обработки
предиката factorial? Если вы вызываете factorial с Х=3, то factorial
обратится к себе с Х=2. Будет ли тогда Х иметь два значения, или
второе значение затирает первое, или...?
Дело в том, что компьютер создает новую копию предиката factorial
таким образом, что factorial становится способным вызывать сам себя
как полностью самостоятельную процедуру. При этом, конечно, код
выполнения не будет копироваться, но все аргументы и
промежуточные переменные копируются.
Информация хранится в области памяти, называемой стековым
фреймом (stack frame) или просто стеком (stack), который создается
каждый раз при вызове правила, когда выполнение правила
завершается, занятая его стековым фреймом память овобождается
(если это не недетерминированный откат), и выполнение
продолжается в стековом фрейме правила-родителя.
Преимущества рекурсии
Рекурсия имеет три основных преимущества:



она может выражать алгоритмы, которые нельзя удобно
выразить никаким другим образом;
она логически проще метода итерации;
она широко используется в обработке списков.
Рекурсия - хороший способ для описания задач, содержащих в себе
подзадачу такого же типа. Например, рекурсивная сортировка (для
сортировки списка, он разделяется на части, части сортируются и
затем объединяются вместе).
Логически рекурсивным
алгоритмам
присуща
структура
индуктивного математического доказательства. Приведенная выше
рекурсивная программа вычисления факториала pro39_2.pro
описывает бесконечное множество различных вычислений с помощью
всего лишь двух предложений. Это позволяет легко увидеть
правильность этих предложений. Кроме того, правильность каждого
предложения может быть изучена независимо от другого.
На следующем шаге мы начнем рассматривать оптимизацию
хвостовой рекурсии.
Шаг 40.
Основы логического программирования.
Оптимизация хвостовой рекурсии
На этом шаге мы рассмотрим оптимизацию хвостовой
рекурсии.
У рекурсии есть один большой недостаток - она съедает память.
Всякий раз, когда одна процедура вызывает другую, информация о
выполнении вызывающей процедуры должна быть сохранена для
того, чтобы она (вызывающая процедура) могла, после выполнения
вызванной процедуры, возобновить выполнение на том же месте, где
остановилась. Это означает, что если процедура вызывает себя 100
раз, то 100 различных состояний должно быть записано одновременно
(состояния выполнения решения сохраняются в стековом фрейме).
Максимальный размер стека у 16-битных платформ, работающая под
DOS, составляет 64 Кбайт, что позволяет разместить максимум 3000
или 4000 стековых фреймов. На 32-битных платформах стек
теоретически может возрасти до нескольких гигабайт; но здесь
проявятся другие системные ограничения, прежде чем стек
переполнится. Что же можно сделать, чтобы избежать использования
столь большого стекового пространства?
Рассмотрим специальный случай, когда процедура может вызвать
себя без сохранения информации о своем состоянии. Что, если
вызывающая процедура не собирается возобновлять свое
выполнение после завершения вызванной процедуры?
Предположим, что процедура вызывается последний раз, т. е. когда
вызванная процедура завершит работу, вызывающая процедура не
возобновит свое выполнение. Это значит, что вызывающей процедуре
не нужно сохранять свое состояние, потому что эта информация уже
не понадобится. Как только вызванная процедура завершится, работа
процессора должна идти в направлении, указанном для вызывающей
процедуры после ее выполнения.
Например, допустим, что процедура А вызывает процедуру B, а B C в качестве своего последнего шага. Когда B вызывает C, B не
должна больше ничего делать. Поэтому, вместо того чтобы сохранить
в стеке процедуры B информацию о текущем состоянии C, вы можете
переписать старую сохраненную информацию о состоянии B (которая
больше не нужна) на текущую информацию о C, сделав
соответствующие изменения в хранимой информации. Когда C
закончит выполнение, она будет считать, что она вызвана
непосредственно процедурой А.
Предположим, что на последнем шаге выполнения процедура B
вместо процедуры C вызывает себя. Получается, что когда B
вызывает B, стек (состояние) для вызывающей B должен быть
заменен стеком для вызванной B. Это очень простая операция, просто
аргументам присваиваются новые значения и затем выполнение
процесса возвращается в начало процедуры B. Поэтому, с
процедурном точки зрения, происходящее очень похоже на всего
лишь обновление управляющих переменных в цикле.
Эта операция называется оптимизацией хвостовой рекурсии
(tail recursion optimization) или оптимизацией последнего вызова
(last-call optimization). Обратите внимание, что по техническим
причинам оптимизация последнего вызова неприменима к
рекурсивным функциям.
Как задать хвостовую рекурсию
Что значит фраза "одна процедура вызывает другую, выполняя
свой самый последний шаг"? На языке Пролог это значит:


вызов является самой последней подцелью предложения;
ранее в предложении не было точек возврата.
Ниже приводится удовлетворяющий обоим условиям пример:
count(N):write(N),
nl,
NewN=N+l,
count(NewN).
Эта процедура является хвостовой рекурсией, которая вызывает
себя без резервирования нового стекового фрейма, и поэтому не
истощает запас памяти. Как показывает программа pro40_1.pro, если
вы дадите ей целевое утверждение
count(0).
то предикат count будет печатать целые числа, начиная с 0, и никогда
не остановится. В конечном счете произойдет целочисленное
переполнение, но остановки из-за истощения памяти не произойдет.
/* Программа с хвостовой рекурсией,
память */
predicates
count(integer)
clauses
которая не истощает
count(N):write(N),
NewN=N+1,
count(NewN).
goal
nl,
count(0).
Текст этой программы вы можите взять здесь.
Из-за чего возникает не оптимизированная хвостовая рекурсия
Выше был продемонстрирован правильный пример использования
хвостовой рекурсии, программа pro40_2.pro показывает три
ошибочных способа организации хвостовой рекурсии.






Если рекурсивный вызов - не самый последний шаг, процедура
не является хвостовой рекурсией. Например:
badcountl(X):write(X),
NewX=X+1,
badcountl(NewX),
nl.
Каждый раз, когда badcount1 вызывает себя, стек должен
быть сохранен для того, чтобы обработку можно было вернуть к
вызывающей процедуре, которая должна выполняться до nl.
Поэтому она сделает всего несколько тысяч рекурсивных
вызовов до исчерпания памяти.








Другой
способ
сделать
хвостовую
рекурсию
не
оптимизированной
оставить
некоторую
возможную
альтернативу
непроверенной
к
моменту
выполнения
рекурсивного вызова. Тогда стек должен быть сохранен, т. к. в
случае
неудачного
завершения
рекурсивного
вызова
вызывающая процедура может откатиться и начать проверять
эту альтернативу. Например:
badcount2(X):write(X) ,
NewX=X+l,
badcount2(NewX).
badcount2(X):X<0,
write("X отрицательно.").
Здесь первое предложение badcount2 вызывает себя, когда
второе предложение еще не выполнено. Снова программа
истощает память после определенного количества вызовов.










Для потери оптимизации хвостовой рекурсии не обязательно
иметь непроверенную альтернативу как отдельное предложение
рекурсивной процедуры. Непроверенная альтернатива может
быть и в любом вызываемом предикате. Например:
badcount3(X):write(X),
NewX=X+l,,
check(NewX),
badcount3(NewX).
check(Z):Z>=0.
check(Z) :Z<0.
Предположим, что X - положительная величина, как это
обычно бывает. Когда badcount3 вызывает себя, первое
предложение check достигает цели, а второе предложение
check еще не проверено. Поэтому badcount3 должен сохранить
копию своего стекового фрейма, чтобы иметь возможность
вернуться и начать проверять второе предложение check в
случае, если рекурсивный вызов завершится неудачно.
/*32-битной
архитектуре
эти
примеры
будут
выполняться достаточно долго,
занимая много памяти и значительно уменьшая общую
производительность системы.*/
predicates
badcount1(integer)
badcount2(integer)
badcount3(integer)
check(integer)
clauses
%badcountl: Рекурсивный вызов - не последний шаг.
badcount1(X):write(X),
NewX=X+1,
badcount1(NewX),
nl.
% badcount2: Это предложение, которое не выполняется
во время
% осуществления рекурсивного вызова
badcount2(X):write(X),
NewX=X+1,
badcount2(NewX).
badcount2(X):X<0,
write("X отрицательно.").
% badcount3: Непроверенная альтернатива в процедуре,
% вызванной перед рекурсивным вызовом.
badcount3(X):write(X) ,
NewX=X+1,
check(NewX),
badcount3(NewX).
check(Z):Z>=0.
check(Z):Z<0.
Текст этой программы можно взять здесь.
Заметьте, что badcount2 и badcount3 хуже, чем badcount1,
потому что они генерируют точки возврата.
Вероятно, вы сейчас думаете, что невозможно гарантировать, что
процедура является оптимизированной хвостовой рекурсией. Хотя
довольно просто сделать рекурсивный вызов в последней подцели
заключительного предложения, но как гарантировать, что в любых
других вызываемых предикатах нет альтернатив?
Отсечение позволяет отвергать все возможные излишние
альтернативы. Чтобы установить cut, необходимо использовать
директиву компилятора check_determ.
Вы можете исправить
(модифицируя его имя):
badcount3
следующим
образом
cutcount3(X):write(X),
NewX=Х+1,
check(NewX),
!,
cutcount3(NewX).
Команда "отсечение" означает "сжечь мосты за собой", или, точнее,
"однажды достигнув этой точки, не обращать внимания на
альтернативные предложения этого предиката и альтернативные
решения предыдущих подцелей в данном предложении". Что точнее решайте сами. Поскольку альтернативы исключаются, фрейм стека не
нужен и рекурсивный вызов может свободно идти дальше.
"Отсечение" также эффективно и в badcount2, если переместить
проверку из второго предложения в первое:
cutcount2(X):X>=0,
!,
write(X) ,
NewX = Х+1,
cutcount2(NewX).
cutcount2(X) :write("X is negative.").
Отсечение - это действительно решение. Оно используется всякий
раз, когда альтернативы нам не интересны. В исходной версии
предыдущего примера второе предложение должно было оставлять
выбор, т. к. первое предложение не содержало проверки X.
Переместив проверку в первое предложение и отрицая ее, решение
можно принять уже там, а отсечение установить в соответствии с
утверждением: "Теперь я знаю, что я не должен писать, что X
отрицателен".
То же касается cutcount3. Предикат check показывает ситуацию,
когда вы хотите совершить некую дополнительную операцию над X,
основанную на знаке. Однако код для check недетермирован, и
отсечение после его вызова - это все, на что вам надо решиться.
Однако вышесказанное немного искусственно - возможно, было бы
правильнее, чтобы check был детерминиронан:
check(Z):Z>=0,
!,
... % использование Z
check(Z):Z<0,
... % использование Z
Проверка во втором предложении check - полное отрицание
проверки в первом. Поэтому check можно переписать как:
check(Z):Z>=0,
Если отсечение выполняется, компьютер предполагает, что
непроверенных альтернатив нет, и не создает стековый фрейм.
Программа pro40_3.pro содержит измененные версии badcount2 и
badcount3.
/*Показывает, как bedcount2 и bedcount3 могут быть
улучшены объявлением отсечения ("cut") для исключения
непроверенных предложений. Эти версии используют
оптимизированную
хвостовую рекурсию.*/
predicates
cutcount2(integer)
cutcount3(integer)
check(integer)
clauses
cutcount2(X):X>=0,
!,
write(X),
NewX=X+1,
cutcount2(NewX).
cutcount2(_):write("X отрицательно.").
cutcount3(X):write(X) ,
NewX=X+1,
check(NewX),
!,
cutcount3(NewX).
check(Z):Z>=0.
check(Z):Z<0.
Текст этой программы можно взять здесь.
К сожалению, отсечение не сможет помочь с badcount1, в котором
необходимость создания копий стековых фреймов не связана с
непроверенными
альтернативами.
Единственный
способ
усовершенствовать badcount1 - произвести вычисления таким
образом, чтобы рекурсивный вызов происходил в конце предложения.
Со следующего шага мы начнем рассматривать использование
аргументов в качестве переменных цикла.
Шаг 41.
Основы логического программирования.
Использование аргументов в качестве переменных цикла
На этом шаге мы рассмотрим использование аргументов в
качестве переменных цикла.
Сейчас, после освоения хвостовой рекурсии, как бы вы поступили с
циклическими переменными и счетчиками? Чтобы ответить на этот
вопрос, мы совершим небольшое преобразование с Pascal на
Пролог, предполагая, что вы знакомы с языком Pascal. Обычно
результаты прямых переводов между двумя языками, как
естественными, так и языками программирования, достаточно убоги.
И хотя приведенный ниже пример неплох и является разумной
иллюстрацией чисто процедурного программирования на Прологе,
вам никогда не следует писать программы на Пролог методом
слепого перевода их с другого языка. Пролог - очень мощный и
выразительный язык, и правильно написанные Пролог-программы
показывают иной стиль программирования и имеют совсем иные
проблемы, нежели программы на других языках.
На шаге 39 мы показали вычисление факториала с помощью
рекурсивной процедуры. Здесь мы используем для этого итерацию. В
Pascal это выглядело бы так:
Р:=1;
for I:=1 to N do P:=P*I;
FactN:=P;
Если вы знакомы с Pascal, то знаете, что := является оператором
присваивания и произносится как "присвоить". Здесь 4 переменных. N
- число, факториал которого будет вычисляться; FactN - результат
вычисления; I - циклическая переменная, изменяемая от 1 до N; P суммирующая переменная. Конечно, опытный программист на Pascal
объединил бы FactN и Р, но для перевода на Пролог так будет
удобнее.
Первый шаг в переводе на Пролог - замена for более простой
формулировкой для цикла, точнее определяющей, что происходит с I
на каждом шаге. Используем для этого определение while:
Р:=1; /* Инициализация Р и I */
I:=1;
while I <= N do /* Задание цикла */
begin
Р:=Р*I; /* Обновление Р и I */
I:=I+1
end;
FactN:= P; /* Показать результат */
Программа показывает переведенный на Пролог цикл while языка
Pascal.
predicates
factorial(integer,integer)
factorial_aux(integer,integer,integer,integer)
clauses
factorial(N, FactN):factorial_aux(N,FactN,1,1).
factorial_aux(N,FactN,I,P):I <= N,!,
NewP=P * I,
New1=I+1,
factorial_aux(N,FactN,New1,NewP).
factorial_aux(N,FactN,I,P):I > N,
FactN=P.
Текст этой программы можно взять здесь.
Результат работы программы можно посмотреть на рис.1
Рис.1. Результат работы программы pro41_1.pro
Рассмотрим программу более детально. У предложения для
предиката factorial есть только два аргумента - N и FactN. Oни
являются как бы входом и выходом, если смотреть с точки зрения
того,
кто
вычисляет
факториал.
Предложения
для
factorial_aux(N,FactN,I,P) фактически обесчивают рекурсию. Их
аргументами являются четыре переменные, которые должны
предаваться из одного шага в другой. Поэтому factorial просто
вызывает factorial_aux, передавая ему N и FactN с начальными
значениями для I и Р:
factorial(N,FactN):factorial_aux(N,FactN,1,1).
Так I и P инициализируются.
Но как factorial передает FactN, ведь у нее пока нет еще значения?
Ответ заключается в том, что Пролог здесь унифицирует
переменную, названную FactN в одном предложении, с переменной,
названной FactN в другом предложении. Таким же образом
factorial_aux передает себе FactN в качестве аргумента и
рекурсивном вызове. В конечном счете последняя FactN получит
значение, и после этого все другие FactN, которые унифицировались
с ней, получат такое же значение. Пролог может определить из
исходного кода, что FactN в действительности не используется перед
вторым предложением factorial_aux, а все время передается одна и
та же FactN.
Теперь о работе factorial_aux. Обычно этот предикат проверяет
предложение "I меньше либо равно N" для циклического вычисления,
а затем рекурсивно вызывает себя с новыми значениями для I и P.
Здесь проявляется еще одна особенность Пролога. В Прологе
верное для арифметики выражение
P=Р+1
совсем не является определением присвоения (как это должно быть
на большинстве других языков программирования).
Замечание: Вы не можете изменить значение переменной в
Прологе.
В Прологе это так же абсурдно, как и в алгебре. Вместо этого вы
должны создать новую переменную и придать ей нужное значение.
Например:
NewP=Р+1
Поэтому первое предложение выглядит следующим образом:
factorial_aux(N,FactN,I,P):I <= N,!,
NewP=P*I,
Newl=I+1,
factorial_aux(N,FactN,Newl,NewP).
Как и в случае cutcount2, в этом предложении отсечение будет
обеспечивать оптимизацию хвостовой рекурсии, хотя оно и не
является последним предложением в предикате.
В конечном счете I будет превышать N; текущие значения P и FactN
унифицируются и рекурсия прекратится. Это реализуется во втором
предложении, которое выполнится, когда проверка I<= N в первом
предложении будет неуспешна.
factorial_aux(N,FactN,I,P):I>N,
FactN=P.
Здесь нет необходимости делать FactN=P отдельным шагом;
унификация может происходить в списке аргументов. Подстановка
одинакового названия переменных требует, чтобы аргументы в этих
позициях были равны. Более того, проверка I>N избыточна, т. к.
обратное было проверено в первом предложении. Это дает
завершающее предложение:
factorial_aux(_,FactN,_,FactN).
На следующем шаге мы начнем рассматривать списки и
рекурсию.
Шаг 42.
Основы логического программирования.
Списки и рекурсия
На этом шаге мы рассмотрим списки и рекурсию.
Что такое список?
В Прологе список - это объект, который содержит конечное число
других объектов. Списки можно грубо сравнить с массивами в других
языках, но, в отличие от массивов, для списков нет необходимости
заранее объявлять их размер.
Конечно, есть другие способы объединить несколько объектов в
один. Если число объектов заранее известно, то вы можете сделать
их аргументами одной составной структуры данных. Если число
объектов не определено, то можно использовать рекурсивную
составную структуру данных, такую как дерево. Но работать со
списками обычно легче, т.к. Пролог обеспечивает для них более
четкую запись.
Список, содержащий числа 1, 2 и 3, записывается так:
[1,2,3]
Каждая составляющая списка называется элементом. Чтобы
оформить списочную структуру данных, надо отделить элементы
списка запятыми и заключить их в квадратные скобки. Вот несколько
примеров:
[dog,cat,canary]
["valerie ann","jennifer caitlin","benjamin thomas"]
Объявление списков
Чтобы объявить домен для списка целых, надо использовать
декларацию домена, такую как:
domains
integerlist=integer*
Символ (*) означает "список чего-либо"; таким образом, integer*
означает "список целых".
Обратите внимание, что у слова "список" нет специального
значения в Прологе. С тем же успехом можно назвать список
"занзибаром". Именно обозначение * (а не название), говорит
компилятору, что это список.
Элементы списка могут быть любыми, включая другие списки.
Однако все его элементы должны принадлежать одному домену.
Декларация домена для элементом должна быть следующего вида:
domains
elementlist = elements*
elements = ....
Здесь elements имеют единый тип (например: integer, real или
symbol) или являются набором отличных друг от друга элементов,
отмеченных разными функторами. В Прологе нельзя смешивать
стандартные типы в списке. Например, следующая декларация
неправильно определяет список, составленный из элементов,
являющихся
целыми
и
действительными
числами
или
идентификаторами:
elementlist=elements*
elements=integer; real; symbol /* Неверно */
Чтобы объявить список, составленный из целых, действительных и
идентификаторов, надо определить один тип, включающий все три
типа с функторами, которые покажут, к какому типу относится тот или
иной элемент. Например:
elementlist=elements*
% функторы здесь i,r и s
elements=i(integer); r(real); s(symbol)
Головы и хвосты
Список является рекурсивным составным объектом. Он состоит из
двух частей - головы, которая является первым элементом, и хвоста,
который является списком, включающим все последующие элементы.
Хвост списка - всегда список, голова списка - всегда элемент.
Например:
голова [а,b,с] есть а
хвост [а,b,с] есть [b,с]
Что происходит, когда вы доходите до одноэлементного списка?
Ответ таков:
голова[с] есть с
хвост [с] есть [ ]
Если выбирать первый элемент списка достаточное количество раз,
вы обязательно дойдете до пустого списка [ ].
Пустой список нельзя разделить на голову и хвост. Это значит, что
список имеет структуру дерева, как и другие составные объекты.
Структура дерева [а,b,с,d] представлена на рис. 1.
Рис.1. Структура дерева
Одноэлементный список, как, например [а], не то же самое, что
элемент, который в него входит, потому что [а] на самом деле - это
составная структура данных, как показано на рис. 2.
Рис.2. Составная структура данных
На следующем шаге мы начнем рассматривать работу со
списками.
Шаг 43.
Основы логического программирования.
Работа со списками
На этом шаге мы рассмотрим работу со списками.
В Прологе есть способ явно отделить голову от хвоста. Вместо
разделения элементов запятыми, это можно сделать вертикальной
чертой "|". Например:
[а,b,с] эквивалентно [а| [b,с]]
и, продолжая процесс,
[а| [b,с]] эквивалентно [а | [b| [с]]], что эквивалентно [а| [b| [с| []
]]]
Можно использовать оба вида разделителей в одном и том же
списке при условии, что вертикальная черта есть последний
разделитель. При желании можно набрать [а,b,с,d] как [a,b| [c,d]]. В
таблице 1 вы найдете другие примеры.
Таблица 1. Головы и хвосты списков
Список
Голова
Хвост
['a','b','с']
'a'
['b','с']
['a']
'a'
[] % пустой список
[]
Не определена Не определен
[[1,2,3], [2,3,4],[ ]] [1,2,3]
[[2,3,4],[ ]]
В таблице 2 приведены несколько примеров на присвоение в
списках.
Таблица 2. Присвоение в списках
Список 1
[X,Y,Z]
Список 2
Присвоение переменным
[эгберт, ест,мороженое] Х=эгберг, У=ест, Z=мороженое
[7]
[X | Y]
Х=7,Y=[ ]
[1,2,3,4]
[X, Y | Z]
X=1, Y = 2, Z = [3,4]
[1,2]
[3|X]
fail % неудача
На следующем шаге мы начнем рассматривать использование
списков.
Шаг 44.
Основы логического программирования.
Использование списков
На этом шаге мы рассмотрим использование списков.
Список является рекурсивной составной структурой данных,
поэтому нужны алгоритмы для его обработки. Главный способ
обработки списка - это просмотр и обработка каждого его элемента,
пока не будет достигнут конец.
Алгоритму этого типа обычно нужны два предложения. Первое из
них говорит, что делать с обычным списком (списком, который можно
разделить на голову и хвост), второе - что делать с пустым списком.
Печать списков
Если нужно напечатать элементы списка, это делается так, как
показано ниже.
domains
list = integer* % Или любой тип, какой вы хотите
predicates
write_a_list(list)
clauses
write_a_list([]). % Если список пустой - ничего не делать
write_a_list([H |T]):- % Присвоить Н-голова,Т-хвост, затем...
write(H),nl,
write_a_list(T).
goal
write_a_list([1,2,3]).
Текст этой программы можно взять здесь.
Результат работы программы можно посмотреть на рис.1
Рис.1. Результат работы программы pro44_1.pro
Вот два целевых утверждения write_a_list, описанные на обычном
языке:
Печатать пустой список - значит ничего не делать.
Иначе, печатать список - означает печатать его голову
(которая
является
одним
элементом),
затем печатать его хвост (список).
При первом просмотре целевое утверждение таково:
write_a_list([1,2,3]).
Оно удовлетворяет второму предложению при H=1 и T=[2,3].
Компьютер напечатает 1 и вызовет рекурсивно
write_a_list:write_a_list([2,3]). % Это write_a_list(T)
Этот рекурсивный вызов удовлетворяет второму предложению. На
этот раз H=2 и T=[3], так что компьютер печатает 2 и снова рекурсивно
вызывает write_a_list с целевым утверждением
write_a_list([3]).
Итак, какому предложению подходит это целевое утверждение?
Вспомним, что, хотя список [3] имеет всего один элемент, у него есть
голова 3 и хвост [ ]. Следовательно, целевое утверждение снова
подходит под второе предложение с H=3 и T=[ ]. Программа печатает
3 и делает рекурсивный вызов:
write_a_list([ ]).
Теперь видно, что этому целевому утверждению подходит первое
предложение. Второе предложение не подходит, т. к. [ ] нельзя
разделить на голову и хвост. Так что, если бы не было первого
предложения, целевое утверждение было бы невыполнимым. Но
первое предложение подходит, и целевое утверждение выполняется
без дальнейших действий.
Подсчет элементов списка
Рассмотрим, как можно определить число элементов в списке. Что
такое длина списка? Вот простое логическое определение:
Длина [ ] - 0.
Длина любого другого списка - 1 плюс длина его хвоста.
Можно ли применить это? В Прологе - да. Для этого нужны два
предложения.
domains
list = integer* % или любой другой тип
predicates
length_of(list,integer)
clauses
length_of([],0).
length_of([_|T],L):length_of(T,TailLength),
L=TailLength + 1.
Текст этой программы можно взять здесь.
Посмотрим сначала на второе предложение. Действительно, [_ |T]
можно сопоставить любому непустому списку, с присвоением T хвоста
списка. Значение головы не важно, главное, что оно есть, и компьютер
может посчитать его за один элемент.
Таким образом, целевое утверждение
length_of([1,2,3],L).
подходит второму предложению при T=[2,3]. Следующим шагом будет
подсчет длины T. Когда это будет сделано (не важно как), TailLength
будет иметь значение 2, и компьютер добавит к нему 1 и затем
присвоит L значение 3. Итак, как компьютер выполнит промежуточный
шаг? Это шаг, в котором определяется длина [2,3] при выполнении
целевого утверждения
length_of([2,3],TailLength).
Другими словами, length_of вызывает сама себя рекурсивно. Это
целевое утверждение подходит второму предложению с присвоением:


[3] из целевого утверждения присваивается T в предложении;
TailLength из целевого утверждения присваивается L
предложении.
в
Напомним, что TailLength в целевом утверждении не совпадает с
TailLength в предложении, потому что каждый рекурсивный вызов в
правиле имеет свой собственный набор переменных.
Итак, целевое утверждение состоит в том, чтобы найти длину [3],
т.е. 1, а затем добавить 1 к длине [2,3], т.е. к 2, и т. д.
Таким образом, length_of вызывает сама себя рекурсивно, чтобы
получить длину списка [3]. Хвост [3] - [ ], так что T будет присвоен [ ], а
целевое утверждение будет состоять в том, чтобы найти длину [ ] и,
добавив к ней 1, получить длину [3].
На сей раз все просто. Целевое утверждение
length_of([],TailLength)
удовлетворяет первому предложению, которое присвоит 0
переменной TailLength. Пролог добавит к нему 1 и получит длину [3],
затем вернется к вызывающему предложению. Оно в свою очередь
снова добавит 1, получит длину [2,3] и вернется в вызывающее его
предложение. Это начальное предложение снова добавит 1 и получит
длину [1,2,3].
Не запутались? Надеемся, что нет. Посмотрите иллюстрацию всех
вызовов. В ней мы пользовались названиями, чтобы показать, что
переменные с одним идентификатором, но из разных предложений
или из разных вызовов одного предложения отличаются друг от друга.
length_of([1,2,3],L1).
length_of([2,3],L2).
length_of([3],L3).
length_of([],0).
L3=0+1=1.
L2=L3+1=2.
L1=L2+1=3.
Со следующего шага мы начнем рассматривать хвостовую
рекурсию.
Шаг 45.
Основы логического программирования.
Хвостовая рекурсия
На этом шаге мы рассмотрим хвостовую рекурсию.
Возможно, вы заметили, что length_of не является и не может быть
хвостовой рекурсией потому, что рекурсивный вызов не является
последним шагом в своем предложении. Можно ли написать предикат
для определения длины списка, который будет хвостовой рекурсией?
Да, но это потребует некоторых усилий.
Проблема использования length_of заключается в том, что нельзя
подсчитать длину списка, пока не подсчитана длина хвоста. Но есть
обходной путь. Для определения длины списка вам потребуется
предикат с тремя аргументами:



первый - это сам список, который компьютер уменьшает при
каждом вызове, пока список не опустеет так же, как и раньше;
второй - свободный параметр, который будет хранить
промежуточный результат (длину);
третий - счетчик, который начинается с нуля и увеличивается на
1 при каждом вызове.
Когда список станет пустым, унифицируем счетчик со свободным
результатом.
Рассмотрим пример.
domains
list = integer* % или любой другой тип
predicates
length_of(list,integer, integer)
clauses
length_of([], Result, Result).
length_of([_|T],Result,Counter):NewCounter=Counter + 1,
length_of(T, Result, NewCounter).
goal
length_of([1,2,3],L,0), % начать со счетчика = 0
write("L=",L),nl.
Текст этой программы можно взять здесь.
Результат работы программы можно посмотреть на рис.1
Рис.1. Результат работы программы pro45_1.pro
Данная версия предиката length_of более сложная и менее
логичная, чем предыдущая. Она продемонстрирована лишь для
доказательства того, что можно найти хвостовые рекурсивные
алгоритмы для целевых утверждений, которые, возможно, требуют
другого типа рекурсии.
Иногда необходимо преобразовать один список в другой. Вы
делаете это, работая со списком поэлементно, заменяя каждый
элемент вычисленным значением. Вот пример: программа добавит 1 к
каждому элементу числового списка.
domains
list = integer*
predicates
add1(list,list)
clauses
add1([], []).
add1([Head|Tail],[Head1|Tail1]):-% отделить голову списка
Head1= Head+1,% добавить 1 к 1-му элементу
add1(Tail,Tail1).% вызвать элемент из остатка списка
Текст этой программы можно взять здесь.
Переведя это на естественный язык, получим:
Чтобы добавить 1 ко всем элементам пустого списка, надо
создать
другой
пустой
список.
Чтобы добавить 1 ко всем элементам любого непустого
списка,
надо добавить 1 к голове и сделать полученный элемент
головой
результирующего
списка,
затем добавить 1 к каждому элементу хвоста списка и
сделать это хвостом результата.
Запустите программу с целевым утверждением
add1([1,2,3,4],NewList)
Пролог выдаст результат:
NewList=[2,3,4,5].
1 решение
Является ли add1 хвостовой рекурсией? Если вы знакомы с
языками Лисп или Паскаль, то можете подумать, что нет, т. к.
считаете, что предикат выполняет следующие операции:
1. Разделяет список на Head и Tail.
2. Добавляет 1 к Head и результат присваивает Head1.
3. Рекурсивно добавляет 1 ко всем элементам Tail, присваивает
результат Tail1.
4. Объединяет Head1 и Tail1 и присваивает результат новому
списку.
Эта процедура не является хвостовой рекурсией, потому что
рекурсивный вызов - это не последний шаг.
Но, что важно - Пролог делает это не так; в нем add1 является
хвостовой рекурсией, потому что шаги на самом деле следующие:
1. Связать голову и хвост исходного списка с Head и Tail.
2. Связать голову и хвост результата с Head1 и Tail1 (Head1 и Tail1
пока не определены).
3. Добавить 1 к Head и присвоить результат Head1.
4. Рекурсивно добавить 1 ко всем элементам Tail, присваивая
результат Tail1.
Когда все будет завершено, Head1 и Tail1 уже являются головой и
хвостом результата, и нет отдельной операции для их объединения.
Таким образом, рекурсивный вызов является последним шагом.
Конечно, не всегда нужно заменять каждый элемент. Далее следует
пример программы, которая просматривает список из чисел и делает
из него копию, отбрасывая отрицательные числа.
domains
list=integer*
predicates
discard_negatives(list,list)
clauses
discard_negatives([], []).
discard_negatives([H |T],ProcessedTail):H<0, % если Н отрицательно, то пропустить
!,
discard_negatives(T, ProcessedTail).
discard_negatives([H| T],[H |ProcessedTail]):discard_negatives(T, ProcessedTail).
Текст этой программы можно взять здесь.
К примеру, целевое утверждение
discard_negatives([2,-45,3,468], X)
получит
X = [2,3,468].
Далее приведем предикат, который копирует элементы списка,
заставляя каждый элемент появляться дважды:
doubletalk([ ],[ ]).
doubletalk([H |T],[H,H |DoubledTail]):doubletalk(T,DoubledTail).
На следующем шаге мы рассмотрим принадлежность к списку.
Шаг 46.
Основы логического программирования.
Принадлежность к списку
На этом шаге мы рассмотрим принадлежность к списку.
Предположим, что у вас есть список имен John, Leonard, Eric и
Frank, и вы хотите, используя Пролог, проверить, имеется ли
заданное имя в этом списке. Другими словами, вам нужно выяснить
отношение "принадлежность" между двумя аргументами - именем и
списком имен. Это выражается предикатом
member(name,namelist). % "name" принадлежит "namelist"
В программе, которая приведена ниже, первое предложение
проверяет голову списка. Если голова списка совпадает с именем,
которое вы ищете, то можно заключить, что Name (имя) принадлежит
списку. Так как хвост списка не представляет интереса, он
обозначается анонимной переменной. По первому предложению
целевое утверждение
member(john,[john,leonard,eric,frank])
будет выполнено.
domains
namelist=name*
name=symbol
predicates
member(name,namelist)
clauses
member(Name,[Name |_]).
member(Name,[_ |Tail]):member(Name,Tail).
Текст этой программы можно взять здесь.
Если голова списка не совпадает с Name, то нужно проверить,
можно ли Name найти в хвосте списка.
На обычном языке:
Name принадлежит списку, если Name есть первый элемент
списка,
или
Name принадлежит списку, если Name принадлежит хвосту.
Второе предложение member, выражающее
принадлежности, в Прологе выглядит так:
отношение
member(Name,[_ |Tail]):member(Name,Tail).
Объединение списков
В том виде, как он дан в предыдущей программе, предикат member
работает двумя способами. Рассмотрим его предложения еще раз:
member(Name,[Name |_ ]) .
member(Name,[_ |Tail]):member(Name,Tail).
На эти предложения можно смотреть с двух различных точек
зрения: декларативной и процедурной.

С декларативной точки зрения предложения сообщают:
Name принадлежит списку, если голова совпадает с Name;
если нет, то Name принадлежит списку, если оно
принадлежит его хвосту.

С процедурной точки зрения эти два предложения можно
трактовать так:
Чтобы найти элемент списка, надо найти его голову;
иначе надо найти элемент в хвосте.
Эти две точки зрения соотносятся с целевым утверждением:
member(2, [1, 2, 3, 4]).
и
member[X, [1, 2, 3, 4]).
В результате, первое целевое утверждение "просит" Пролог
выяснить, верно ли утверждение, второе, - найти всех членов списка
[1,2,3,4]. Предикат member одинаков в обоих случаях, но его
поведение может быть рассмотрено с разных точек зрения.
Рекурсия с процедурной точки зрения
Особенность Пролога состоит в том, что часто, когда вы задаете
предложения для предиката с одной точки зрения, они будут
выполнены с другой. Чтобы увидеть эту двойственность, создадим в
следующем примере предикат для присоединения одного списка к
другому. Определим предикат append с тремя аргументами:
append(List1,List2,List3)
Он объединяет List1 и List2 и создает List3. Еще раз
воспользуемся рекурсией (на этот раз с процедурной точки зрения).
Если List1 пустой, то результатом объединения List1 и List2
останется List2. На Прологе это будет выглядеть так:
append([],List2,List2).
Если List1 не пустой, то можно объединить List1 и List2 для
формирования List3, сделав голову List1 головой List3. (В
следующем утверждении переменная H используется как голова для
List1 и для List3.) Хвост List3 - это L3, он состоит из объединения
остатка List1 (т. е. L1) и всего List2. To есть:
append([H |L1],List2,[H |L3]):-
append(L1,List2,L3).
Предикат append выполняется следующим образом: пока List1 не
пустой, рекурсивное предложение передает по одному элементу в
List3. Когда List1 станет пустым, первое предложение унифицирует
List2 с полученным List3.
Предикат append определен в программе, приведенной ниже.
Загрузите эту программу.
domains
integerlist=integer*
predicates
append(integerlist,integerlist,integerlist)
clauses
append([],List,List).
append([H |L1],List2,[H |L3]):append(L1,List2,L3).
Текст этой программы можно взять здесь.
Задайте следующее целевое утверждение:
append([1,2,3],[5,6],L).
А теперь попробуйте это:
append([1,2],[3],L),
append(L,L,LL).
Предикат может иметь несколько вариантов использования
Рассматривая append с декларативной точки зрения, вы
определили отношение между тремя списками. Однако это отношение
сохранится, даже если List1 и List3 известны, a List2 - нет. Оно также
справедливо, если известен только List3. Например, чтобы
определить, какие из двух списков можно объединить для получения
известного списка, надо составить целевое утверждение такого вида:
append(L1,L2,[1,2,4]).
По этому целевому утверждению Пролог найдет следующие
решения:
L1=[], L2=[1,2,4]
L1=[1], L2=[2,4]
L1=[1,2],L2=[4]
L1=[1,2,4],L2=[]
4 решения
Можно также применить append, чтобы определить, какой список
можно подсоединить к [3,4] для получения списка [1,2,3,4].
Запустите целевое утверждение
append(L1,[3,4],[1,2,3,4]).
Пролог найдет решение:
L1=[1,2]
Предикат append определил отношение между входным набором и
выходным набором таким образом, что отношение применимо в обоих
направлениях. Задавая такое отношение, вы спрашиваете:
Который
выход
соответствует
данному
входу?
или
Который вход соответствует данному выходу?
Состояние аргументов при вызове предиката называется потоком
параметров. Аргумент, который присваивается или назначается в
момент вызова, называется входным аргументом и обозначается
буквой i; а свободный аргумент - это выходной аргумент,
обозначается буквой о.
У предиката append есть возможность работать с разными
потоками параметров, в зависимости от того, какие исходные данные
вы ему дадите. Однако не для всех предикатов имеется возможность
быть вызванными с различными потоками параметров. Если
предложение Пролога может быть использовано с различными
потоками параметров, оно называется обратимым предложением.
Когда вы записываете собственные предложения в Прологе, помните,
что обратимые предложения имеют дополнительные преимущества, и
их создание добавляет "мощности" предикатам.
На следующем шаге мы начнем рассматривать поиск всех
решений для цели сразу.
Шаг 47.
Основы логического программирования.
Поиск всех решений для цели сразу
На этом шаге мы рассмотрим поиск всех решений для цели
сразу.
Преимущество рекурсии состоит в том, что, в отличие oт поиска с
возвратом, она передает информацию (через параметры) от одного
рекурсивного вызова к следующему. Поэтому рекурсивная процедура
может хранить память о промежуточных результатах или счетчиках по
мере того, как она выполняется.
Но есть одна вещь, которую поиск с возвратом может делать, а
рекурсия - нет. Это поиск всех альтернативных решений в целевом
утверждении. Может оказаться, что вам нужны все решения для
целевого утверждении, и они необходимы все сразу, как часть единой
сложной составной структуры данных. Встроенный предикат
использует целевые утверждения в качестве одного из своих
аргументов и собирает все решения для этого целевого утверждения
в единый список. У предиката findall три аргумента:



varName(имя переменной) - определяет параметр, который
необходимо собрать в список;
myPredicate (мой предикат) - определяет предикат, из которого
надо собрать значения;
ListParam (список параметров) - содержит список значений,
собранных методом поиска с возвратом. Заметьте, что должен
быть определенный пользователем тип, которому принадлежат
значения ListParam.
Следующая программа использует findall для печати среднего
возраста группы людей.
domains
name,address=string
age=integer
list=age*
predicates
person(name,address,age)
sumlist(list,age,integer)
clauses
sumlist([],0,0).
sumlist([H |T],Sum,N):sumlist(T,S1,N1),
Sum=H+S1,
N=1+N1.
person("Sherlock Holmes","22B Baker Street",42).
person("Pete Spiers","Apt. 22, 21st Street", 36).
person("Mary Darrow", "Suite 2, Omega Home", 51).
goal
findall(Age,person(_, _, Age),L),
sumlist (L,Sum,N),
Ave=Sum/N,
write("Average=", Ave),nl.
Текст этой программы можно взять здесь.
Результат работы программы можно посмотреть на рис.1
Рис.1. Результат работы программы pro47_1.pro
Предложение findall в этой программе создает список L, в котором
собраны все возвраты, полученные из предиката person. Если бы вы
захотели собрать список из всех людей, которым 42 года, то вам
следовало бы выполнить следующее подцелевое утверждение:
findall(Who,person(Who,_,42),List)
Но эта подцель требует от программы, чтобы та содержала
объявление домена для результирующегo списка:
slist=string*
На следующем шаге мы рассмотрим составные списки.
Шаг 48.
Основы логического программирования.
Составные списки
На этом шаге мы рассмотрим составные списки.
Список целых может быть объявлен просто:
integerlist=integer*
Это же справедливо и для списка действительных чисел, списка
идентификаторов или списка строк. Часто бывает важно иметь внутри
одного списка комбинацию элементов, принадлежащих разным типам:
[2,3,5.12,["food","goo"],"new"] % Некорректно в Прологе.
Составные списки - это списки, в которых используется более чем
один тип элементов. Для работы со списками из разнотипных
элементов нужны специальные декларации, потому что Пролог
требует, чтобы все элементы списка принадлежали одному типу. Для
создания списка, который мог бы хранить различные типы элементов,
в Прологе необходимо использовать функторы, потому что домен
может содержать более одного типа данных в качестве аргументов
для функторов.
Пример объявления доменов для списка, который может содержать
символы, целые, строки или списки:
domains % функторы l,i,c и s
llist=l(list); i(integer); с(char); s(string)
list=llist*
Список
[2,9,["food","goo"],"new"] % Некорректно в Прологе
должен быть представлен как:
[i(2),i(9),1([s("food"),s("goo")]),s("new")] % Корректно.
Ниже приведен пример, показывающий объединение списков и
использование объявления доменов в типичном случае работы со
списками.
domains
llist=l(list); i(integer); c(char); s(string)
list=llist*
predicates
append(list,list,list)
clauses
append([],L,L).
append([X |Ll],L2,[X |L3]):append(Ll,L2,L3).
goal
append([s(likes),l([s(bill), s(mary)])], [s(bill), s(sue)],Ans),
write("First list: ", Ans,"\n\n"),
append( [l( [s("This") ,s("is") ,s("a") ,s("list") ] ) ,s(bee) ],
[c('c')],Ans2),
write("Second list: ", Ans2, '\n', '\n').
Текст этой программы можно взять здесь.
Со следующего шага мы начнем рассматривать реализацию
арифметических и логических операций.
Шаг 49.
Основы логического программирования.
Арифметические вычисления и сравнения
На этом шаге мы рассмотрим арифметические вычисления и
сравнения.
Возможности вычислений и сравнений в Прологе аналогичны
соответствующим возможностям таких языков программирования, как
С, Pascal. Пролог включает полный набор арифметических функций.
Вычислительные возможности Пролога уже были показаны на
нескольких простых примерах.
На этом шаге приведен обзор встроенных в Пролог предикатов и
функций для выполнения вычислений и сравнений.
Арифметические выражения
Арифметические выражения состоят из операндов (чисел и
переменных), операторов (+,-,/,*,div и mod) и скобок. Символы в
правой части от знака равенства (который является предикатом =)
составляют арифметическое выражение. Например:
А=1+6/(11+3)*Z
Шестнадцатеричные и восьмеричные числа начинаются с "0х" или
"0о" соответственно. Например:
OxFFF=4095
86=0о112+12
Значение выражения может быть вычислено, только если все
переменные в момент вычисления определены. При этом вычисления
производятся в порядке, определяемом приоритетом операции.
Операции с высшим приоритетом выполняются первыми.
Операции
Пролог может выполнять все четыре осноные арифметические
операции (сложение, вычитание, умножение и деление) между
целыми и вещественными числами. Тип результата приведен и
таблице 1.
Таблица 1. Арифметические операции
Операнд 1
Оператор
Операнд 2
Результат
Целое
+, -, *
Целое
Целое
Вещественное
+, -, *
Целое
Вещественное
Целое
+, -, *
Вещественное
Вещественное
Вещественное
+, -, *
Вещественное
Вещественное
/
Целое
вещественное
Целое
Div
Целое
Целое
Целое
Mod
Целое
Целое
Целое
вещественное
или
или
Вещественное
В случае смешанной целочисленной арифметики, включающей и
знаковые и безнаковые числа, результат знаковый. Размер результата
будет равен большему из размеров двух операндов.
Порядок вычислений
Арифметические операции вычисляются в следующем порядке:



если
выражение
содержит
подвыражение
в
скобках,
подвыражение вычисляется первым;
если выражение содержит операции умножения (*) или деления
(/, div или mod), эти операции выполняются слева направо;
если выражение содержит операции сложения (+) и вычитания (), они выполняются также слева направо.
Порядок операций показан в таблице 2.
Таблица 2. Порядок операций
Операция
Приоритет
-, +
1
x, /, mod, div
2
-, +(унарные)
3
В выражении A=1+6/(11+3)*Z, предположим, что Z имеет значение
4, ибо переменные должны быть связаны до вычисления. Вычислим
это выражение:
1. (11+3) - первое вычисляемое подвыражеие, т. к. оно заключено в
скобки, оно вычисляется как 14.
2. Затем вычисляется 6/14, т. к. / и * вычисляются слева направо. В
результате получим 0.428571.
3. Далее 0.428571*4 дает 1.714285.
4. Наконец, вычисляя 1+1.714285, получаем значение выражения
2.714285.
А получит значение 2.714285, которое принадлежит вещественному
домену.
Следует поупражняться в управлении вещественными числами. В
большинстве случаев они не представлены точно, и маленькие
ошибки могут накапливаться, выдавая непредсказуемые результаты.
Функции и предикаты
Пролог имеет полный набор встроенных математических функций
и предикатов, которые используют целые и вещественные значения.
Полный их список приведен в таблице 3.
Таблица 3. Арифметические предикаты и функции Пролога
Имя
Описание
X mod Y
Возвращает остаток от деления (модуль) X на Y
X div Y
Возвращает частное от деления X на Y
abs(X)
Если значение X- положительная величина value, abs(X)
возвращает это значение; в противном случае - 1*value
cos(X)
Возвращает косинус своего аргумента
sin(X)
Возвращает синус своего аргумента
tan(X)
Возвращает тангенс своего аргумента
arctan(X)
Возвращает арктангенс вещественного значения, с
которым связан X
exp(X)
Возводит е в степень X
ln(X)
Логарифм X по основанию е
sqrt(X)
Корень квадратный из X
random(X)
Присваивает X случайное вещественное число; 0<=X<1
random(X,Y) Присваивает Y случайное целое число; 0<=Y<X< b>
round(X)
Округляет значение X. Результат вещественный
trunc(X)
Усекает X. Результат вещественный
Замечание. Тригонометрические функции требуют, чтобы X был
величиной, представляющей угол в радианах.
Генератор случайных чисел
Для генерации случайных чисел в Прологе предусмотрены два
стандартных предиката. Один из них возвращает случайное
вещественное число в диапазоне от 0 до 1, другой возвращает
случайное целое число в диапазоне от 0 до данного числа. Кроме
того, случайная числовая последовательность может быть
переинициализирована.
Предикат random/1
Эта версия random возвращает случайное вещественное число
RandomReal, которое удовлетворяет условию:
0<=RandomReal<1.
random/1 имеет формат:
random(RandomReal) % (о)
Предикат random/2
Эта версия random имеет два аргумента, его формат:
random(MaxValue,Randomlnt) % (i,о)
Этот предикат ставит в соответствие RandomInt случайное целое,
удовлетворяющее условию:
0<=RandomInt<MaxValue
Предикат random/2 работает значительно быстрее, чем random/1,
т.к. random/2 использует только целочисленную арифметику.
В качестве примера приведем программу, которая использует
random/1 для набора трех имен из пяти случайным обpaзом.
predicates
person(integer,symbol)
rand_int_1_5(integer)
rand_person(integer)
clauses
person(1,fred).
person(2,tom).
person(3,mary).
person(4,dick).
person(5,george).
rand_int_1_5(X):random(Y),
X=Y*4+1.
rand_person(0):-!.
rand_person(Count):rand_int_1_5(N),
person(N,Name),
write(Name),nl,
NewCount=Count-1,
rand_person(NewCount).
goal
rand_person(3).
Текст этой программы можно взять здесь.
На следующем шаге мы
вещественную арифметику.
рассмотрим
целочисленную
и
Шаг 50.
Основы логического программирования.
Целочисленная и вещественная арифметика
На этом шаге мы рассмотрим целочисленную и вещественную
арифметику.
Пролог поддерживает предикаты и функции модульной
арифметики, целого деления, квадратные корни и абсолютные
значения,
тригонометрические
и
трансцендентные
функции,
округление (вверх или вниз) и усечение. Они приводятся в таблице 1
из шага 49 и поясняются ниже.
Функция mod/2
Функция mod вычисляет остаток от деления X на Y (где X и Y целые).
X mod Y % (i,i)
Выражение Z=X mod Y ставит в соответствие Z результат - остаток
от деления X на Y. Например:
Z=7 mod 4 %Z будет равно 3
Y=4 mod 7 % Y будет равно 4
Функция div/2
Функция div вычисляет целое частное от деления X ни Y (где X и Y целые).
X div Y % (i,i)
Выражение Z=X div Y ставит в соответствие Z целую часть
результата. Например:
Z=7 div 4 %Z будет равно 1
Y=4 div 7 % Y будет равно 0
Функция abs/1
Функция abs возвращает абсолютное значение своего аргумента.
abs(X) % (i)
Выражение Z=abs(X) ставит в соответствие Z (если оно свободно)
результат, или возвратит успех/неуспех, если Z уже определено.
Например:
Z=abs(-7) % Z будет равно 7
Функция cos/1
Функция cos возвращает значение косинуса своего аргумента.
cos(X) % (i)
Выражение Z=cos(X) ставит в соответствие Z (если оно свободно)
результат, или возвратит успех/неуспех, если Z уже определено.
Например:
Pi=3.141592653,
Z=cos(Pi) % Z будет равно -1
Функция sin/1
Функция sin возвращает значение синуса своего аргумента.
sin(X) % (i)
Выражение Z=sin(X) ставит в соответствие Z (если оно свободно)
результат, или возвратит успех/неуспех, если Z уже определено.
Например:
Pi=3.141592653,
Z=sin(Pi) % Z будет почти равно 0
Функция tan/1
Функция tan возвращает значение тангенса своего аргумента.
tan(X) % (i)
Выражение Z=tan(X) ставит в соответствие Z (если оно свободно)
результат, или возвратит успех/неуспех, если Z уже определено.
Например:
Pi=3.141592653,
Z=tan(X) % Z будет равно почти 0
Функция arctan/1
Функция arctan возвращает
значения, с которым связано X.
арктангенса
от
вещественного
arctan(X) % (i)
Выражение Z=arctan (X) ставит в соответствие Z (если оно
свободно) результат,или возвратит успех/неуспех, если Z уже
определено. Например:
Pi=3.141592653,
Z=arctan(Pi) % Z будет равно 1.2626272556
Функция ехр/1
Функция ехр возвращает значение е в степени значения, с которым
связано X.
ехр(Х) % (i)
Выражение Z=ехр(Х) ставит в соответствие Z (если оно свободно)
результат, или возвратит успех/неуспех, если Z уже определено.
Например:
Z=ехр(2.5) % Z будет равно 12.182493961
Функция ln/1
Функция ln возвращает значение натурального логарифма от X (по
основанию е).
ln(X) % (i)
Выражение Z=ln(X) ставит в соответствие Z (если оно свободно)
результат, или возвратит успех/неуспех, если Z уже определено.
Например:
Z=ln(12.182493961) % Z будет равно 2.5
Функция log/1
Функция log возвращает значение логарифма по основанию 10 от
X.
log(X) % (i)
Выражение Z=log(X) ставит в соответствие Z (если оно свободно)
результат, или возвратит успех/неуспех, если Z уже определено.
Например:
Z=log(2.5) % Z будет равно 0.39794000867
Функция sqrt/1
Функция sqrt возвращает квадратный корень от X.
sqrt(X) % (i)
Выражение Z=sqrt(X) ставит в соответствие Z (если оно свободно)
результат, или возвратит успех/неуспех, если Z уже связано.
Например:
Z=sqrt(25) % Z будет равно 5
Функция round/1
Функция round возвращает округленное значение X.
round(X) % (i)
Функция round округляет X до ближайшего целого, но не
производит преобразование типа. Например:
Z1=round(4.51) % Z1 будет равно 5.0
Z2=round(3.40) % Z2 будет равно 3.0
Оба Z1 и Z2 - вещественные значения, как показано выше; только
дробные части аргументов round округляются до ближайшего целого.
Функция trunc/1
Функция trunc усекает X справа до десятичной точки, отбрасывая
дробную часть. Как и round, trunc не выполняет преобразование
типов:
trunc(X) % (i)
Например:
Z=trunc(4.7) % Z будет равно 4.0,
Z - вещественное число.
На следующем шаге мы рассмотрим сравнение.
Шаг 51.
Основы логического программирования.
Сравнение
На этом шаге мы рассмотрим сравнение.
Пролог может сравнивать арифметические выражения так же, как
символы, сроки и идентификаторы. Следующее выражение в
Прологе эквивалентно выражению: "Сумма X плюс 4 меньше 9 минус
Y".
X+49-Y
Оператор отношения "меньше чем" (<) показывает отношение
между двумя выражениями: X+4 и 9-Y.
Пролог использует инфиксную нотацию, которая означает, что
оператор располагается между операндами (X+4) вместо того, чтобы
предшествовать им (+(X,4)). Полный ряд отношений, разрешенных в
Прологе, приведен в таблице 1.
Таблица 1. Операторы отношения
Идентификатор
Отношение
<
Меньше
<=
Меньше
равно
=
Равно
или
Идентификатор
Отношение
>
Больше
>=
Больше
равно
<> или ><
Не равно
или
Равенство и предикат равенства
В Прологе такие операторы, как N=N1-2 показывают отношение
между тремя объектами (N,N1 и 2) или отношение между двумя
объектами (N и величиной N1-2). Если N еще свободно, оператор
может быть удовлетворен присвоением N значения выражения N1-2.
Это приблизительно соответствует тому, что в других языках
программирования называется оператором присваивания. Заметим,
что поскольку N1 является частью вычисляемого выражения, оно
всегда должно быть определено (т.е. должно быть связано со
значением).
Когда вы для сравнения вещественных величин используете
предикат равенства (=), нужно позаботиться о том, чтобы
приближенное представление вещественных чисел не привело к
непредсказуемым результатам. Например, цель
7/3*3=7
потерпит неудачу. Программа, приведенная ниже, иллюстрирует еще
один пример.
predicates
test(real,real)
clauses
test(X,X):-!,
write("ok\n").
test(X,Y):Diff=X-Y,
write(X,"<>",Y,"\nX-Y=",Diff,'\n').
goal
X=47,
Y=4.7*10,
test(X,Y).
Текст этой программы можно взять здесь.
Результат будет следующий:
47<>47
X-Y=7.1054273576E-15
Следовательно, сравнивая два вещественных числа на равенство,
вам всегда следует проверять, чтобы эти два числа находились в
определенном диапазоне друг относительно друга.
Приведем следующий пример:
Следующая
программа
показывает,
как
обработать
приблизительное равенство. Она представляет собой итеративную
процедуру вычисления квадратного корня для определения решений
квадратного уравнения:
A*Х*Х+В*Х+С=0
Наличие решений
определяемого как
зависит
от
значения
дискриминанта
D,
D=В*В-4*А*С
В данном случае:
1. D>0 - означает, что существует два различных решения;
2. D=0 - означает, что существует единственное решение;
3. D<0 - означает, что решений нет, если X - вещественное (могут
быть одно или два комплексных решения).
predicates
solve(real,real,real)
reply(real,real,real)
mysqrt(real,real,real)
equal(real,real)
clauses
solve(A,B,C):D=B*B-4*A*C,
reply(A,B,D),nl.
reply(_,_,D):D<0,
write("No solution"), !.
reply (A,B,D):D=0,
X=-B/(2*A),
write("x=", X),!.
reply(A,B,D):mysqrt(D,D,SqrtD),
X1=(-B+SqrtD)/(2*A),
X2=(-B-SqrtD)/(2*A),
write("Xl = ",X1," and X2 = ", X2).
mysqrt(X,Guess,Root):NewGuess=Guess-(Guess*Guess-X)/2/Guess,
not(equal(NewGuess,Guess)),
!,
mysqrt(X,NewGuess,Root).
mysqrt(_,Guess,Guess).
equal(X,Y):X/Y>0.99999,
X/Y<1.00001.
goal
solve(1,2,1).
Текст этой программы можно взять здесь.
Чтобы решить квадратное уравнение, эта программа вычисляет
квадратный корень из дискриминанта D. Программа вычисляет
квадратные корни по итеративной формуле, где новое значение
(NewGuess) квадратного корня от X может быть получено из
предыдущего значения (Guess):
NewGuess=Guess-(Guess*Guess-X)/2/Guess
Каждая итерация немного приближается к квадратному корню от X.
Когда
условие
equal(X,Y)
удовлетворяется,
дальнейшего
приближения достичь нельзя - вычисление заканчивается. Теперь
программа может решить квадратное уравнение, используя значения
Xl и Х2
X1=(-В+sqrtD)/(2*A)
Х2=(-В-sqrtD)/(2*A)
На следующем шаге мы рассмотрим сравнение символов, строк
и идентификаторов.
Шаг 52.
Основы логического программирования.
Сравнение символов, строк и идентификаторов
На этом шаге мы рассмотрим сравнение символов, строк и
идентификаторов.
Кроме числовых выражений вы можете также сравнивать простые
символы, строки и идентификаторы. Рассмотрим следующие
сравнения:
'а' < 'b' % Символы
"antony" > "antonia" % Строки
P1=peter,Р2=sally,P1>Р2 % Литералы (идентификаторы)
Символы
Используя соответствующее значение кода ASCII для каждого
символа, Пролог преобразует 'a' < 'b' в эквивалентное
арифметическое выражение 97<98.
Строки
Когда строки или идентификаторы сравниваются, результат зависит
от сравнения символов на соответствующих позициях. Результат
равен тому, который вы получили бы при сравнении первых символов
в том случае, если эти два символа не равны. Если же они равны,
Пролог сравнивает следующую соответствующую пару символов и
возвращает результат в том случае, если на сей раз они не равны. В
противном случае проверяется третья пара и т.д. Сравнение
заканчивается, когда либо найдены два различных символа, либо
достигнут конец любой из строк. Если конец достигнут, и пара
различных символов не найдена, более короткая строка считается
меньшей.
Сравнение "antony" > "antonia" оценится как true (истинное), т. к.
первая пара различных символов содержит в первой строке букву у
(значение 79 в коде ASCII), а в другой строке букву i (ASCII значение
69). Аналогично, сравнение "аа" > "а" истинно.
Выражение "peter" > "sally" будет ложным, т. к. определяется
сравнением значений ASCII первых букв слов peter и sally. Символ р
располагается перед символом s в алфавите, поэтому р имеет
меньшее значение кода ASCII. Таким образом, выражение будет
оценено как ложное.
Идентификаторы
Идентификаторы не могут непосредственно сравниваться из-за их
синтаксиса. В предыдущем примере ( P1=peter,P2...,) идентификатор
peter не может непосредственно сравниваться с идентификатором
sally. Они должны быть связаны с переменными, которые
сравниваются или записаны как строки.
На следующем шаге мы рассмотрим запись и чтение.
Шаг 53.
Основы логического программирования.
Запись и чтение
На этом шаге мы рассмотрим стандартные предикаты
ввода/вывода.
Рассмотрим стандартные предикаты, предназначенные для ввода и
вывода информации.
Запись
В Пролог включены три стандартных предиката для вывода. Это:



предикат write;
предикат nl;
предикат writef.
Предикат write может быть вызван с произвольным числом
аргументов:
write(Param1,Param2,Param3,...,ParamN) % (i,i,i,...,i)
Эти аргументы могут быть либо константами из стандартных
доменов, либо переменными. Если это переменные, то они должны
быть входными параметрами.
Стандартный предикат nl (от англ. new line - новая строка) всегда
используется вместе с предикатом write. Он обеспечивает переход на
новую строку на экране дисплея. Например, следующие подцели:
pupil(PUPIL,CL),
write(PUPIL,"is in the",CL,"class"),
nl,
write("----------------------------").
могут привести к выводу на экран такого результата:
Helen Smith is in the fourth class
------------------------------,
а цель:
....,
write("List1= ",L1,",List2= ",L2)
может дать:
Listl=[cow,pig,rooster],List2=[1,2,3].
В свою очередь, если My_sentence связана с записью
sentence(subject(john),sentence_verb(sleeps))
то, выполняя следующую программу:
domains
sentence=sentence(subject,sentence_verb)
subject=subject(symbol); .....
sentence_verb=sentence_verb(verb); ......
verb=symbol
clauses
......
write( " SENTENCE= ",My_sentence).
вы сможете увидеть на дисплее:
SENTENCE=sentence(subject(john),sentence_verb(sleeps))
Обратите внимание на наличие в строках обратного слэша (\). Это
управляющий символ. Чтобы напечатать непосредственно символ \
(обратный слэш), вы должны ввести два обратных слэша подряд.
Например, для порождения строки пути к файлу в DOS
A:\PROLOG\MYPROJS\MYFILE.PRO в программе на Прологе нужно
ввести a:\\prolog\\myprojs\\myfile.pro.
За обратным слэшем может следовать какой-либо из специальных
символов управления печатью:


'n' - новая строка и возврат каретки;
't' - табуляция;
Также за обратным слэшем могут следовать до трех десятичных
цифр, задающих специальный код ASCII. Однако не следует
вставлять последовательность \0 в строку, если в этом нет
необходимости. Пролог использует те же соглашения относительно
строк, завершающихся нулем, что и язык С.
Предикат write сам по себе не предоставляет всю полноту функций
управления печатью для таких сложных объектов, как, например,
списки, но написать программы, которые вам их предоставят, просто.
Следующие три небольших примера демонстрируют эти возможности.
Примеры использования предиката write
Эти примеры показывают, как можно использовать предикат write
для того, чтобы получить возможность выводить на печать такие
объекты, как списки и сложные структуры данных.
1. Следующая программа печатает список без открывающей [ и
закрывающей ] квадратных скобок.
2.
domains
3.
integerlist=integer*
4.
namelist=symbol*
5.
predicates
6.
writelist(integerlist)
7.
writelist(namelist).
8.
clauses
9.
writelist([]).
10.
writelist([H|T]):11.
write(H," "),
12.
writelist(T).
Текст этой программы можно взять здесь.
Обратите внимание, как эта программа использует рекурсию
для обработки списка. Попробуйте ввести в программу и
исполнить такую цель:
writelist([1,2,3,4]).
13.
В следующем примере программа выводит элементы
списка не более чем по пять элементов на строке.
14. domains
15.
integerlist=integer*
16. predicates
17.
writelist(integerlist)
18.
write5(integerlist,integer).
19. clauses
20.
writelist(NL):21.
nl,
22.
write5(NL,0),nl.
23.
write5(TL,5) :-!,
24.
nl,
25.
26.
27.
28.
29.
30.
write5(TL,0).
write5([H|T],N):-!,
write(H," "),
N1=N+1,
write5(T,N1).
write5([],_).
Текст этой программы можно взять здесь.
Если вы дадите программе такую цель
writelist([2,4,6,8,10,12,14,16,18,20,22]).
Пролог ответит
2 4 6 8 10
12 14 16 18 20
22
31.
Пример предиката, который выводит сложные структуры
данных в более удобном для чтения виде, приведен в
программе, расположенной ниже. Программа выводит объекты
типа
32. plus(mult(x,number(99)),mult(number(3),x))
в виде
x*99+3*x
Это называется инфиксной записью.
domains
expr=number(integer); x; log(expr);
plus(expr, expr); mult(expr, expr)
predicates
writeExp(expr)
clauses
writeExp(x):write(x1).
writeExp(number(No)):write(No).
writeExp(log(Expr)). write("log ("),
writeExp(Expr),
write (')').
writeExp(plus(U1, U2)):-
writeExp(Ul),
write('+'),
writeExp(U2).
writeExp(mult(U1,U2)):writeExp(U1),
write('*'),
writeExp(U2).
goal
writeExp(plus(mult(x,number(99)),mult(number(3),x))),nl.
Текст этой программы можно взять здесь.
На следующем шаге мы рассмотрим предикат writef.
Шаг 54.
Основы логического программирования.
Предикат writef
На этом шаге мы рассмотрим предикат writef.
Предикат writef/*
Предикат writef позволяет выполнить форматированный вывод; он
имеет следующий формат:
writef(FormatString, Arg1,Arg2,Arg3,...,ArgN) % (i,i,i,i, . ..,i)
Аргументы Arg1,...,ArgN должны быть константами или связанными
переменными, принадлежащими стандартным доменам. Сложные
домены форматировать нельзя. Строка форматирования содержит
обычные символы и форматные спецификаторы; обычные символы
печатаются без модификации, а форматные спецификаторы имеют
следующую форму:
%-m.pf
Символы спецификаторов формата, следующие за знаком процента
(%), являются необязательными и имеют значения, описанные в
таблице 1.
Таблица 1. Значения символов спецификаторов формата
Символ
Дефис
(-)
Значение
Показывает, что поле выравнивается слева (по умолчанию
выравнивается справа)
m поле Десятичное число, определяющее минимальную длину поля
p поле
Десятичное
число,
описывающее
либо
точность
представления с плавающей точкой, либо максимальное
число символов, печатаемых в строке
f поле
Описывает форматы, отличные от форматов, принятых по
умолчанию для данного объекта. Например, поле f может
описывать то, что целые числа должны быть напечатаны как
беззнаковые или шестнадцатеричные числа
Пролог распознает спецификаторы формата поля f, описанные в
таблице 2.
Таблица 2. Спецификаторы формата поля f
Спецификатор
формата
Описание
f
Вещественные
в
представлении
с
фиксированной точкой (типа 123.4 или 0.004321)
e
Вещественные
в
экспоненциальном
представлении (типа 1.234е2 или 4.321 е-3)
g
Вещественные в формате f или е (формат по
умолчанию)
d
Целые, как знаковые десятичные числа
u
Целые, как беззнаковые десятичные целые
x
Целые, как шестнадцатеричные числа
X
Целые, как шестнадцатеричные длинные числа
c
Целые, как символы (char)
s
Как строки (simbols и string)
R
Как числа ссылки во внешних базах данных
(только для домена ref)
Примеры форматированного вывода
1. Следующая программа иллюстрирует действие различных
спецификаторов формата при форматированном выводе с
помощью writef.
2.
goal
3.
A=one,
4.
B=330.12,
5.
C=4.3333375,
6.
D="one two three",
7.
writef("A= '%-7' \nB='%8.1e'\n",A,B),
8.
9.
10.
11.
writef("A='%' \nB='%8.4e'\n",A,B) ,nl,
writef("С ='%-7.7g' \nD = '%7.7' \n",C,D),
writef("C='%-7.0f' \nD =' %0 '\n",C, D),
writef("char: %c,decimal: %d,hex: %x",'a','a','a','a').
Текст этой программы можно взять здесь.
После запуска эта программа создаст следующий вывод:
А ='one'
В ='3.3Е+02'
А ='one'
В ='3.3012Е+021'
С ='4.3333375'
D ='one two'
С ='4'
D ='one two three'
char: a,decimal: 97, hex: 61
12.
Пример, показывающий, как можно использовать writef для
форматного вывода, приведен в следующей программе.
13. database
14.
person(string,integer,real)
15. predicates
16.
run
17. clauses
18.
person("Pete Ashton",20,11111.111).
19.
person("Marc Spiers",32,33333.333).
20.
person("Kim Clark",28,66666.666).
21. run:22.
% Name is left-justified, at least 15 characters wide
23.
% Age is right-justified, 2 characters wide
24.
% Income is right-justified, 9 characters wide, with 2
25.
% decimal places, printed in fixed-decimal notation
26.
person(N, A, I),
27.
writef("Name= %-15, Age= %2, Income= $%9.2f \n",N,A,I),
28.
fail
29.
;
30.
true.
31. goal
32.
run.
Текст этой программы можно взять здесь.
Результат программы будет следующим:
Name= Pete Ashton , Age= 20, Income= $ 11111.11
Name= Marc Spiers , Age= 32, Income= $ 33333.33
Name= Kim Clark , Age= 28, Income= $ 66666.67
На следующем шаге мы рассмотрим чтение.
Шаг 55.
Основы логического программирования.
Чтение
На этом шаге мы рассмотрим чтение.
Пролог включает в себя несколько стандартных предикатов для
чтения. Из них четыре основных:




readln - для чтения всей строки символов;
readint - для чтения целых значений;
readreal - для чтения вещественных значений;
readchar - для чтения символьных значений.
И дополнительно - readterm - для чтения любых термов, включая
составные объекты.
Все эти предикаты могут быть переориентированы для чтения из
файлов.
Другой, более специализированный предикат, относящийся к
категории чтения, - это file_str - для чтения всего текстового файла в
строку.
Предикат readln/1
Предикат readln читает текстовую строку, используя формат:
readln(Line) % (о)
Домен для переменной Line должен быть строкового типа. Перед
тем как вы вызовете readln, переменная Line должна быть свободна,
readln считывает до 254 символов (плюс возврат каретки) с
клавиатуры и до 64 Кбайт с других устройств. Если во время ввода с
клавиатуры нажать клавишу Esc, то readln потерпит неудачу.
Предикаты readint/1, readreal/1, readchar/1
Предикат readint считывает целое значение, используя формат:
readint(X) % (о)
Домен для переменной X должен быть целого типа, а X перед
вызовом должна быть одна. Предикат readint будет считывать целое
значение с текущего входного устройства (возможно, с клавиатуры),
пока не будет нажата клавиша Enter. Если считанная строка не
соответствует синтаксису целых, readint терпит неудачу, и Пролог
вызывает механизм поиска с возвратом. Если во время ввода с
клавиатуры нажать клавишу Esc, то readint также терпит неудачу.
Предикат readreal работает в соответствии со своим названием: он
считывает вещественные числа (аналогично тому, как readint
считывает целые). Предикат readreal ипользует следующий формат:
readreal(X) % (о)
Домен для переменной X должен быть вещественного типа, а X
должна перед вызовом быть свободна. Предикат readreal будет
читать вещественные значения с текущего устройства ввода, пока не
будет нажат клавиша Enter. Если ввод не соответствующих обычному
синтаксису вещественных чисел, то readreal терпит неудачу. Если во
время ввода нажать клавишу Esc, readreal также терпит неудачу.
Предикат readchar считывает один символ с текущего устройства
ввода, используя формат:
readchar(CharParam) % (о)
Перед вызовом предиката readchar переменная CharParam должна
быть свободной и принадлежать домену char. Если текущим
устройством ввода является клавиатура, readchar ждет, пока будет
нажат один символ, после чего возвращает его. Если во время ввода
нажать клавишу Esc, то readchar терпит неудачу.
Предикат readterm/2
Предикат readterm
следующий формат:
считывает
сложные
термы.
Он
имеет
readterm(DomainName,Term) % (i,i)
Вы вызываете readterm с двумя аргументами: именем домена и
термом, readterm читает строку и превращает ее в терм указанного
домена. Если строка не имеет вид объекта, сформированного
предикатом write, то readterm дает ошибку. Стандартный предикат
readtermerror может быть использован для получения информации об
ошибках, произошедших в readterm.
Предикат readterm может использоваться для обработки термов в
текстовых файлах Например, вы можете создать собственную версию
consult.
Предикат file_str/2
Предикат file_str читает символы из файла в строку, или создает
файл и записывает в этот файл строку. Он использует формат:
file_str(Filename,Text) % (i,o),(i,i)
Если перед вызовом file_str переменная Text свободна, file_str
читает символы из файла Filename, пока не встретит символ конца
файла (обычно это комбинации клавиш Ctrl+Z). Содержимое файла
Filename пересылается в переменную Text. Например, вызов:
file_str ("T.DAT",My_text)
свяжет Mytext с содержимым файла T.DAT. При этом строка может
содержать символы возврата каретки.
Если My_text связана с текстом из T.DAT, то file_str("Т.ВАК",
Mytext) создаст файл с именем Т.ВАК, который содержит текст из
T.DAT. Если Т.ВАК уже существует, он будет перезаписан.
Рассмотрим несколько примеров использования стандартных
предикатов чтения, демонстрирующих, как можно использовать
стандартные предикаты чтения для работы со сложными структурами
данных и вводимыми списками.
1. Следующая программа показывает, как извлекать сложные
структуры данных из входных строк.
2.
domains
3.
person=p(name,age,telno,job)
4.
age=integer
5.
telno,name,job=string
6.
predicates
7.
readperson(person)
8.
run
9.
clauses
10.
readperson(p(Name,Age,Telno,Job)):11.
write("Which name?"),
12.
readln(Name),
13.
write("Job?"),
14.
readln(Job),
15.
write("Age?"),
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
readint(Age),
write("Telephone no?"),
readln(Telno).
run:readperson(P),nl,
write(P),nl,nl,
write("Is this compound object OK(y/n)"),
readchar(Ch),Ch='у',
!.
run:nl,
write("Alright,try again"),
nl,nl,
run.
goal
run.
Текст этой программы можно взять здесь.
Результат работы программы можно посмотреть на рис.1
Рис.1. Результат работы программы pro55_1.pro
32.
В данном примере показано использование предиката
readint для чтения целых чисел и преобразования их в список.
Он читает одно целое число на строке, пока вы не введете не
целое (например, клавишу Х). После этого readint потерпит
неудачу, и Пролог выведет список на экран.
33. domains
34.
list=integer*
35. predicates
36.
readlist(list)
37. clauses
38.
readlist([H|T]):39.
write("> "),
40.
readint(H),!,
41.
readlist(T).
42.
readlist([ ]).
43. goal
44.
write("*************** Integer List *****************"),nl,
45.
write("Type in a column of integers, like this:"),nl,
46.
write("integer (press ENTER)\n integer (press ENTER)\n"),
47.
write(" etc.\n\n Type X (and press ENTER) to end the
list.\n\n"),
48.
readlist(TheList),nl,
49.
write("The list is: ",TheList).
Текст этой программы можно взять здесь.
Загрузите программу и запустите ее. После подсказки введите
столбец целых чисел (например, 1234, 567, 89, 0), затем
нажмите комбинацию клавиш X+Enter для окончания ввода.
Со следующего шага мы начнем рассматривать файловую
систему.
Шаг 56.
Основы логического программирования.
Файловая система в Прологе
На этом шаге мы рассмотрим файловую систему.
На этом шаге мы представим вам файловую систему в Прологе и
стандартные предикаты, обращающиеся с файлами. Познакомимся с
переназначением ввода/вывода - эффективным методом связи ввода
и вывода с различными устройствами.
Пролог использует current_readdevice (текущее устройство
чтения), с которого считывается ввод, и current_write_device
(текущее устройство записи), на которое посылается вывод. Как
правило, текущим устройством чтения является клавиатура, а
текущим устройством записи - экран дисплея. Однако вы можете
назначить другие устройства. Например, ввод может читаться из
файла, хранимого во внешней памяти (возможно, на диске). Можно
даже переопределить устройства текущего ввода и вывода во время
исполнения программы.
Независимо от того, какими устройствами чтения и записи вы
пользуетесь, в программе на Прологе чтение и запись
обрабатываются идентично. Для доступа к файлу вы должны сначала
его открыть. Файл может быть открыт:




для чтения;
для записи;
для добавления;
для модификации.
Файл открытый для любого действия, отличного от чтения, должен
быть закрыт после завершения операции. В противном случае
внесенные в файл изменения могут быть потеряны. Можно открыть
несколько файлов одновременно. При этом и ввод вывод могут быть
быстро переназначены между открытыми файлами. Открытие и
закрытие файлов занимает намного больше времеми, чем
переназначение потоков данных между ними.
Когда Пролог открывает файл, он связывает символическое имя с
действительным именем файла операционной системы и использует
это символическое имя для направления ввода и вывода.
Символические имена файлов должны начинаться с маленькой буквы
и должны быть объявлены в описании домена file. Например:
file=file1; source; auxiliary; somethingElse
В любой программе разрешен только один домен file. Пролог
распознает пять встроенных альтернатив file, описанных в таблице 1.
Таблица 1. Встроенные альтернативы домена file
Альтернатива
Описание
keyboard
Чтение с клавиатуры (по умолчанию)
screen
Запись в монитор
stdin
Чтение из стандартного ввода
stdout
Запись в стандартный вывод
stderr
Запись на стандартное устройство для вывода ошибок
Эти встроенные альтернативы не должны встречаться в описании
file. Открывать и закрывать их не требуется.
Открытие и закрытие файлов
Следующие разделы описывают стандартные предикаты для
открытия и закрытии файлов.
Замечание. Во время открытия файла необходимо помнить, что
обратный слэш (\), используемый для указания подкаталога диска, в
DOS-ориентированных версиях Пролога является ESC-символом
(управляющим). Поэтому при указании пути доступа файла в
программе нужно всегда указывать два обратных слэша (\\).
Например, строка:
"с:\\prolog\\include\\iodecl.con"
представляет путь доступа к файлу:
с:\prolog\include\iodecl.con
Предикат openread/2
Предикат openread открывает файл OSFileName дли чтения,
используя формат:
openread(SymbolicFileName,OSFileName) % (i, i)
Пролог обращается к открытому файлу по символическому имени
SymbolicFileName, объявленному в домене file. Если файл не может
быть открыт, Пролог выдаст сообщение об ошибке.
Предикат openwrite/2
Предикат openwrite открывает файл OSFileName для записи,
используя формат:
openwrite(SymbolicFileName,OSFileName) % (i,i)
Если файл уже существует, то он уничтожается. В противном
случае Пролог создает новый файл и помещает его в
соответствующем каталоге. Если файл не может быть создан, Пролог
выдаст сообщение об ошибке.
Предикат openappend/2
Предикат openappend открывает файл OSFileName для записи в
конец файла. При этом используется формат:
openappend(SymbolicFileName,OSFileName) % (i,i)
Если файл не может быть открыт на запись, Пролог сообщит об
ошибке.
Предикат openmodify/2
Предикат openmodify открывает файл OSFileName и для записи, и
для чтения; если файл уже существует, он не будет перезаписан,
openmodify имеет формат:
openmodify(SymbolicFileName,OSFileName) % (i,i)
Если система не может открыть OSFileName, Пролог сообщит об
ошибке. Для заполнения файла с произвольным доступом предикат
openmodify может использоваться вместе со стандартным
предикатом filepos.
Предикат filemode/2
При открытии файла в текстовом режиме предикат filemode
устанавливает указанный файл в текстовый или двоичный режим,
используя формат:
filemode(SymbolicFileName,FileMode) % (i,i)
Если FileMode=0, файл SymbolicFileName устанавливается в
двоичный режим; если FileMode=1, то он устанавливается в
текстовый режим.
В текстовом режиме при записи к новым строкам добавляются
символы "возврат каретки"\"перевод строки", а при чтении эта пара
символов интерпретируется как новая строка.
Carriage
return
(возврат
каретки)
Line feed (перевод строки) = ASCII 10
=
ASCII
13
В двоичном режиме никаких преобразований не производится. Для
чтения двоичного файла вы можете использовать только предикат
readchar или предикаты для доступа к двоичным файлам.
Предикат closefile/1
Предикат closefile закрывает указанный файл; он использует
формат:
closefile(SymbollcFileName) % (i)
Этот предикат всегда завершается успешно, даже если файл не
был открытым.
Предикат readdevice/1
Предикат readdevice переопределяет current_read_device
(текущее устройство чтения) или возвращает его имя. Предикат имеет
формат:
readdevice(SymbolicFileName) % (i), (о)
Предикат readdevice переопределяет текущее устройство чтения,
если переменная SymbolicFileName определена, и файл открыт для
чтения. Если SymbolicFileName является свободной переменной, то
readdevice присвоит ей имя текущего активного устройства чтения.
Предикат writedevice/1
Предикат writedevice либо назначает, либо позволяет получить имя
current_ write_device (текущего устройства записи). Он имеет
формат:
writedevice(SymbolicFileName) % (i), (о)
Предикат writedevice переопределит устройство записи, если
указанный файл открыт для записи или добавления. Если переменная
SymbolicFileName свободна, writedevice присвоит ей имя текущего
активного устройства записи.
Примеры открытия файла, записи в файл и закрытия файла:
1. Следующая последовательность открывает файл MYDATA.FIL
для записи, затем направляет весь вывод, порождаемый
операторами между двумя предикатами writedevice, в этот
файл. MYDATA.FIL соответствует символическому имени
destination, появляющемуся в описании домена file.
2.
domains
3.
file=destination
4.
goal
5.
openwrite(destination, "MYDATA.FIL"),
6.
writedevice(OldOut), % Получаем текущее устройство
вывода
7.
writedevice(destination), % Перенаправляем вывод в
файл
8.
:
9.
:
10.
writedevice(OldOut), % Восстанавливаем устройство
вывода
11.
Программа помещает символы, набранные наклавиатуре, в
файл TRYFILE.ONE на текущем диске, использует стандартные
предикаты read и write. Набираемые символы не выводятся на
экран дисплея. Для вас будет хорошим упражнением написать
программу, которая выводила бы эти символы и на экpaн. Файл
закрывается при нажатии клавиши #.
12. domains
13.
file=myfile
14. predicates
15.
readloop
16. clauses
17.
readloop:18.
readchar(X),
19.
X<>'#',
20.
!,
21.
write(X),
22.
readloop.
23.
readloop.
24. goal
25.
write("This program reads your input and writes it to"),nl,
26.
write("tryflie.one. For stop press #"),nl,
27.
openwrite(myfile,"tryfile.one") ,
28.
writedevice(myfile),
29.
readloop,
30.
closefile(myfile),
31.
writedevice(screen),
32.
write("Your input has been transferred to the file
tryflie.one"),
33.
nl.
Текст этой программы можно взять здесь.
Переопределение стандартного ввода/вывода
Домен file имеет три дополнительные опции: stdin, stdout, stderr.
Преимущество этих файловых потоков в том, что вы можете
переназначить стандартный ввод/вывод в командной строке (таблица
2).
Таблица 2. Файловые потоки
Файловый
поток
Описание
stdin
Стандартный ввод является файлом, доступным только
для
чтения.
По
умолчанию
это
клавиатура,
readdevice(stdin) назначает устройством ввода stdin
stdout
Стандартный вывод является файлом, доступным
только для записи. По умолчанию это экран терминала,
writedevice(stdout) назначает устройством ввода stdout
stderr
Стандартный вывод ошибок является файлом, который
доступен только для записи. По умолчанию это экран
терминала. Writedevice(stderr) назначает устройством
для вывода сообщений об ошибках stderr
На следующем шаге мы начнем рассматривать работу с
файлами.
Шаг 57.
Основы логического программирования.
Работа с файлами
На этом шаге мы рассмотрим работу с файлами.
На этом шаге мы опишем несколько других предикатов для работы
с файлами. Это предикаты filepos, eof, flush, existfile, deletefile,
renamefile, disk и copyfile.
Предикат filepos
Предикат filepos может управлять позицией, где производится
чтение или запись. Он имеет формат:
filepos(SymbolicFileName,FilePosition,Mode) % (i,i,i),(i,o,i)
Если FilePosition - связанная переменная, предикат может
изменить позицию чтения и записи для файла с именем
SymbolicFileName. Если при вызове FilePosition является свободной
переменной, то SymbolicFileName возвращает текущую позицию в
файле.
Mode является целой величиной и указывает, как должно
интерпретироваться (согласно таблице 1) значение FilePosition.
Таблица 1. Интерпретация значения FilePosition
Mode
FilePosition
0
Относительно начала файла
1
Относительно текущей позиции
2
Относительно
позиции 0)
конца
файла
(конец
файла
соответствует
Когда возвращается FilePosition, то filepos возвращает позицию
относительно начала файла независимо от значения Mode.
Предикат eof/1
Предикат eof проверяет, является ли позиция, полученная в
процессе чтения, концом файла. В этом случае eof успешен. В
противном случае он терпит неуспех. Предикат eof имеет вид:
eof(SymbolicFileName) % (i)
eof выдает ошибку во время выполнения, если файл был открыт с
правами только на запись. Обратите внимание, что предикат не
придает особого значения символу конца файла DOS (комбинация
клавиш Ctrl+Z).
Пример, как предикат eof можно использовать для определения
предиката repfile, который полезен при работе с файлами, repfile
генерирует точку возврата до тех пока не будет достигнут конец
файла.
predicates
repfile(FILE)
clauses
repflie(_).
repfile(F) :not(eof(F)),
repfile(F).
Следующая программа преобразует один файл в другом все буквы
которого являются заглавными.
domains
file=input; output
predicates
convert_file
repfile(FILE)
clauses
convert_file:repfile(input),
readln(Ln), % Перевод букв строки в заглавные
upper_lower(LnInUpper,Ln) ,
write(LnInUpper),nl,
fail.
convert_file.
repfile(_).
repfile(F):not(eof(F)),
repfile(F).
goal
write("Which file do you want convert?"),
readln(InputFileName),nl,
write("What is the name of the output file?"),
readln(OutputFileName),nl,
openread(input,InputFileName),
readdevice(input),
openwrite(output,OutputFileName),
writedevice(output),
convert_file,
closefile(input),
closefile(output).
Текст этой программы можно взять здесь.
Предикат flush/1
Предикат flush записывает содержимое внутреннего буфера в
именованный файл. Он имеет формат.
flush(SymbolicFileName) % (i)
Он же запрашивает систему "сбросить все буферы".
Предикат existfile/1
Предикат existfile выполняется успешно, если файл OSFileName
будет найден. Формат его следующий:
existfile(OSFileName) % (i)
Предикат OSFileName может содержать каталог, а само имя может
содержать знаки подстановки, как c:\psys\*.cfg. Предикат existfile
завершается неуспешно, если имя файла не найдено в обозначенном
пути каталога. Однако, заметьте, несмотря на то, что existfile находит
все файлы, включая файлы с установленными атрибутами "system"
(Системный) и "hidden" (Скрытый), он не находит каталоги.
Для проверки того, что файл присутствует на диске (прежде чем
открывать его), вы можете воспользоваться:
open(File, Name):existfile(Name),
!,
openread(File,Name).
open(_, Name):writeName("Error: the file",Name," is not found").
Предикат deletefile/1
Предикат deletefile удаляет файл, заданный его аргументом, и
имеет формат:
deletefile(OSFileName) % (i)
Предикат deletefile даст ошибку, если не сможет удалить файл.
OSFileName не может содержать подстановочные символы.
Предикат renamefile/1
Предикат renamefile переименовывает файл
OldOSFileName в NewOSFileName. Он имеет формат:
с
именем
renamefile(OldOSFileName, NewOSFileName) %(i,i)
Предикат renamefile будет успешен, если файл с именем
NewOSFileName не существует, и оба имени являются допустимыми
файловыми именами. В противном случае будет выдана ошибка.
Предикат disk/1
Предикат disk используется для изменения текущего диска и/или
каталога/подкаталога и имеет формат:
disk(Path) %(i),(о)
При вызове со свободной переменной в качестве параметра, disk
возвратит текущий каталог. В DOS-ориентированных версиях для
переключения на другой диск без изменения существующего текущего
каталога на этом диске используйте D:. Здесь D - буква,
обозначающая устройство.
На следующем шаге мы рассмотрим основные предикаты
управления строкой.
Шаг 58.
Основы логического программирования.
Основные предикаты управления строкой
На этом шаге мы рассмотрим основные предикаты управления
строкой.
Строки и их обработка подчиняются определенным правилам. Так,
в строках Пролога обратный слэш (\) является управляющим
символом, позволяющим вставлять в строки символы, которых нет на
клавиатуре.
Предикаты, описываемые на этом шаге, являются основой
обработки в Прологе; они служат для нескольких целей:








разделение строки на подстроки и лексемы;
построение строки из определенных подстрок и лексем;
проверка, состоит ли строка из определенных подстрок или
лексем;
возврат лексемы или списка лексем из данной строки;
проверка или определение длины строки;
создание пустой строки определенной длины;
проверка, является ли строка термином, определенным в
Прологе;
форматирование переменного числа аргументов в строковую
переменную.
Предикат frontchar/3
Предикат frontchar действует согласно равенству:
String1 = объединение Char и String2
и имеет следующий формат:
frontchar(String1,Char,String2) %(i,o,o) (i,i,о) (i,o,i) (i,i,i) (o,i,i)
Предикат frontchar имеет три аргумента: первый из них - строка,
второй - симвoл (первый символ первой строки), третий - остаток
первой строки.
Предикат frontchar можно использовать для расщепления строки в
последовательность символов или для создания строки из
последовательности символов, а также проверки символов в строке.
Если аргумент String1 связан со строкой нулевой длины, то предикат
завершается неуспешно.
В следующей программе frontchar используется для определения
предиката, преобразующего строку в список символов.
Задайте цель:
string_chlist("ABC",Z)
После обработки этого целевого утверждения Z будет связано со
списком ['A','В','С'].
domains
charlist=char*
predicates
string_chlist(string,charlist)
clauses
string_chlist("",[ ]):-!.
string_chlist(S,[H |T]):frontchar(S,H,S1),
string_chlist(S1,T).
Текст этой программы можно взять здесь.
Предикат fronttoken/3
Предикат fronttoken выполняет три взаимосвязанные функции, в
зависимости от типа аргументов, который используется для
обращения к нему.
fronttoken(String1,Token,Rest) %(i,о,о) (i,i,о) (i,o,i) (i,i,i) (o,i,i)
В случае потока (i,o,o) fronttoken находит первую лексему в
String1, связывает ее с Token, а остаток String1 связывает с Rest.
Варианты потока (i,i,o), (i,o,i), а также (i, i, i) служат для проверки:
если связанные аргументы соответствуют частям String1 (первой
лексеме, всему, что находится после первой лексемы, или же и тому и
другому), то fronttoken завершается успешно, в противном случае неуспешно.
В случае если использован поток (о,i,i), предикат создает
объединение Token и Rest, связывая String1 с результатом.
Последовательность
представляет собой:



знаков
является
лексемой,
имя в соответствии с синтаксисом Пролога;
число (предшествующий ему знак является
лексемой);
отличный от пробела знак.
если
она
отдельной
Предикат fronttoken отлично приспособлен для разбиения строки
на лексические символы.
Пример в следующей программе иллюстрирует способ
использовании предиката fronttoken для разбития предложения на
список имен. Если задать целевое утверждение:
string_namelist("bill fred tom dick harry",X).
то после выполнения X будет связано со списком:
[bill,fred,tom,dick,harry]
domains
namelist=name*
name=symbol
predicates
string_namelist(string,namelist)
clauses
string_namelist(S,[H |T]):fronttoken(S,H,S1),!,
string_namelist(S1,T).
string_namelist(_, []).
Текст этой программы можно взять здесь.
Предикат frontstr/4
Предикат frontstr расщепляет String1 на две части. Синтаксис
предиката:
frontstr(NumberOfChars,String1,StartStr,EndStr) % (i,i,o,o)
где StartStr содержит NumberOfChars первых символов из String1, a
EndStr содержит остаток. При обращении к frontstr первые два
параметра должны быть связанными, а последние два - свободными.
Предикат concat/3
Предикат concat устанавливает, что строка String3 является
результатом сцепления String1 и String2. Он имеет форму:
concat(String1,Sring2,String3) % (i,i,o), (i,o,i), (o,i,i), (i,i,i)
По крайней мере два параметра должны быть связаны перед тем,
как вы вызываете concat; это означает, что этот предикат всегда дает
только одно решение (другими словами, он - детерминированный).
Например, мы вызываем
concat("croco","dile",In_a_while)
связывая In_a_while с crocodile. В другом случае, если See_ya_later
связано, то вращение к предикату
concat("alli","gator",See_ya_later)
завершается успешно, только если See_ya_later является связанным
с alligator.
Предикат str_len/2
Предикат str_len решает следующие задачи: определяет или
проверяет длину строки или возвращает строку пробелов заданной
длины. Он имеет формат:
str_len(StringArg,Length) % (i,o), (i,i), (o,i)
Предикат str_len связывает переменную Length с длиной строки
StringArg или проверяет, имеет ли StringArg данную длину Length.
Length - это беззнаковое целое. В версии предиката с третьим
потоком str_len возвращает строку пробелов данной длины, что
может быть использовано для распределения буферов и других
операций.
Предикат isname/1
Предикат isname проверяет, является ли аргумент допустимым
именем согласно синтаксису Пролога, и имеет формат:
isname(String) % (i)
Имя начинается с буквы алфавита или символа подчеркивания, за
которым следует любое число букв, цифр и символов подчеркивания.
Предыдущие и последующие пробелы игнорируются.
Предикат format/*
Предикат format выполняет преобразования, аналогичные writef, но
format выдает результат в виде строковой переменной.
format(OutputString,FormatString,Arg1,Arg2,Arg3,
%(o,i,i,i,...,i)
...,
ArgN)
На следующем шаге мы рассмотрим преобразование типов.
Шаг 59.
Основы логического программирования.
Преобразования типов
На этом шаге мы рассмотрим преобразования типов.
На этом шаге мы рассмотрим стандартные предикаты,
предназначенные для преобразования типов. Это предикаты char_int,
str_char, str_int, str_real, upper_lower.
Предикат char_int/2
Предикат char_int преобразует символ в целое число или целое в
символ и имеет формат:
char_int(Char,Integer) % (i,o), (o,i), (i,i)
Если оба аргумента связаны, то char_int проверяет, соответствуют
ли значения аргументов. Если один аргумент связан, а другой
свободен, char_int выполняет преобразование и связывает выходной
параметр с преобразованной формой входной параметра.
Предикат str_char/2
Предикат str_char преобразует строку, содержащую один и только
один символ, в символ или символ в строку из одного символа;
предикат имеет формат:
str_char(String,Char) % (i,o), (o,i), (i,i)
В случае если поток параметров - (i,i), то предикат str_char
завершается успешно, если при этом String связан со строкой из
одного символа, который соответствует Char. Если длина строки - не
единица, то str_char завершается неуспешно.
Предикат str_int/2
Предикат str_int преобразует строку, содержащую целое число, в
его текстовое представление и имеет формат:
str_int(String,Integer) % (i,o), (o,i), (i,i)
В случае если поток параметров - (i,i), то str_int завершается
успешно, при условии, что Integer связан с целым эквивалентом
числа, представленного с помощью String.
Предикат str_real/2
Предикат str_real преобразует строку в вещественное число или
вещественное число в строку и имеет формат:
str_real(String,Real) % (i,o), (o,i), (i,i)
В случае если поток параметров - (i,i), то str_real завершается
успешно, если Real связан с вещественным числом, равным числу,
представленному в String.
Предикат upper_lower/2
Предикат upper_lower преобразует строку, все символы (или часть
символов) которой являются символами верхнего регистра, в строку
соответствующих символов нижнего регистра, и наоборот. Формат
предиката:
upper_lower(Upper,Lower) % (i,o), (o,i), (i,i)
Если оба параметра связаны, то upper_lower завершается
успешно, если Upper и Lower связаны со строками, которые являются
конвертируемыми друг в друга. Напримеp, целевое утверждение:
goal
str1=samPLEstrING,
str2=SAMpleSTRing,
upper_lower(Str1,Str2).
успешно.
Рассмотрим несколько примеров:
1. Пример, приведенный ниже, определяет предикат scanner,
который преобразует строку в список лексем. Лексемы
классифицируются с помощью связывания функтора с каждой
лексемой. В этом примере используются предикаты isname,
str_int, str_char для определения природы лексемы, полученной
с помощью fronttoken.
2.
domains
3.
tok=numb(integer); name(string); char(char)
4.
toklist=tok*
5.
predicates
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
scanner(string,toklist)
maketok(string,tok)
clauses
scanner("",[ ]).
scanner(Str,[Tok |Rest]):fronttoken(Str,Sym,Str1),
maketok(Sym,Tok),
scanner(Str1,Rest).
maketok(S,name(S)):isname(S).
maketok(S,numb(N)):str_int(S,N).
maketok(S,char(C)):str_char(S,C).
goal
write("Enter some text:"),nl,
readln(Text),nl,
scanner(Text,T_List),
write(T_List).
Текст этой программы можно взять здесь.
Результат работы программы можно посмотреть на рис.1
Рис. 1. Результат работы программы pro59_1.pro
26.
Преобразования между типами доменов symbol и string, а
также между integer и real производятся автоматически при
использовании стандартных предикатов или в процессе оценки
математического выражения. Вещественные числа округляются
в
процессе
автоматического
преобразования.
Пролог
производит это автоматическое преобразование по мере
необходимости при вызове предикатов:
27. predicates
28.
p(integer)
29. clauses
30.
p(X):31.
write("The integer value is ",X,'\n').
Текст этой программы можно взять здесь.
Следующие цели дадут одинаковый эффект в этом примере:
Х=97.234, р(Х).
Х=97, р(Х).
Х='а', р(Х).
На следующем шаге мы рассмотрим внутренюю базу фактов.
Шаг 60.
Основы логического программирования.
Внутренняя база фактов
На этом шаге мы рассмотрим внутреннюю базу фактов.
На этом шаге мы опишем, как нужно объявлять разделы внутренних
баз фактов Пролога (internal fact databases) и как можно изменить
содержание вашей внутренней базы фактов.
Внутренняя база фактов состоит из фактов, которые вы можете
непосредственно добавлять и удалять из вашей программы на
Прологе во время ее исполнения. Вы можете объявлять предикаты,
описывающие внутреннюю базу данных в разделе database
программы и применять эти предикаты таким же образом, как
используются предикаты, описанные в разделе predicates.
Для добавления новых фактов в базу данных в Прологе
используются предикаты insert, asserta, assertz, а предикаты retract и
retractall служат для удаления существующих фактов. Вы можете
изменить содержание вашей базы фактов, сначалa удалив факт, а
потом вставив новую версию этого факта (или совершенно другой
факт). Предикаты consult/1 и consult/2 считывают факты из файла и
добавляют их к внутренней базе данных, a save/1 и save/2 сохраняют
содержимое внутренней базы фактов в файле.
Пролог интерпретирует факты, принадлежащие к базе данных,
таким же образом, как обычные предикаты. Факты предикатов
внутренней базы фактов хранятся в таблице, которую можно легко
изменять, тогда как обычные предикаты для достижения
максимальной скорости компилируются в двоичный код.
Объявление внутренней базы фактов
Ключевое слово database определяет начало объявления раздела
database. Раздел database состоит из последовательности
объявлений предикатов, описывающих соответствующую внутреннюю
базу фактов. Во время выполнения можно с помощью предикатов
assert и assertz добавлять факты (но не правила) в базу фактов. Или,
вызвав стандартный предикат consult, вы можете извлечь
добавляемые факты из файла на диске. Раздел database может
выглядеть так, как в следующем примере:
domains
name,address=string
age=integer
gender=male; female
database
person(name,address,age,gender)
predicates
male(name,address,age)
female(name,address,age)
child(name,age,gender)
clauses
male(Name,Address,Age):person(Name,Address,Age,male).
...
В этом примере вы можете использовать предикат person таким же
образом, как используются другие предикаты (male, female, child).
Единственное отличие состоит в том, что вы можете добавлять и
удалять факты для предиката person во время работы программы.
Следует отметить следующие два ограничения на предикаты,
объявленные в разделе фактов:


разрешается добавлять в базу данных только факты, но не
правила;
факты базы не могут содержать свободные переменные.
Допускается наличие нескольких разделов database, но для этого
нужно явно указать имя каждого раздела database.
database - mydatabase
myFirstRelation(integer)
mySecondRelation(real,string)
myThirdRelation(string)
/* etc. */
Описание раздела database с именем mydatabase создает базу
данных фактов с именем mydatabase. Если вы не даете имени
внутренней базе фактов, то по умолчанию ей присваивается
стандартное имя dbasedom.
На следующем шаге мы рассмотрим использование внутренних
баз фактов.
Шаг 61.
Основы логического программирования.
Использование внутренних баз фактов
На этом шаге мы рассмотрим использование внутренних баз
фактов.
Поскольку Пролог представляет реляционную базу данных как
коллекцию фактов, вы можете использовать его в качестве мощного
языка запросов к внутренним базам фактов. Алгоритм унификации
Пролога автоматически выбирает факты с правильными значениями
для известных аргументов и присваивает значения неизвестным
аргументам, пока его алгоритм поиска с возвратом выдает все
решения для заданного запроса.
Доступ к внутренней базе фактов
Предикаты, принадлежащие внутренней базе фактов, доступны
точно так же, как и другие предикаты. Единственное видимое
различие состоит в том, что объявления таких предикатов
расположены в разделе database вместо раздела predicates. В
следующем примере:
domains
name=string
sex=char
database
person(name,sex)
clauses
person("Helen",'F').
person ("Maggie",'F').
person("Suzanne",'F').
person("Per",'M').
вы можете вызвать person с целью person (Name,'F') для нахождения
всех женщин, или person ("Maggie",'F') для проверки того, что
женщина по имени Maggie существует в вашей базе данных.
По своей природе предикаты в разделе database всегда
недетерминированные. Так как факты могут быть добавлены в любой
момент во время выполнения программы, компилятор всегда должен
учитывать, что существует возможность найти альтернативные
решения в ходе поиска с возвратом.
На следующем шаге мы рассмотрим объявление внутренней
базы фактов.
Шаг 62.
Основы логического программирования.
Обновление внутренней базы фактов
На этом шаге мы рассмотрим обновление внутренней базы
фактов.
Факты для предикатов базы фактов могут быть определены во
время компиляции в разделе clauses, как это показано в последнем
примере. Во время выполнения факты могут быть добавлены и
удалены, используя описанные ниже предикаты. Обратите внимание,
что факты, определенные во время компиляции в разделе clauses,
также могут быть удалены, они ничем не отличаются от фактов,
добавленных во время выполнения.
Стандартные предикаты Пролога для работы с фактами: assert,
asserta, assertz, retract, retractall, consult и save - могут иметь один
или два аргумента. Необязательный второй аргумент представляет
собой имя внутренней базы фактов. Обозначение /1 и /2 после
каждого имени предиката указывает необходимое число аргументов
для данной версии предиката. Комментарии (такие как /* (i) */ и /* (o,i)
*/) показывают поток(и) параметров для этого предиката.
Занесение фактов во время выполнения программы
Но время выполнения факты могут быть добавлены во внутреннюю
базу данных фактов посредством предикатов: assert, asserta и
assertz, или путем загрузки фактов из файла с помощью consult.
Существует три предиката для добавления одного факта во время
выполнения:
asserta(the fact) % (i)
asserta(the fact,facts_sectionName) % (i,i)
assertz(the fact) % (i)
assertz(the fact,facts_sectionName) % (i,i)
assert(the fact) % (i)
assert(the fact,facts_sectionName) % (i,i)
Предикат asserta вставляет новый факт в базу данных фактов
перед имеющимися фактами для данного предиката, a assertz
вставляет факты после имеющихся фактов данного предиката.
Использование предиката assert дает результат, аналогичный
использованию assertz.
Поскольку имена предикатов базы фактов уникальны внутри
программы, для предикатов asserta и assertz всегда известно, в какую
базу данных фактов нужно добавлять факт. Однако для того, чтобы
обеспечить работу с требуемой базой данных фактов, в целях
проверки типа можно использовать необязательный второй аргумент.
Первый предикат следующего примера вставит факт о Suzanne,
описанный предикатом person, после всех фактов person,
хранящихся на текущий момент в памяти. Второй - факт о Michael
перед всеми имеющимися фактами предиката person. Третий - факт о
John после всех других фактов likes в базе данных фактом
likesDatabase, а четвертый вставит факт о Shannon в той же базе
данных фактов перед всеми другими фактами likes.
assertz(person("Suzanne","New Haven",35)).
assertz(person("Michael","New York",26)).
assertz(likes("John","money"),likesDatabase).
asserta(likes("Shannon","hard work"),likesDatabase).
После вызова этих предикатов база фактов будет выглядеть так,
как будто вы начали работу со следующими фактами:
% Внутренняя база фактов - dbasedom
person("Michael","New York",26).
% ... другие факты person ...
person("Suzanne","New Haven",35).
% Внутренняя база фактов - likesDatabase
likes("Shannon","hard work").
% ... другие факты likes...
likes("John","money").
Остерегайтесь случайно написать код, утверждающий один и тот же
факт дважды. Внутренние базы фактов не предусматривают никакой
уникальности, поэтому один и тот же факт может появляться во
внутренней базе данных фактов много раз. Однако версию assert с
проверкой на уникальность написать очень просто:
database - people
person(string,string)
predicates
uassert(people)
clauses
uassert(person(Name,Address)):person(Name,Address) ,
!,
; % OR
assert(person(Name,Address)).
Считывание фактов из файла
Предикат consult считывает из файла fileName факты, описанные в
разделе database, и вставляет их в вашу программу в конец
соответствующей базы фактов. Предикат consult имеет один или два
аргумента:
consult(fileName) % (i)
consult(fileName,databaseName) % (i,i)
Однако в отличие от assertz, если вы вызовите consult только с
одним аргументом (без имени базы фактов), то будут считаны лишь
факты, которые были описаны в разделе без имени (по умолчанию
dbasedom).
Если вы вызовите consult с двумя аргументами (имя файла и имя
базы фактов), то будут проверены только факты из указанной базы
фактов. Если файл содержит еще что-нибудь, кроме фактов указанной
базы, то предикат consult, когда он дойдет до этой строки, возвратит
ошибку.
Обратите внимание, что предикат consult считывает по одному
факту. Если файл содержит десять фактов, а в седьмом факте
имеется какая-нибудь синтаксическая ошибка, consult занесет шесть
первых фактов в базу данных фактов, после чего выдаст сообщение
об ошибке.
Отметим, что предикат consult может считывать файлы только в
том формате, который создает save. Файлы не должны содержать:





символов верхнего регистра, за исключением тех, которые
содержатся внутри строк в двойных кавычках;
пробелов, за исключением тех, которые содержатся внутри строк
в двойных кавычках;
комментариев;
пустых строк;
идентификаторов (symbol) без двойных кавычек.
При создании или изменении файла с фактами в редакторе нужно
соблюдать аккуратность.
Удаление фактов во время выполнения программы
Предикат retract унифицирует факты и удаляет их из внутренней
базы фактов. Он имеет следующий формат:
retract(the fact) % (i)
retract(the fact,databaseName) % (i,i)
Предикат retract удаляет первый факт из вашей базы данных,
который совпадает с фактом the fact, связывая свободные
переменные the fact во время выполнения программы. Удаление
фактов из внутренней базы фактов эквивалентно процессу доступа к
ним с побочным эффектом удаления унифицировавшихся фактов.
retract является недетерминированным, если предикат базы фактов,
удаляемый retract, не был объявлен детерминированным. При поиске
с возвратом предикат retract удаляет все унифицировавшиеся факты,
пока они имеются, после чего он далее не находит нужных фактов и
завершается неуспешно.
Предположим, в вашей программе имеются следующие разделы
database
person(string,string,integer)
database - likesDatabase
likes(string,string)
dislikes(string,string)
clauses
person("Fred","Capitola",35).
person("Fred","Omaha",37).
person("Michael","Brooklyn",26).
likes("John","money").
likes("Jane","money").
likes("Chris","chocolate").
likes("John","broccoli").
dislikes("Fred","broccoli").
dislikes("Michael","beer").
Имея такие разделы database, Прологу можно задать следующие
цели:
retract(person("Fred",_,_)), % 1
retract(likes(_,"broccoli")), % 2
retract(likes(_,"money"),likesDatabase),
%3
retract(person("Fred",_, _),likesDatabase).% 4
Первая цель удалит первый факт person о Fred из базы фактов
dbasedom. С помощью второй цели из базы фактов likesDatabase
будет удален первый факт, совпадающий с likes(X,"broccoli"). В
случае обеих целей, Пролог знает, из какой базы производить
удаление, поскольку имена предикатов базы фактов уникальны:
предикат person находится только в неименованной базе данных
фактов, a likes - только в базе likesDatabase.
Третья и четвертая цель показывают, как вы можете использовать
для проверки типа второй аргумент. Третья цель успешно
реализуется, удаляя первый факт, совпадающий с likes(_,"money") из
likesDatabase, а четвертая цель выдаст ошибку, потому что нет (и не
может быть) факта person в базе данных фактов likesDatabase.
Сообщение об ошибке выглядит следующим образом:
506 Type error:The functor does not belong to the domain. (Ошибка
типа: Функтор не относится к данному домену)
Следующая цель иллюстрирует, как вы можете получить значения
из предиката
retract:
goal
retract(person(Name,Age)),
write(Name,",",Age),nl,
fail.
Когда вы в качестве второго аргумента retract задаете имя базы
фактов, вы можете не указывать имя предиката базы фактов, из
которого вы удаляете факты. В этом случае retract будет искать и
удалять все факты в указанной базе данных. Например:
goal
retract(X,mydatabase),
write(X),
fail.
Удаление нескольких фактов сразу
Предикат retractall удаляет из вашей базы фактов все факты,
сопадающие с образцом the fact. Предикат retractall имеет
следующий формат:
retractall(the fact)
retractall(the fact,databaseName)
действие retractall аналогично действию, заданному
retractall(X):retract(X),
fail,
retractall(_).
но значительно быстрее него.
Очевидно, предикат retractall всегда завершается успешно. Из
retractall выходные значения получить нельзя. Это означает, что, как
и в случае not, нужно использовать символ подчеркивания для
свободных переменных.
Так же, как и в случае предикатов assert и retract, для проверки
типа можно использовать второй аргумент. И, как в случае предиката
retract, если при вызове retractall используется символ
подчеркивания, то из указанного раздела database можно удалить все
факты.
Следующая цель удаляет все факты о мужчинах из базы фактов с
фактами person:
retractall(person(_,_,_,male)).
Следующая цель удаляет все факты из базы mydatabase.
retractall(_,mydatabase).
Сохранение базы фактов во время работы программы
Предикат save сохраняет факты из указанной базы фактов (раздела
database) в файле. Этот предикат имеет один или два аргумента:
save(fileName) % (i)
save(fileName,databaseName) % (i,i)
При вызове предиката save только с одним аргументом (без имени
базы фактов), в файле fileName будут сохранены факты из базы
фактов dbasedom, используемой по умолчанию.
При вызове предиката save с двумя аргументами (имя файла и имя
базы фактов), в указанном файле будут сохранены факты из раздела
database базы фактов с именем databaseName.
На следующем шаге мы рассмотрим создание базы данных,
располагающейся в оперативной памяти.
Шаг 63.
Основы логического программирования.
Создание базы данных, располагающейся в оперативной памяти
На этом шаге мы рассмотрим создание
располагающейся в оперативной памяти.
базы
данных,
Процесс создания базы данных в начинается с этапа
проектирования базы. При этом требуется учесть следующие
факторы:



размер базы данных;
организацию элементов базы данных;
способы работы и содержания базы данных.
Использование баз данных, располагающихся в оперативной
памяти (резидентных), вполне оправданно, если эта БД имеет не
слишком большой объем.
Для начала необходимо задать начальные данные и создать саму
базу. Затем наступает черед системы управления базой данных
(СУБД), ориентированной на диалог с пользователем. Любая система
такого рода должна содержать как минимум такие возможности:



занесение в базу новых данных;
удаление данных из базы;
выборка и вывод содержащихся в базе данных.
Эти требования предполагают наличие в системе меню,
представляющее пользователю возможность легко ориентироваться
при обращениии к стандартными функциям СУБД, а также оконной
системы, дающей четкое представление о доступных пользователю
средствах.
Обсуждение проекта базы данных
Лучший способ начать проектирование программы - это нарисовать
ее структурную схему (рис. 1).
Рис.1. Структурная схема резидентной БД
Она показывает, что модуль Меню позволяет пользователю
выбрать между четырьмя модулями: process(1) для записи данных в
базу, process(2) для удаления данных, process(3) для вывода данных
на экран, и process(4) для выхода из системы.
Создание базы данных
Создадим футбольную БД. Данные представляют собой
информацию о пяти футболистах. Они включают имена игроков,
названия их команд, номера, позиции, рост, вес, количество
сыгранных сезонов. Вся используемая информация приведена в
таблице 1.
Таблица 1. Данные об игроках
Имя игрока
Егор Титов
Команда Номер Позиция Рост Вес Кол-во
Спартак
пз
182
72
78
Сергей Овчинников Локомотив 1
в
188
74
82
Дмитрий Булыкин
Динамо
н
188
80
53
Дмитрий Лоськов
Локомотив 9
н
180
78
102
пз
174
70
77
Дмитрий Парфенов Спартак
9
11
7
Для работы с ней необходим
информацию. Подходящим является:
предикат,
player(p_name, /* полное имя игрока (string) */
t_name, /*название команды (string) */
p_number, /*номер игрока (integer) */
pos, /*позиция игрока (string) */
height, /*рост (string) */
weight, /*вес (integer) */
кодирующий
эту
nfl_exp) /*стаж выступлений (integer) */
Пролог требует, чтобы все утверждения одного и того же
предиката были сгруппированы в одном месте. В соответствии с этим
требованием группа предиката player записывается в виде:
player("Егор Титов","Спартак",9,"пз","182",72,78).
player("Сергей Овчинников","Локомотив",1,"в","188",74,82).
player("Дмитрий Булыкин","Динамо",11,"н","188",80,53).
player("Дмитрий Лоськов","Локомотив",9,"н","180",78,102).
player("Дмитрий Парфенов"," Спартак ",7,"пз","174",70,77).
Заметим, что в том случае, когда объекты утверждений являются
строками и начинаются с заглавных букв, они заключаются в кавычки.
Также отметим, что рост игрока задается в виде строки, хотя он и
подразумевает числовое значение; в БД как число он не
используется.
Следующей
фазой
создания
БД
является
задание
соответствующих описаний типов. Раздел программы domains будет
выглядеть так:
domains
p_name, t_name, pos, height=string
p_number, weight, nfl_exp=integer
Предикаты динамической базы данных описываются в разделе
программы database. В нашем случае необходим лишь один такой
предикат:
database
dplayer(p_name, t_name, p_number, pos, height, weight, nfl_exp)
Когда программа запускается на счет, утверждения динамической
БД помещаются в оперативной памяти отдельно от "обычных"
утверждений. (Это одна из причин того, что предикаты динамической
БД описываются в специальном разделе программы.) В этот момент
БД полностью готова к работе.
В разделе predicates следует описать все другие предикаты,
используемые в программе. Эти описания выглядят так:
predicates
repeat
/* повтор */
do_mbase /* цель */
assert_database /* создание БД */
menu /* интерфейс в виде меню */
process(integer) /* различные операции из перечня меню */
clear_database /* очистка БД */
player(p_name, t_name, p_number, pos,height, weight, nfl_exp)
error /* выдача сообщения об ошибке */
Как уже ранее говорилось, в начале работы программы необходимо
занести в динамическую БД предназначенную для нее информацию,
содержащуюся в статической БД. Эту задачу выполняет предикат
assert_database. Предикат clear_database предназначен для очистки
БД перед окончанием работы программы.
Задачей предиката error является
неправильной входной информации.
реагирование
на
ввод
Предикат player предназначен для задания начального
содержимого базы данных, той информации, которая отражена в
таблице 1. Когда программа начинает работу, эта информация
засылается в утверждения предиката dplayer.
Предикат do_mbase является главным правилом (модулем)
программы. Он также присутствует в целевом утверждении. Предикат
menu определяет правило, осуществляющее интерфейс с
пользователем при помощи меню. Предикат process(integer)
определяет различные правила, выполняющие все возможные
операции над БД.
Раздел программы goal содержит правило do_mbase.
Теперь можно обобщить все сказанное выше и определить "скелет"
нашей программы БД. Тела правил будут определены в разделе
программы в разделе программы clauses.
Написание программных модулей
После завершения стадии проектирования можно приступить к
стадии реализации проекта. Теперь нашей задачей явится написание
предикатных выражений и вспомогательных правил, которые
потребуются основным модулям.
1. Главный модуль программы do_mbase является
одновременно и целью программы:
do_mbase:assert_database,
makewindow(1,7,7,"Футбольная база данных",0,0,25,80),
menu,
clear_database.
Модуль засылает в базу информацию из утверждений player,
создает окно, выводит меню и очищает БД по окончании работы
программы.
Исходное содержимое БД задается при помощи утверждений с
использованием статических предикатов. Правилом для занесения в
базу этой информации служит:
assert_database:player(P_name,T_name,P_number,Pos,Ht,Wt,Exp),
assertz(dplayer(P_name,T_name,P_number,Pos,Ht,Wt,Exp)),
fail.
assert_database:-!.
Этот предикат использует метод отката после неудачи для
перебора всех утверждений player.
Предикат очистки базы данных:
clear_database:retract(dplayer(_,_,_,_,_,_,_,_)),
fail.
clear_database:-!.
Так как объекты утверждений dplayer в этом правиле не
представляют интереса, то используются анонимные переменные.
Меню предназначено для удобства пользователя в выборе
программных функций. Для обеспечения отвечающего требованиям
экранного простанства создается окно на весь экран (25 строк, 80
колонок). Модуль menu выводит четыре доступные пользователю
опции:




занесение новой информации об игроках,
удаление данных об игроках,
просмотр информации,
выход из программы.
Модуль menu в основном состоит из предикатов write, которые
выводят на экран перечисленные выше опции.
menu:repeat,
clearwindow,
write(" * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *"),nl,
write(" *
* "),nl,
write(" * 1. Занесение новой информации об игроках
* "),nl,
write(" * 2. Удаление данных об игроках
* "),nl,
write(" * 3. Просмотр информации
* "),nl,
write(" * 4. Выход из программы
* "),nl,
write(" *
* "),nl,
write(" * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *"),nl,
nl,
write(" Выберите номер: 1, 2, 3 или 4 : "),
readint(Choice),nl,
process(Choice),
Choice=4,
!.
Обратим внимание на технику, использованную для обеспечения
повторных вызовов модуля menu. Если пользователь введет число,
не равное 4 (4 вызывает окончание программы), подцель Choice=4
становится неуспешной, что вызывает откат к предикату repeat.
2. Модуль для ввода данных
Правило process(1) предназначено для занесения в базу данных.
Этот модуль создает окно для текста, просит пользователя ввести
данные с клавиатуры, считывает их и заносит в БД новое
утверждение dplayer. Вслед за этим модуль убирает вновь созданное
окно и возвращает управление главному меню.
Отдельное, меньшее по размеру окно создается для обеспечения
диалога с программой. За предикатом makewindow, создающим это
окно, идут предикаты write, readln и readint, которые информируют
пользователя о том, какие данные он должен ввести, и считывают эти
данные с клавиатуры:
process(1):makewindow(2,7,7,"Добавить данные в базу",2,20,18,58),
shiftwindow(2),
write("Имя игрока: "),
readln(P_name),
write("Команда: "),
readln(T_name),
....
Аналогично при помощи write, readln и readint вводятся номер
игрока, его позиция, рост, вес, стаж выступлений. За предикатами
write, readln и readint следует предикат assertz. Этот предикат
помещает новые утверждения dplayer вслед за уже имеющимися.
Чтобы вводить данные на русском языке можно воспользоваться
,например, руссификатором Паскаля rkega.com, запустив его до
загрузки Пролога. Переход на русский и обратно правый и левый
Shift.
Объектами этого нового утверждения являются значения,
присвоенные переменным P_name, T_name, P_number и т.п.:
assertz(dplayer(P_name,T_name,P_number,Pos,Ht,Wt,Exp)),
write(P_name," добавлен в базу данных."),
nl,!,
write("Нажмите пробел. "),
readchar(_),
removewindow.
Последние строки сигнализируют об окончании процесса ввода и
убирают дополнительное окно.
3. Модуль для удаления данных
Назначением модуля process(2) является удаление информации из
базы данных. Это правило, также как и правило process(1), создает
свое собственное окно, запрашивает у пользователя имя игрока и
удаляет из БД утверждение, содержащее информацию об этом
игроке. После очистки окна управление вновь передается главному
меню.
Вслед за предикатами, создающими окно и сдвигающими его, идут
предикаты, запрашивающие имя игрока. Введенное пользователем
значение присваивается переменной P_name:
process(2):makewindow(3,7,7,"Удаление
игрока
данных",10,30,7,40),
shiftwindow(3),
write("Задайте имя для удаления: "),
readln(P_name),
.....
из
базы
Следующая часть правила осуществляет операцию удаления
утверждения из БД, посылает короткое сообщение об этом
пользователю, ждет нажатия им произвольной клавиши и убирает с
экрана дополнительное окно:
retract(dplayer(P_name,_,_,_,_,_,_,_)),
write(P_name," был успешно удален из базы данных."),
nl,!,
write("Нажмите пробел."),
readchar(_),
removewindow.
Для удаления из базы выбранного пользователем утверждения
применен предикат retract. Так как любая другая информация об
игроке, кроме его имени, не представляет интерес в данной операции,
то на месте всех других объектов стоят анонимные переменные.
4. Модуль для выборки данных
Назначением модуля process(3) является поиск содержащихся в
базе данных. Этот модуль, как и два уже разобранных, создает свое
собственное окно, а затем запрашивает имя игрока. Если в БД
находится утверждение, содержащее введенное имя, модуль
производит выборку данных и выводит их на экран в удобном
формате:
process(3):makewindow(4,7,7,"Просмотр данных", 7,30,16,47),
shiftwindow(4),
write("Укажите имя: "),
readln(P_name),
dplayer(P_name,T_name,P_number,Pos,Ht,Wt,Exp),
.....
Предикат dplayer ищет нужное утверждение в базе данных и
выбирает строковые и целые значения по каждому запрашиваемому
пункту. Целый ряд предикатов write затем выводит полученные
значения:
nl,write(" Имя игрока:
",P_name),
nl,write(" Команда:
",T_name),
nl,write(" Позиция:
",Pos),
nl,write(" Номер:
",P_number),
nl,write(" Рост:
",Ht," см"),
nl,write(" Вес:
",Wt," кг "),
nl,write(" Кол-во игр:
",Exp),
nl,nl,!,
nl,write("Нажмите пробел"),
readchar(_),
removewindow.
Если в БД отсутствует утверждение с введенным пользователем
именем игрока, программа выдает сообщение об ошибке. Окно для
этого сообщения должно располагаться на видном месте, например, в
центре экрана. Вариант process(3), отвечающий за выдачу сообщения
об ошибке, выглядит так:
process(3):makewindow(5,7,7," Ошибка ",14,7,5,60),
shiftwindow(5),
write("Такого игрока в базе данных нет."),nl,
nl,!,
write("Нажмите пробел."),
readchar(_),
removewindow,
shiftwindow(1).
5. Модуль окончания работы с программой
Модуль process(4) обеспечивает нормальное окончание сеанса
работы с базой данных. Этот модуль не создает своего собственного
окна. Новое окно здесь излишне, так как сообщения очень коротки и
не требуют много места на экране. Модуль, однако, требует от
пользователя четкого ответа на вопрос, хочет ли он окончить работу с
программой:
process(4):write("Закончить работу с программой? (y/n)"),
readln(Answer),
frontchar(Answer,'y',_),!.
Отметим наличие в правиле предиката frontchar. Он успешен
только в том случае, если ответ пользователя на запрос программы
начинается с буквы y. Если вводится иная буква, предикат неуспешен,
поэтому происходит откат к предикату repeat модуля menu.
6. Модуль реакции на ошибку
Аккуратно написанная программа должна надлежащим образом
реагировать на допущенные пользователем ошибки при вводе. Если
пользователь введет число, меньшее 1 или большее 4, будет
успешным одно из правил:
process(Choice):Choice<1, error.
process(Choice):Choice>4, error.
Оба эти правила вызывают модуль error:
error:write("Задайте значение от 1 до 4. "),
write("(Нажмите пробел)"),
readchar(_).
Футбольная база данных
Реализацией приведенного проекта динамической базы данных
является программа "Футбольная база данных".
/* Программа Ф у т б о л ь н а я б а з а д а н н ы х
*/
/* Назначение. Демонстрация примера работающей базы
*/
/*
данных. База данных допускает следуюдующие
операции: */
/*
добавление, удаление и выборку данных. Выборка
включает */
/*
просмотр данных. */
domains
p_name, t_name, pos, height=string
p_number, weight, nfl_exp=integer
database
dplayer(p_name,t_name,p_number,pos,height,weight,nfl_exp)
predicates
repeat
do_mbase
assert_database
menu
process(integer)
clear_database
player(p_name,t_name,p_number,pos,height,weight,nfl_exp)
error
goal
do_mbase.
clauses
repeat.
repeat:repeat.
/* Ф у т б о л ь н а я б а з а д а н н ы х */
player("Егор Титов","Спартак",9,"пз","182",72,78).
player("Сергей Овчинников","Локомотив",1,"в","188",74,82).
player("Дмитрий Булыкин","Динамо",11,"н","188",80,53).
player("Дмитрий Лоськов","Локомотив",9,"н","180",78,102).
player("Дмитрий Парфенов"," Спартак ",7,"пз","174",70,77).
/* К о н е ц н а ч а л ь н ы х д а н н ы х */
assert_database:-
player(P_name,T_name,P_number,Pos,Ht,Wt,Exp),
assertz(dplayer(P_name,T_name,P_number,Pos,Ht,Wt,Exp)),
fail.
assert_database:-!.
clear_database:retract(dplayer(_,_,_,_,_,_,_)),fail.
clear_database:-!.
/* Диалог с этой БД осуществляется по принципу меню. */
/* При этом используются оконнные средства Пролога. */
/*
Основываясь
на
запросе
пользователя,
СУБД
активизирует */
/* соотствующие процессы для удовлетворения этого
запроса */
/* Меню можно расширить за счет включения */
/* новых функций. */
/* З а д а н и е ц е л и в в и д е п р а в и л а */
do_mbase:assert_database,
makewindow(1,7,7,"Футбольная база данных",0,0,25,80),
menu,
clear_database.
menu:repeat,
clearwindow,
write(" * * * * * * * * * * * * * * * * * * * * * * * * * * * *"),nl,
write(" *
* "),nl,
write(" * 1. Занесение новой информации об игроках
*
"),nl,
write(" * 2. Удаление данных об игроках
* "),nl,
write(" * 3. Просмотр информации
* "),nl,
write(" * 4. Выход из программы
* "),nl,
write(" *
* "),nl,
write(" * * * * * * * * * * * * * * * * * * * * * * * * * * * *"),nl,
nl,
write(" Выберите номер: 1, 2, 3 или 4 : "),
readint(Choice),nl,
process(Choice),
Choice = 4,
!.
/* Добавление информации об игроке в БД */
process(1):makewindow(2,7,7,"Добавить данные в базу",2,20,18,58),
shiftwindow(2),
write("Имя игрока: "),
readln(P_name),
write("Команда: "),
readln(T_name),
write("Номер: "),
readint(P_number),
write("Амплуа: "),
readln(Pos),
write("Рост: "),
readln(Ht),
write("Вес: "),
readint(Wt),
write("Количество игр: "),
readint(Exp),
assertz(dplayer(P_name,T_name,P_number,Pos,Ht,Wt,Exp)),
write(P_name," помещен в базу данных."),
nl,!,
write("Нажмите пробел. "),
readchar(_),
removewindow.
/* Удаление информации об игроке из БД */
process(2):makewindow(3,7,7,"Удаление из базы",10,30,7,40),
shiftwindow(3),
write("Задайте имя для удаления: "),
readln(P_name),
retract(dplayer(P_name,_,_,_,_,_,_)),
write(P_name," был успешно удален из базы."), nl, !,
write("Нажмите пробел."),
readchar(_),
removewindow.
/* Просмотр информации об игроке */
process(3):makewindow(4,7,7," Просмотр информации",7,30,16,47),
shiftwindow(4),
write("Задайте имя игрока: "),
readln(P_name),
dplayer(P_name,T_name,P_number,Pos,Ht,Wt,Exp),nl,
nl,write(" Имя: ",P_name),
nl,write(" Команда:",T_name),
nl,write(" Амплуа:",Pos),
nl,write(" Номер:",P_number),
nl,write(" Рост:",Ht," см"),
nl,write(" Вес:",Wt," кг "),
nl,write(" Количество игр:",Exp),
nl,nl,!,
nl,write("Нажмите пробел"),
readchar(_),
removewindow.
process(3):makewindow(5,7,7," Ошибка ",14,7,5,60),
shiftwindow(5),
write("Такого игрока нет в базе данных."),nl,
nl,!,
write("Нажмите пробел."),
readchar(_),
removewindow,
shiftwindow(1).
/* Выход из диалога */
process(4):write("Закончить работу с программой? (y/n)"),
readln(Answer),
frontchar(Answer,'y',_), !.
/* Неправильное обращение к БД */
process(Choice):Choice<1,error.
process(Choice):Choice>4,error.
error:write("Укажите число от 1 до 4."),
write("(Нажмите пробел для продолжения)"),
readchar(_).
/* К о н е ц программы */
Текст этой программы можно взять здесь.
Результат работы программы при выборе, например, пункта меню
3- просмотр информации можно посмотреть на рис.2.
Рис.2. Результат работы программы pro63_1.pro
Каждый из фрагментов программы, спроектированный и описанный
в предыдущих разделах главы, занимает отведенное ему в программе
место.
Исходная информация для БД помещается в начале раздела
clauses. Когда программа запускается на счет, подцель
assert_database создает утверждения dplayer, содержащие такие же
данные, что и утверждения статического предиката player, и заносит
эти утверждения в динамическую БД. После этого можно добавлять,
удалять или просматривать данные, выбирая соответствующие опции
меню.
На следующем шаге мы рассмотрим примеры использования
внутренней базы фактов.
Шаг 64.
Основы логического программирования.
Примеры использования внутренней базы фактов
На этом шаге мы рассмотрим
внутренней базы фактов.
примеры
использования
1. Следующая программа - простой пример того, как с помощью
внутренней базы фактов
написать классификационную
экспертную систему. Важным преимуществом использования
базы фактов в этом примере является то, что вы можете
добавлять знания (и удалять их) во время работы программы.
2.
domains
3.
thing=string
4.
conds=cond*
5.
cond=string
6.
database - knowledgeBase
7.
is_a(thing,thing,conds)
8.
type_of(thing,thing,conds)
9.
false(cond)
10. predicates
11.
run(thing)
12.
ask(conds)
13.
update
14. clauses
15.
run(Item):16.
is_a(X,Item,List),
17.
ask(List),
18.
type_of(ANS,X,List2),
19.
ask(List2),
20.
write("The ",Item,"you need is a/an ",Ans),nl.
21.
run(_):22.
write("This program does not have enough "),
23.
write("data to draw any conclusions."), nl.
24.
ask([ ]).
25.
ask([H |T]):26.
not(false(H)),
27.
write("Does this thing help you to "),
28.
write (H," (enter y/n)"),
29.
readchar(Ans),nl,
30.
Ans='y',
31.
ask(T).
32.
ask([H |_]):33.
assertz(false(H)),
34.
fail.
35.
36. is_a(language,tool,["communicate"]).
37. is_a(hammer,tool,["build a house","fix a fender","crack a
nut"]).
38. is_a(sewing_machine, tool,["make clothing","repair sails"]).
39. is_a(plow, tool, ["prepare field","farm"]).
40.
41. type_of(english,language,["communicate with people"]).
42. type_of(prolog,language,["communicate with a computer"]).
43.
44. update:45.
retractall(type_of(prolog,language,
46.
["communicate with a computer"])),
47.
48.
49.
50.
asserta(type_of("Visual Prolog", language,
["communicate with a personal computer"])),
asserta(type_of(prolog, language,
["communicate with a mainframe computer"])).
Текст этой программы можно взять здесь.
Следующие факты могли бы быть занесены с помощью
предиката asserta или assert, или считаны из файла с помощью
предиката consult. Однако в этом примере они расположены в
разделе clauses.
is_a(language,tool,["communicate"]).
is_a(hammer,tool,["build a house","fix a fender","crack a
nut"]).
is_a(sewing_machine,tool,["make clothing","repair sails"]).
is_a(plow, tool,["prepare fields","farm"]).
type_of(english,language,["communicate with people"]).
type__of (prolog,language,["communicate with a computer"]).
В качестве цели введите:
goal
run(tool).
Теперь введите следующую цель:
update, run(tool).
Предикат update включен в исходный код программы, удаляет
факт
type_of(prolog,language,["communicate with a computer"]).
из внутренней базы фактов knowledgeBase и добавляет два
новых факта в программу:
type_of(prolog,language,["communicate with a mainframe
computer"])
type_of("prolog",language,["communicate with a personal
computer"])
С помощью вызова предиката save/2 с именами текстового
файла и базы фактов в качестве его аргументов можно
сохранить всю базу данных факт и knowledgeBase в текстовом
файле. Например, после вызова
save("mydata.dba",knowledgeBase)
файл mydata.dba будет аналогичен разделу clauses обычной
программы Пролога, и каждый факт будет записан в отдельной
строке. С помощью предиката consult можно считать факты из
этого файла в память:
consult("mydata.dba",knowledgeBase).
51.
Вы можете манипулировать фактами, описывающими
предикаты баз фактов (фактами, описанными в разделах
database) так, как будто они являются термами. При объявлении
базы
фактов
Пролог
генерирует
внутренний
домен,
соответствующий фактам из раздела database. Например:
52. database- dbal % dbal домен для этих предикатов
53.
person(name,telno)
54.
city(cno,cname)
Получив такие объявления, компилятор Пролога сгенерирует
соответствующий домен dba1:
domains
dbal=person(name,telno); cityf(cno,cname)
Этот домен dba1 может быть использован так же, как любой
другой домен.
Со следующего шага мы начнем рассматривать создание и
работу с окнами.
Шаг 65.
Основы логического программирования.
Создание и работа с окнами
На этом шаге мы рассмотрим создание и работу с окнами.
В систему программирования Пролог включено пять предикатов,
позволяющих программе управлять окнами различных размеров. Это
предикаты makewindow, shiftwindow, removewindow, clearwindow и
gotowindow. С их помощью можно создавать несколько окон,
выполнять ввод и вывод в определенные окна и устанавливать
атрибуты окон.
Создание окон
Предикат makewindow является основой всех операций над окнами
в Прологе. Его общий вид:
makewindow(<номер окна>, <атрибуты окна>, <атрибуты
рамки>,
<заголовок
окна>,
<начальный
номер
строки>,
<начальный номер столбца>, <высота окна>, <ширина окна>).
Значения восьми параметров определяют характеристики окна.
Параметр номер окна, целое число, идентифицирует окно в
программе. Это число используется в качестве ссылки на окно, в
частности, предикатом gotowindow.
Целочисленное значение, присвоенное аргументу атрибуты окна,
определяет цвета символов и фона. Выбираемые значения зависят от
того, какой монитор используется - цветной или монохромный.
Приведем значения этого аргумента для цветного экрана (таблицы
1 и 2).
Таблица 1. Цвета текста
Значение
Цвет
Значение
Цвет
0
Черный
8
Серый
1
Синий
9
Светло-синий
2
Зеленый
10
Светло-зеленый
3
Голубой
11
Светло-голубой
4
Красный
12
Светло-красный
5
Фиолетовый 13
Светло-фиолетовый
6
Коричневый 14
Желтый
7
Белый
Интенсивно-белый
15
Таблица 2. Цвета фона
Значение
Цвет
Значение
Цвет
0
Черный 64
Красный
16
Синий
80
Фиолетовый
32
Зеленый 96
Коричневый
48
Голубой 112
Белый
Чтобы вычислить значение этого параметра для различных
комбинаций цветов, прежде всего выберите необходимый цвет текста
и цвет фона. Затем сложите соответствующие значения атрибутов.
Если вы хотите, чтобы символы мерцали, прибавьте к
результирующему значению 128. Вычисленное значение используется
как второй аргумент в предикате makewindow.
Например, чтобы создать окно с белыми символами на черном
фоне, сложите 7 (белый текст) и 0 (черный фон). Их сумма 7 является
значением этого аргумента для предиката makewindow. Чтобы
создать окно с красными символами на желтом фоне, сложите 4
(красный текст) и 104 (желтый фон), результат будет 108. Чтобы
создать окно с синими символами на бледно красном фоне, сложите 1
(синий текст) и 12 (бледно красный фон). Их сумма - 41, используется
как второй аргумент рассматриваемого предиката.
Выбор рамки окна. Третий аргумент предиката makewindow есть
целое число, значение которого определяет рамку окна. Если
значение атрибута равно 0, то окно не имеет видимой границы. Другие
значения определяют рамку окна с параметрами, указанными в
таблице 3.
Часть значений атрибута рамки окна задает ее цвет. Это делается
аналогично заданию значения атрибута экрана. Если задается
мерцающая граница, то она всегда будет белой, с мерцающей тонкой
линией (в середине границы), имеющей указанный цвет.
Таблица 3. Цвета рамки окна
Значение Вид рамки окна Значение
Вид рамки окна
0
Нет рамки
-1
Мерцающая белая рамка
1
Синяя рамка
-2
Мерцающая желтая рамка
2
Зеленая рамка
-3
Мерцающая
рамка
3
Светло-синяя
рамка
-4
Мерцающая красная рамка
4
Красная рамка
-5
Мерцающая
рамка
5
Фиолетовая
рамка
-6
Мерцающая светло-зеленая
рамка
6
Желтая рамка
-7
Мерцающая синяя рамка
7
Белая рамка
-8
Мерцающая серая рамка
фиолетовая
светло-синяя
Коричневая рамка -
8
-
Аргумент Заголовок окна задает метку окна. Например, меткой
окна может быть "Главное Меню", "Столбиковая Диаграмма",
"Вспомогательное Меню" и т.п. Строка, задаваемая в качестве
значения этого атрибута будет размещена в центре верхней линии
рамки окна. Значение метки окна также может быть не определено,
что соответствует отсутствию метки. В этом случае вводится
аргумент, состоящий из двух последовательных знаков кавычки.
Определение размеров и положения окон. Аргумент начальный
номер строки предиката makewindow есть целое число,
определяющее верхнюю строку (линию) создаваемого окна.
Максимальное количество строк, умещающихся на экране, 25.
Значение 4 указывает, что окно начинается с четвертой строки. Для
рассматриваемого аргумента можно использовать значения от 0 до
24.
Аргумент Начальный номер столбца предиката makewindow
есть целое число, указывающее крайний левый столбец окна.
Максимальное число столбцов, умещающихся на экране, 80 (от 0 до
79).
Аргумент Высота окна есть целое число, определяющее
количество строк, занимаемых создаваемым окном. Максимально
возможное значение аргумента 25.
Аргумент Ширина окна есть целое число, указывающее число
столбцов, занимаемых окном. Максимальное значение аргумента 80.
Если случайно будут указаны такие значения номера верхней
строки и размера окна по вертикали, что нижняя строка окна окажется
за нижней границей экрана, Пролог сообщит об ошибке следующим
образом: The parameters in makewindow are illegal. Приведем
несколько примеров использования этого предиката:


makewindow(1,7,7,"Полный экран",0,0,25,80) - данному окну
присвоен номер 1. Символы в нем будут белые, фон черный,
рамка белая и метка окна - "Полный экран". Верхний левый угол
окна находится на строке 0, столбце 0, а само окно имеет 25
строк и 80 столбцов;
makewindow(2,4,1,"Меню",4,20,16,40) - этому окну присвоен
номер 2. Оно имеет метку "Меню". Символы в нем красные на
черном фоне, и ограничено оно синей рамкой. Окно начинается с
4-й строки и 20-го столбца, имеет 16 строк и 40 столбцов.
Если какие-то аргументы предиката makewindow являются
неопределенными переменными, то им присваиваются значения
параметров текущего окна.
На следующем шаге мы рассмотрим использование других
предикатов для работы с окнами.
Шаг 66.
Основы логического программирования.
Использование других предикатов для работы с окнами
На этом шаге мы рассмотрим использование других предикатов
для работы с окнами.
Перечислим остальные предикаты, используемые при работе с
окнами.
1. Предикат shiftwindow используется для смены текущего окна
(переключения). Его форма:
shiftwindow(<номер окна>).
Параметр
номер
окна
является
целым
числом,
приписываемым окну при его создании. Например, задание
предиката shiftwindow(3) вызывает переназначение всех
операций ввода и вывода в окно, с номером 3.
2. Предикат
gotowindow
позволяет
выполнять
быстрое
переключение между двумя окнами, которые не перекрываются.
Его форма:
gotowindow(<номер окна>).
Этот предикат выполняется быстрее, чем shiftwindow, и
поэтому его следует использовать для переключения между
окнами, содержащими большое количество текста.
3. Предикат clearwindow удаляет из текущего окна все текстовые и
графические изображения. Предикат не имеет аргументов:
clearwindow.
Окно и рамка окна, если она имеется, не разрушаются. Окно
целиком закрашивается соответствующим цветом фона.
4. Предикат removewindow удаляет текущее окно с экрана.
Предикат аргументов не имеет, поэтому его синтаксис прост:
removewindow.
Все текстовые и графические изображения, находящиеся в
окне, также удаляются. Если за данным окном находится другое
окно, то это окно и его содержимое становятся видимыми. Если
удаляется последнее из заданных окон, на экране появляется то
изображение, которое было на нем до создания окон.
На следующем шаге мы начнем рассматривать использование
окон для ввода и вывода.
Шаг 67.
Основы логического программирования.
Использование окон для ввода и вывода
На этом шаге мы рассмотрим использование окон для ввода и
вывода.
Стандартные предикаты ввода и вывода - read, readint, readchar,
write и nl - также работают с любым текущим окном. Это означает, что
предикаты ввода и вывода работают с любым окном, которое было
сделано текущим при помощи makewindow, gotowindow или
shiftwindow.
По умолчанию предполагается, что курсор расположен в верхнем
левом углу окна. Однако можно использовать предикат cursor и
помещать курсор в любую позицию текущего окна. Этот предикат
имеет вид:
cursor(<номер строки>,<номер столбца>).
Аргументы предиката являются целыми числами, задающими
номера строки и столбца, по отношению к верхней строке и крайнему
левому столбцу экрана. Строки и столбцы нумеруются, начиная с
нуля: предикат cursor(0,0) обеспечивает вывод текста, начиная с
верхнего левого угла экрана. Если случайно указывается позиция
курсора, выходящая за рамки текущего окна, то во время выполнения
программы будет выдано сообщение об ошибке.
Например, следующие три предиката могут быть использованы для
вывода сообщения в центре экрана:
makewindow(1,7,7,"",1,1,8,28),
cursor(4,12),
write("Отличный день").
Если опустить предикат cursor, данное сообщение будет выведено,
начиная с левого верхнего угла окна. Аргументами предиката cursor
могут
также
быть
переменные,
которым
присваиваются
целочисленные значения. Другой формой подцели, размещающей
сообщение в центре окна, будет:
makewindow(1,7,7,"",1,1,8,28),
Row=4,
Col=12,
cursor(Row,Col),
write("Отличный день").
Если в качестве аргументов предиката cursor используются
неопределенные переменные, то этим переменным присваиваются
текущие значения строки и столбца. Подцель:
makewindow(1,7,7,"",1,1,8,28),
Row=4,
Col=12,
cursor(Row,Col),
write("Отличный день "),
cursor(What_row,What_column).
присвоит переменной What_row
What_column - значение 27.
значение
4,
а
переменной
На следующем шаге мы рассмотрим создание перекрывающихся
окон.
Шаг 68.
Основы логического программирования.
Создание перекрывающихся окон
На этом шаге мы рассмотрим создание перекрывающихся окон.
На этом шаге мы рассмотрим пример использования изученных
предикатов.
В
частности,
разберем
механизм
создания
перекрывающихся окон.
Перекрывающиеся окна создаются так, чтобы они
удовлетворяли потребностям программ и были
пользователя. Порядок, в котором окна создаются,
реализацией программы. Следующая программа
создание перекрывающихся окон.
одновременно
удобны для
определяется
иллюстрирует
/* Программа О к н а */
/* Назначение. Создание перекрывающихся окон */
predicates
make_windows_write_text
goal
make_windows_write_text.
clauses
make_windows_write_text:makewindow(1,7,7,"Жизнь звезды",3,12,10,40),
cursor(3,8),
write("ПОЯВИЛАСЬ ЧЕРНАЯ ДЫРА."),nl,
makewindow(2,7,7," Жизнь звезды ",5,14,10,40),
shiftwindow(2),
cursor(3,12),
write("ЗВЕЗДА ВЗОРВАЛАСЬ."),nl,
makewindow(3,7,7," Жизнь звезды ",7,16,10,40),
shiftwindow(3),
cursor(3,11),
write("ЗВЕЗДА СВЕТИТ."),nl,
makewindow(4,7,7," Жизнь звезды ",9,18,10,40),
shiftwindow(4),
cursor(3,11),
write("ЗВЕЗДА РОДИЛАСЬ."),nl,
cursor(6,4),
write("Нажмите пробел"),
readchar(_),
removewindow,
cursor(6,2),
write("Нажмите пробел "),
readchar(_),
removewindow,
cursor(6,2),
write("Нажмите пробел "),
readchar(_),
removewindow,
cursor(7,2),
write("Нажмите пробел "),
readchar(_),
removewindow,
exit.
/* К о н е ц программы */
Текст этой программы можно взять здесь.
Результат работы программы можно посмотреть на рис. 1.
Рис. 1. Результат работы программы
В
данной
программе
целью
является
правило
make_windows_write_text. Задача этого правила - создать четыре
перекрывающихся окна и вывести в них определенный текст. Текст
состоит
из
четырех
предложений,
описывающих
историю
существования звезды. Окна создаются в том порядке, который задан
их номерами, а удаление производится в обратном порядке, то есть,
начиная с окна 4. При таком порядке удаления сообщения с экрана
исчезают постепенно. Заметьте, что окна слегка сдвинуты друг
относительно друга. Это сделано для того, чтобы показать
существование нескольких окон одновременно.
На следующем шаге мы рассмотрим создание меню с помощью
окон.
Шаг 69.
Основы логического программирования.
Создание меню при помощи окон
На этом шаге мы рассмотрим создание меню при помощи окон.
Программы, дружелюбные по отношению к пользователю,
проектируют таким образом, чтобы он имел возможность выбрать
одну задачу из некоторого ряда, либо определить порядок
выполнения задач. Выбор часто оформляют в виде меню. После того
как выбор сделан, программа может либо выполнить указанную
задачу, либо предоставить дочернее меню, в котором перечислены
подзадачи. Одну из них также можно выбрать для выполнения.
Представляется, что оба указанных способа использования окон
являются наиболее эффективными при организации работы
программ.
Основной модуль данной программы создает главное меню. Из
этого меню, пользователь может выбрать: 0 - выход из меню
(окончание программы), 1 - удалить файл, 2 - переименовать файл, 3 создать файл, 4 - выход в операционную систему. Главное и каждое
дочернее меню выводятся в отдельные окна. Каждое дочернее окно
позволяет выполнить необходимое действие.
Рассмотренную систему меню реализует программа МЕНЮ. Ее
целевым утверждением является правило run_file_utility. Правило
show_menu составляет главный модуль:
show_menu:repeat,
makewindow(1,7,7,"Главное меню",4,10,16,36),nl,
write("
Выберите действие:"),nl,nl,
write("
0
выход из программы"),nl,
write("
1
удалить файл"),nl,
write("
2
переименовать файл"),nl,
write("
3
создать файл"),nl,
write("
4
выход в DOS"),nl,nl,
write("Укажите номер:(0-4) "),
readint(X),
X<5,
process(X),
X=0,
!.
Предикат repeat используется таким образом, что пользователь
всегда возвращается в главное меню по окончании выполнения
подмодуля; исключением является выбор 0, в этом случае
выполнение программы прекращается.
Заметьте, что данное правило использует один из вариантов
метода отката после неудачи. Предикат readint(X) считывает
целочисленное значение и присваивает его переменной X. Правило
X<5 проверяет, что введенное значение меньше 5.
Если введенное число больше или равно 5, это правило
выполнится неуспешно и произойдет откат. В противном случае
правило закончится успехом и будет сделана попытка выполнить
следующее правило, process(X).
Каждое правило process содержит предикат makewindow и
предикат write для вывода сообщения, идентифицирующего задачу и
сообщения "Процесс будет реализован позднее". В окончательно
написанной программе, предикат write, выполняющий вывод
последнего сообщения, будет предикатом, реализующим выбранную
задачу. Целью приведенной здесь программы является только
иллюстрация разработки меню, а не выполнение каких-либо
конкретных задач. Заметим, что каждая задача оканчивается
удалением соответствующего окна.
После того, как правило process заканчивается успешно, делается
попытка выполнить остальные правила. Если X равно нулю,
отсечение (!) препятствует откату к правилу repeat, и программа
заканчивается.
/* Программа М е н ю */
/* Назначение. Иллюстрация создания меню при помощи окон.
*/
predicates
repeat
process(integer)
show_menu
run_file_utility
goal
run_file_utility.
clauses
run_file_utility:show_menu,
nl,write("Нажмите клавишу ПРОБЕЛ"),
readchar(_),
exit.
repeat.
repeat:repeat.
show_menu:repeat,
makewindow(1,7,7,"Главное меню",4,10,16,36),nl,
write(" Выберите действие"),nl,nl,
write("
0
выход из программы"),nl,
write("
1
удалить файл"),nl,
write("
2
переименовать файл"),nl,
write("
3
создать файл"),nl,
write("
4
выход в DOS"),nl,nl,
write("Укажите номер:(0-4) "),
readint(X),
X<5,
process(X),
X=0,
!.
process(0):nl,write(" Выбран выход из программы!"),nl.
process(1):makewindow(2,7,7,"Удаление файла",12,36,10,36),
write("Выбрано удаление файла."),nl,nl,
write("Процесс будет реализован позднее."),nl, nl,
write("Для продолжения нажмите пробел."),
readchar(_),
removewindow.
process(2):makewindow(3,7,7,"Переименование файла",10,40,10,36),
write("Выбрано переименование файла."), nl,nl,
write("Процесс будет реализован позднее."),nl, nl,
write("Для продолжения нажмите пробел."),
readchar(_),
removewindow.
process(3):makewindow(4,7,7,"Создание файла",5,10,15,60),
write("Выбрано создание файла."), nl,nl,
write("Процесс будет реализован позднее."),nl, nl,
write("Для продолжения нажмите пробел."),
readchar(_),
removewindow.
process(4):makewindow(5,7,7,"Выход в DOS",10,40,10,35),
write("Выбран временный выход в DOS."), nl,nl,
write("Процесс будет реализован позднее."),nl, nl,
write("Для продолжения нажмите пробел."),
readchar(_),
removewindow.
/* К о н е ц программы */
Текст этой программы можно взять здесь.
Заметим, что пользователь не будет иметь затруднений в
определении окна текущей задачи, поскольку окна имеют метки.
Результат работы программы при выборе, например, пункта меню
1- удалить файл можно посмотреть на рис. 1.
Рис. 1. Пример работы программы
Со следующего шага мы начнем рассматривать графику.
Шаг 70.
Основы логического программирования.
Графика
На этом шаге мы рассмотрим основные предикаты работы с
графикой.
Для работы в графическом режиме следует воспользоваться
стандартным предикатом graphics. Для возврата к текстовому режиму
используется стандартный предикат text.
Предикат graphics имеет вид
graphics(ModeParam, Palette, Background)
и выполняет начальные установки экрана со средней, высокой и
сверхвысокой разрешающей способностью.
Возможные значения переменной ModeParam (Параметр режима)
и получающиеся в результате форматы экрана приведены в таблице
1.
Таблица 1. Возможные значения переменной ModeParam
Параметр
режима
Число
столбцов
Число
строк
Описание
1
320
200
Средняя степень разрешения,
4 цвета
2
640
200
Высокая степень разрешения,
черно-белое изображение
3
320
200
Средняя степень разрешения,
16 цветов
4
640
200
Высокая степень разрешения,
16 цветов
5
640
350
Сверхвысокая
степень
разрешения, 13 цветов
Цвета определяются одной из двух палитр, выбираемых в
зависимости от того, с каким значением связана переменная Palette
(палитра) - с 0 или с 1 (таблица 2).
Таблица 2. Возможные значения переменной Palette
Палитра
Цвет1
Цвет2
Цвет3
0
Зеленый
Красный
Желтый
1
Циан
Фуксин
Белый
Переменная Background (Фон) имеет целое значение, выбираемое
из таблицы 2 шага 65.
Основными предикатами, используемыми в графике, являются
предикат dot (точка) и line (линия).
Вызов предиката dot(Row,Column,Color) приводит к размещению
точки в месте, определяемом значениями Row (Строка) и Column
(Столбец). Первые два параметра - это целые значения в диапазоне
от 0 до 31999.
Аналогично предикат line(Row1,Col1,Row2,Col2,Color) определяет
линию.
Типичная последовательность вызовов стандартных предикатов,
используемых для графики, приведена в программе:
goal
write("Before graphics"),
readchar(_),
graphics(1,1,0),
line(0,0,10000,20000,2),
write("ordinary write during graphics mode"),
readchar(_),
text,
write("Alter graphics").
Текст этой программы можно взять здесь.
На следующем шаге мы продолжим рассматривать графику.
Шаг 71.
Основы логического программирования.
Графика (продолжение)
На этом шаге мы продолжим рассмотрение графики.
Предикаты графического режима
Действие этих предикатов зависит от следующих факторов:



направление движения,
рисует "перо" или нет (активизировано ли оно),
цвет пера.
Стандартный предикат pendown (перо вниз) активизирует перо, а
предикат penup (перо вверх) приводит его в пассивное состояние.
После вызова предиката graphics перо активизировано. Цвет следа
определяется параметром предиката pencolor.
Движение пера управляется четырьмя стандартными предикатами:
forward (вперед), back (назад), right (вправо), left (влево).
Например, предикат forward (Step) показывает, на сколько шагов
должно переместиться перо. Чтобы повернуть перо вводится
переменная Angle (Угол). Угол измеряется в градусах. Например,
right(Angle) поворачивает перо вправо.
Приведем тексты программ, реализующих некоторые графические
объекты:
Треугольник
goal
graphics(2,1,0),
pendown,
forward(5000), right(120),
forward(5000), right(120),
forward(5000), right(120).
Текст этой программы можно взять здесь.
Звезда
goal
graphics(2,1,0),
forward(5000), right(144), forward(5000), right(144),
forward(5000), right(144), forward(5000), right(144),
forward(5000), right(144), forward(5000).
Текст этой программы можно взять здесь.
Окружность
predicates
circle1
goal
graphics(2, 1, 0),
circle1.
clauses
circle1:forward(100), right(4), circle1.
Текст этой программы можно взять здесь.
Со следующего шага мы
программирования Пролог.
начнем
рассматривать
среду
Шаг 72.
Основы логического программирования.
Среда программирования Turbo Prolog 2.0
На этом шаге мы рассмотрим среду программирования Turbo
Prolog 2.0.
Здесь мы кратко опишем основные пункты меню среды
программирования Turbo Prolog 2.0. Выход на строку меню
происходит по нажатию клавиши ESC, для перемещения по пунктам
меню используются клавиши управления курсором.
1. Пункт меню Files содержит большое количество подпунктов,
осуществляющих работу с файлами. К ним относятся (рис. 1):
Рис.1. Общий вид среды программирования Turbo Prolog 2.0.
Пункт меню Load
Load
загрузка
программирования;
содержимого
файла
в
среду
Pick - вывод на экран окно со списком ранее загружавшихся
файлов. Перемещение по списку - клавиши управления
курсором. Загрузка выбранного файла - клавиша Enter;
New File - очистка окна редактора. Если содержимое окна не
было сохранено, то будет выведено сообщение, нужно ли его
сохранять;
Save - сохранение содержимого окна редактора в файле с
ранее заданным именем;
Write to - сохранение содержимого окна редактора в файле,
имя которого нужно задать в появившемся окне. По умолчанию,
Пролог-программы сохраняются в файлах с расширением PRO;
Directory - просмотр содержимого текущей директории. Перед
выводом ее содержимого запрашивается маска, которая затем
используется при выводе имен файлов (отображаются имена
только тех файлов, которые удовлетворяют заданной маске).
Символы ..\, расположенные в списке файлов, позволяют выйти
из текущей директории на предыдущий уровень;
Change dir - задание текущей (рабочей) директории;
OS shell - временный выход в DOS;
Quit - выход из среды программирования в операционную
систему.
2. Пункт меню Edit используется для перехода в окно
редактирования программы.
3. Пункт меню Run применяется для выполнения программы. Если
в программе присутствует раздел goal, то начинают
выполняться предикаты, расположенные после этого служебного
слова. Если раздел goal в программе отсутствует, то в окне
Dialog появляется сообщение о необходимости ввести целевое
утверждение.
4. Пункт меню Compile активизирует процесс компиляции
программы. Перечислим подпункты данного пункта (рис. 2):
Рис.2. Общий вид среды программирования Turbo Prolog 2.0.
Пункт меню Compile
Memory - размещение оттранслированной программы в опер
ативной памяти;
OBJ file - создание только объектных файлов (файлов с
расширением OBJ);
EXE file (auto link) - создание исполняемого файла (файла с
расширением EXE);
Project (all modules) - компиляция всех модулей проекта.
5. Пункт меню Options содержит несколько подпунктов,
устанавливающих некоторые параметры компиляции (рис. 3):
6. Шаг 73.
Основы логического программирования.
Текстовый редактор Turbo Prolog 2.0
7.
На этом шаге мы рассмотрим текстовый редактор Turbo
Prolog 2.0.
Программа на языке Пролог находится в текстовом файле с
расширением PRO. Для ее создания можно воспользоваться
любым
текстовом
редактором,
однако
система
программирования снабжена специализированным редактором,
из которого можно осуществить проверку программы,
оттранслировав ее компилятором системы.
9.
При обнаружении ошибки активизируется окно редактора и
курсор устанавливается в начало ошибочного текста программы
(рис. 1).
8.
10.
Рис.1. Общий вид среды программирования Turbo Prolog 2.0
при возниконовении ошибки
11.
После устранения ошибки вновь можно оттранслировать
программу, не выходя из текстового редактора.
12.
Основные возможности редактора системы Turbo Prolog
2.0 перечислены в таблицах 1-4.
Таблица 1. Команды перемещения курсора
Значение
На
влево.
Клавиши
символ
Вниз
страницу.
Ctrl+S или
На
символ
Ctrl+D или
вправо.
На слово влево.
На
вправо.
Ctrl+A
слово Ctrl+F
Значение
Клавиши
на
Ctrl+C или PgDn
К
началу
Ctrl+Q, S или HOME
строки.
или
К концу строки. Ctrl+Q, D или END
или К
началу Ctrl+Q,
R
файла.
Ctrl+PgUp
или
или
Вверх на строку.
К концу файла.
Ctrl+Q,
C
Ctrl+PgDn
Вниз на строку.
К
блока.
Ctrl+Q, B
Вверх
страницу.
на Ctrl+R
PgUp
или
началу
К концу блока.
Ctrl+Q, K
Таблица 2. Команды вставки и удаления
Значение
Режим вставки/замены.
Клавиши
Ctrl+V
INSERT
или
Стереть левый символ.
BACKSPACE
Стереть символ под курсором.
DELETE
Стереть строку.
Ctrl+BACKSPACE
Стереть символы до конца строки, начиная с
Ctrl+Q, Y
позиции, на которую указывает курсор.
Таблица 3. Команды работы с текстовыми блоками
Значение
Клавиши
Отметить начало блока.
Ctrl+K, B
Отметить конец блока.
Ctrl+K, K
Скопировать блок.
Ctrl+K, C
Передвинуть блок.
Ctrl+K, V
Стереть блок.
Ctrl+K, Y
Считать блок с диска.
Ctrl+K, R или F7
Записать блок на диск.
Ctrl+K, W
Снять отметку с блока.
Ctrl+K, H
Таблица 4. Команды общего назначения
Значение
Клавиши
Дополнительный редактор. F8
Перейти к строке.
Ctrl+F2
Закончить редактирование. ESC или F10
Поиск.
Ctrl+F3
Продолжение поиска.
Shift+F3
Поиск и замена.
F4
16.
Остановимся более подробно на организации поиска
фрагментов текста. Прежде всего, нужно поместить курсор в
произвольное место файла выше искомого фрагмента текста.
Самым подходящим местом является начало программы.
17.
Для поиска фрагмента текста пользуются клавишами
Ctrl+F3. В нижней части окна редактирования появляется строка,
где набирается искомый текст. После этого снова нажимаются
клавиши Ctrl+F3 - курсор переместится на строку, где находится
первый искомый фрагмент. Для повторного поиска того же
фрагмента используются клавиши Shift+F3.
18.
Поиск и замена позволяет найти фрагмент текста и
заменить его другим. Сначала нажимается клавиша F4.
Появившаяся подсказка предложит задать заменяемый текст.
После набора этого текста снова нажимается клавиша F4.
Другая подсказка предлагает задать заменяющий текст, после
которой нажимается клавиша Enter.
19.
После этого задается, какую замену нужно произвести:
локальную или глобальную. Если нажать клавишу G, то будет
произведена глобальная замена. Если воспользоваться
клавишей L, то будет осуществлена локальная замена: система
заменит только первое вхождение искомого текста ниже курсора.
20.
Затем система просит указать, нужно ли запрашивать
разрешение на замену при каждом обнаружении искомого
фрагмента текста. Если ответить на этот вопрос, нажав клавишу
Y, то при нахождении каждого экземпляра текста для замены
редактор будет выдавать сообщение, нужно ли замещать этот
экземпляр.
21.
Дополнительный редактор вызывается при нажатии
клавиши F8. Появляется окно со списком файлов, которые
можно разместить в этом окне. После выбора файла можно
осуществлять его редактирование.
22.
На следующем шаге мы рассмотрим средства
отладки.
Рис.3. Общий вид среды программирования Turbo Prolog 2.0.
Пункт меню Options
Link options - определяет нужно ли создавать MAP-файл
(Map file) и позволяет подключить библиотеку на этапе
компиляции (Libraries);
Edit PRJ file - позволяет отредактировать главный файл
проекта;
Compiler directives - содержит параметры компиляции. Среди
них
наиболее
важным
является
Memory
allocation,
позволяющий задать размеры кодового сегмента, сегмента
стека, кучи и т.п.
23.
Пункт меню Setup содержит подпункты, позволяющие
осуществить настройку системы программирования Turbo
Prolog. Перечислим их (рис. 4):
Рис.4. Общий вид среды программирования Turbo Prolog 2.0.
Пункт меню Setup
Colors - задание цветов изображения и фона. После выбора
этого подпункта на экране появляется еще одно окно, где
выбирается элемент, цвет которого нужно изменить.
Window Size - задание размеров окон. После выбора этого
подпункта на экране появляется еще одно окно, где выбирается
элемент, размеры которого нужно изменить. Для изменения
размеров окна используются клавиши управления курсором.
Местоположение окна производится Shift+клавиши управления
курсором. Нажатие клавиши ESC осуществляет переход к меню
команды Setup;
Directories - задание имен директорий, к которым
осуществляется обращение по умолчанию. Таковыми являются:
Current directory (текущая директория), OBJ directory
(директория объектных файлов), EXE directory (директория для
выполняемых файлов), Turbo directory (системная директория);
Miscellaneous - задание параметров настройки графического
адаптера, текстового редактора и строки подсказки;
Save SYS file - сохранение файла конфигурации;
Load SYS file - загрузка файла конфигурации.
На следующем шаге мы рассмотрим текстовый редактор
Пролога.
Шаг 74.
Основы логического программирования.
Средства отладки в Turbo Prolog 2.0
На этом шаге мы рассмотрим средства отладки в Turbo Prolog
2.0.
Turbo Prolog 2.0 не обладает таким разветвленными средствами
отладки
программ
по
сравнению
с
другими
системами
программирования, здесь они практически отсутствуют. Однако язык
программирования Turbo Prolog предоставляет пользователю
возможность трассировки программы. Для того, чтобы задействовать
это средство, необходимо включить в программный файл директиву
trace, которая должна размещаться в программе перед разделом
predicates. Лучше всего разместить ее перед разделом domains.
Общий вид директивы trace следующий:
trace <список предикатов>,
где список предикатов - перечень предикатов, перечисленных через
запятую, для которых нужно выполнить трассировку.
Если список предикатов не указан, то трассировка начинается с
выполнения самого первого предиката.
Выполнение директивы трассировки приостанавливает выполнение
программы после каждой попытки удовлетворить подцель. В этот
момент в окне трассировки (окно Trace среды программирования)
печатаются значения переменных и некоторая другая информация
(рис.1):
RETURN - после этого служебного слова размещается предикат,
для которого были найдены все значения переменных;
CALL - после этого служебного слова размещается предикат,
являющийся
подцелью.
Неизвестные
переменные
в
нем
обозначаются символами "_";
REDO - после этого служебного слова размещается предикат,
являющийся подцелью. Данное сообщение появляется тогда, когда
осуществляется переход от одной реализации предиката к другой;
FAIL - после этого служебного слова размещается предикат, для
которого не были найдены все значения переменных. Это
свидетельствует о том, что данная подцель не была разрешена. Для
управления трассировкой можно использовать следующие клавиши
(комбинации клавиш):
F10 - выполнение следующего шага программы;
Shift+F10 - изменение размеров и местоположения текущего окна;
Alt+T - изменение работы режима отладки. При нажатии этой
комбинации клавиш на экране появляется "плавающее" окно,
содержащее значения трех пунктов. Изменить значения можно с
помощью клавиши Enter, убрать окно с экрана - клавиша F10.
Перечислим пункты этого окна (по умолчанию все они имеют значения
On):
Рис.1. Общий вид среды программирования Turbo Prolog 2.0 при
включенной трассировке
Status - отключить режим отладки;
Trace Window - режим отладки работает, только не изменяется
содержимое окна Trace;
Edit Window - режим отладки работает, но в окне редактора кода
(окне Editor) отсутствует курсор, указывающий выполняемый
предикат;
End - завершение выполнения программы.
Если нужно продолжить выполнение программы в обычном режиме,
то для выключения трассировки применяется директива trace(off).
Кроме директивы trace можно использовать директиву shorttrace,
которая является аналогом trace с той лишь разницей, что в окно
Trace выдается более краткая информация.
Мы закончили изучение основ языка программирования Prolog.
Download