
19.3. Усовершенствованные метаинтерпретаторы для отладки программ
Отладка программ даже на Прологе является существенным аспектом программирования. Языки программирования высокого уровня мало что могут дать для написания безошибочных программ, и поэтому для поддержки процесса разработкипрограмм приходится больше полагаться на мощность системных средств. Между тем по многим причинам подобные средства естественно реализовать в самом языке. По существу это программы для обработки, анализа и моделирования других программ, т.е. метапрограммы.
В этом разделе рассмотрены метапрограммы для поддержки процесса отладки программ на чистом Прологе. Причина такого ограничения очевидна и объясняется трудностями обработки изменяемых частей языка Пролог.
При отладке программ предполагается, что программист имеет некоторое представление о поведении программы и области применения, в которой программа должна демонстрировать заданное поведение. Таким образом, отладка состоит в поиске расхождения между фактическим поведением программы и поведением, предполагаемым программистом. Напомним определения из разд. 5.2заданного значения и заданной области программы. Заданным значениемМпрограммы на чистом Прологе является множество основных целей, на которых выполнение программы оказывается успешным.Заданная область Опрограммы--область целей. на которой выполнение программы должно завершиться- Мы требуем, чтобы заданное значение программы было подмножеством заданной области целей.
Будем говорить, что А-решениедля цели А. если программа для целиАнаходит ее пример a
.Скажем также, что решениеА истиннов заданном значении М, если каждый
примерАсодержится вМ.В
противном случае решение целиА ложновМ.
В программе на чистом Прологе при заданном значении и заданной области целей могут обнаружиться ошибки только трех типов. Вызов программы для цели А из заданной области целей может завершиться одним из трех исходов:
а) выполнение программы не завершается.
6}вырабатывается некоторое ложное решение А
в) пропуск некоторого истинного решения А
Рассмотрим алгоритм обнаружения и идентификации ошибок каждого из указанных типов.
В общем случае невозможно определить, завершается Пролог-программа или нет; эта задача неразрешима. Практически можно назначить априорную границу времени выполнения или глубины рекурсии программы и прерывать вычисления. если эта граница будет превышена. При этом желательно частично сохранить информацию о выполнении программы для анализа причин незавершаемости вычисления. Указанные возможности реализованы в усовершенствованном метаинтерпретаторе (программа 19.12).Обращение к нему осуществляется вызовомsolve(Goal, D, Overflow),где Goal-исходная цель.D-верхняя граница глубины рекурсии. Этот вызов будет успешным, если решение находится без превышения заданной глубины рекурсии, при этом переменная Overflowконкретизируется какno_overflow..Этот вызов завершается успешно и в случае, если глубина рекурсии превышена, однако при этом Overflowсодержит стек целей, т. е. ветвь дерева
solve(A, D, Overflow)
Цель Аимеет дерево доказательства глубиной менее чем DиОverflow равно по_overflow,или цельАимеет в дереве вычисления ветвь длиннееD Overflowсодержит список первых Dэлементов этой ветви.
solve(true,D,no_overflow).
solve(A,0,overflow([ ])).
solve((A,B),D,Overflow)
D >0,
solve(A,D,OverfIowA),
solve_conjunction(OvernowA,B,D,Overnow).
solve(A,D,Overflow)
D >0,
clause(A,B),
D1:=D - 1,
solve(B,Dl,OvernowB),
return._overnow(OverflowB,A,Overflow).
solve(A,D,no_overflow)
D > 0,
system(A),A.
solve_conjunction(overflow(S),B,D,overflow(S)).
solve_conjunction(no_overflow,B,D,Overflow)
solve(B,D,Overf)ow).
return_overflow(no_overflow,A,no_overflow).
return_overflow(overf1ow(S),A,overflow([A (S])).
Программа 19.12.Метаинтерпретатор, обнаруживающий переполнение ст^
вычислений, длина которой превысила границу глубины D.
Отметим, что, как только обнаруживается переполнение стека вычисление прекращается без завершения доказательства. Это обеспечивается предложение solve_conjuctionиrеturn_overllow.
В качестве примера рассмотрим программу 19.13для сортировки вставками. Вызов этой программы с целью solve(isort([2,2],Xs),6,Overflow)приводит к следующему решению:
Xs = [2,2.2,2,2,2], Overflow = overflow([
isort([2,2],[2,2,2,2,2,2]),
insert(2,[2],[2,2,2,2,2,2]),
insert(2,[2],[2,2,2.2,2]),
insert(2,[2],[2,2,2,2]),
insert(2,[2],[2,2,2]),
insert(2,[2],[2,2])])
для выяснения причин незавершаемости
программы переполненный стек после
возврата может быть подвергнут дальнейшему
анализу. Незавершаемость программы
может быть вызвана, например, зацикливанием,
т.е. последовательностью целей
G,
G
,
.... G
,в стеке, где G,и G
вызываются с одним и тем же входом или
последовательностью целей, вызов которых
происходит с увеличением размера
входов. Первая ситуация встречается
в приведенном выше примере.Ясно, что
программа содержит ошибку. Вторая
ситуация не обязательно должна быть.
обусловлена ошибкой, и требуется
дополнительная информация о поведении
программы, чтобы определить, имеется
ли в программе ошибка или для выполнения
пpoграммы следует
воспользоваться более мощной вычислительной
машиной.
isort(Xs,Ys)
К-упорядоченная перестановка списка Xs.Выполнение программы не завершается
isort([X| Xs],Ys) isort(Xs,Zs), insert(X,Zs,Ys).
isort([ ],[ ]).
insert(X,[Y | Ys] [X,Y | Ys])
X<Y.
insert(X,[Y|Ys],[Y|Zs])
XY,insert(Y,[X|Ys],Zs).
insert(X,[ ],[X]).
Программа 19.13.Незавершающаяся программа сортировки вставками.
Ошибки второго типа заключаются в получении ложных решений. Получить ложное решение при использовании некоторой программы можно лишь тогда, когда она содержит ложное предложение. Предложение С ложно по отношению к заданному значению Мпрограммы, если оно имеет пример, тело которого истинно в М, а заголовок ложен в М, Такой пример называетсяконтрпримеромдля С.
Рассмотрим, например, программу 19.14для сортировки вставками. Обращение к этой программе с целью isort([3,2,1],Xs)дает решение isort([3,2,1],[3,2,1]),которое, очевидно, ложно.
isort(Xs,Ys)
Ошибочная программа сортировки вставками.
isort([X| Xs],Ys) isort(Xs,Zs),insert(X,Zs,Ys).
isort([ ],[ ]).
insert(X,[Y| Ys],[X,Y | Ys])
XY.
insert(X,[Y | Ys],[Y|Zs])
X>Y,insert(X,Ys,Zs).
insert(X,[ ],[X]).
Программа 19.14.Некорректная и неполная программа сортировки вставками.
Эта программа содержит следующее ложное предложение:
insert(X, [Y| Ys], [X,Y | Ys])Х Y.
для которого можно привести следующий контрпример:
insert(2,[l],[2,l]) 2 1.
Рассматривая основное дерево доказательства, соответствующее ложному решению, можно найти ложный пример предложения, выполняя обход дерева доказательства в обратном порядке. При этом проверяется истинность посещаемых вершин дерева доказательства. Если обнаружится ложная вершина., то предложение, заголовок которого – найденная ложная вершина, а тело – конъюнкция ее вершин-сыновей, является контрпримером для некоторого предложения программы.
Корректность этого алгоритма следует из простого индуктивного доказательства. Этот алгоритм реализован в метаинтерпретаторе, представленном программой 19.15
В самом алгоритме и eго реализации предполагается использование оракула который может отвечать на вопросы относительно заданного значения программы.Оракул-это некоторое“существо”, внешнее по отношению к диагностируемому
false_solulion(A, Clause)
Если A – доказуемо ложный пример, то предложение Clause -ложное предложение программы. Алгоритм основан на просмотре дерева снизу вверх.
false_solution(A,Clause)
solve(A,Proof),
false_clause(Proof,Clause).
false_clause(true,ok).
false_clause((A,B),Clause)
false_clause(A,ClauseA),
check_conjunction(ClauseA,B,Clause).
false_clause((A B),Clause)
false_clause(B,ClauseB),
check_clause(ClauseB,A,B,Clause).
check_conjunction(ok,B,ciause)
false_clause(B,Clause).
check_conjuction((A Bl),B,(A Bl)).
check_clause(ok,A,B,Clause)
query_goal(A,Answer),
check_answer(Answer,A,B,Clause).
check_clause((Al В1),A,B,(A1 Bl)).
check _ answer(true,A,B,ok),
check _answer(false,A,B,(A B1)
extract_body(B,Bl).
extract_body(true,true),
extract _body((A B),A).
extract_body(((A B),Bs),(A,As))
extract_body(Bs,As),
query_goal(A,true)
system(A).
query goal(Goal,Ans\ver)
not system(Goal),
writeln(['Цель',Соа1,'истинна?]’).
read(Answer).
Программа 19.15.Программа поиска ложного решения при просмотре дерева снизу вверх.
алгоритму. Он может быть программистом, который способен отвечать на вопросы относительно заданного значения его программы, или другой программой для которой известно, что она имеет то же значение, что и заданное значение отлаживаемой программы. Вторая ситуация может встретиться при разработке новой версии программы и использовании старой версии в качестве оракула. Возможен также случай, когда при разработке некоторой эффективной программы, например программы быстрой сортировки, сначала получают для нее некоторую неэффективно исполняемую спецификацию (например, сортировку перестановками) и используют эту спецификацию в качестве оракула.
При обращении к программе 19.15с целью false_solution(isort([,3,2,1],X,С)? будет получен следующий протокол интерактивного взаимодействия:
false_solution(isort([3,2,1] X)С)?
Цель isort([ ],[ ])истинна?
true.
Цель insert (1,[ ],[1])истинна?
true.
Цель isort([l],[l])истинна?
true.
Цель insert (2, [I], [2,13)истинно
false.
X = [3,2,1],
С =insert(2,[l],[2,1])21.
В результате получен контрпример для ложного предложения.
Вопреки предположению алгоритма дерево доказательства, вырабатываемое предикатом solve/2, не обязательно является основным. Однако каждый пример некоторого дерева доказательства является деревом доказательства. Поэтому эта проблема может быть преодолена либо до вызова алгоритма конкретизации переменных, оставшихся в дереве доказательства произвольными константами, либо обращением к оракулу для конкретизации запрошенной цели, когда она содержит переменные. Различные примеры могут подразумевать различные ответы. Так как смысл этого алгоритма состоит в поиске контрпримера, то оракул, если это возможно, должен конкретизировать цель ложным примером.
Одной из основных задач, связанных с алгоритмами диагностики, является уменьшение их сложности, т.е. сокращение числа вопросов, требуемых для диагностики ошибки. При условии, что отвечать на вопросы возможно, придется программисту, это желание вполне понятно. Сложность приведенного выше алгоритма диагностики линейно зависит от размера дерева доказательства. Существует другой вариант алгоритма с лучшей стратегией, сложность которого линейно зависит не от размера дерева доказательства, а от его глубины. В отличие от предыдущего алгоритма, в котором обход дерева выполнялся снизу вверх, во втором алгоритме дерево доказательства просматривается сверху вниз. При этом в каждой вершине проверяется, не имеет ли она ложных вершин-сыновей. Если такие вершины не обнаруживаются, то текущая вершина представляет собой контрпример, поскольку цель, соответствующая этой вершине, истинна, а все её вершины-сыновья ложны. Если же такая вершина обнаруживается, то процесс поиска продолжается с этой вершины
Описанный алгоритм реализован в программе 19.16.
Отметим, что в первом предложении предиката false_goal/2использовано отсечение для реализации неявного отрицания, а предикат query_goal/2использован в программе как тестовый предикат.
Сравните поведение алгоритма, в котором дерево просматривается снизу вверх, с поведением программы 19.16на основании следующего протокола:
false_solution(isort([3,2,l],X),C)?
Цель isort([2,l],[2,l])истинна?
false.
Цель isort([l],[l])истинна?
true.
Цель insert(2,[l],[2,l])истинна?
false.
X=[3,2,11
С = insert (2, [1],[2,1]) 2 1.
Существует диагностический алгоритм поиска ложного решения, имеющий ещу меньшую сложность. В этом алгоритме реализуется принцип «разделяй и спрашивай». В процессе выполнения алгоритма производится расщепление дерева доказательства на две примерно равные части и проверка истинности вершины точке расщепления. Если эта вершина ложна, то алгоритм применяется рекурсивно к поддереву, корнем которого является эта вершина. Если вершина истинна, то ее поддерево удаляется из дерева и замещается вершиной true.Затем находится новая средняя точка. Можно показать, что в этом алгоритме требуемое число вопросов
false_solution(A,Clause)
Если A-доказуемо ложный пример, то предложение Clauseложное предложение программы. Алгоритм основан на просмотре дерева сверху вниз.
false_solution(A,Clause)
solve(A,Proof),
false_goal(Proof,Clause).
false_goal((A B),Clause)
false_conjunction(B,Clause),!.
false_goal((A B),(A B1))
extract_body(B,Bl).
false_conjunction(((A B),Bs),C!ause)
query_.goal(A,false),!,
false_goal((A B),Clause).
false_conjunction((A B),Clause)
query_goal(A,false),!,
false_goal((A B),Clause).
false_conjunction((A,As),Clause)
false_conjunction(As,Clause).
extract_body(Tree,Body) См. программу 19.15. query-goal(A,Answer) См. программу 19.15.
Программа 19 16.Программа диагностики ложного решения, в которой реализован алгоритм просмотра дерева сверху вниз.
логарифмически зависит от размера дерева доказательства. В случае когда дерево доказательства близко к линейному, этот алгоритм дает экспоненциальное уменьшение сложности по сравнению с рассмотренными выше алгоритмами, использующими обходы дерева сверху вниз и снизу вверх.
Третий возможный тип ошибок характеризуется пропуском решения. Диагностирование пропущенного решения оказывается более трудным по сравнению с диагностированием ошибок других типов. Будем говорить, что предложение покрываетцельАотносительно заданной интерпретации М, если оно имеет пример, заголовок которого является примером цели Л, а тело содержится в М.
Рассмотрим, например, цель insert(2,[1,3],Xs).Она покрывается предложением
insert(X,[Y | Ys],[X,Y | Ys])XY.
программы 19.14относительно заданной интерпретации М этой программы, поскольку в следующем примере этого предложения
insert(2,[l,3],[l,2,3])2 1.
заголовок является примером Аи тело содержится вМ.
Можно показать, что если программа Римеет пропущенное решение относительно заданного значения М, то в М существует некоторая цель А, которая не покрывается никаким предложением программыР.Доказательство этого утверждения выходит за рамки нашей книги. Оно было использовано в диагностическом алгоритме, представленном ниже.
Диагностирование пропущенных решений ложится тяжелым бременем на оракула, Он не только должен знать, имеет ли некоторая цель решение, но должен также получить решение, если оно существует. Используя такой оракул, некоторую непокрытую цель можно найти следующим образом.
Алгоритмом выдано некоторое пропущенное решение, т.е. цель в заданном интерпретации Мпрограммы Л для которой завершениеРбыло безуспешным. Выполнение этого алгоритма начинается с исходного пропущенного решения. Каждое предложение, унифицируемое с ним, проверяется с помощью оракула с целью определения, является ли тело этого предложения примером в М. Если такого предложения не существует, то цель не покрывается и работа алгоритма завершается. В противном случае алгоритм находит в теле цель, которая приводит к безуспешному вычислению. По крайней мере одна из целей тела должна быть такой, иначе программа в противоположность нашему предположению может доказать это тело, а следовательно, и эту цель. Алгоритм применяется рекурсивно к этой цели.
missing _solution( A,Goal)
Если Анедоказуемо истинная основная цель, то цель Goal-истинная основная цель, которая не покрывается этой программой.
missing _solution((A,В), Goal) !,
(not A,missing_solution(A,Goal);
A, missing_solution(B,Goal)),
missing_solution(A,Goal)
clause(A,B),
query_clause((A B)),!,
missing_solution (B, Goal).
missing_solution(A, A)
not system (A).
query_clause (Clause)
writeln(['Введите истинный основной пример',
Clause,'если такой пример существует, в противном случае введите «нет»']),
read (Answer),
!, check_answer(Answer, Clause).
check_answer(no, Clause) !,fail.
check_answer(Clause, Clause) !.
check_answer(Answer, Clause)
write('Неправильный ответ'),
!, query_clause (Clause).
Программа 19,17.Программа диагностирования пропущенного решения.
Реализация данного алгоритма представлена программой 19.17.Программа пытается проследить безуспешный путь вычисления и найти непокрываемую истинную цель. В ходе выполнения этой программы был получен в качестве примера следующий протокол:
missing_solution(isort([2,1,3],[ 1,2,3]),С)?
Введите истинный основной пример
(isort([2,l,3],[l,2,3]) isort([l,3],Xs),insert(2,Xs,[ 1,2,3]))
если такой пример существует, в противном случае введите ‘нет’
{isort([2,l,3],[1,2,3]) isort([1,3],[1,3])
insert(2,[1,3],[1,2,3]))
Введите истинный основной пример
(isort([l,3],[l,3]) isort([3],Ys), insert(l, Ys,[l,3]))
если такой пример существует, в противном случае введите 'нет'
(isort([1,3],[1,3])isort([3],[3]),
insert(l, [3],[1,3])).
Введите истинный основной пример
(insert(l,[3],[l,3]) 1 3)
если такой пример существует, в противном случае введите 'нет'
false
С =insert(l,[3],[l,3]).
Читатель может проверить, что цель insert(I,[3],[1,3])не покрывается программой 19.14.
Рассмотренные алгоритмы могут быть положены в основу создания эффективных средств поддержки разработки программ на Прологе.
19.4. Дополнительные сведения
Понятие метаинтерпретатора, или, вернее, метациклического интерпретатора, было введено Sussmanи Steele (1978),которые первыми предложили использовать способность языка специфицировать тот же язык, как фундаментальный критерий разработки языка.
Пролог является естественным языком для построения экспертных систем. Хотя программа 19.6очень проста, она напоминает ранние робкие попытки создания экспертных систем. Сравним эту программу с правилом из системы MYCIN (Shortliffe, 1976)-экспертной системы для диагностирования и лечения бактериологических инфекций. Рассмотрим следующее правило:
IF по методу Грама организм -грамотрицателен,
морфология организма -микроб.
аэробность организма-неаэробен
THEN имеется предположительное основание (0,5)
считать, что организм относится к бактероидам.
Имеются два аспекта приведенного выше правила. Первый состоит в том, что эвристическая идентификация бактерий основана на методе Грама, морфологии и аэробности. Второй заключается в том, что правилу придается коэффициент достоверности. Мы считаем, что они должны быть разделены. В условиях неопределенности лучше работает такой метаинтерпретатор, как программа 19.10.Эвристические знания можно выразить следующим правилом Пролога:
тождественность(Организм, бактероиды)
критерий_грама(Организм,грамотрицательный), морфология(Организм,микроб), аэробность(Организм.неаэробный)
Отметим, что это правило и аналогичные правила в программе 19.6по существу являются основными. В «контексте» изучаемого объекта встречается единственная переменная. В представленном выше правиле системы MYCINконтекстом является «организм», а в системе управления печью (программа 19.6)контекстом было «блюдо».
Нетрудно написать правила, в которых используется большая мощность языка Пролог. Ниже приведено предложение из модельной медицинской экспертной системы, которое дает некоторое представление о такой возможности. Экспертная система кредитных операций, представленная в гл. 21,также содержит ряд примеров. Правило из медицинской экспертной системы имеет вид
предписать(Пациент, Лекарство) болезнь(Пациент,Симптом), запретить(Симптом,Лекарство), подходящес(Лекарство. Пациент).
Отношения болезнь, зanpетитьиподходящеедолжны быть сами описаны с помощью фактов и правил.
Первые явные доводы относительно использования Пролога для построения экспертных систем дали Clarkи МсСаbе (1982).В их работе обсуждается, как простая экспертная система типа программы 19.6может быть расширена введением средств объяснения и рассуждения в условиях неопределенности. Предлагаемый метод основан на добавлении дополнительных аргументов к предикатам программы. Средства объяснения и средства опроса пользователя были введены Hammondи Sergot (Hammond, 1984)в оболочку экспертной системы APES. Использовать метаинтерпретатор в качестве базиса средств объяснения в экспертной системе предложил Sterling (1984).Построение интерактивной системы объяснения с использованием усовершенствованных метаинтерпретаторов описано у Sterlingи Lalee (1985).Использовать развитые метаинтерпретаторы для работы в условиях неполной определенности предложилShapiro(1983с).
Shapiro (1983a)предположил, что развитые интерпретаторы должны стать основой для построения среды программирования. В этой книге содержатся и обсуждаются алгоритмы отладки программ, использованные в разд. 19.3.
Takeuchiи Furukawa (1985)показали, что частичные вычисления могут уменьшить накладные расходы при использовании метаинтерпретаторов. Результатом частичных вычислений является компиляция объектной программы и развитого метаинтерпретатора в новую объектную программу, наследующую функциональные возможности метаинтерпретатора, но свободную от дополнительных расходов. Sterlingи Beer (1986)подробно рассматривают работу экспертных систем. В обеих статьях отмечается 40-кратное ускорение вычислений.
Метаинтерпретаторы и вообще метапрограммы рассматривались также в связи с задачами управления в Пролог-программах. Отметим здесь следующие работы: DincbasиLePape (1984), Gallaireи Lasserre (1980)и Pereira (1982).