Prolog谓词自变量:可读性与效率



我想询问谓词参数中不同Prolog表示的优缺点。

例如,在练习4.3中:写一个谓词second(X,List),检查X是否是List的第二个元素解决方案可以是:

second(X,List):- [_,X|_]=List.

或者,

second(X,[_,X|_]).

两个谓词的行为类似。至少对我来说,第一个比第二个更可读。但第二个在执行过程中使用了更多的堆栈(我用trace检查了这一点)。

一个更复杂的例子是练习3.5:二进制树是所有内部节点都有两个子节点的树。最小的二叉树只由一个叶节点组成。我们将把叶子节点表示为叶子(标签)。例如,叶(3)和叶(7)是叶节点,因此是小的二叉树。给定两个二叉树B1和B2,我们可以使用如下的函子树/2将它们组合成一个二叉树树:树(B1,B2)。因此,从叶子叶子(1)和叶子(2),我们可以构建二叉树(叶子(1。从二叉树(叶(1),叶(2))和叶(4。现在,定义一个谓词swap/2,它生成二叉树的镜像,这是它的第一个参数解决方案是:

A2.1:

swap(T1,T2):- T1=tree(leaf(L1),leaf(L2)), T2=tree(leaf(L2),leaf(L1)).
swap(T1,T2):- T1=tree(tree(B1,B2),leaf(L3)), T2=tree(leaf(L3),T3), swap(tree(B1,B2),T3).
swap(T1,T2):- T1=tree(leaf(L1),tree(B2,B3)), T2=tree(T3,leaf(L1)), swap(tree(B2,B3),T3).
swap(T1,T2):- T1=tree(tree(B1,B2),tree(B3,B4)), T2=tree(T4,T3), swap(tree(B1,B2),T3),swap(tree(B3,B4),T4).

或者,

A2.2:

swap(tree(leaf(L1),leaf(L2)), tree(leaf(L2),leaf(L1))).
swap(tree(tree(B1,B2),leaf(L3)), tree(leaf(L3),T3)):- swap(tree(B1,B2),T3).
swap(tree(leaf(L1),tree(B2,B3)), tree(T3,leaf(L1))):- swap(tree(B2,B3),T3).
swap(tree(tree(B1,B2),tree(B3,B4)), tree(T4,T3)):- swap(tree(B1,B2),T3),swap(tree(B3,B4),T4).

第二个解决方案的步骤数比第一个少得多(再次,我使用trace进行了检查)。但关于可读性,我认为第一个会更容易理解。

可读性可能取决于Prolog技术的水平。我是Prolog的学习者,习惯于用C++、Python等进行编程。所以我想知道熟练的Prolog程序员是否同意上述可读性。

此外,我想知道步数是否可以很好地衡量计算效率。

你能给我你的意见或指导原则来设计谓词论点吗?


已编辑。

根据@coder的建议,我制作了第三个版本,其中包含一条规则:

A2.3:

swap(T1,T2):-
( T1=tree(leaf(L1),leaf(L2)), T2=tree(leaf(L2),leaf(L1)) );
( T1=tree(tree(B1,B2),leaf(L3)), T2=tree(leaf(L3),T3), swap(tree(B1,B2),T3) );
( T1=tree(leaf(L1),tree(B2,B3)), T2=tree(T3,leaf(L1)), swap(tree(B2,B3),T3) );
( T1=tree(tree(B1,B2),tree(B3,B4)), T2=tree(T4,T3), swap(tree(B1,B2),T3),swap(tree(B3,B4),T4) ).

我比较了每个解决方案的跟踪中的步骤数:

  • A2.1:36步
  • A2.2:8步
  • A2.3:32步

A2.3(可读单规则版本)似乎比A2.1(可读四规则版本)更好,但A2.2(不可读四规则版)仍然表现出色。

我不确定trace中的步数是否反映了实际的计算效率。A2.2中的步骤较少,但在自变量的模式匹配中使用了更多的计算成本。因此,我比较了40000个查询的执行时间(每个查询都是一个复杂的查询,交换(树(树(树木(树木(树叶(3),树叶(4)),树叶(5))),_))。结果几乎相同(分别为0.954秒、0.944秒和0.960秒)。这表明三个读数A2.1、A2.2和A2.3具有相近的计算效率。你同意这个结果吗?(可能这是一个具体的案例;我需要改变实验设置)。

对于像Stackoverflow这样的论坛来说,这个问题是问题的一个很好的例子。我写答案是因为我觉得你可能会用到一些建议,这也是非常主观的。如果这个问题以"基于观点"的方式结束,我不会感到惊讶。但首先,对演习和解决方案的看法:

列表的第二个元素

毫无疑问,second(X, [_,X|_]).是首选。它只是看起来更熟悉。但无论如何,您都应该使用标准库:nth1(2, List, Element)

镜像二叉树

教科书中建议的树表示有点。。。非正统的二叉树几乎总是被表示为嵌套项,使用两个函子,例如:

  1. t/3,它是一个非空树,具有t(Value_at_node, Left_subtree, Right_subtree)
  2. nil/0为空树

以下是一些二进制树:

  • 空树:nil
  • 包含{1,2,3}:t(2, t(1, nil, nil), t(3, nil, nil))的二进制搜索树
  • 一个包含列表[1,2,3]的退化左倾二叉树(如果您按顺序遍历它):t(1, t(2, t(3, nil, nil), nil), nil)

所以,要"镜像"一棵树,你可以写:

mirror(nil, nil).
mirror(t(X, L, R), t(X, MR, ML)) :-
mirror(L, ML),
mirror(R, MR).

镜像的空树就是空树。镜像的非空树会交换和镜像其左子树和右子树。

仅此而已。不需要交换,真的,或其他任何东西。它也很有效:对于任何参数,都只计算两个子句中的一个,因为第一个参数是不同的函子nil/0t/3(有关此方面的详细信息,请查阅"第一个参数索引")。如果你改为写:

mirror_x(T, MT) :-
(   T = nil
->  MT = nil
;   T = t(X, L, R),
MT = t(X, MR, ML),
mirror_x(L, ML),
mirror_x(R, MR)
).

这不仅可读性较差(嗯…),而且可能效率也较低。

论可读性与效率

代码由人读取,并由机器进行评估。如果你想写可读的代码,你可能仍然想把它交给其他程序员,而不是要评估它的机器。Prolog实现在评估代码方面越来越高效,对读写过大量Prolog代码的人来说也更可读(你知道反馈环吗?)。如果您真的对可读性感兴趣,您可能想看看Prolog的编码指南。

习惯Prolog的第一步是尝试解决99个Prolog问题(还有其他网站也有相同的内容)。按照建议避免使用内置程序。然后,研究解决方案。然后,研究Prolog实现的文档,看看这些问题中有多少是通过内置谓词或标准库解决的。然后,研究实现。你可能会在那里找到一些真正的宝石:我最喜欢的例子之一是nth0/3的库定义。看看这美丽;-)。

还有一整本关于优秀Prolog代码的书:Richard O’Keefe的《Prolog的工艺》。然而,效率测量已经相当过时了。基本上,如果你想知道你的代码有多高效,你最终会得到一个至少有三个维度的矩阵:

  • Prolog实现(SWI-Prolog、SICSTUS、YAP、Gnu-Prolog…)
  • 使用的数据结构和算法
  • 实施提供的设施

你最终会在矩阵中得到一些整体。示例:读取基于行的输入、对每行执行某些操作并输出的最佳方法是什么?逐行阅读,做事情,输出?一次读取所有内容,在内存中执行所有操作,一次输出?使用DCG?在SWI Prolog中,从版本7开始,您可以执行以下操作:

read_string(In_stream, _, Input),
split_string(Input, "n", "", Lines),
maplist(do_x, Lines, Xs),
atomics_to_string(Xs, "n", Output),
format(Out_stream, "~sn", Output)

这是简洁而高效的。注意事项:

  • 可用内存可能是瓶颈
  • 字符串不是标准的Prolog,所以您只能使用具有它们的实现

这是一个非常基本的例子,但它至少证明了在回答您的问题时存在以下困难:

  • 实现之间的差异
  • 对可读或惯用Prolog的看法
  • 关于标准重要性的意见

上面的例子甚至没有详细说明你的问题,比如你对每一行做了什么。只是发短信吗?你需要解析这些行吗?为什么不使用Prolog术语流?等等

效率测量

不要使用跟踪器中的步骤数,甚至不要使用报告的推断数。你真的需要用现实的投入来衡量时间。例如,使用sort/2进行排序总是算作一个推理,无论排序的列表的长度是多少。另一方面,任何Prolog中的sort/2都与您机器上的排序一样高效,所以这是个问题吗?只有在衡量了表现之后,你才能知道。

当然,只要你对算法和数据结构做出明智的选择,你至少可以知道解决方案的复杂性。只有当你注意到你的期望和你的测量之间存在差异时,进行效率测量才是有趣的:显然,这是一个错误。要么你的复杂性分析错误,要么你的实现错误,甚至你正在使用的Prolog实现正在做一些意想不到的事情。

除此之外,还有高级库的固有问题。对于一些更复杂的方法,您可能无法轻松判断给定解决方案的复杂性(约束逻辑编程,如CHR和CLPFD,就是一个典型的例子)。大多数真正适合这种方法的问题都会更容易编写,而且比不需要大量精力和非常具体的代码所能做的更高效。但是要足够花哨,你的CHR程序可能甚至不想再编译了。

谓语首的统一

这不再是基于意见。如果可以的话,只要在头上统一一下就行了。Prolog程序员可读性更强,效率更高。

PS

"立即学习Prolog!"是一个很好的起点,但仅此而已。只要努力通过它并继续前进。

在第一种方式中,例如练习3.5,您使用规则swap(T1,T2)四次,这意味着prolog将检查所有这四个规则,并将为这四个调用中的每一个返回true或fail。因为这些规则不可能全部为true(每次其中一个将返回true),对于每一个输入,您都会浪费三个不会成功的调用(这就是为什么它需要更多的步骤和时间)。在上述情况下,唯一的优点是,通过使用第一种方式进行编写,它更具可读性。通常,当你有这种模式匹配的情况时,最好用一种定义良好的方式编写规则,而不是两个(或多个)规则与一个输入匹配,当然,如果你只需要一个答案,比如上面例子的第二种编写方式。最后,一个要求多个规则与输入匹配的例子是编写输入的谓词成员:

member(H,[H|_]).
member(H,[_|T]):- member(H,T). 

在这种情况下,您需要多个答案。

在第三种方式中,您只需编写没有模式匹配的第一种方式。它的形式为(condition1);...;(condition4),如果condition1不返回true,它将检查下一个条件。大多数情况下,第四个条件返回true,但它调用并测试了返回false的condition1-3。因此,它几乎是编写解决方案的第一种方法,除了在第三个解决方案中,如果它找到true condition1,它将不会测试其他条件,因此您将节省一些浪费的调用(与解决方案1相比)。至于运行时间,预计几乎相同,因为在最坏的情况下,解决方案1和3所做的测试/调用是解决方案2的四倍。因此,如果解决方案2是O(g)复杂性(对于某些函数g),那么解决方案1与3是O(4g)复杂性,因此运行时间将非常接近。

最新更新