Реализация "последнего" в 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|...].
в самом деле, он использует постоянный космос!
там теперь есть два выхода из этого:
попробуйте оптимизировать определение. Да, вы можете это сделать, но вам нужно быть очень умным! Например, определение @back_dragon неверно. Часто бывает, что начинающие пытаются оптимизировать программу, когда на самом деле разрушают ее семантику.
спросите себя, действительно ли вы определяете тот же предикат, что и
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 не вернется после достижения этой точки