Clojure: 如何在异常时进行递归?

44

我试图在遇到异常前多次执行一个函数,但在Clojure中从catch块中进行recur不被认为是有效的。如何实现这一点?

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

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

我能找到的最好解决方案是以下笨拙的方法(将其包装在函数中并调用它)

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

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

47

宏调用...

这样怎么样:

(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 [] ~@body)))

这是一个很好的解决方案。我会将它添加到clojure.contrib或其他地方。 - GabiMe
宏仅仅保存一个#,以供调用者创建thunk。在Clojure中使用宏仅仅为了省略#是惯用的吗? - Dax Fohl
4
为什么你要把 thunk 的结果放在一个向量中?为什么不能直接将其作为“裸值”放置呢? - Christophe De Troyer
4
否则,如果(thunk)返回nil,则会将其视为if-let的错误结果。 - noisesmith
我认为对于一个通用解决方案来说重要的一个特性是“异常过滤器”,这样调用者就可以选择哪些异常会导致重试,哪些应该立即中止(例如,在数据库事务死锁时重试,但在“表不存在”时中止)。 - Andy
显示剩余2条评论

13

kotarak的想法是可行的,但这个问题引起了我的兴趣,我想提供一个同样的主题来改进它,因为它不使用循环/递归:

(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”。这种行为会更加灵活但不太方便(调用者必须检查::fail来确定是否成功,而不能只是检查nil),因此最好实现为可选的第二个参数:

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

1
如果您遇到了Error(Throwable的子类)中的一个,您可能不想重试... - oshyshko

9

一个try-times宏是优雅的,但对于一次性的操作,只需将您的whentry块中取出:

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

4

我的建议:

(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 ~@expr) true]
                   (catch Exception e#
                     (when (zero? err#)
                       (throw e#))
                     [nil false]))]
       (if no-retry#
         result#
         (recur (dec err#))))))

一旦运行,将会打印出“no errors here”:

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

将会打印3次"trying",然后抛出除以零错误:

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

0

还有一种解决方案,不需要宏

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

0
如果您在循环中添加一个名为result的参数,您可以将(try)块嵌套在(recur)中。我是这样解决的:
(loop [result nil tries 10]
  (cond (some? result) result
        (neg? tries) nil
        :else (recur (try (might-throw-exception)
                          (catch Exception e nil))
                     (dec tries))))

0
这样可以捕获多个异常,并提供有关重试原因的一些反馈。
(defmacro try-n-times
  "Try running the body `n` times, catching listed exceptions."
  {:style/indent [2 :form :form [1]]}
  [n exceptions & body]
  `(loop [n# ~n
          causes# []]
     (if (> n# 0)
       (let [result#
             (try
               ~@body
               ~@(map (partial apply list 'catch) exceptions (repeat `(e# e#))))]
         (if (some #(instance? % result#) ~exceptions)
           (recur (dec n#) (conj causes# result#))
           result#))
       (throw (ex-info "Maximum retries exceeded!"
                       {:retries ~n
                        :causes causes#})))))

0

这里是另一种方法:

(loop [tries 10]
  (let [res (try
              (might-throw-exception)
              (catch Exception e
                (if (pos? tries)
                  ::retry
                  (throw e))))]
    (if (#{::retry} res)
      (recur (dec tries))
      res)))

但我也可以推荐一个很酷的小技巧,不要设置重试次数,而是提供一系列休眠时间:

(loop [tries [10 10 100 1000]]
  (let [res (try
              (might-throw-exception)
              (catch Exception e
                (if tries
                  ::retry
                  (throw e))))]
    (if (#{::retry} res)
      (do
        (Thread/sleep (first tries))
        (recur (next tries)))
      res)))

最后,如果你希望更简洁一些,可以将所有内容放入一个宏中。
(defmacro with-retries
  [retries & body]
  `(loop [retries# ~retries]
     (let [res# (try ~@body
                     (catch Exception e#
                       (if retries#
                         'retry#
                         (throw e#))))]
       (if (= 'retry# res#)
         (do (Thread/sleep (first retries#))
             (recur (next retries#)))
         res#))))

(with-retries [10 10 100 1000]
  (might-throw-exception))

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接