如何在Prolog中计算平均值(2017年)
How to Average in Prolog (2017)

原始链接: https://storytotell.org/how-to-average-in-prolog

这篇文字幽默地批判了学术界学习 Prolog 的典型方法。它认为,讲师们常常劝退学生使用 Prolog 的内置函数,而强调递归,导致代码复杂且低效。 作者将这种方法与一种更实用的方法进行了对比,后者使用了诸如 `findall/3`(用于列表生成)和 `averagelist/2`(用于计算平均值)之类的内置函数。然而,作者讽刺地展示了这些解决方案将如何被拒绝,转而采用手动实现循环和累加(使用递归、冗长的算术运算和额外的谓词)。其目的是模仿过程式代码,并展示手动创建列表的过程,从而强调当被迫避免使用 Prolog 强大的声明式特性时,Prolog 的乏味和晦涩。作者总结道,这种学习 Prolog 的方式往往涉及将过程式循环转换为递归辅助谓词。

这个Hacker News帖子讨论了一篇文章,文章批评了Prolog的一些教学方法,特别是人为的限制(例如禁止使用标准库或强制使用递归),这些限制使得这门语言显得枯燥乏味,并掩盖了其真正的强大之处。评论者分享了他们在入门编程课程中相似的经历,这些课程优先考虑底层实现,而不是高效的解决方案。 一些人认为这些限制是有意为之的,是为了灌输基本原理,即使这感觉像是无用功。另一些人则认为这是有害的,会导致对这门语言的误解和反感。大家普遍认为,有些教师有时无法意识到这些人为的限制并没有帮助,向学生展示高效的策略非常重要。讨论涉及到基础学习和实践应用之间的平衡,一些人还开玩笑地提到了他们在工程项目中遇到的荒谬的限制。

原文

If I had to average a list of numbers, I would probably do it like this:

averagelist(List, Avg) :- 
  length(List, N), sumlist(List, Sum), 
  Avg is Sum / N.

This resembles the actual mathematical definition. Then you could just make a list of numbers and average that. @lurker is right, this is a terrible way to go, but it would work:

average(N, Avg) :- 
  findall(I, between(1, N, I), Is),
  averagelist(Is, Avg).

This is building up abstraction. But of course, this is for a class and the important thing is to not use Prolog or learn declarative programming or solve actual problems but rather to perform meaningless inductive calisthenics to prove you understand recursion. So a better” (i.e. worse but likelier to be accepted by a clueless professor) solution is to take the procedural code:

average(list) ::= 
  sum := 0
  count := 0
  repeat with i ∈ list
    sum := sum + i
    count := count + 1
  return sum / count

and convert it into equivalent Prolog code:

average(List, Result) :- average(List, 0, 0, Result).

average([], Sum, Count, Result) :- Result is Sum / Count.
average([X|Xs], Sum, Count, Result) :- 
  Sum1 is Sum + X,
  succ(Count, Count1),
  average(Xs, Sum1, Count1, Result).

The list result of my findall/3 must be delicately hand-assembled using only tools available in the 18th century lest anyone develop a sense that Prolog can be used effectively in fewer than 40 lines of code:

iota(N, Result)        :- iota(1, N, Result).
iota(X, Y, [X|Result]) :- X < Y, succ(X,X1), iota(X1, Y, Result).
iota(X, X, [X]).

Then you could build averagelist/2 without the taint of library code (of course, you’ll have to write length/2 and sumlist/2, and probably member/2 even though it isn’t used, but just because it’s clever and useful and it sort of seems like it should be in the source file next to all this other stuff we might need), but it would look generally like this:

average(N, Avg) :-
  iota(N, List),
  averagelist(List, Avg).

Now, of course, it will be pointed out that the introduction of additional predicates that are not directly answers to the take home assignment are illegitimate and will be penalized as doing such leads to readability, maintainability, breaking problems down into manageable pieces and other things that are not directly related to the goal of the assignment (to make Prolog appear tedious yet opaque) so we could now look at this and realize that if we want to flatten these two predicates together we ought to be able to by just smushing together their state variables and doing all the work of both, like this:

average(N, Avg) :- average(1, N, 0, 0, Avg).

average(X, Y, Sum, Count, Avg) :-
    X < Y,
    Sum1 is Sum + X,
    succ(Count, Count1),
    succ(X, X1),
    average(X1, Y, Sum1, Count1, Avg).
average(X, X, Sum, Count, Avg) :-
    Sum1 is Sum + X,
    succ(Count, Count1),
    Avg is Sum1 / Count1.

Now this is starting to look like Professor of Programming Languages code! We went from basically four little readable lines to 9 or 10 repetitive lines and a lot of book-keeping and state! I think we’re on the right track now, let’s review how it works:

  1. average/2 is just a call to average/5 with our state initialized (no sum, no count, starting value = 1).
  2. average/5 has two cases: a base case where the count-up-to value and the current-count value are equal, and an inductive case where the current-count is less.
  3. add up the blah blah blah you get the point

The key takeaways here are: 1) Prolog has a terse, high-level, readable and comprehensible standard library, which you are prohibited from using in school, and 2) any procedural loop can be made working Prolog by creating a recursive helper predicate and moving the code around.


Date
June 9, 2017


联系我们 contact @ memedata.com