Каковы плюсы и минусы использования ручной итерации списка по сравнению с рекурсией через сбой

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

Я пытаюсь выяснить, использовать ли метод 1 или 2, и каковы плюсы и минусы каждого, особенно большого количества фактов.

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

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

Одна проблема, которую я вижу, заключается в том, что каждый раз, когда вызывается fail, мы теряем возможность передать что-либо обратно вызывающему предикату, например. если бы это было methodtwo(SeasonResults), так как мы постоянно намеренно теряем предикат. Таким образом, methodtwo нужно будет утверждать факты для сохранения состояния.

Предположительно (?) Метод 2 будет быстрее, поскольку он не обрабатывает (большой) список?

Я мог представить, что если бы у меня был список, то methodone был бы правильным выбором... или это всегда так? Имеет ли смысл в каких-либо условиях утверждать список фактов, используя methodone, а затем обрабатывать их, используя второй метод? Полное безумие?

Но опять же, я читал, что утверждение фактов — очень «дорогое» дело, поэтому обработка списков может быть подходящим способом, даже для больших списков?

Есть предположения? Или иногда лучше использовать один, а не другой, в зависимости от (какой) ситуации? например. для оптимизации памяти используйте метод 2, включая утверждение фактов, а для скорости используйте метод 1?

season(spring).
season(summer).
season(autumn).
season(winter).

 % Season handling
showseason(Season) :-
    atom_length(Season, LenSeason),
    write('Season Length is '), write(LenSeason), nl.

% -------------------------------------------------------------
% Method 1 - Findall facts/iterate through the list and process each
%--------------------------------------------------------------
% Iterate manually through a season list
lenseason([]).
lenseason([Season|MoreSeasons]) :-
    showseason(Season),
    lenseason(MoreSeasons).


% Findall to build a list then iterate until all done
methodone :-
    findall(Season, season(Season), AllSeasons),
    lenseason(AllSeasons),
    write('Done').

% -------------------------------------------------------------
% Method 2 - Use fail to force recursion
%--------------------------------------------------------------
methodtwo :-
    % Get one season and show it
    season(Season),
    showseason(Season),

    % Force prolog to backtrack to find another season
    fail.

% No more seasons, we have finished
methodtwo :-
    write('Done').

person magus    schedule 16.03.2012    source источник


Ответы (3)


первый метод кажется расточительным, поскольку факты доступны, зачем создавать их список (особенно большой список). Это также должно иметь последствия для памяти, если список достаточно велик?

Да, метод 1 занимает Θ(n) памяти. Его основное преимущество заключается в том, что он является декларативным, то есть имеет прямое логическое значение.

Метод 2, "управляемый отказом цикл", как его называют программисты на Прологе, использует постоянную память, является процедурным и может быть предпочтительным, когда вы все равно делаете процедурные (внелогические) вещи; т. е. в коде ввода-вывода его можно использовать.

Обратите внимание, что в SWI-Prolog есть третий способ записи этого цикла:

forall(season(S), showseason(S)).

Это работает только в том случае, если showseason успешно выполняется для каждой привязки season(S).

person Fred Foo    schedule 16.03.2012
comment
спасибо - я не знал о forall() - это ускользнуло от меня. Хороший - это полезно для меня прямо сейчас. - person magus; 17.03.2012
comment
@larsmans: Но forall/2, по сути, представляет собой цикл, управляемый сбоями. Нет возможности удержать крепления от forall! То есть forall(A,B)\+ \+ forall(A,B) - person false; 17.03.2012
comment
@false: да, но с той разницей, что FDL всегда терпит неудачу, в то время как forall может быть успешным. Для реализации цикла этого достаточно. - person Fred Foo; 17.03.2012
comment
@larsmans: forall/2 определенно лучше, чем цикл, управляемый ошибкой по умолчанию, который не различает успех и неудачу. То есть это неупоминаемое doall(Goal) :- Goal, fail ; true. Тем не менее, оно по своей природе чрезвычайно чувствительно к воплощениям. - person false; 17.03.2012

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

В своем примере вы сделали очень интересное открытие: названия всех времен года имеют одинаковую длину. Какое потрясающее понимание! Но подождите, это правда? Самый простой способ проверить это:

?- season(S), atom_length(S,L).
S = spring,
L = 6 ;
S = summer,
L = 6 ;
S = autumn,
L = 6 ;
S = winter,
L = 6.

Не нужно findall/3, не нужно write/1.

При большем количестве ответов визуальный осмотр нецелесообразен. Представьте себе 400 сезонов. Но мы можем проверить это с помощью:

?- season(S), atom_length(S,L), dif(L,6).
false.

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

Это мой самый первый ответ на ваш вопрос:

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

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

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

  • Оболочка верхнего уровня используется многими другими пользователями и поэтому очень хорошо протестирована. Ваше собственное письмо часто ошибочно и непроверено. Подумайте об ограничениях. Подумайте о написании поплавков. Будете ли вы использовать write/1 и для поплавков? Как правильно записывать числа с плавающей запятой, чтобы их можно было точно прочитать? Есть способ сделать это в разделе изо-пролог. Вот ответ:

В ISO writeq/1,2, write_canonical/1,2, write_term/2,3 с опцией quoted(true) гарантируют, что числа с плавающей запятой могут быть точно прочитаны. То есть они одинаковые w.r.t. (==)/2

  • Оболочка верхнего уровня показывает вам допустимый текст Prolog. На самом деле ответ сам по себе является вопросом! Его можно вставить обратно на верхний уровень - только чтобы получить тот же самый ответ. Таким образом вы изучите более экзотические, но неизбежные детали Пролога, такие как цитирование, экранирование и заключение в скобки. В противном случае выучить синтаксис практически невозможно, поскольку синтаксические анализаторы Пролога часто чрезвычайно либеральны.

  • Ваша программа, скорее всего, будет более доступна для декларативных рассуждений.

Скорее всего, ваши две процедуры methodone и methodtwo неверны: вы забыли новую строку после написания Done. Итак, methodone, methodone содержит искаженную строку. Как это легко проверить?

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

Соображения эффективности

Системы Prolog часто реализуют сбой, освобождая стек. Следовательно, циклы, вызванные сбоями, не требуют сборщика мусора. Вот почему люди считают, что циклы, основанные на неудачах, эффективны. Однако это не обязательно так. Для такой цели, как findall(A, season(A), As), каждый ответ на A копируется в некоторое место. Это тривиальная операция для чего-то вроде атома, но представьте себе более крупный термин. Сказать:

blam([]).
blam([L|L]) :- blam(L).

bigterm(L) :- length(L,64), blam(L).

Во многих системах findall/3 или assertz/1 для этого большого термина заморозят систему.

Кроме того, такие системы, как SWI, YAP, SICStus, имеют довольно сложные сборщики мусора. А использование меньшего количества циклов, вызванных сбоями, поможет еще больше улучшить эти системы, поскольку это создает спрос на более сложные методы.

person false    schedule 20.03.2012

Если уже используется findall, почему бы не использовать и maplist:

findall(S, season(S), L), maplist( showseason, L).

Оба не находятся в чистом логическом ядре Пролога. И да, вы выделяете целый список для всех решений.

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

person Will Ness    schedule 16.03.2012
comment
maplist/2,... является чистым и монотонным (при условии, что он называет себя только чистыми и монотонными предикатами), а findall/3 — нет. - person false; 17.03.2012
comment
вы правы в том, что список карт чистый, если мы допустим call/_ то есть. Но это так в том смысле, что вызов maplist с определенной целью может быть написан на чистом Прологе по той же схеме, да. Я не знаю, что здесь означает монотонность. - person Will Ness; 17.03.2012
comment
call/1..8 - это ISO-пролог. В.р.т. монотонный: как в монотонной логике. Например, что произойдет с программой, если мы добавим еще один факт? Будет ли все, что раньше было правдой, по-прежнему будет правдой? Если у вас есть findall/3 или отрицание, это уже не так. Чистое сердце Пролога — его монотонное подмножество, состоящее из call/1..8, dif/2 и множества ограничений. Он не содержит (==)/2, (\+)/1, !, var/1, общих if-then-else и всех тех встроенных модулей с побочными эффектами, имена которых пока ускользают от меня... - person false; 18.03.2012
comment
Для полноты есть и другое значение монотонности. Вкратце, если выполняется G, будет ли также выполняться G, ..., G? var/1 не имеет этого свойства. Но он есть у nonvar/1 и ground/1 (вдобавок ко всей чистой части Пролога). - person false; 18.03.2012
comment
Спасибо за ваш ответ - знаю, я понимаю, почему findall является одним из самых медленных разделов моего кода, когда я его профилирую ... если он каждый раз утверждает факты для создания списка. - person magus; 19.03.2012
comment
@magus: многие реализации больше не заносят решения в базу данных. Они скорее копируют их прямо в кучу (также известный как копировальный стек). - person false; 22.03.2012