пролог - бесконечное правило

У меня есть следующие правила

% Signature: natural_number(N)/1
% Purpose: N is a natural number.
natural_number(0).
natural_number(s(X)) :-
   natural_number(X).

ackermann(0, N, s(N)). % rule 1
ackermann(s(M),0,Result):-
   ackermann(M,s(0),Result). % rule 2
ackermann(s(M),s(N),Result):-
   ackermann(M,Result1,Result),
   ackermann(s(M),N,Result1). % rule 3

Запрос: ackermann (M,N,s(s(0))).

Теперь, как я понял, в третьем расчете мы получили бесконечный поиск (ветвь отказа). Я проверяю это и получаю конечный поиск (отказная ветвь).

Поясню: В первом мы получили замену M=0, N=s(0) (правило 1 - успех!). Во втором мы получили замену M=s(0),N=0 (правило 2 - успех!). Но что теперь? Я пытаюсь сопоставить M=s(s(0)) N=0, но получил конечный поиск - ветвь отказа. Почему компилятор не пишет мне "fail".

Спасибо.


person Tom    schedule 24.06.2011    source источник


Ответы (2)


Было немного сложно понять, что именно Том спрашивает здесь. Возможно, предполагается, что предикат натуральное_число/1 каким-то образом влияет на выполнение ackermann/3. Он не будет. Последний предикат является чисто рекурсивным и не создает подцелей, зависящих от natural_number/1.

Когда три показанных предложения определены для ackermann/3, цель:

?- ackermann(M,N,s(s(0))).

заставляет SWI-Prolog найти (с возвратом) два решения, о которых сообщает Том, а затем перейти к бесконечной рекурсии (что приводит к ошибке «Out of Stack»). Мы можем быть уверены, что эта бесконечная рекурсия включает в себя третье предложение, данное для ackermann/3 (правило 3 согласно комментариям Тома в коде), потому что в его отсутствие мы получаем только два признанных решения, а затем явный отказ:

M = 0,
N = s(0) ;
M = s(0),
N = 0 ;
false.

Мне кажется, Том просит объяснить, почему изменение представленного запроса на тот, который устанавливает M = s(s(0)) и N = 0, производя конечный поиск (который находит одно решение, а затем терпит неудачу при возврате), согласуется с бесконечная рекурсия, созданная предыдущим запросом. Я подозреваю, что существует неправильное понимание того, что движок Пролога пытается отследить (для исходного запроса), поэтому я собираюсь углубиться в это. Надеюсь, это прояснит ситуацию для Тома, но посмотрим, так ли это. По общему признанию, моя трактовка многословна, но механизм выполнения Пролога (объединение и разрешение подцелей) достоин изучения.

[Добавлено: Предикат имеет очевидную связь со знаменитой функцией Аккермана, которая полностью вычислима, но не примитивно-рекурсивный. Известно, что эта функция быстро растет, поэтому нам нужно быть осторожными, заявляя о бесконечной рекурсии, потому что возможна очень большая, но конечная рекурсия. Однако в третьем предложении два рекурсивных вызова располагаются в порядке, противоположном тому, который я бы сделал, и это обращение, похоже, играет решающую роль в бесконечной рекурсии, которую мы обнаруживаем при пошаговом выполнении кода ниже.]

Когда передается цель верхнего уровня ackermann(M,N,s(s(0))), SWI-Prolog пробует предложения (факты или правила), определенные для ackermann/3, пока не найдет то, чья "голова" унифицируется с данным запросом. Механизм Пролога не должен далеко смотреть в качестве первого предложения, этот факт:

ackermann(0, N, s(N)).

унифицирует, связав M = 0 и N = s(0) как уже было описано как первый успех.

Если вас попросят вернуться, например. когда пользователь вводит точку с запятой, механизм Пролога проверяет, есть ли альтернативный способ выполнить это первое предложение. Нет. Затем механизм Prolog пытается выполнить следующие предложения для ackermann/3 в заданном порядке.

Опять же, поиск не должен идти далеко, потому что заголовок второго предложения также унифицируется с запросом. В этом случае у нас есть правило:

ackermann(s(M),0,Result) :- ackermann(M,s(0),Result).

Объединение запроса и заголовка этого правила дает привязки M = s(0), N = 0 с точки зрения переменных, используемых в запросе. Что касается переменных, используемых в правиле, как указано выше, M = 0 и Result = s(s(0)). Обратите внимание, что унификация сопоставляет термины по их внешнему виду в качестве аргументов вызова и не рассматривает имена переменных, повторно используемые через границу запроса/правила, как обозначающие идентичность.

Поскольку этот пункт является правилом (имеющим не только заголовок, но и тело), ​​унификация — это только первый шаг в попытке преуспеть в его применении. Механизм Prolog теперь пытается выполнить одну подцель, указанную в теле этого правила:

ackermann(0,s(0),s(s(0))).

Обратите внимание, что эта подцель достигается заменой «локальных» переменных, используемых в правиле, значениями унификации, M = 0 и Result = s(s(0)). Механизм Prolog теперь рекурсивно вызывает предикат ackermann/3, чтобы проверить, может ли быть достигнута эта подцель.

Может, поскольку первое предложение (факт) для ackermann/3 унифицируется очевидным образом (действительно так же, как и раньше, в отношении переменных, используемых в предложении). И таким образом (при успешном выполнении этого рекурсивного вызова) мы получаем второе решение, успешное во внешнем вызове (запросе верхнего уровня).

Если пользователь просит механизм Пролога вернуться еще раз, он снова проверяет, может ли текущее предложение (второе для ackermann/3) быть удовлетворено альтернативным способом. Это невозможно, поэтому поиск продолжается, переходя к третьему (и последнему) предложению для предиката ackermann/3:

ackermann(s(M),s(N),Result) :-
    ackermann(M,Result1,Result),
    ackermann(s(M),N,Result1).

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

   query     head
     M       s(M)
     N       s(N)
   s(s(0))  Result

Принимая во внимание, что переменные, имеющие в запросе то же имя, что и переменные в правиле, не ограничивают унификацию, эту тройку термов можно унифицировать. Запрос M будет заголовком s(M), то есть составным термином, включающим функтор s, применяемый к некоторой пока неизвестной переменной M, появляющейся в заголовке. То же самое для запроса N. Единственным «основным» термином на данный момент является переменная Result, появляющаяся в заголовке (и теле) правила, которая связана с s(s(0)) из запроса.

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

ackermann(M,Result1,s(s(0))).

Позвольте мне указать, что я использовал здесь «локальные» переменные предложения, за исключением того, что я заменил Result значением, с которым оно было связано при унификации. Теперь обратите внимание, что помимо замены N в исходном запросе верхнего уровня именем переменной Result1, мы просто задаем то же самое, что и исходный запрос в этой подцели. Конечно, это важная подсказка, что мы можем вот-вот войти в бесконечную рекурсию.

Однако необходимо немного больше обсуждения, чтобы понять, почему мы не получаем сообщений о дальнейших решениях! Это связано с тем, что для первого успеха этой первой подцели (как было обнаружено ранее) потребуются M = 0 и Result1 = s(0), и механизм Пролога затем должен перейти к попытке выполнить вторую подцель предложения:

ackermann(s(0),N,s(0)).

К сожалению, эта новая подцель не унифицируется с первым пунктом (фактом) для ackermann/3. Он действительно объединяется с заголовком второго предложения следующим образом:

   subgoal     head
     s(0)      s(M)
      N         0
     s(0)     Result

но это приводит к под-подцели (из тела второго пункта):

ackermann(0,s(0),s(0)).

Это не унифицируется с главой ни первого, ни второго предложения. Он также не унифицируется с заголовком третьего предложения (которое требует, чтобы первый аргумент имел форму s(_)). Итак, мы достигли точки отказа в дереве поиска. Механизм Prolog теперь возвращается назад, чтобы увидеть, можно ли удовлетворить первую подцель тела третьего предложения альтернативным способом. Как мы знаем, может быть (поскольку эта подцель в основном совпадает с исходным запросом).

Теперь M = s(0) и Result1 = 0 этого второго решения приводят к этому для второй подцели тела третьего предложения:

ackermann(s(s(0)),N,0).

Хотя это не унифицируется с первым предложением (фактом) предиката, оно унифицируется с заголовком второго предложения:

   subgoal     head
   s(s(0))     s(M)
      N         0
      0       Result

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

ackermann(s(s(0)),s(0),0).

Мы легко видим, что это не может быть унифицировано с заголовком ни первого, ни второго предложения для ackermann/3. Его можно объединить с главой третьего предложения:

  sub-subgoal  head(3rd clause)
    s(s(0))       s(M)
      s(0)        s(N)
       0         Result

Как должно быть уже известно, механизм Пролога проверяет, может ли быть достигнута первая подцель тела третьего предложения, которая равна этой под-под-подцели:

ackermann(s(0),Result1,0).

Это не унифицируется с первым предложением (факт), но унифицируется с заголовком второго предложения, связывающего M = 0, Result1 = 0 и Result = 0 , создавая (по знакомой логике) под-под-под-подцель:

ackermann(0,0,0).

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

  sub-sub-subgoal  head(3rd clause)
       s(0)             s(M)
      Result1           s(N)
         0             Result

и тогда задача механизма Пролога состоит в том, чтобы удовлетворить эту под-под-под-подцель, полученную из первой части тела третьего предложения:

ackermann(0,Result1,0).

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

ackermann(M,Result1,s(s(0))).

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

person hardmath    schedule 26.06.2011

Позвольте мне перефразировать ваш вопрос: запрос ackermann(M,N,s(s(0))). находит два решения, а затем зацикливается. В идеале он должен завершиться после этих двух решений, поскольку нет других N и M, значение которых равно s(s(0)).

Так почему же запрос не завершается универсально? Понять это может быть довольно сложно, и лучший совет — не пытаться понять точный механизм выполнения. На это есть очень простая причина: механизм выполнения Пролога оказывается настолько сложным, что вы все равно легко поймете его неправильно, если попытаетесь понять его, шагая по коду.

Вместо этого вы можете попробовать следующее: Вставьте цели false в любое место вашей программы. Если результирующая программа не завершится, то и исходная программа не завершится.

В твоем случае:

ackermann(0, N, s(N)) :- false.
ackermann(s(M),0,Result):- false,
   ackermann(M,s(0),Result).
ackermann(s(M),s(N),Result):-
   ackermann(M,Result1,Result), false,
   ackermann(s(M),N,Result1).

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

ackermann(s(M),s(N),Result):-ackermann(M,Result1,Result), false.

Теперь эта программа завершается, только если известен первый аргумент. Но в нашем случае это бесплатно...

То есть: рассматривая небольшую часть программы (называемую failure-slice) мы уже смогли вывести свойство всей программы. Подробности см. в этой статье и других на сайт.

К сожалению, такого рода рассуждения работают только для случаев нерасторжения. С прекращением дела обстоят сложнее. Лучше всего попробовать такой инструмент, как cTI, который выводит условия завершения и пытается доказать их оптимальность. Я уже вошел в вашу программу, так что попробуйте изменить ее и посмотрите на эффект!

Если мы в этом: этот небольшой фрагмент также говорит нам, что второй аргумент не влияет на завершение1. Это означает, что такие запросы, как ackermann(s(s(0)),s(s(0)),R)., также не завершатся. Поменяйтесь голами, чтобы увидеть разницу...


1 Чтобы быть точным, термин, который не объединяется с s(_), будет влиять на завершение. Подумайте о 0. Но любые s(0), s(s(0)), ... не повлияют на завершение.

person false    schedule 24.06.2011
comment
Спасибо за ваш ответ, но я должен понять, как работает система (для моих теоретических исследований). Вы хорошо объяснили мне, как проверить эту проблему по прологу, но я должен знать, почему эта проблема возникает. - person Tom; 25.06.2011
comment
@false: я не согласен с тем, что механизм выполнения Prolog настолько сложен, что вы все равно легко поймете его неправильно, если попытаетесь понять его, пройдясь по коду. Упрощение кода, как вы предложили, может быть полезным, но разве мы не должны, по крайней мере, иметь возможность строго связать поведение исходной программы с вашим упрощенным, используя этот пример? - person hardmath; 25.06.2011
comment
@hardmath: существует строгая связь между упрощенной программой (отказоустойчивый фрагмент) и исходной: если отказоустойчивый фрагмент не завершится, исходная программа также не завершится. К сожалению, это единственная связь. - person false; 25.06.2011
comment
@hardmath: Что касается сложности механизма выполнения Пролога: вы видите, когда вышеприведенная программа завершается? Я не осознавал, что он завершается (помимо других случаев), когда 1-й и 3-й аргумент связаны. Вы можете увидеть это с помощью cTI (ссылка выше). - person false; 25.06.2011
comment
@false: предикат представляет собой своего рода реализацию функции Аккермана (ссылка добавлена ​​к моему ответу), поэтому он вполне может демонстрировать занятое поведение. Мне кажется, что Том ставит две основные подцели своего последнего пункта (правила) в обратном порядке. Согласно вашему ответу, это было бы хорошей модификацией, которую можно попробовать с инструментом cTI. - person hardmath; 26.06.2011
comment
@hardmath: обмен целями приведет к прекращению ackermann(b,b,f), что в настоящее время не так. С этой целью можно использовать указанную выше ссылку cTI. - person false; 26.06.2011