44 votes

Clojure : Comment récidiver en cas d'exception ?

J'essaie d'exécuter une fonction plusieurs fois avant d'abandonner sur des exceptions. Mais il n'est pas valable en Clojure de récurrer à partir du bloc catch. Comment cela peut-il être réalisé ?

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

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

Le mieux que j'ai pu trouver est la solution maladroite suivante (envelopper dans un func et l'appeler)

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

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

47voto

kotarak Points 11177

Les macros appellent...

Que dites-vous de ça ?

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

0 votes

C'est une bonne solution. Je l'ajouterais à clojure.contrib ou autre.

0 votes

C'est en fait la même solution que celle proposée par l'affiche. Mais les macros rendent la chose plus facile à faire dans le cas général. Les macros sont la caractéristique principale de toute variante de Lisp.

0 votes

Ce n'est pas exactement la même solution. La suggestion de l'auteur de l'article ne prend pas en compte la valeur de retour du bloc, et si c'était le cas, le bloc ne pourrait pas retourner nil. De plus, les exceptions sont avalées. Mais vous avez raison : c'est fondamentalement la même idée. Les macros ne font que masquer le texte passe-partout.

13voto

amalloy Points 29125

L'idée de kotarak est la meilleure, mais cette question m'a chatouillé l'esprit et j'aimerais donc proposer un riff sur le même thème que je préfère parce qu'il n'utilise pas de boucle/récurrence :

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

Et laissez la macro des essais telle qu'elle est.

Si vous voulez permettre au thunk de retourner nil, vous pouvez laisser tomber la paire let/when, et laisser ::fail représenter "la fonction a échoué n fois", alors que nil signifie "la fonction a retourné nil". Ce comportement serait plus flexible mais moins pratique (l'appelant doit vérifier ::fail pour voir si la fonction a fonctionné plutôt que de retourner nil), donc peut-être serait-il préférable de l'implémenter comme un second paramètre optionnel :

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

1 votes

Probablement, vous ne voulez pas réessayer si vous obtenez une erreur (descendant de Throwable)...

9voto

Eugene Beresovksy Points 3852

A try-times est élégante, mais pour un usage ponctuel, il suffit de tirer votre when de la try bloc :

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

4voto

mlatu Points 857

Ma proposition :

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

Imprime une fois "aucune erreur ici" :

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

Imprime 3 fois "essayer", puis lance Diviser par zéro :

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

0voto

piercebot Points 1211

Si vous ajoutez un result à votre boucle, vous pouvez imbriquer l'argument (try) à l'intérieur du bloc (recur) . J'ai résolu le problème comme suit :

(loop [result nil tries 10]
  (cond (some? result) result
        (neg? tries) nil
        :else (recur (try (might-throw-exception)
                          (catch Exception e nil))
                     (dec tries))))

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X