В первом дереве самое нижнее умножение 2*3можно свернуть, получив 6, но во втором дереве нет поддеревьев, которые было бы можно свернуть. Однако, используя коммутативность умножения, можно добавить к таблице следующее правило, которое позволит справиться с данным конкретным случаем:


s(*,X*Y,W,X*Z):- integer(Y), integer(W), Z is Y*W.


Более общая алгебраическая система может быть построена путем простого добавления дополнительных s-утверждений вместо увеличения объема программы для привести.

Рис. 7.4

7.13. Применение предикатов clause и retract

В этой книге мы неоднократно сталкивались с применением встроенных предикатов. На самом деле многие из них можно определить на Прологе, используя более простые встроенные предикаты. В этом разделе мы рассмотрим несколько таких определений. Они могут найти практическое применение у тех программистов, которые используют неполную в каких-либо отношениях Пролог-систему, однако в любом случае они интересны, как примеры программирования на Прологе. Может быть, они наведут вас на мысль о разработке несколько отличающихся версий этих предикатов для своего собственного применения.

Мы можем определить с помощью предиката clauseнекоторую версию процедуры listing.Определим предикат распеч1такой, что при согласовании цели распеч1(Х)с базой данных из последней будут выводиться на печать утверждения, заголовки которых совпадают с X. Поскольку определение распеч1включает использование предиката clause, у которого Xзадан как первый аргумент, то мы вынуждены поставить условие, что переменная Xконкретизирована таким образом, что главный функтор утверждения известен. Рассмотрим определение распеч1:


распеч1(Х):-clause(Х,Y),выв_утвержд(Х,Y),write('.'),nl,fail.

распеч1(Х).

выв_утвержд(Х,true):-!, write(X).

выв_утвержд(Х,Y):- write((X:- Y)).


При попытке согласовать с базой данных цель распеч1(Х)первое утверждение осуществляет поиск в базе данных такого утверждения, у которого заголовок совпадает с X. Если такое утверждение найдено, то оно выводится на печать и затем с помощью предиката failинициируется механизм возврата. Возвратный ход опять приводит нас к предикату clause,который находит другое такое же утверждение, если оно имеется, и т. д. Когда таких утверждений больше нет, цель clauseбольше не удается согласовать с базой данных. В этом случае будет выбрано второе утверждение определения предиката распеч1,и потому цель будет согласована с базой данных. «Побочным эффектом» этих действий является печать соответствующих утверждений. Определение предиката выв_утверждзадает способ печати найденных утверждений. Выделяется специальный случай, когда тело утверждения есть true.В этом случае на печать выводится только заголовок утверждения. Иначе на печать выводится заголовок и тело утверждения, соединенные функтором ':-'. Отметим, что использование «отсечения» здесь имеет целью указать, что в случае, когда тело есть true,можно применять только первое правило. Поскольку данный пример построен на использовании механизма возврата, то задание отсечения здесь существенно.

Встроенный предикат clauseможно также применить при написании Пролог-интерпретатора на самом Прологе. Это означает, что мы можем определить действия, которые представляют собой выполнение Пролог-программы, причем исполнителем этих действий также является Пролог-программа. Ниже приводится определение предиката интерпреттакого, что цель интерпрет(Х)согласуется в том и только в том случае, когда X, рассматриваемая как цель, согласуется с базой данных.

Предикат интерпретнапоминает встроенный предикат call,но является более ограниченным.


интерпрет(true):-!.

интерпрет((Gl,G2)):-!, интерпрет(G1), интерпрет(G2).

интерпрет(Цель):-clause(Цель,ЕщеЦели), интерпрет(ЕщеЦели).


Первые два утверждения рассчитаны на специальные случаи, когда цель есть trueи когда цель представляет собой конъюнкцию целей. Последнее утверждение рассчитано на случай простой цели. Данная процедура находит утверждение, заголовок которого совпадает с заданной целью, и затем интерпретирует цели, входящие в тело этого утверждения. Заметим, что приведенное определение не рассчитано на программы, где используются встроенные предикаты, поскольку у таких предикатов нет определяющих их в обычном смысле утверждений.

Рассмотрим определение предиката consult.Разумеется, предикат consultпредусмотрен среди встроенных предикатов большинства Пролог-систем, однако интересно посмотреть, как он может быть определен на Прологе.


consult(Файл):-seeing(Input),sее(Файл),repeat,read(Tepм),обработать(Терм),seen,see(Input),!.

обработать(Терм):- маркер_конца_файла(Терм),!.

обработать((?- Q)):-!, call(Q),!, fail.

обработать(Утвержд):- assertz(Утвержд), fail.


Это определение отличается рядом интересных особенностей. Во-первых, цель seeing(Input)и ее партнер see(Input)призваны гарантировать, что текущий файл ввода не будет «забыт» после применения предикат consult.Во-вторых, предикат маркер_конца_файлаздесь использован без определения. По замыслу он должен быть истинным только в том случае, когда его аргумент конкретизирован термом, используемым для представления конца файла (который мог бы встретиться при выполнении read). Вразных реализациях Пролога для представления «конца файла» используются разные термы, поэтому маркер_конца_файлав разных реализациях может быть определен по-разному. Одно из возможных определений выглядит так:


маркер_конца_файла(конец_файла).


В определении предиката обработатьинтересна организация выполнения соответствующих действий для каждого терма, считанного из входного файла. Целевое утверждение обработатьдоказуемо только, когда его аргументом является маркер конца файла. Иначе после соответствующего действия имитируется неудача доказательства и инициируется механизм возврата, который возвращает программу к предикату repeat.Отметим важность «отсечения» в конце определения предиката consult.Оно фиксирует выбор, сделанный предикатом repeat [13].И последнее замечание. Если терм, считанный из файла, представляет собой вопрос (см. второе утверждение определения предиката обработать),то делается попытка немедленно согласовать соответствующую цель с помощью предиката call(см. разд. 6.7).

В качестве примера использования предиката retractздесь приведено определение полезного предиката уберивсе.При согласовании с базой данных целевого утверждения уберивсе(Х)все утверждения, заголовки которых совпадают с X, удаляются из базы данных. Поскольку в данном определении используется предикат retract,то переменная Xне может быть неконкретизированной, так как в противном случае не с чем будет сопоставлять утверждения из базы данных. Данное определение должно распознавать два вида утверждений с заголовками, совпадающими с X, - факты и правила. При обработке этих двух видов утверждений в вызове retractзадаются разные аргументы. В определении используется то свойство, что retractбудет срабатывать при возврате до тех пор, пока все утверждения, сопоставимые с его аргументами, не будут удалены из базы данных.


уберивсе(Х):- retract(X), fail.

уберивсе(Х):- retract((X:- Y)), fail. уберивсе(_).


В качестве примера использования предиката уберивсе здесь приведено определение предиката reconsultна Прологе. Назначение предиката reconsultсходно с назначением предиката consult,с той лишь разницей, что при reconsultкаждое считанное утверждение замещает существующее утверждение того же предиката, а не добавляется к нему (см. разд. 6.1).


reconsult(Файл):-уберивсе(сделано(_)),seeing(Старый),sее(Файл),repeat,read(Терм),проверить(Терм),seen,see(Старый),!.

проверить(Х):- маркер_конца_файла(Х),!.

проверить((?- Цели)):-!, call (Цели),!, fail.

проверить(Утверждение):-заголовок(Утверждение, Заголовок), запись_сделана(3аголовок), assertz(Утверждение), fail.

запись_сделана(3аголовок):- сделано(Заголовок),!.

запись_сделана(3аголовок):- functor(Заголовок,Func,Arity), functor(Proc,Func,Arity), asserta(cдeлaнo(Proc)), уберивсе(Ргос),!.

заголовок((А:- В), А):-!.

заголовок (А, А).


Это определение похоже на определение consult,в котором вместо предиката обработатьиспользуется предикат проверить.Основное различие заключено в предикате запись_сделана. Когда в файле появляется первое утверждение для данного предиката, то, прежде чем к базе данных будет добавлено хотя бы одно новое утверждение, из нее должны быть удалены все старые утверждения для данного предиката. Удаление этих утверждений нельзя откладывать до момента, когда в базе данных появятся их новые версии, поскольку в этом случае мы удалили бы из базы данных те утверждения, которые только что были введены. Как же определить, что некоторое утверждение в файле является первым для соответствующего предиката? Ответ заключается в том, что мы регистрируем в базе данных предикаты, для которых уже нашлись утверждения в файле. Это делается с помощью предиката сделано. Когда из файла считывается первое утверждение например, для предиката fooс двумя переменными, то имеющиеся утверждения удаляются и новое утверждение добавляется к базе данных. Кроме того, к базе данных добавляется факт:


сделано(foo(_,_)).


В дальнейшем, при чтении остальных утверждений для предиката too,мы сможем проверить, что старые утверждения уже удалены из базы данных. Тем самым удается избежать ошибочного удаления новых утверждений. Для данного определения важно, что мы не добавляем в базу данных что-нибудь вроде:


сделано(foо(а,Х)).


поскольку в этом случае аргумент предиката сделаноне обязательно совпадает с заголовком утверждения для foo.Пара запросов

…,functor(Заголовок,Func,Arity),functor(Proc,Func,Arity),…

конкретизирует Ргосструктурой, имеющей тот же функтор, что и заголовок Заголовок, но с переменными в качестве аргументов (см. разд. 6.5).

ГЛАВА 8. ОТЛАДКА ПРОЛОГ-ПРОГРАММ

На приведенных выше примерах вы уже приобрели опыт применения программ и научились их изменять, а также успели написать и свои собственные программы. Теперь самое время заняться вопросом: что делать, когда программа ведет себя не так, как ожидалось. Подобные неожиданности связаны с наличием ошибок в программе, а процесс устранения этих ошибок называется отладкой. По нашему убеждению самым разумным подходом к программированию является тот, который может быть охарактеризован как «программирование с предупреждением ошибок». Перефразируя старую поговорку, можно сказать, что грамм тщательного программирования стоит килограмма отладки.В этой главе мы расскажем о некоторых методах отладки, но начнем с того, как избежать проникновения ошибок в программы. Сознавая, что в общем виде эта проблема неразрешима, мы хотим просто поделиться некоторыми неформальными приемами, которые уже приносили пользу другим программистам, работающим на Прологе.

Как и всякое творчество, будь то сочинение музыки, литература или архитектура, программирование располагает множеством способов представленияобъектов и обработкиэтих объектов и отношений между ними в конкретной программе. В общем случае для некоторого элемента данных в программе существует несколько возможных способов его представления или обработки. Всякий раз, когда программист решает использовать в данной программе один из них, мы говорим, что программист принял проектное решение.

Новички, впервые столкнувшиеся с задачей выбора проектных решений, часто испытывают трудности. Помочь им может знание возможных вариантов решений, и, кроме того, важно, чтобы руководитель разъяснил им методику программирования в целом, поскольку искусство принятия проектных решений в программировании – это самостоятельная дисциплина. Мы уже пытались затронуть эту проблему в разд. 1.1, где обсуждались различные способы интерпретации утверждений. Эти вопросы связаны с представлениемобъектов и отношений. В разд. 7.7 мы снова столкнулись с этой проблемой при описании трех различных способов упорядочения списка объектов. Эти вопросы связаны с разными способами обработкиобъектов и отношений.

В данной книге мы старались оказать помощь в выборе проектных решений двумя путями. Во-первых, наличие в книге большого числа примеров программ должно способствовать усвоению идей некоторых решений, выработанных опытными программистами. Во-вторых, в данной главе можно найти некоторые советы и рекомендации, специфические для Пролога.

8.1. Расположение текстов программ

После того как программист принял решение о том, как представлять и обрабатывать объекты и отношения в программе, ему следует убедиться в том, что текст программы расположен удобно для чтения и ее синтаксические конструкции ясны. Набор утверждений для данного предиката называется процедурой.Возможно, вы уже обратили внимание на то, что в примерах данной книги каждое утверждение в процедуре начинается с новой строки, а между собой процедуры разделяются одной пустой строкой. Например, один из способов определения предиката эквивалентности для множеств (при представлении множеств в виде списков) состоит в использовании трех предикатов, каждый из которых определяется с помощью процедуры из двух строк:


равмнож(Х,Х):-!.

равмнож(Х,Y):- равспис(Х,Y).

равспис([],[]).

равспис([Х|L1],L2):- удалить(Х,L2,LЗ), равспис(L1,LЗ).

удалить(Х,[Х|Y],Y).

удалить(Х,[Y|L1],[Y|L2]):- удалить(Х,L1,L2).


Данный пример, возможно, не является наилучшим определением эквивалентности множеств, однако он показывает, как нужно размещать тексты процедур. Заметим, что утверждения каждой процедуры сгруппированы вместе, а процедуры разделяются пустой строкой. Другое соглашение, которого придерживаются многие программисты на Прологе, - это писать каждое утверждение на отдельной строке, если оно на ней умещается. Если нет, то писать заголовок утверждения и знак ':-' на первой строке, а каждую цель в конъюнкции целей писать с новой строки со сдвигом. Для примера рассмотрим запись программы порождения всех перестановок списка:


перест([],[]).

перест(L,[Н|Т]):-

 присоединить(Y,[Н|U],L),

 присоединить(V,U,W),

 перест(W,T).

В этом определении перестановки элементов списка выполняет предикат присоединить,а механизм возврата при каждой попытке вновь согласовать целевое утверждение перест(Х, Y)обеспечивает порождение из Xновой перестановки Y. Следует обратить внимание на способ размещения на странице конъюнктов второго утверждения.

Главное – остановить свой выбор на каком-либо согласованном наборе правил оформления текста программы, а на каком – не так уж важно. Как правило, полезно добавлять комментарии, надлежащим образом группировать термы, в случае сомнений относительно приоритета выполнения операций – использовать круглые скобки, а также разумно распоряжаться пустым пространством (пробелами и пустыми строками). Комментарии должны подсказывать, как следует интерпретировать аргументы структур или утверждений: в каком порядке они поступают и каким структурам данных (константам или структурам) они должны соответствовать. Полезно также указывать в комментариях, как должны конкретизироваться переменные в результате согласования данного утверждения с базой данных.

В плане общей организации программы полезно разделять текст программы на более или менее замкнутые части, например, желательно, чтобы все процедуры обработки списков оказались в одном и том же файле. Процедура Пролога, содержащая более пяти – десяти правил, может оказаться трудной для понимания, поэтому путем определения более мелких предикатов попытайтесь как можно более естественным образом разбить ее на части. Если в программе используется много фактов, таких как правила упрощения из разд. 7.12, то все эти факты должны содержаться в одном файле. Большое количество фактов легче читается, чем большое количество правил, и хотя даже несколько правил могут быть трудны для понимания, многие страницы описания конкретного факта могут не вызвать затруднений, поскольку сама семантика фактов более проста.

Еще один момент, который влияет на легкость чтения Пролог-программ, - это использование точки с запятой («или») и восклицательного знака («отсечение»). С проблемами, связанными с чрезмерно широким использованием «отсечения», мы познакомились в гл. 4. Всегда следует пытаться обойтись без ';', возможно за счет дополнительных утверждений. Например, следующая программа:


nospy(X):-

 проверить(Х,Функтор,ЧисАрг,А), !,

 (контрточка(_,Функтор,А), !,

  (отказ(контрточка(Заголовок, Функтор, ЧисАрг),_),

  устконтрточку(Заголовок, Тело),

  отказ(3аголовок, Тело),

  write('контр.точка с терма'), печтерм(Функтор, ЧисАрг),

  write(' удалена.'),nl,

  fail

   ; true

 )

 ; write('Heт контр.точек на терме'),

  write(X), put(46), nl

),

!.


гораздо труднее для понимания, нежели:

nospy(X):-проверить(Х,Функтор,ЧисАрг,А),!,

попыт_убр(Х,Функтор,ЧисАрг,А).

попыт_убр(_,Функтор, ЧисАрг,А):- контрточка(_,Функтор,А),!,убрконтрточку(Функтор, ЧисАрг, А).

попыт_убр(Х,_,_,_):- write('Heт контр.точек на терме '), write(X), put(46), nl,!.

убрконтрточку(Функтор, ЧисАрг, А):-

 отказ(контрточка(Заголовок,Функтор, ЧисАрг), _), устконтрточку(Заголовок,Тело), отказ(3аголовок, Тело),

 write('Koнтp.точка с терма'), печтерм(Функтор, ЧисАрг), write(' удалена.'), nl, fail.

убрконтрточку (_,_,_).


которая делает в точности то же самое. Если же вам действительно необходимо использовать «или», то полезно так организовать текст конъюнкции целей, чтобы «или» не терялось среди целей. Полезно также заключить соответствующие цели в скобки, чтобы явно выделить область действия этого «или».

На протяжении всей книги подчеркивалась важность умения учитывать при решении многих задач наряду с общим правилом и граничные условия. Всюду, где это возможно, мы старались разместить граничные условия перед всеми другими утверждениями процедуры. Это выделяет граничные условия и, кроме того, служит в какой-то мере защитой от циклических определений. Однако бывают случаи, когда желательно размещать граничные условия после всех утверждений процедуры. Очевидно, что правила-«ловушки», с которыми мы уже сталкивались несколько раз, нужно размещать в конце процедуры.

Разбирая Пролог-процедуру всегдаполезно обращать внимание на следующие важнейшие моменты ее записи:

• Проверьте посимвольно,как записано имя каждого предиката и каждой переменной в процедуре. Ошибки в написании имен довольно распространены.

• Проверьте число аргументовкаждого функтора, используемого в процедуре. Убедитесь в том, что число аргументов (и порядок их следования) соответствуют вашим проектным решениям.

• Выделите из утверждений все операторыи определите их приоритет, ассоциативность, а также то, где находятся их аргументы. Это можно сделать на основании определений операторов, и исходя из наличия скобок. Если есть сомнения, добавьте дополнительные скобки. Для проверки соответствия действия оператора вашим представлениям попробуйте распечатать некоторые простые термы, используя display.

• Обратите внимание на область определениякаждой переменной и выделите в этой области переменные, сходные с ней по имени. Проследите, какие переменные «сцепляются», когда одной из них будет присвоено значение. Проверьте, встречаются ли в теле утверждения переменные из его заголовка.

• Постарайтесь определить, какие переменные конкретизированы, а какие нет в момент перед применением утверждения.

• Выделите утверждения, составляющие граничные условия. Проверьте, учтены ли все возможные граничные условия.

После того как подобным образом процедура будет разобрана «по косточкам», вы поймете ее гораздо лучше.

8.2. Типичные ошибки

В этом разделе мы рассмотрим те ошибки, которые допускают как начинающие, так и опытные программисты, использующие Пролог. Эти ошибки делятся на две группы: синтаксическиеошибки и ошибки управления последовательностьюобработки.

После того, как программист решил, какую программу он будет писать и как разместит ее текст на странице печатающего устройства (или на экране видеотерминала), ему остается ввести текст программы, записав его в файл или непосредственно в базу данных Пролог-системы. Основная задача данного этапа – это обеспечить синтаксическуюправильность программы. Ниже приводится список типичных синтаксических ошибок. Если программист сам не выявит эти ошибки, то при попытке выполнить предикат consultПролог может выдать соответствующее сообщение.

• Одной из типичных синтаксических ошибок является отсутствие точки '.' в конце утверждения. Точкой должен заканчиваться и любой терм, считываемый с помощью предиката read. После точки нужно оставлять хотя бы один пробел. Поэтому избегайте заканчивать файл вместе с вводом точки последнего утверждения – убедитесь в том, что в самом конце вы не забыли нажать клавишу RETURN.

• Некоторые специальные литеры используются парами. К ним относятся круглые скобки '(' и ')', используемые для группирования термов, квадратные скобки '[' и ']', используемые для задания списков, и фигурные скобки '{' и '}', используемые для записи правил грамматики (см. гл. 9). Сюда же относятся двойные кавычки '"' используемые для ограничения строк и одиночная кавычка '", используемая для задания атомов. Составные скобки '/*' и '*/' используются для ограничения комментариев. Убедитесь, что скобок каждого вида задано не больше и не меньше чем необходимо.

• Остерегайтесь неправильного написания слов, особенно имен встроенных предикатов. Это может привести к самым неожиданным ошибкам, поскольку неверно записанные имена предикатов вряд ли сопоставятся с каким-либо утверждением в базе данных. Или наоборот, они могут неожиданно сопоставиться с утверждениями, заголовок которых случайно совпадает с получившимся именем.

• Еще одним источником возможных ошибок являются операторы. Когда вы не уверены в ассоциативных свойствах оператора, то используйте круглые скобки, чтобы задать нужные свойства явно. Проверяйте экспериментально действие введенных вами операторов с помощью предиката display.

• Когда имеете дело с операциями над списками, проверяйте себя с помощью следующих вопросов и ответов:

• Как сопоставляются [а,b,с]и [X|Y]? ( X конкретизируется значением a, a Y- значением [b,с]).

• Сопоставимы ли [а]и [X|Y]? (Да, причем X конкретизируется значением a, a Yзначением []).

• Сопоставимы ли []и [X|Y]? (Нет).

• Имеет ли смысл запись [X, Y|Z]? (Да).

• Имеет ли смысл запись [X|Y,Z]? (Нет).

• Имеет ли смысл запись [Х|[Y|Z]? (Да, это то же самое, что [X,Y|Z])

• Как сопоставляются [a, b]и [А |В]? ( А конкретизируется значением а, а В- значением [b] ).

• Существует ли более одного способа сопоставления приведенных выражений? (Нет, никогда).

Необходимо подчеркнуть, что когда приходится иметь дело со списками или другими структурами подобного рода, полезно прибегать к помощи «древовидных диаграмм», о которых говорилось в гл. 2.

Даже в тех случаях, когда вы уверены, что в программе нет синтаксических ошибок, она все-таки при попытке согласования целевых утверждений может вести себя непредсказуемо; характерными признаками этого являются: впечатление, что программа никогда не остановится («бесконечный цикл»), неожиданные появления отрицательных ответов (нет), или конкретизация переменных не теми значениями, какие ожидались. Приведем обычные причины подобных ошибок:

• Циклические определения, о которых упоминалось в гл. 3.

• Недостаточность граничных условий или какая-либо другая недоопределенность задачи.

• Бесполезные процедуры, которые переопределяют встроенные предикаты.

• Задание неверного количества аргументов для функтора. Это не рассматривается как синтаксическая ошибка, поскольку количество аргументов функтора может быть разным.

• Неожиданный выход на конец файла при выполнении предиката чтения read.

Остерегайтесь следующих заблуждений относительно принципов работы механизма возврата:

Заблуждение:Одна из причин использования возвратного хода состоит в том, что Пролог может вернуться к предыдущему сопоставлению и выполнить его снова некоторым другим способом.

На самом деле:Когда Пролог просматривает базу данных, пытаясь сопоставить цели что-либо из базы данных (какой-нибудь факт или заголовок правила), то сопоставление бывает либо успешным, либо неудачным. Пролог никогда не делает возвратный ход ни к какому сопоставлению, в попытке осуществить его по-другому, поскольку сопоставить конкретную цель с конкретным утверждением в базе данных можно только одним-единственным способом.