请在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

被 Timur Latypoff 选择
 
最佳答案

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
"尾位置"是控制流图的一个属性,它可以近似为语法的一个属性。控制流图在字节码级别上是存在的,并且是相同的。"tail position"有点类似Clojure语法级别。

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

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