我想在谓词的论证中询问不同Prolog表示的利弊。
例如在Exercise 4.3中:写一个谓词秒(X,List),它检查X是否是List的第二个元素。解决方案可以是:
second(X,List):- [_,X|_]=List.
或者,
second(X,[_,X|_]).
这两个谓词的行为类似。第一个比第二个更可读,至少对我来说。但是第二个在执行期间使用了更多堆栈(我使用 trace 进行了检查)。
更复杂的例子是Exercise 3.5:二叉树是树,其中所有内部节点都有两个子节点。最小的二叉树仅由一个叶节点组成。我们将叶子节点表示为叶子(Label)。例如,叶(3)和叶(7)是叶节点,因此是小二叉树。给定两个二叉树B1和B2,我们可以使用仿函树树/ 2将它们组合成一个二叉树,如下所示:树(B1,B2)。因此,从叶子叶子(1)和叶子(2)我们可以构建二叉树树(叶子(1),叶子(2))。并且从二叉树树(叶子(1),叶子(2))和叶子(4)我们可以构建二叉树树(树(叶子(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) ).
我比较了每个解决方案的 trace 中的步骤数:
A2.3(可读的单规则版本)似乎比A2.1(可读的四规则版本)更好,但A2.2(不可读的四规则版本)仍然优于。
我不确定 trace 中的步数是否反映了实际的计算效率。 A2.2中的步骤较少,但它在参数的模式匹配中使用了更多的计算成本。 因此,我比较了40000个查询的执行时间(每个查询都很复杂, swap(树(树(树(树(叶(3),叶(4)),叶(5)),树(树(树(树(叶(3),叶(4)),叶(5)),叶(4)),叶(5))),树(树(叶(3),树(树(叶(3),叶(4)),叶(5))),树(树(树(树(叶(3),叶(4)),叶(5)),叶(4)),叶( 5)))),_)。)。结果几乎相同(分别为0.954秒,0.944秒,0.960秒)。这表明三个重新表示A2.1,A2.2,A2.3具有接近的计算效率。 你同意这个结果吗? (可能这是一个特定的案例;我需要改变实验设置)。
答案 0 :(得分:4)
这个问题是Stackoverflow等论坛糟糕问题的一个很好的例子。我正在写一个答案,因为我觉得你可能会使用一些建议,这也是非常主观的。如果这个问题以“基于意见为基础”的问题被关闭,我不会感到惊讶。但首先是对练习和解决方案的意见:
当然,second(X, [_,X|_]).
是首选。它看起来更熟悉。但是你应该使用标准库:nth1(2, List, Element)
。
教科书建议的树形象有点......非正统?二叉树几乎总是用嵌套术语表示,使用两个仿函数,例如:
t/3
这是一个非空树,t(Value_at_node, Left_subtree, Right_subtree)
nil/0
这是一棵空树以下是一些二叉树:
nil
t(2, t(1, nil, nil), t(3, nil, nil))
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/0
和t/3
(查找"第一个参数索引&#34 ;有关此内容的更多信息)。如果您愿意写:
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代码的人来说,这些代码也更具可读性(你是否认识到反馈循环?)。如果您对可读性非常感兴趣,可能需要查看Coding Guidelines for Prolog。
习惯Prolog的第一步是尝试解决99 Prolog Problems(还有其他具有相同内容的网站)。按照建议避免使用内置插件。然后,看看解决方案并研究它们。然后,研究Prolog实现的文档,看看有多少这些问题已经通过内置谓词或标准库解决。然后,研究实现。你可能会在那里找到一些真正的宝石:我最喜欢的一个例子是nth0/3
的库定义。看看这个美丽; - )。
还有一本关于Prolog代码主题的全书:" Prolog的工艺"作者:Richard O' Keefe。但效率测量已经过时了。基本上,如果您想知道代码的效率,最终会得到一个至少包含三维的矩阵:
你最终会在矩阵中出现一些整体。示例:读取基于行的输入,对每行执行某些操作并输出它的最佳方法是什么?逐行阅读,做事,输出?一次读取所有内容,在内存中执行所有操作,立即输出?使用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, "~s\n", Output)
这简洁而且非常有效。注意事项:
这是一个非常基本的例子,但它在回答你的问题时至少表现出以下困难:
上面的示例甚至没有详细介绍您的问题,例如您对每一行的处理方式。它只是文字吗?你需要解析线条吗?你为什么不使用Prolog术语来代替?等等。
不要使用跟踪器中的步骤数,甚至是报告的推断数。你真的需要用真实的输入来衡量时间。例如,使用sort/2
进行排序始终只计算一个推理,无论列表的长度是多少。另一方面,任何Prolog中的sort/2
都与您机器上的排序一样高效,那么这是一个问题吗?在衡量绩效之前,你无法知道。
当然,只要您对算法和数据结构做出明智的选择,您至少可以了解解决方案的复杂性。只有当您发现预期与衡量之间存在差异时,才能进行效率测量:显然,存在错误。您的复杂性分析是错误的,或者您的实现是错误的,或者您正在使用的Prolog实现正在做出意想不到的事情。
除此之外,还存在高级库的固有问题。对于一些更复杂的方法,您可能无法轻易判断给定解决方案的复杂程度(约束逻辑编程,如CHR和CLPFD,是一个主要示例)。大多数适合该方法的真正问题将更容易编写,并且比没有相当大的努力和非常具体的代码所做的更有效。但是得到足够的想法,你的CHR程序可能甚至不想再编译。
这不再是基于意见的了。如果可以的话,只要做头脑中的统一。 对Prolog程序员来说更具可读性,效率更高。
"立即学习Prolog!"是一个很好的起点,但仅此而已。只需顺其自然,继续前进。
答案 1 :(得分:1)
在练习3.5的第一种方式中,您使用规则swap(T1,T2)
四次,这意味着prolog将检查所有这四个规则,并且对于这四个调用中的每一个都将返回true或fail.Because不可能一起都是真的(每次其中一个都会返回true),对于每一个输入你浪费三个不会成功的调用(这就是为什么它需要更多的步骤和更多的时间)。在上述情况下,唯一的优点是通过第一种方式书写,它更具可读性。通常,当您遇到模式匹配的情况时,最好以定义良好的方式编写规则,而不是两个(或更多)规则匹配输入,如果您当然只需要一个答案,例如第二种方式写上面的例子。
最后,需要多个规则与输入匹配的一个示例是编写它的谓词成员:
member(H,[H|_]).
member(H,[_|T]):- member(H,T).
在这种情况下,您需要多个答案。
在第三种方式中,您只需编写没有模式匹配的第一种方式。它具有(condition1);...;(condition4)
形式,如果condition1没有返回true,则检查下一个条件。大多数时候第四个条件返回true,但是它已经调用并测试了条件1-3,它返回了false。所以它几乎是编写解决方案的第一种方式,除了在第三种解决方案中如果它找到真正的条件1,它将不会测试其他条件,因此你将节省一些浪费调用(与solution1相比)。
至于运行时间,预计几乎是相同的,因为在最坏的情况下,解决方案1和3执行解决方案2所做的测试/调用的四倍。因此,如果解决方案2是O(g)复杂度(对于某些函数g),然后溶液1和3是O(4g),这是O(g)的复杂性,因此运行时间非常接近。