Clojure: Как вернуться к исключению?

Я пытаюсь выполнить func несколько раз, прежде чем отказаться от исключений. Но это недействительно в Clojure для возврата из блока catch. Как это можно достичь?

(loop [tries 10]
  (try
    (might-throw-exception)
    (catch Exception e
      (when (pos? tries) (recur (dec tries))))))

java.lang.UnsupportedOperationException: Cannot recur from catch/finally 

Лучшее, что я смог найти, это следующее неуклюжие решения (обертывание в func и вызов его)

(defn do-it []
  (try
    (might-throw-exception)
    (catch Exception e nil)))

(loop [times 10]
  (when (and (nil? (do-it)) (pos? times))
    (recur (dec times))))

Ответ 1

Макросы вызывают...

Как насчет этого:

(defn try-times*
  "Executes thunk. If an exception is thrown, will retry. At most n retries
  are done. If still some exception is thrown it is bubbled upwards in
  the call chain."
  [n thunk]
  (loop [n n]
    (if-let [result (try
                      [(thunk)]
                      (catch Exception e
                        (when (zero? n)
                          (throw e))))]
      (result 0)
      (recur (dec n)))))

(defmacro try-times
  "Executes body. If an exception is thrown, will retry. At most n retries
  are done. If still some exception is thrown it is bubbled upwards in
  the call chain."
  [n & body]
  `(try-times* ~n (fn [] [email protected])))

Ответ 2

Идея kotarak - это путь, но этот вопрос щекотал мою фантазию, поэтому я хотел бы предоставить рифф на ту же тему, которую я предпочитаю, потому что она не использует loop/recur:

(defn try-times* [thunk times]
  (let [res (first (drop-while #{::fail}
                               (repeatedly times
                                           #(try (thunk)
                                                 (catch Throwable _ ::fail)))))]
    (when-not (= ::fail res)
      res)))

И оставить макрос try-times как есть.

Если вы хотите разрешить thunk возвращать nil, вы можете отпустить пару let/when и позволить:: fail представлять "сбой функции n раз", в то время как nil означает "возвращаемая функция nil". Такое поведение было бы более гибким, но менее удобным (вызывающий должен проверить, чтобы:: не смог увидеть, работает ли он, а не только нуль), поэтому, возможно, его лучше всего реализовать как необязательный второй параметр:

(defn try-times* [thunk n & fail-value]
  (first (drop-while #{fail-value} ...)))

Ответ 3

Мое предложение:

(defmacro try-times
  "Retries expr for times times,
  then throws exception or returns evaluated value of expr"
  [times & expr]
  `(loop [err# (dec ~times)]
     (let [[result# no-retry#] (try [(do [email protected]) true]
                   (catch Exception e#
                     (when (zero? err#)
                       (throw e#))
                     [nil false]))]
       (if no-retry#
         result#
         (recur (dec err#))))))

Будет напечатан "нет ошибок здесь" один раз:

(try-times 3 (println "no errors here") 42)

Будет напечатано три раза три раза, затем разделите Divide на ноль:

(try-times 3 (println "trying") (/ 1 0))

Ответ 4

Макрос

A try-times является элегантным, но для одноразового просто вытащите when из блока try:

(loop [tries 10]
  (when (try
          (might-throw-exception)
          false ; so 'when' is false, whatever 'might-throw-exception' returned
          (catch Exception e
            (pos? tries)))
    (recur (dec tries))))

Ответ 5

Еще одно решение без макроса

(defn retry [& {:keys [fun waits ex-handler]
                :or   {ex-handler #(log/error (.getMessage %))}}]
  (fn [ctx]
    (loop [[time & rem] waits]
      (let [{:keys [res ex]} (try
                               {:res (fun ctx)}
                               (catch Exception e
                                 (when ex-handler
                                   (ex-handler e))
                                 {:ex e}))]
        (if-not ex
          res
          (do
            (Thread/sleep time)
            (if (seq rem)
              (recur rem)
              (throw ex))))))))