Реализация "последнего" в Prolog

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

упражнение 2.6 на стр. 20 требует рекурсивной реализации предиката last1, который ведет себя как встроенный предикат last. Моя попытка это так:

last1([_ | Rest], Last) :- last1(Rest, Last).
last1([Last], Last).

он дает правильный ответ, но для списков с более чем одним элементом я должен ввести точку с запятой, чтобы завершить запрос. Это делает last1 отличается от встроенного last.

?- last1([1], Last).
Last = 1.

?- last1([1, 2], Last).
Last = 2 ;
false.

если я переключаю порядок, в котором я объявил правило и факт, то мне нужно ввести точку с запятой в обоих случаях.

думаю, я знаю, почему Пролог думает, что last1 может иметь одно решение (таким образом точка с запятой.) Я предполагаю, что он следует последовательности оценки

last1([1, 2], Last).
==>  last1([2], Last).
==>  last1([], Last).    OR    Last = 2.
==>  false    OR    Last = 2.

это, кажется, предполагает, что я должен искать способ избежать сопоставления Rest С []. Несмотря на это, у меня нет объяснения, почему переключение порядка объявления должно иметь какой-либо эффект вообще.

Вопрос 1: каково правильное объяснение поведению last1?

Вопрос 2: как я могу реализовать предикат last1 что неотличимый от встроенного last?

3 ответов


Вопрос 1:

системы Prolog не всегда могут решить, будет ли предложение применяться до его выполнения. Конкретные обстоятельства зависят от выполнения. То есть вы не можете полагаться на это решение в целом. Системы улучшаются от выпуска к выпуску. Рассмотрим как самый простой случай:

?- X = 1 ; 1 = 2.
X = 1 ;
false.

очень умный Пролог может обнаружить, что 1 = 2 всегда терпит неудачу, и поэтому просто ответ . С другой стороны, такие "ум" очень дорогостоящ в реализации, и время лучше потратить на оптимизацию более частых случаев.

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

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

в вашем примере с last1/2 вы столкнулись с таким случаем. И вы уже сделали что-то очень умное, кстати: вы попытался минимизировать запрос, чтобы увидеть первое появление неожиданного поведения.

в вашем примере query last1([1,2],X) система Prolog не смотрит на весь список [1,2] но только смотрит на главный функтор. Поэтому для системы Prolog запрос выглядит так же, как last1([_|_],X), когда он решает, какие пункты применить. Эта цель теперь подходит для обоих предложений, и именно поэтому Prolog будет помнить второе предложение в качестве альтернативы для опробования.

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

?- length(L,10000000), last1(L,E).
ERROR: Out of local stack

С другой стороны, предопределенные last/2 работает:

?- length(L,10000000), last(L,E).
L = [_G343, _G346, _G349, _G352, _G355, _G358, _G361, _G364, _G367|...].

в самом деле, он использует постоянный космос!

там теперь есть два выхода из этого:

  1. попробуйте оптимизировать определение. Да, вы можете это сделать, но вам нужно быть очень умным! Например, определение @back_dragon неверно. Часто бывает, что начинающие пытаются оптимизировать программу, когда на самом деле разрушают ее семантику.

  2. спросите себя, действительно ли вы определяете тот же предикат, что и last/2. На самом деле, нет.

вопрос 2:

считаем:

?- last(Xs, X).
Xs = [X] ;
Xs = [_G299, X] ;
Xs = [_G299, _G302, X] ;
Xs = [_G299, _G302, _G305, X] ;
Xs = [_G299, _G302, _G305, _G308, X] 
...

и

?- last1(Xs, X).
** loops **

таким образом, ваше определение отличается в этом случае определением SWI. Обменяйте порядок пунктов.

?- length(L,10000000), last2(L,E).
L = [_G343, _G346, _G349, _G352, _G355, _G358, _G361, _G364, _G367|...] ;
false.

опять же, это false! Но на этот раз, большой список работ. И на этот раз, минимальный запрос:

?- last2([1],E).
E = 1 ;
false.

и ситуация очень похожа: опять же, Prolog будет смотреть на запрос так же, как last2([_|_],E) и придет к выводу, что применяются оба положения. По крайней мере, теперь у нас постоянные накладные расходы вместо линейных.

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


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

Я бы попытался сделать last1 детерминированным таким образом:

last1([_,H|Rest], Last) :- !, last1([H|Rest], Last).
last1([Last], Last).

но я не думаю, что это indistinguishable С последние. Скрывается в исходном коде библиотеки (это просто как ?- edit(last).)

%%  last(?List, ?Last)
%
%   Succeeds when Last  is  the  last   element  of  List.  This
%   predicate is =semidet= if List is a  list and =multi= if List is
%   a partial list.
%
%   @compat There is no de-facto standard for the argument order of
%       last/2.  Be careful when porting code or use
%       append(_, [Last], List) as a portable alternative.

last([X|Xs], Last) :-
    last_(Xs, X, Last).

last_([], Last, Last).
last_([X|Xs], _, Last) :-
    last_(Xs, X, Last).

мы можем оценить хорошо продуманный реализации.


этот код будет работать:

last1([Last], Last).
last1([_ | Rest], Last) :- last1(Rest, Last), !.

это потому, что вещи пролога могут быть больше комбинаций, но с этим символом:!, prolog не вернется после достижения этой точки