请在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可以有一个finally块,它会在catch块之后运行,所以不能从catch内部递归,因为它不一定处于尾递归位置。

by
但是,Alex,据我所见,`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”会执行。这就像(let [r e] (do-finally-stuff) r),do-finally-stuff 不是一个尾部,但它的存在意味着 e 也不是一个尾部。

在没有“finally”的情况下,可以认为“catch”是尾部,但我不确定 JVM 字节码验证器是否允许从这里反向分支。
...