请在2024年度Clojure调查!中分享您的想法。

欢迎!请在关于页面了解更多关于这个网站的信息。

+1

目前,无法在`try`块跨过执行recur

  (let [RETRIABLE-FUNCTION (fn [] (println "Function called") (throw (ex-info "Function failed" {})))]
    (loop [retries 3]
      (try
        (RETRIABLE-FUNCTION)
        (catch Exception ex
          (when (< 1 retries)
            (recur (dec retries)))))))

Syntax error (UnsupportedOperationException) compiling recur at (src/repl.clj:7:13). Can only recur from tail position

因此,人们不得不将代码重写为类似于以下内容

  (let [RETRIABLE-FUNCTION (fn [] (println "Function called") (throw (ex-info "Function failed" {})))]
    (loop [retries 3]
      (let [result (try
                     (RETRIABLE-FUNCTION)
                     (catch Exception ex
                       (when (< 1 retries)
                         ::retry)))]
        (if (= ::retry result)
          (recur (dec retries))
          result))))

这种做法相当麻烦,尤其是在嵌套重试或更复杂的状态机的情况下。
或者,可以使用许多重试宏库中的一个——我认为对于这样简单的用例来说,这是一个相当大的代价。而且通常比就地操作要少很多灵活性。

在理念上,我觉得这里应该有问题,因为`catch`块中的最后一个表达式看起来像是一个尾递归。

这是JVM的技术限制使得跨`try`执行`recur`变得不可能(或者可能是性能非常低,所以应该劝阻这样做)吗?还是它只是一个低优先级的改进用户体验的功能,所以从来没有人实现过?

1 个回答

 
最佳回答

try块可以有一个finally子句,它在catch子句之后运行,所以您不能在catch块内部递归,因为这不一定在尾部位置。

by
但如您所见,`finally`中的表达式不在“尾部位置”(因此不要改变其他地方的尾部位置状态),因为它们的返回值被忽略,所以从技术上讲,即使存在`finally`,每个`catch`中的最终表达式仍在尾部位置,据我所知。

    (try
      (throw (ex-info "Exception" {}))
      1
      (catch Exception _ 2)
      (finally 3))

^ 此处的评估结果为2,`finally`子句中的3被忽略。

我假设,在JVM字节码级别(我对这个不太熟悉),它们不在尾部位置——但既然JVM没有尾部调用,"尾部位置"主要与Clojure语法级别有关。
by
我相信在某些情况下这是可能的,但这是Clojure编译器和基于如何将异常转换为字节码的Java字节码的特别复杂部分。
by
"尾部位置"是控制流图的一个属性,可以近似为语法的属性。控制流图在字节码级别上存在且相同。JVM强制执行一些控制流的不变量(我记不清楚具体是什么,但例如分支目标需要始终以相同的抽象堆栈状态跳转)。

控制流图中的"尾部位置"是指将控制流转移到某个地方,然后返回,然后立即再次返回。尾部调用优化是将这些双重返回合并成一个返回。在这两次返回之间有一个finally子句来执行意味着它不再是尾部。finally子句的返回值被忽略,但finally本身不会被忽略,它会被执行。finally就像(let [r e] (do-finally-stuff) r),do-finally-stuff不是一个尾部,但其存在意味着e也不是尾部。

在没有finally的情况中,你可以认为catch是尾部,但我不确定JVM字节码验证器是否允许你从这里回跳。
...