请在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`中的最后一个表达式似乎是一个尾部表达式。

是否有技术限制使穿越`try`的`recur`变得不可能(或者可能性能非常低,因此认为这是阻止它的好主意)?或者这只是低优先级的特性,因此从未实现过?

1 个回答

+3

选定
 
最佳答案

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字节码验证器是否允许你从这里向后分支。
...