Skip to content

Instantly share code, notes, and snippets.

@ericnormand
Last active September 24, 2019 14:37
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ericnormand/0a95751f1cf39c1b0f12b1d181b9e49d to your computer and use it in GitHub Desktop.
Save ericnormand/0a95751f1cf39c1b0f12b1d181b9e49d to your computer and use it in GitHub Desktop.

retry three times

One of the beautiful things about functional programming is higher-order functions. We write functions that operate on functions. This lets us pass around code to run later, bundled up as a function.

In last week's challenge, we saw how we could wrap a function in another to make it idempotent. This week, we will make a function that retries another function three times, or until it succeeds.

But first, what does it mean to fail? On the JVM, failure is commonly represented with a thrown exception. So, your task is to write a function that will call its argument. If it throws an exception, it tries again, up to three times total.

(defn retriably
([f]
(retriably f 2))
([f attempts]
(fn [& args]
(loop [n attempts]
(let [result (try
(apply f args)
(catch Throwable t t))]
(if (instance? Throwable result)
(if (zero? n)
(throw result)
(recur (dec n)))
result))))))
(comment
(defn risky-greeting [name]
(if (< (Math/random) 0.5)
(throw (ex-info "Awkward interaction" {:name name}))
(str "Hello, " name)))
(def brave-greeting (retriably risky-greeting))
(brave-greeting "Dan"))
(ns clojure-experiments.purely-functional.puzzles.0344-retry
"https://purelyfunctional.tv/issues/purelyfunctional-tv-newsletter-344-tip-thank-a-clojure-oss-dev-today/")
;;; Write a functiont that will re-try given function 3 times (retry when the fn throws an exception)
;;; and re-throws if no success after that.
(defn retry
([f] (retry 3 f))
([n f]
(fn [& args]
(loop [i 0]
(let [[result exception] (try
[(apply f args)]
(catch Exception e
;; here we could just 'recur' or throw but cannot do that (compiler error)
(println "WARN: got an exception when trying to call f with given args. " (.getMessage e))
[::error e]))]
(if (= ::error result)
(if (< i n)
(recur (inc i))
(do (println "ERROR: max retries " n " reached!")
(throw exception)))
result))))))
;; throws ArithmeticException
#_((retry /) 1 0)
;; success case
((retry /) 10 2)
;; => 5
;; eventually succeeds
((retry (let [counter (atom 1)
max-failures 3] ;; change this to 4 or more to get ArithmeticException
(fn div-failing-only-2-times [& args]
(try
(apply / args)
(catch Exception e
(if (> @counter max-failures)
(do (println "I'm not gonna failed anymore")
:sentinel)
(do
(swap! counter inc)
(throw e))))))))
1 0)
;; => :sentinel
(defn t3
"try f up to 3 times on failures"
[f]
(let [callst (atom {})]
(loop [a 2] ; 2 retries
(try (f)
(prn "success")
(swap! callst assoc :status :success)
(catch Exception e (swap! callst assoc :status :fail)))
(if (or (= a 0) (= (:status @callst) :success))
(prn (str "done, with status " (:status @callst)))
(do
(prn (str "fail #" (- 3 a)))
(recur (dec a)))))
(:status @callst)))
;; test function generator
(defn works-eventually
"return a function that fails until the nth try"
[n]
(let [fatom (atom n)]
(fn []
(swap! fatom dec)
(if (= 0 @fatom) 0 (/ 1 0)))))
;; test runs
(t3 (works-eventually 2)) ;; works on 2nd try
;; :success
;; trace
"fail #1"
"success"
"done, with status :success"
(t3 (works-eventually 3)) ;; works on 3rd try
;; :success
;; trace
"fail #1"
"fail #2"
"success"
"done, with status :success"
(t3 (works-eventually 4)) ;; would have worked on 4th try but doesn't get there
;; :fail
;; trace
"fail #1"
"fail #2"
"done, with status :fail"
(defn retry [f]
(loop [n 0]
(let [[ret e] (try
[(f) nil]
(catch Throwable e
[nil e]))]
(if e
(if (< n 3)
(recur (inc n))
(throw e))
ret))))
(defn flakey []
(if (< (Math/random) 0.3)
"yay"
(throw (RuntimeException. "oh no"))))
(retry flakey)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment