Clojure什么时候检查recur是否出现在尾部位置?

时间:2017-01-15 21:59:12

标签: recursion clojure lisp tail-recursion

我试图理解Clojure对非尾部位置recur的保护是如何起作用的。

如果编写类似的代码,Clojure会抛出异常:

(def some_var (recur))

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

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

如果您尝试在REPL中运行此代码,它似乎无限循环。我希望它能抛出异常。

我的问题:

当Clojure确实检查复发是否处于尾部位置时?

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

3 个答案:

答案 0 :(得分:6)

观察到的行为的说明(无限循环)

您的eval电话实际上会导致编码recur 在尾部位置发生的代码。

这是因为eval的实现方式 - 如果您将表单传递给eval这是一个Clojure持久集合,但它看起来不像def形式,以fn形式包装,fn形式是实际编译的形式,然后调用生成的函数。

以下是您的示例:

(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

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

请注意,出于同样的原因,将(recur)键入当前内置的REPL(从1.9.0-alpha14开始)也会无休止地循环。不同的REPL实现可能会或可能不会预先处理输入表单,以防止这种情况发生,然后再将其移交给eval

recur

的语义

正如Alex Miller在他的回答和评论中所述的官方文档中所解释的那样。总而言之,必须在建立recur目标的表单中使用recur; loopfnreify(内部方法实现)都是此类表单的示例。

如何强制执行语义

上述语义是在编译时通过使用少量动态Vars强制执行的,当编译器下载到为编译传递的顶级表单的各种子表单时,编译器会对其进行适当的绑定。如果您想详细了解控制流程,请在Compiler.java中搜索NO_RECURLOOP_LABELLOOP_LOCALS的用法。它的要点是,如果表单不在尾部位置,这些Vars将绑定到表示编译时就是这种情况的值。

ClojureScript使用的设置可能更容易理解,尽管它基于相同的基本思想。请参阅analyzer.clj(使用v1.9标记的稳定链接);具体为*recur-frames*disallowing-recur

答案 1 :(得分:0)

  

CompilerException java.lang.UnsupportedOperationException:只能从尾部位置重现

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

另外,来自recur的文件(强调补充):

  

recur功能正常,它在尾部位置的使用验证编译器

请注意(技术上),(recur)recur出现在尾部位置的形式,尽管我认为很难说使用recur之外的fn是正确的。一个建立递归点的表单(例如,loop 0-59 * * * * bash /root/scripts/test.sh )。

答案 2 :(得分:-1)

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

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