Welcome! Please see the About page for a little more info on how this works.

+1 vote
in Clojure by

Currently, it's not possible to recur across a try block:

  (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

So instead on has to rewrite the code as something like this:

  (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))))

— which is pretty unwieldy, especially in the cases of nested retries or more complex state machines.
Alternatively, one could use one of many retry macro libraries — which is a pretty heavy price for such simple use case, I think. And usually much less flexible than what one could do in-place.

"Ideologically", it seems to me that there should be problem there, as the last expression in catch look to me like a tail position.

Is there a technical limitation of the JVM that makes the recur-across-try impossible (or maybe has very low performance, so it's a good idea to discourage that)? Or is it just a low-priority quality-of-life feature, so it never got to an implementation?

1 Answer

+2 votes
by
selected by
 
Best answer

try can have a finally which runs after the catch, so you can't recur from inside a catch as it is not necessarily the tail position.

by
But Alex, as I see it, expressions in `finally` are not in "tail position" (so don't change other places' tail position status), since their return values are ignored, so technically even if `finally` is present, ultimate expressions in each `catch` are in tail positions, as I understand it.

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

^ this evaluates to 2, and 3 in `finally` clause is ignored.

I assume, on the JVM byte-code level (which I am not very familiar with), they are not in tail position — but since JVM doesn't have tail calls anyway, "tail position" is mostly about Clojure-syntax level.
by
I'm sure it is in some cases possible to do so, but this is a particularly complicated part of both the Clojure compiler and the Java bytecode based on how exceptions are translated into bytecode (and that has actually changed since the compiler was written).
...