为什么按分号后程序又回到深度递归?

Why after pressing semicolon program is back in deep recursion?

我正在尝试了解分号功能。

我有这个代码:

del(X,[X|Rest],Rest).
del(X,[Y|Tail],[Y|Rest]) :-
    del(X,Tail,Rest).

permutation([],[]).
permutation(L,[X|P]) :- del(X,L,L1), permutation(L1,P).

这是显示给定列表所有排列的简单谓词。

我在 SWI-Prolog 中使用了内置的图形调试器,因为我想了解它是如何工作的,并且我了解第一种情况 return 参数中给出的列表。这是我为更好地理解而制作的图表。

但我不明白另一个解决方案。当我按下分号时,它并没有从它结束的地方开始,而是从一些深度递归开始,其中 L=[] (就像在第 9 步中一样)。不明白,递归不是提前结束了吗?它必须走出递归 return 的答案,在分号之后它再次深入递归。

有人可以向我澄清一下吗?提前致谢。

functional/imperative 编程语言和 Prolog 中的递归有很大区别(直到最近 2 周左右我才真正清楚):

在 functional/imperative 编程中,您递归调用链,然后返回,展开堆栈,然后输出结果。结束了。

在 Prolog 中,您向下递归 AND-OR tree(实际上,交替的 AND 和 OR 节点),选择一个谓词来调用 OR 节点("choicepoint"),从左到右,并在 AND 节点上依次调用每个谓词,也是从左到右。一棵 可接受的树 在每个 OR 节点下只有一个谓词返回 TRUE,并且所有谓词在每个 AND 节点下都返回 TRUE。一旦构建了可接受的树,通过搜索过程,我们(即 "search cursor" 是)在 最右最底部的节点 上。

成功构建可接受的树也意味着找到了在 Prolog Toplevel(REPL)输入的查询的解决方案:输出变量值,但保留树(除非没有选择点)。

这也很重要:所有变量都是全局变量,因为如果变量 X 从谓词到调用链一路向下传递谓词到最右边的最底部节点,然后在最后可能的时刻通过将其与 2 统一来进行约束,例如 X = 2,那么 Prolog Toplevel 会毫不费力地意识到这一点:不需要向上传递调用链。

如果您现在按 ;,搜索不会在树的顶部重新开始,而是在底部重新开始,即在当前光标位置:要求最近的父 OR 节点以提供更多解决方案。在构建新的可接受树之前,这可能会导致大量搜索,我们处于新的最右最底部节点。输出新的变量值,您可以再次输入 ;.

这个过程循环,直到不能再构造出可接受的树,然后输出false

请注意,将此 AND-OR 作为运行时可检查和可修改的数据结构可以部署一些神奇的技巧。

记录这棵树的调试工具肯定有很多强大的功能,以帮助用户从应该工作的 Prolog 程序中获得可怕的 sphynxian false。现在有 Time Traveling Debuggers 函数式和命令式语言,毕竟...

我发现一个有助于揭开 Prolog 神秘面纱的类比是,回溯就像嵌套循环,当最内层循环的变量值全部找到时,循环暂停,报告 vars 的值,然后恢复循环。

举个例子,让我们写下简单的生成和测试程序,找出所有大于 0 且总和为质数的自然数对。假设 is_prime/1 已经给了我们。

我们在 Prolog 中将其写为

above(0, N), between(1, N, M), Sum is M+N, is_prime(Sum).

我们用命令式伪代码写成

for N from 1 step 1:
  for M from 1 step 1 until N:
     Sum := M+N
     if is_prime(Sum):
        report_to_user_and_ask(Sum)

现在调用 report_to_user_and_ask 时,它会打印 Sum 并询问用户是中止还是继续。循环没有退出,相反,它们只是暂停。因此,所有让我们走到这一步的循环变量值——并且在循环链上可能有更多有时成功有时失败的测试——都被保留下来,即计算状态被保留,并且计算已准备好从中恢复那一点,如果用户按下 ;.

我第一次看到这个是在 Peter Norvig 的 AI 书中,在 Common Lisp 中实现了 Prolog。不过,他使用映射(Common Lisp 的 mapcan,即 Haskell 中的 concatMap 或许多其他语言中的 flatMap)作为循环结构,我花了很多年才看到 嵌套循环才是真正的意义所在。

目标连接表示为循环的嵌套;目标 disjunction 表示为要循环的 alternatives

进一步的扭曲是嵌套循环的结构从一开始就不是固定的。它是 fluid,可以根据该循环的当前状态创建给定循环的嵌套循环,即取决于当前正在探索的替代方案; 我们边写边写循环。在(大多数)不可能动态创建嵌套循环的语言中,可以使用嵌套递归/函数调用/循环内部对其进行编码。 (这里是 ,带有一些伪代码。)

如果我们在内存中保留所有此类循环(为每个备选方案创建),即使在它们完成后,我们得到的是 AND-OR 树(在另一个答案中提到)因此在搜索时创建space 正在探索并找到解决方案。

(非巧合,这种流动性也是"monad"的本质;非确定性list monad; 而 list monad 的基本操作是我们在上面看到的 flatMap 操作。使用 fluid 结构个循环是"Monad"固定结构"Applicative Functor"没有结构的简单循环(根本没有嵌套):简单的"Functor"([=中使用的概念104=] 等)。也有助于揭开它们的神秘面纱。)

因此,正确的口号可以是 回溯就像嵌套循环,要么是固定的,从一开始就知道,要么是在我们进行时动态创建的。不过时间有点长。 :)


Here's also a Prolog example, which "as if creates the code to be run first (N nested loops for a given value of N), and then runs it." (There's even a whole dedicated tag for it on SO, too, it turns out, .)

还有一个 in Scheme ("creates nested loops with the solution being accessible in the innermost loop's body"), and a C++ example ("create n nested loops at run-time, in effect enumerating the binary encoding of 2n, and print the sums out from the innermost loop").