Реверсивные численные вычисления в Prolog
При чтении SICP я столкнулся с логическим программированием, глава 4.4. Затем я начал изучать язык программирования Prolog и пытался понять некоторые простые назначения в Prolog. Я обнаружил, что у Пролога, похоже, есть проблемы с численными расчетами.
Ниже приведено вычисление факториала в стандартном прологе:
f(0, 1).
f(A, B) :- A > 0, C is A-1, f(C, D), B is A*D.
Я обнаружил, что мне нужно ввести две вспомогательные переменные (C
и D
), новый синтаксис (is
) и что проблема не обратима (т.е. f(5,X)
работает как ожидаемый, но f(X,120)
нет).
Наивно, я ожидаю, что по крайней мере C is A-1, f(C, D)
выше может быть заменено на f(A-1,D)
, но даже это не работает.
Мой вопрос: Зачем мне делать лишний "материал" в числовых вычислениях, но не в других запросах?
Я понимаю (и SICP совершенно ясно об этом), что в целом информации о том, "что делать", недостаточно, чтобы ответить на вопрос "как это сделать". Таким образом, декларативное знание (по крайней мере, некоторых) математических задач недостаточно для решения этих проблем. Но это вызывает следующий вопрос: как этот лишний "материал" в Prolog помогает мне ограничивать формулировку только теми проблемами, где "что делать" достаточно, чтобы ответить "как это сделать"?
Ответы
Ответ 1
Забудьте о переменных и подумайте, что A
и B
- это просто имя для значения, которое можно поместить в это предложение (X :- Y).
, чтобы сделать его доступным. Подумайте о X = (2 + (3 * 4))
на пути структур данных, которые представляют математическое выражение. Если вы попросите пролог достичь цели f(A-1, B)
, он попытается найти такой атом f(A-1,B).
или правило (f(A-1,B) :- Z), Z.
, которое будет унифицировано для "успеха".
is/2
пытается объединить первый аргумент с результатом интерпретации второго аргумента как выражения. Рассмотрим eval/2
как вариант is/2
:
eval(0, 1-1). eval(0, 2-2). eval(1,2-1).
eval(Y, X-0):- eval(Y, X).
eval(Y, A+B):- eval(ValA, A), eval(ValB, B), eval(Y, ValA + ValB).
eval(4, 2*2).
eval(0, 0*_). eval(0, _*0).
eval(Y, X*1):- eval(Y, X).
eval(Y, 1*X):- eval(Y, X).
eval(Y, A*B):- eval(ValA, A), eval(ValB, B), eval(Y, ValA * ValB).
Причина, по которой f(X,120)
не работает, прост. >/2
работает только тогда, когда ее аргументы связаны (т.е. вы не можете сравнивать что-то еще не определенное как X
с чем-либо еще). Чтобы исправить это, вы должны разделить это правило на:
f(A,B) :- nonvar(A), A > 0, C is A-1, f(C, D), B is A*D.
f(A,B) :- nonvar(B), f_rev(A, B, 1, 1).
% f_rev/4 - only first argument is unbound.
f_rev(A, B, A, B). % solution
f_rev(A, B, N, C):- C < B, NextN is (N+1), NextC is (C*NextN), f_rev(A, B, NextN, NextC).
Обновление: (исправлено f_rev/4
)
Вы можете быть заинтересованы в решении конечных доменов. question было сказано об использовании таких вещей. Используя #>/2
и #=/2
, вы можете описать некоторые формулы и ограничения, а затем разрешить их. Но эти предикаты используют специальные способности некоторых прологовых систем, которые позволяют связать имя с некоторыми атрибутами, которые могут помочь ограничить множество возможных значений путем пересечения ограничений. Некоторые другие системы (как правило, одни и те же) позволяют изменить порядок обработки целей ( "приостановить" ).
Кроме того, member(X,[1,2,3,4,5,6,7]), f(X, 120)
, вероятно, делает то же самое, что и ваши "другие запросы".
Если вас интересуют логические языки вообще, вы также можете посмотреть на язык Карри (там все нечистые функции "приостановлены" до тех пор, пока неопределенное значение не будет унифицировано).
Ответ 2
is/2
является очень низким и ограниченным. Как вы правильно заметили, он не может использоваться во всех направлениях и, следовательно, не является истинным отношением.
Для обратимой арифметики используйте свои решатели ограничений системы Prolog.
Например, SWI-Prolog Руководство пользователя CLP (FD) содержит следующее определение n_factorial/2
:
:- use_module(library(clpfd)).
n_factorial(0, 1).
n_factorial(N, F) :- N #> 0, N1 #= N - 1, F #= N * F1, n_factorial(N1, F1).
Следующие примерные запросы показывают, что их можно использовать во всех направлениях:
?- n_factorial(47, F).
F = 258623241511168180642964355153611979969197632389120000000000 ;
false.
?- n_factorial(N, 1).
N = 0 ;
N = 1 ;
false.
?- n_factorial(N, 3).
false.
Конечно, это определение по-прежнему зависит от унификации, и поэтому вы не можете подключать произвольные целочисленные выражения. Термин вроде 2-2
(который является -(2,2)
в префиксной нотации) не имеет значения с 0
. Но вы можете легко это разрешить, если переписать это:
:- use_module(library(clpfd)).
n_factorial(N, F) :- N #= 0, F #= 1.
n_factorial(N, F) :- N #> 0, N1 #= N - 1, F #= N * F1, n_factorial(N1, F1).
Пример запроса и его результат:
?- n_factorial(2-2, -4+5).
true .
Ответ 3
В этом ответе мы используем clpfd, как и этот предыдущий ответ.
:- use_module(library(clpfd)).
Для легкого сравнения "голова к голове" (далее) мы называем предикат, представленный здесь n_fac/2
:
n_fac(N_expr,F_expr) :-
N #= N_expr, % eval arith expr
F #= F_expr, % eval arith expr
n_facAux(N,F).
Как и в этом предыдущем ответе, n_fac/2
допускает использование арифметических выражений.
n_facAux(0,1). % 0! = 1
n_facAux(1,1). % 1! = 1
n_facAux(2,2). % 2! = 2
n_facAux(N,F) :-
N #> 2,
F #> N, % redundant constraint
% to help `n_fac(N,N)` terminate
n0_n_fac0_fac(3,N,6,F). % general case starts with "3! = 6"
Хелперный предикат n_facAux/2
делегирует любую "реальную" работу на n0_n_fac0_fac/4
:
n0_n_fac0_fac(N ,N,F ,F).
n0_n_fac0_fac(N0,N,F0,F) :-
N0 #< N,
N1 #= N0+1, % count "up", not "down"
F1 #= F0*N1, % calc `1*2*...*N`, not `N*(N-1)*...*2*1`
F1 #=< F, % enforce redundant constraint
n0_n_fac0_fac(N1,N,F1,F).
Сравним n_fac/2
и n_factorial/2
!
?- n_factorial(47,F).
F = 258623241511168180642964355153611979969197632389120000000000
; false.
?- n_fac(47,F).
F = 258623241511168180642964355153611979969197632389120000000000
; false.
?- n_factorial(N,1).
N = 0
; N = 1
; false.
?- n_fac(N,1).
N = 0
; N = 1
; false.
?- member(F,[3,1_000_000]), ( n_factorial(N,F) ; n_fac(N,F) ).
false. % both predicates agree
OK! Идентично, до сих пор... Почему бы вам не попробовать немного грубой силы?
?- time((F1 #\= F2,n_factorial(N,F1),n_fac(N,F2))).
% 57,739,784 inferences, 6.415 CPU in 7.112 seconds (90% CPU, 9001245 Lips)
% Execution Aborted
?- time((F1 #\= F2,n_fac(N,F2),n_factorial(N,F1))).
% 52,815,182 inferences, 5.942 CPU in 6.631 seconds (90% CPU, 8888423 Lips)
% Execution Aborted
?- time((N1 #> 1,N2 #> 1,N1 #\= N2,n_fac(N1,F),n_factorial(N2,F))).
% 99,463,654 inferences, 15.767 CPU in 16.575 seconds (95% CPU, 6308401 Lips)
% Execution Aborted
?- time((N1 #> 1,N2 #> 1,N1 #\= N2,n_factorial(N2,F),n_fac(N1,F))).
% 187,621,733 inferences, 17.192 CPU in 18.232 seconds (94% CPU, 10913552 Lips)
% Execution Aborted
Никаких различий для первых нескольких сотен значений N in 2..sup
... Хорошо!
Перемещение: как насчет следующего (предлагается в комментарии к этому ответу)?
?- n_factorial(N,N), false.
false.
?- n_fac(N,N), false.
false.
Выполнение штрафа! Идентичное поведение завершения... Подробнее?
?- N #< 5, n_factorial(N,_), false.
false.
?- N #< 5, n_fac(N,_), false.
false.
?- F in 10..100, n_factorial(_,F), false.
false.
?- F in 10..100, n_fac(_,F), false.
false.
Хорошо! Все еще идентичные свойства завершения! Пусть рыть немного глубже! Как насчет следующего?
?- F in inf..10, n_factorial(_,F), false.
... % Execution Aborted % does not terminate universally
?- F in inf..10, n_fac(_,F), false.
false. % terminates universally
D'oh! Первый запрос не заканчивается, второй делает.
Какое ускорение!:)
Сделайте некоторые эмпирические измерения времени выполнения!
?- member(Exp,[6,7,8,9]), F #= 10^Exp, time(n_factorial(N,F)) ; true.
% 328,700 inferences, 0.043 CPU in 0.043 seconds (100% CPU, 7660054 Lips)
% 1,027,296 inferences, 0.153 CPU in 0.153 seconds (100% CPU, 6735634 Lips)
% 5,759,864 inferences, 1.967 CPU in 1.967 seconds (100% CPU, 2927658 Lips)
% 22,795,694 inferences, 23.911 CPU in 23.908 seconds (100% CPU, 953351 Lips)
true.
?- member(Exp,[6,7,8,9]), F #= 10^Exp, time(n_fac(N,F)) ; true.
% 1,340 inferences, 0.000 CPU in 0.000 seconds ( 99% CPU, 3793262 Lips)
% 1,479 inferences, 0.000 CPU in 0.000 seconds (100% CPU, 6253673 Lips)
% 1,618 inferences, 0.000 CPU in 0.000 seconds (100% CPU, 5129994 Lips)
% 1,757 inferences, 0.000 CPU in 0.000 seconds (100% CPU, 5044792 Lips)
true.
Вау! Еще несколько?
?- member(U,[10,100,1000]), time((N in 1..U,n_factorial(N,_),false)) ; true.
% 34,511 inferences, 0.004 CPU in 0.004 seconds (100% CPU, 9591041 Lips)
% 3,091,271 inferences, 0.322 CPU in 0.322 seconds (100% CPU, 9589264 Lips)
% 305,413,871 inferences, 90.732 CPU in 90.721 seconds (100% CPU, 3366116 Lips)
true.
?- member(U,[10,100,1000]), time((N in 1..U,n_fac(N,_),false)) ; true.
% 3,729 inferences, 0.001 CPU in 0.001 seconds (100% CPU, 2973653 Lips)
% 36,369 inferences, 0.004 CPU in 0.004 seconds (100% CPU, 10309784 Lips)
% 362,471 inferences, 0.036 CPU in 0.036 seconds (100% CPU, 9979610 Lips)
true.
Нижняя строка?
- Код, представленный в этом ответе, как низкоуровневый, как вы должны: Forget
is/2
!
- Резервные ограничения могут и могут окупиться.
- Порядок арифметических операций (подсчет "вверх" и "вниз" ) тоже может иметь значение.
- Если вы хотите вычислить факториал некоторого "большого"
N
, рассмотрите возможность использования другого подхода.
- Использовать clpfd!
Ответ 4
Есть некоторые вещи, которые вы должны помнить при просмотре Prolog:
-
При вызове предиката не существует неявного возвращаемого значения. Если вы хотите получить значение из звонка, вам нужно добавить дополнительные аргументы, которые можно использовать для "возврата" значений, второй аргумент в вашем предикате f/2
. Хотя он более подробен, он имеет преимущество в том, что легко вернуть многие значения.
-
Это означает, что автоматически "оценка" аргументов в вызове действительно бессмысленна, поскольку нет значения для возврата, и это не делается. Таким образом, нет вложенных вызовов, в этом отношении Prolog является плоским. Поэтому, когда вы вызываете f(A-1, D)
, первым аргументом в f/2
является структура A-1
или действительно -(A, 1)
, поскольку -
является оператором инфикса. Поэтому, если вы хотите получить значение от вызова foo
в вызове bar
, вам нужно явно использовать переменную, чтобы сделать это следующим образом:
foo(..., X), bar(X, ...),
-
Итак, вам нужен специальный предикат, который заставляет арифметическую оценку is/2
. Второй аргумент представляет собой структуру, представляющую арифметическое выражение, которое он интерпретирует, оценивает и унифицирует результат с его первым аргументом, который может быть либо переменным, либо численным значением.
-
В принципе вы можете запускать вещи в обратном направлении с помощью большинства вещей, которые вы не можете сделать. Обычно это только простые предикаты, работающие над структурами, для которых это возможно, хотя есть некоторые очень полезные случаи, где это возможно. is/2
не работает в обратном направлении, это было бы исключительным, если бы это произошло.
Вот почему вам нужны дополнительные переменные C
и D
и не могут заменить C is A-1, f(C, D)
на f(A-1,D)
.
(Да, я знаю, что вы не совершаете звонки в Prolog, но оцениваете цели, но мы начинаем с функциональной точки зрения здесь)