我试图了解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)
方法clojure.lang.Compiler
- 链接到Clojure 1.8的代码.
请注意,出于同样的原因,当前输入(recur)
内置REPL(从1.9.0-alpha14开始)也会无休止地循环.不同的REPL实现可能会或可能不会预先处理输入表单,以防止这种情况发生之前eval
.
recur
这些与Alex Miller在他的回答和评论中所关联的官方文档中所解释的完全相同.总而言之,recur
必须在建立recur
目标的表格内使用; loop
,fn
和reify
(方法实现内部)是这样的形式的所有实施例.
上面的语义是在编译时通过使用少量动态Vars来强制执行的,当编译器下载到为编译传递的顶级表单的各种子表单时,编译器会对其进行适当的绑定.如果你想跟着详细的控制流程,搜索的用途NO_RECUR
,LOOP_LABEL
并LOOP_LOCALS
在Compiler.java.它的要点是,如果表单不在尾部位置,这些Vars将绑定到表示编译时就是这种情况的值.
尽管ClojureScript基于相同的基本思想,但它使用的设置可能更容易理解.请参阅analyzer.clj(使用v1.9标记的稳定链接); 特别是*recur-frames*
和disallowing-recur
.