我试图理解Clojure对非尾部位置recur
的保护是如何起作用的。
如果编写类似的代码,Clojure会抛出异常:
(def some_var (recur))
但是,如果我评估动态创建的代码呢?
(def code '(recur))
(def some_var (eval code))
如果您尝试在REPL中运行此代码,它似乎无限循环。我希望它能抛出异常。
我的问题:
当Clojure确实检查复发是否处于尾部位置时?
我的第二个代码示例的确切语义是什么(动态执行非尾部位置的重复)?
答案 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.8的clojure.lang.Compiler
方法。
请注意,出于同样的原因,将(recur)
键入当前内置的REPL(从1.9.0-alpha14开始)也会无休止地循环。不同的REPL实现可能会或可能不会预先处理输入表单,以防止这种情况发生,然后再将其移交给eval
。
recur
正如Alex Miller在他的回答和评论中所述的官方文档中所解释的那样。总而言之,必须在建立recur
目标的表单中使用recur
; loop
,fn
和reify
(内部方法实现)都是此类表单的示例。
上述语义是在编译时通过使用少量动态Vars强制执行的,当编译器下载到为编译传递的顶级表单的各种子表单时,编译器会对其进行适当的绑定。如果您想详细了解控制流程,请在Compiler.java中搜索NO_RECUR
,LOOP_LABEL
和LOOP_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