为啥按分号后程序又回到深度递归?
Posted
技术标签:
【中文标题】为啥按分号后程序又回到深度递归?【英文标题】:Why after pressing semicolon program is back in deep recursion?为什么按分号后程序又回到深度递归? 【发布时间】:2020-04-14 18:50:49 【问题描述】:我正在尝试理解分号的功能。
我有这个代码:
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 中使用了内置的图形调试器,因为我想了解它是如何工作的,并且我了解返回参数中给出的列表的第一种情况。这是我为更好理解而制作的图表。
但我不明白另一个解决方案。当我按下分号时,它并没有从它结束的地方开始,而是从L=[]
的一些深度递归开始(就像在步骤 9 中一样)。我不明白,递归不是更早结束了吗?它必须退出递归才能返回答案,并且在分号之后它再次深入递归。
有人可以向我澄清一下吗?提前致谢。
【问题讨论】:
【参考方案1】:我发现在揭开 Prolog 的神秘面纱中很有用的一个类比是 回溯就像嵌套循环,当所有最内层循环的变量值都找到时,循环被暂停,变量值被报告,然后继续循环。
作为一个例子,让我们编写一个简单的生成和测试程序来查找所有大于 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 书籍中的 Prolog in Common Lisp 实现中。不过,他使用映射(Common Lisp 的 mapcan
,即 Haskell 中的 concatMap
或许多其他语言中的 flatMap
)作为循环结构,我花了几年时间才发现 nested loops 是什么真的很重要。
Goals 连接表示为循环的嵌套;目标 disjunction 表示为要循环的替代项。
进一步的转折是嵌套循环的结构从一开始就不是固定的。它是 fluid,给定循环的嵌套循环可以根据该循环的当前状态创建,即取决于在那里探索的当前替代方案; 循环是边写边写的。在(大多数)无法动态创建嵌套循环的语言中,可以使用嵌套递归/函数调用/在循环内对其进行编码。 (这里是one example,带有一些伪代码。)
如果我们将所有这样的循环(为每个备选方案创建)即使在它们完成后也保留在内存中,我们得到的是 AND-OR 树(在另一个答案中提到)因此在搜索空间是正在探索并找到解决方案。
(非巧合的是,这种流动性也是 "monad" 的本质;nondeterminism 由 list monad 建模;而本质list monad 的操作是我们上面看到的 flatMap
操作。使用 fluid structure 循环它是 "Monad"; 使用 固定结构它是“Applicative Functor”;没有结构的简单循环(根本没有嵌套):简单的“Functor” em>(在 Haskell 等中使用的概念)。也有助于揭开这些的神秘面纱。)
因此,正确的口号可能是回溯就像嵌套循环,要么是固定的,从一开始就知道,要么是在我们进行时动态创建的。不过时间有点长。 :)
Here's 也是一个 Prolog 示例,“好像创建了要首先运行的代码(N
嵌套循环,用于给定值 N
),然后运行它。”(在 SO 上甚至还有一个完整的专用标签,事实证明,recursive-backtracking。)
这是一个in Scheme(“创建嵌套循环,解决方案可在最里面的循环体中访问”)和一个C++ example(“创建@987654340 @ 运行时的嵌套循环,实际上是枚举 2n 的二进制编码,并从最里面的循环中打印出总和")。
【讨论】:
回溯就像嵌套循环这个类比只有在嵌套数量固定的情况下才成立。所以对于更复杂的程序,这确实是一种误导。 那么实际的 Prolog(-ish) 实现是怎么写成这样的呢?关键是嵌套结构是not固定的;循环是在我们进行时编写的(嵌套递归可用于(大多数)无法动态编写自己的循环的语言中的实际编码)。这就是我所说的“流动性”的意思,将进行更多编辑以澄清。 ---- 流体结构:单子;固定结构:应用函子。没有嵌套:(简单地)函子(也有助于揭开它们的神秘面纱)。这三个概念都有其用途。 嵌套递归in a loop(一个例子,嵌套的数量是动态的,不是固定的)。 --- 我已经编辑了答案,谢谢。 如果你真的坚持要在 Prolog 中解释 full 机制,你将不得不求助于带有yield
和递归的迭代器。
我在这里做一个类比。【参考方案2】:
函数式/命令式编程语言中的递归与 Prolog 之间存在很大差异(我只是在最近两周左右才真正明白):
在函数式/命令式编程中,您递归调用链,然后返回,展开堆栈,然后输出结果。结束了。
在 Prolog 中,您向下递归 AND-OR tree(实际上是交替的 AND 和 OR 节点),从左到右选择要调用 OR 节点(“选择点”)的谓词,然后依次调用每个谓词在 AND 节点上,也是从左到右。一棵可接受的树在每个 OR 节点下只有一个返回 TRUE 的谓词,并且在每个 AND 节点下所有返回 TRUE 的谓词。一旦构建了可接受的树,通过搜索过程,我们(即“搜索光标”是)位于 最右下端节点。
成功构建可接受的树也意味着已找到在 Prolog 顶层(REPL)输入的查询的解决方案:输出变量值,但 保留树(除非存在没有选择点)。
这也很重要:所有变量都是全局变量,如果一个变量X
一直沿着调用链从谓词到谓词到最右边的最底部节点传递,然后在最后一刻通过将其与 2 统一起来进行约束,例如 X = 2
,然后 Prolog Toplevel 毫不费力地意识到这一点:不需要在调用链上传递任何内容。
如果您现在按;
,搜索不会在树的顶部重新开始,而是在底部,即当前光标位置:要求最近的父 OR 节点提供更多解决方案。这可能会导致大量搜索,直到构建了一个新的可接受的树,我们在一个新的最右边的最底部节点。输出新的变量值,您可以再次输入;
。
此过程循环,直到不再可以构造可接受的树,然后输出false
。
请注意,在运行时将此 AND-OR 作为可检查和可修改的数据结构可以部署一些神奇的技巧。
调试工具肯定有很多功能,它记录了这棵树,以帮助从应该可以工作的 Prolog 程序中获取可怕的 sphynxian false
的用户。毕竟现在有Time Traveling Debuggers 用于函数式和命令式语言...
【讨论】:
以上是关于为啥按分号后程序又回到深度递归?的主要内容,如果未能解决你的问题,请参考以下文章