我在试着理解Clojure在非尾部位置对recur
的保护是如何工作的。
如果编写了这样的代码,Clojure将引发异常:
(def some_var (recur))
但是如果我评估动态创建的代码呢?
(def code '(recur))
(def some_var (eval code))
如果您尝试在REPL中运行此代码,它似乎会无限循环我以为它会抛出一个例外。
我的问题:
什么时候Clojure检查一个复发者是否处于尾部位置?
我的第二个代码示例的确切语义是什么(在非尾部位置动态执行的递归)?
最佳答案
对观察到的行为的解释(无限循环)
您的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.8的clojure.lang.Compiler
方法。请注意,出于同样的原因,在内置REPL current中键入
(recur)
(从1.9.0-alpha14开始)也会无休止地循环不同的REPL实现可以或者不可以在将输入表单传递给eval
之前以防止这种情况的方式对其进行预处理。recur
的语义这些正是亚历克斯·米勒在其回答和评论中所链接的官方文件中所解释的总而言之,
recur
必须用于建立recur
目标的表单中;loop
、fn
和reify
(方法实现内部)都是此类表单的示例。如何执行语义
上述语义在编译时通过使用少数动态变量来实现,编译器在将这些动态变量下移到传递给编译的顶级表单的各个子表单中时会适当绑定这些动态变量如果要详细遵循控制流,请在Compiler.java中搜索
NO_RECUR
、LOOP_LABEL
和LOOP_LOCALS
的用法其要点是,如果表单不在尾部位置,则这些变量将绑定到值,这些值指示在编译表单时是这种情况。ClojureScript使用的设置可能更容易遵循,尽管它基于相同的基本思想参见analyzer.clj(使用v1.9标签的稳定链接);特别是
*recur-frames*
和disallowing-recur
。