我目前正在学习编程语言概念和语用学,因此我觉得我需要帮助来区分声明性语言系列的两个子分支。
考虑以下代码片段,分别用Scheme和Prolog编写:
;Scheme
(define gcd
(lambda (a b)
(cond ((= a b) a)
((> a b) (gcd (- a b) b))
(else (gcd (- b a) a)))))
%Prolog
gcd(A, B, G) :- A = B, G = A.
gcd(A, B, G) :- A > B, C is A-B, gcd(C, B, G).
gcd(A, B, G) :- B > A, C is B-A, gcd(C, A, G).
我不明白的是:
这两种不同的编程语言如何表现 不同?
我们在哪里做出改变,以便将它们分类 基于功能或逻辑的编程语言?
就我而言,他们做同样的事情,调用递归函数直到它终止。
答案 0 :(得分:5)
GCD示例仅略微涉及逻辑编程和函数编程之间的差异,因为它们彼此之间的距离比命令式编程更接近。我将专注于Prolog和OCaml,但我相信它具有很强的代表性。
Prolog允许表达部分数据结构,例如在node(24,Left,Right)
这个词中,我们不需要指定Left
和Right
代表什么,它们可能是任何术语。函数式语言可能会插入稍后评估的lazy function或thunk,但在创建该术语时,我们需要知道要插入的内容。
逻辑变量也可以统一(即相等)。 OCaml中的搜索功能可能如下所示:
let rec find v = function
| [] -> false
| x::_ when v = x -> true
| _::xs (* otherwise *) -> find v xs
虽然Prolog实现可以使用统一而不是v=x
:
member_of(X,[X|_]).
member_of(X,[_|Xs]) :-
member_of(X,Xs).
为简单起见,Prolog版本有一些缺点(见下面的回溯)。
Prolog的优势在于连续实例化易于撤消的变量。如果您使用变量尝试上述程序,Prolog将为您返回所有可能的值:
?- member_of(X,[1,2,3,1]).
X = 1 ;
X = 2 ;
X = 3 ;
X = 1 ;
false.
当您需要浏览搜索树时,这是特别方便的,但需要付出代价。如果我们没有指定列表的大小,我们将连续创建满足我们属性的所有列表 - 在这种情况下无限多:
?- member_of(X,Xs).
Xs = [X|_3836] ;
Xs = [_3834, X|_3842] ;
Xs = [_3834, _3840, X|_3848] ;
Xs = [_3834, _3840, _3846, X|_3854] ;
Xs = [_3834, _3840, _3846, _3852, X|_3860] ;
Xs = [_3834, _3840, _3846, _3852, _3858, X|_3866] ;
Xs = [_3834, _3840, _3846, _3852, _3858, _3864, X|_3872]
[etc etc etc]
这意味着您需要更加小心使用Prolog,因为终止更难控制。特别是,做旧的方式(切割操作员!)很难正确使用,并且仍然在讨论最近方法的优点(推迟目标(例如dif),约束算术或一个具体的if)。在函数式编程语言中,回溯通常通过使用堆栈或回溯状态monad来实现。
使用Prolog可能还有一个开胃菜:函数式编程有一个评估方向。我们只能使用find
函数来检查某些v
是否是列表的成员,但我们无法确定哪些列表可以实现此目的。在Prolog中,这是可能的:
?- Xs = [A,B,C], member_of(1,Xs).
Xs = [1, B, C],
A = 1 ;
Xs = [A, 1, C],
B = 1 ;
Xs = [A, B, 1],
C = 1 ;
false.
这些正是包含三个元素的列表,其中包含(至少)一个元素1
。遗憾的是,标准算术谓词不可逆,并且两个数字的GCD始终是唯一的这一事实是您在功能和逻辑编程之间找不到太多差异的原因。
总结:逻辑编程具有变量,允许更容易的模式匹配,可逆性和探索搜索树的多个解决方案。这是以复杂的流量控制为代价的。根据问题,更容易进行回溯执行,有时会受到限制,或者将回溯添加到函数式语言中。
答案 1 :(得分:5)
由于您在逻辑编程版本中使用非常低级谓词,因此您无法轻易看到逻辑编程为您提供的功能编程增加的通用性。
考虑这个稍微编辑的代码版本,它使用 CLP(FD)约束进行低级别的声明性整数算术而不是您目前正在使用的算术:
gcd(A, A, A). gcd(A, B, G) :- A #> B, C #= A - B, gcd(C, B, G). gcd(A, B, G) :- B #> A, C #= B - A, gcd(C, A, G).
重要的是,我们可以将其用作真正的关系,这在所有方向中都有意义。
例如,我们可以问:
是否有两个整数
X
和Y
这样他们的GCD是3?
也就是说,我们可以在其他方向中使用此关系!给定两个整数,我们不仅可以计算他们的GCD。没有!我们也可以使用相同的程序询问:
?- gcd(X, Y, 3). X = Y, Y = 3 ; X = 6, Y = 3 ; X = 9, Y = 3 ; X = 12, Y = 3 ; etc.
我们还可以发布更一般的查询,仍然获取答案:
?- gcd(X, Y, Z). X = Y, Y = Z ; Y = Z, Z#=>X+ -1, 2*Z#=X ; Y = Z, _1712+Z#=X, Z#=>X+ -1, Z#=>_1712+ -1, 2*Z#=_1712 ; etc.
这是一个真正的关系,更通用而不是两个参数的函数!
有关详细信息,请参阅clpfd。
答案 2 :(得分:1)
从一个例子来看差异不是很明显。编程语言根据它们支持的一些特性分为逻辑,功能......,因此它们的设计是为了让每个领域的程序员更容易(逻辑,功能......)。作为示例,命令式编程语言(如c)与面向对象(如java,C ++)非常不同,这里的差异更为明显。
更具体地说,在您的问题中, Prolog 编程语言采用了他的逻辑编程哲学,这对于对数学逻辑有一点了解的人来说是显而易见的。 Prolog有谓词(而不是函数 - 基本上几乎相同),它们根据我们定义的“世界”返回真或假,例如我们已经定义了什么事实和条款,是什么数学事实被定义,更多......所有这些都是由数学逻辑(命题和一阶逻辑)继承的。所以我们可以说Prolog被用作逻辑模型,这使得逻辑问题(如游戏,谜题......)更容易解决。此外,Prolog具有通用语言所具有的一些功能。例如,您可以在示例中编写一个程序来计算gcd:
gcd(A, B, G) :- A = B, G = A.
gcd(A, B, G) :- A > B, C is A-B, gcd(C, B, G).
gcd(A, B, G) :- B > A, C is B-A, gcd(C, A, G).
在你的程序中,如果G与A,B的GCD统一,则使用谓词gcd返回TRUE,并使用多个子句匹配所有情况。当您查询gcd(2,5,1).
时将返回True(请注意,在其他语言中,如shceme,您不能将结果作为参数),而如果您查询gcd(2,5,G).
,它将G与G,A和B统一并返回1,这就像问Prolog什么应该是G gcd(2,5,G).
为真。因此,您可以理解谓词成功的时间,因此您可以拥有多个解决方案,而在函数式编程语言中则不能。
predicate_example(Number,List).
和查询predicate_example(5,List).
,它返回List = ...(列表)并查询
predicate_example(Number,[1,2,3]).
并返回N = ...(数字)。N * N -> R
因此得到属于N(自然数)的A,B参数并返回gcd。但是prolog(程序中有一些更改)可以返回参数A,因此查询gcd(A,5,1).
将给出所有可能的A,使得谓词gcd成功,A = 1,2,3,4,5。所以你可以理解,功能和逻辑语言之间的区别可能并不总是可见,但它们是基于不同的哲学思维方式。
想象一下,解决方案中的井字游戏或N女王问题或人山羊狼白菜问题会有多难。