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

欢迎!请查看关于页面,了解更多关于此平台的信息。

+1
Clojure

目前无法在`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 个答案

+3

选中
 
最佳答案

try 块可以有一个在 catch 块之后执行的 finally 块,所以你无法在 catch 块内部递归,因为它不一定处于尾部位置。

但是,从我来看,`finally` 块中的表达式并不在“尾部位置”(因此不会改变其他地方尾部位置的状态),因为它们的返回值会被忽略,所以从技术上讲,即使有 `finally` 块存在,每个 `catch` 块中的最终表达式都是位于尾部位置的,就像我理解的那样。

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

^ 这会计算为 2,而 `finally` 块中的 3 被忽略。

我假设,在 JVM 字节码级别(我对此不是很熟悉),它们不是在尾部位置——但由于 JVM 没有尾部调用,所以“尾部位置”主要关于 Clojure 文法级别。
我相信在某些情况下这是可能的,但这特别复杂,因为它既涉及 Clojure 编译器,也涉及基于异常如何转换为字节码的 Java 字节码(实际上自从编译器编写以来这一方面已经有所改变)。
"尾部位置"是控制流图的属性,可以近似为语法的属性。控制流图在字节码级别上存在且相同。JVM强制实施一些控制流的公理(具体忘记了,但是分支目标需要始终以相同的抽象堆栈状态跳转)。

控制流图中的“尾部位置”指向某个地方进行控制流转移并返回,然后立即再次返回,尾部调用优化是将这两个返回合并为一个返回。在这两个返回之间执行finally意味着它不再是尾部。finally子句的返回值被忽略,但finally执行了。finally就像(let [r e] (do-finally-stuff) r),do-finally-stuff不是尾部,但是它的存在意味着e也不是尾部。

在没有finally的情况下,可以说catch是尾部,但我不确定JVM字节码验证器是否允许你从这个地方反向分支。
...