16

在阅读 SICP 时,我遇到了逻辑编程第 4.4 章。然后我开始研究 Prolog 编程语言并尝试理解 Prolog 中的一些简单任务。我发现 Prolog 似乎在数值计算方面有问题。

这是标准 Prolog 中阶乘的计算:

f(0, 1).
f(A, B) :- A > 0, C is A-1, f(C, D), B is A*D.

我发现的问题是我需要引入两个辅助变量 (CD)、一个新语法 ( is) 并且问题是不可逆的(即f(5,X)按预期工作,但是f(X,120)没有)。

天真地,我希望至少C is A-1, f(C, D)上面可以替换为f(A-1,D),但即使这样也行不通。

我的问题是:为什么我需要在数值计算中而不是在其他查询中做这些额外的“东西”?

我确实理解(SICP 对此非常清楚)一般而言,关于“做什么”的信息不足以回答“如何做”的问题。因此,(至少一些)数学问题中的陈述性知识不足以实际解决这些问题。但这引出了下一个问题:Prolog 中的这些额外“东西”如何帮助我将公式限制在“做什么”足以回答“如何做”的那些问题上?

4

4 回答 4

9

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 .
于 2010-05-26T00:51:40.070 回答
5

忘记变量,认为Aand B- 只是值的名称,可以放入该子句(X :- Y). 以使其可访问。以表示数学表达式的数据结构的方式思考X = (2 + (3 * 4))。如果您要求 prolog 达到目标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
您可能对有限域求解器感兴趣。有一个关于使用这些东西的问题。通过使用#>/2and#=/2你可以描述一些公式和限制然后解决它们。但是这些谓词使用一些 prolog 系统的特殊功能,允许将名称与某些属性相关联,这可能有助于通过限制的交集来缩小可能值的集合。其他一些系统(通常相同)允许您重新排序处理目标的顺序(“暂停”)。
member(X,[1,2,3,4,5,6,7]), f(X, 120)可能与您的“其他查询”做同样的事情。

如果您一般对逻辑语言感兴趣,您还可以查看 Curry 语言(所有非纯函数都“暂停”,直到统一未定义的值)。

于 2010-05-24T15:11:03.990 回答
5

在这个答案中,我们使用,就像之前的答案一样。

:- 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

好的!相同,到目前为止...为什么不做一点蛮力测试呢?

?- 时间((F1 #\= F2,n_factorial(N, F1 ),n_fac(N, F2 )))。
% 57,739,784 推理,6.415 CPU 在 7.112 秒内(90% CPU,9001245 唇)
% 执行中止
?-时间((F1 #\= F2,n_fac(N, F2 ),n_factorial(N, F1 )))。
% 52,815,182 推理,5.942 CPU 在 6.631 秒内(90% CPU,8888423 唇)
% 执行中止

?- 时间((N1 #> 1,N2 #> 1,N1 #\= N2,n_fac( N1 ,F),n_factorial( N2 ,F)))。
% 99,463,654 推理,15.767 CPU 在 16.575 秒内(95% CPU,6308401 唇)
% 执行中止
?- 时间((N1 #> 1,N2 #> 1,N1 #\= N2,n_factorial( N2 ,F),n_fac( N1 ,F)))。
% 187,621,733 推理,17.192 CPU 在 18.232 秒内(94% CPU,10913552 唇)
% 执行中止

前几百个值没有差异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.

好吧!仍然相同的终止属性!让我们再深入一点!下面的呢?

?- inf..10 中的 F,n_factorial(_,F),错误。
... % Execution Aborted %不会普遍终止
?- inf..10 中的 F,n_fac(_,F),错误。
错误的。% 普遍终止

哦!第一个查询不会终止,第二个会终止。什么加速!:)


让我们做一些经验性的运行时测量!

?- member(Exp,[6,7,8,9]), F #= 10^Exp, time( n_factorial (N,F)) ; 真的。
% 328,700 次推理,0.043 秒内 0.043 CPU(100% CPU,7660054 唇)
% 1,027,296 次推理,0.153 秒内 0.153 CPU(100% CPU,6735634 唇)
% 5,759,864 推理,1.967 CPU 在 1.967 秒内(100% CPU,2927658 唇)
% 22,795,694 推理,23.911 CPU 在 23.908 秒内(100% CPU,953351 唇)
真的。

?- member(Exp,[6,7,8,9]), F #= 10^Exp, time( n_fac (N,F)) ; 真的。
% 1,340 次推理,0.000 CPU 在 0.000 秒内(99% CPU,3793262 唇)
% 1,479 次推理,0.000 CPU 在 0.000 秒内(100% CPU,6253673 唇)
% 1,618 次推理,0.000 CPU 在 0.000 秒内(100% CPU,5129994 唇)
% 1,757 次推理,0.000 秒内 0.000 CPU(100% CPU,5044792 唇)
真的。

哇!多一点?

?- member(U,[10,100,1000]), time((N in 1..U, n_factorial (N,_),false)) ; 真的。
% 34,511 次推理,0.004 秒内 0.004 CPU(100% CPU,9591041 唇)
% 3,091,271 次推论,0.322 秒内 0.322 CPU(100% CPU,9589264 唇)
% 305,413,871 推理,90.732 CPU 在 90.721 秒内(100% CPU,3366116 唇)
真的。

?-成员(U,[10,100,1000]),时间((1..U中的N,n_fac(N,_),假));真的。
% 3,729 次推断,0.001 秒内 0.001 CPU(100% CPU,2973653 唇)
% 36,369 推理,0.004 CPU 在 0.004 秒内(100% CPU,10309784 唇)
% 362,471 推理,0.036 CPU 在 0.036 秒内(100% CPU,9979610 唇)
真的。

底线?

  • 此答案中提供的代码与您应该去的一样低级:忘记is/2
  • 冗余约束可以而且确实有回报。
  • 算术运算的顺序(计数“向上”与“向下”)也可以产生很大的不同。
  • 如果要计算一些“大”的阶乘N,请考虑使用不同的方法
  • 使用
于 2015-09-04T08:42:02.437 回答
3

在查看 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 中进行调用,而是评估目标,但我们是从功能角度开始的)

于 2010-05-26T00:57:17.307 回答