【问题标题】:When does Clojure check if recur appears in tail position?Clojure 何时检查 recur 是否出现在尾部位置?
【发布时间】:2017-01-15 21:59:12
【问题描述】:

我试图了解 Clojure 在非尾部位置对 recur 的保护是如何工作的。

如果编写这样的代码,Clojure 会抛出异常:

(def some_var (recur))

但是如果我评估动态创建的代码呢?

(def code '(recur))
(def some_var (eval code))

如果您尝试在 REPL 中运行此代码,它似乎会无限循环。我预计它会引发异常。

我的问题:

Clojure 究竟什么时候检查一个递归是否在尾部位置?

我的第二个代码示例的确切语义是什么(动态执行的非尾部位置的递归)?

【问题讨论】:

  • 在您使用eval 的代码中,不会出现任何词法变量,包括recur。你得到的错误清楚地表明它发生在编译时。
  • 感谢您的回复!我是否正确理解这一点:如果我 eval 代码它会得到它自己的范围,其中 recur 没有定义?
  • 不完全是.. eval 从不使用代码的词法范围。因此(let [x 19] (eval 'x)) 将评估为全局变量x 或抛出x 未绑定的错误。因此,您甚至不能从eval 中调用本地函数,因为绑定在它被评估的环境中不存在。对于其他词法范围的 lisp 语言也是如此。
  • 啊,我明白了,谢谢!那么我的 eval 所做的是在全球环境中评估 recur 吗?你知道这样做的语义是否在某处定义?
  • @Sylwester, recur 是一种特殊形式,所以我认为这与词法绑定无关。

标签: recursion clojure lisp tail-recursion


【解决方案1】:

观察到的行为解释(无限循环)

您的eval 调用实际上会编译代码,其中recur 确实 出现在尾部位置。

这是因为eval 是如何实现的——如果你将一个表单传递给eval,它是一个Clojure 持久集合,但它看起来不像def 表单,它会被包装在一个@987654328 中@form,那个fnform就是实际编译出来的,然后调用生成的函数。

这是如何应用于您的示例的:

(eval '(recur))

;; does '(recur) look like a def form?
;; → no, so transform the above, in effect, to
((eval '(fn [] (recur)))

;; more precisely, before handing off `'(recur)` to lower-level
;; compilation methods, wrap it in `(fn [] …)`:
(fn [] (recur))

;; then immediately call the resulting function with no arguments

;; ultimate result: loop endlessly

如果您想知道发生这种情况的位置,请查看 clojure.lang.Compilerpublic static Object eval(Object form, boolean freshLoader) 方法 - link to the code as of Clojure 1.8

请注意,出于同样的原因,当前(从 1.9.0-alpha14 开始)在内置 REPL 中键入 (recur) 也会无限循环。不同的 REPL 实现可能会或可能不会在将输入表单交给eval 之前以防止这种情况的方式预处理输入表单。

recur 的语义

这些与 Alex Miller 链接到的官方文档、他的答案和 cmets 中的解释完全相同。总而言之,recur 必须在建立recur 目标的表单内使用; loopfnreify(内部方法实现)都是这种形式的示例。

如何执行语义

上述语义在编译时通过使用少量动态变量来强制执行,编译器会在它下降到传递给编译的顶级表单的各种子表单时适当地绑定这些变量。如果要详细了解控制流程,请在 Compiler.java 中搜索NO_RECURLOOP_LABELLOOP_LOCALS 的用法。它的要点是,如果一个表单不在尾部位置,这些变量将被绑定到表明在编译时是这种情况的值。

ClojureScript 使用的设置可能更容易遵循,尽管它基于相同的基本思想。见analyzer.clj(使用v1.9标签的稳定链接);特别是*recur-frames*disallowing-recur

【讨论】:

  • @le_me 干杯,不用担心。
【解决方案2】:

CompilerException java.lang.UnsupportedOperationException: Can only recur from tail position

CompilerException 表示在编译表单时抛出异常(并因此执行检查)。在eval 的情况下,表单在评估之前立即编译。

另外,来自recur 的文档(已添加重点):

recur 是功能性的,它在尾部位置的使用编译器验证。

请注意(从技术上讲)(recur)recur 出现在尾部位置的一种形式,尽管我认为很难说在建立递归的形式之外使用 recur 是正确的点(例如,fnloop)。

【讨论】:

  • 回复:“虽然我认为很难说在 fn 或循环表单之外使用 recur 是正确的” – recur 必须在建立 recur 的表单内使用目标,但是 fnloop 而不是唯一的此类形式;例如,您可以将recur 放在reify 实现的方法的头部(这在reify 的文档字符串中记录)。
  • 感谢您的回答!我没有接受你的,因为 Michal 的回答更详细。
  • @MichałMarczyk,我试图通过挑出fnloop 来保持简单。但是,是的,还有其他形式可以建立递归点。但是,文档没有明确禁止在建立递归点的任何形式之外使用recur。因此,尽管推断顶层的(recur) 应该被拒绝是合理的(如果不是在编译时,至少在运行时),但该行为在技术上是未指定的。这就是我试图用那句话表达的观点。
  • @NathanDavis 当然,这很公平。我只是为了完整性而发表评论;您的编辑介绍了“例如”之前发生过,我不会评论。至于文档,我同意有人可以合理地争辩说,没有递归点的recur 具有未定义的行为,尽管预先假定存在递归点的措辞(“递归点的绑定”,“执行然后跳回到递归点”等)表明这可能是无意的。注意。在这里,这实际上是观察到的行为比替代方案更好的理由。
  • 我倾向于将定冠词的无限制使用作为一个相当强烈的指示,即没有递归点的recur 将被视为错误,更重要的是在更高级别上该部分的措辞似乎奇怪,除非有人做出这个假设(没有明确否认)。当然,通过为此效果添加显式声明来消除任何剩余的歧义不会有什么坏处,而且最好在(eval '(recur))(和(recur)clojure.main REPL)上抛出。 (实际上,这样做可能比触摸recur 机制更容易……)
【解决方案3】:

recur 是编译器可以理解的特殊形式。在尾部位置以外的任何位置重复出现都是错误(这就是语义)。

此处记录了更多详细信息:https://clojure.org/reference/special_forms#recur

【讨论】:

  • 问题不在于recur 的语义。它是关于何时发生尾部位置检查。
  • 编译器在遇到递归时会检查它是否处于尾部位置。根据上下文,该用法要么处于尾部位置,要么出现错误。有一些与 try/finally 相关的棘手案例(因为 finally 块在 try 主体之后调用)。您的最后一个问题是“... recur in ... 的语义是什么”?
  • 嗯,我阅读问题的方式是专门询问问题中给出的示例中使用的recur 的语义,即(eval '(recur))。换句话说,recurfn / loop 表单之外使用时的语义是什么?我在文档中的任何地方都没有看到这一点。
  • @le_me Alex 为您提供了官方文档中包含两个问题答案的两个段落的链接。问题 1:“Clojure 究竟何时检查重复是否处于尾部位置?”;答案 1(我从链接到的页面引用):“recur 是功能性的,它在尾部位置的使用由编译器验证”。问题 2:“我的第二个代码示例的确切语义是什么(动态执行的非尾部位置的递归)?”;回答 2(我再次引用同一页):“在尾部位置以外重复出现错误”。
  • 现在,链接到的页面实际上并没有说明recur 必须出现在fnloop 表单中,但那是因为情况并非如此——例如reify 形式的协议/接口方法实现也构成recur 目标(这在reify 的文档字符串中记录)。不确定是否有所有可能的内置 recur 目标的详尽列表。
猜你喜欢
  • 2012-03-01
  • 1970-01-01
  • 2016-08-25
  • 2012-06-07
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-04-09
  • 1970-01-01
相关资源
最近更新 更多