May 2016, Clojure Ireland Meetup
(defn check-for-lucky-number [n]
(if (#{4 14 24} n)
n
(throw (ex-info "unlucky number" {:number n}))))
(try
(mapv check-for-lucky-number (range 20))
(catch RuntimeException e
"Not much we can do to recover here"))
Higher level functions specify what action to take in response to an error, the action implementation is provided by the calling function.
Errors are handled when and where they occur rather than catching an exception far away from the origin where we can't to do anything about it.
(ns error-handling-test.condition-system
(:require [slingshot.core :refer [throw+]]
[swell.api :refer [
restart-case
handler-bind
invoke-restart]]))
(defn check-for-lucky-number [n]
(if (#{4 14 24} n)
n
(restart-case
[
:try-again (fn [value]
(println "trying again with "value)
(check-for-lucky-number value))
:treat-as-lucky (fn []
(println "treating the value as lucky")
n)
:skip (fn []
(println "Returning dash instead")
"-")
]
(throw+ {:type ::unlucky-number :number n}
"Landed on unlucky number"))
))
(defn lucky-number? [n]
(handler-bind
[#(= ::unlucky-number (:type %))
(fn [e]
;;choose the restart appropriate for the error
;; could also rethrow error
(invoke-restart
:try-again (rand-int 10)
))]
(if (check-for-lucky-number n) "YES")))
;;Instead of an error being raised the
;;check-for-lucky-number function is called
;;with different values until it finds a lucky number
(lucky-number? 16)
;;Without binding a restart all we just know
;;something went wrong
(try
(mapv check-for-lucky-number (range 20))
(catch RuntimeException e
"Unlucky number in the sequence"))
;;Using restarts we can handle the error
;;appropriately at the point it occurred
(defn lucky-numbers? []
(handler-bind
[#(= ::unlucky-number (:type %))
(fn [e]
(invoke-restart
:skip
))]
(mapv check-for-lucky-number (range 20))))
(lucky-numbers?)
;; ==== errors ===
(defn ^:dynamic *unlucky-number-error* [msg info]
(throw (ex-info msg info)))
;; === restart ===
(defn ^:dynamic *treat-as-lucky* [value]
(throw (ex-info "Restart *treat-as-lucky* is unbound.")))
(defn ^:dynamic *try-again* [value]
(throw (ex-info "Restart *try-again* is unbound.")))
(defn ^:dynamic *skip* [value]
(throw (ex-info "Restart *skip* is unbound.")))
(defn check-for-lucky-number [n]
(if (#{4 13 14 24} n)
n
(binding [*treat-as-lucky* identity
*try-again* (fn [value]
(println "trying again with "value)
(check-for-lucky-number value))
*skip* (fn [] "-")]
(*unlucky-number-error*
"Landed on unlucky number"
{:number n}))))
(defn am-I-lucky? [n]
;;outer function chooses what restart to call based on the error
(binding [*unlucky-number-error*
(fn [msg info]
(*try-again* (rand-int 10)))]
(if (check-for-lucky-number n) "YES")))
(am-I-lucky? 41)
;;Without binding a restart all we just know something went wrong
(try
(mapv check-for-lucky-number (range 20))
(catch RuntimeException e
"Unlucky number in the sequence"))
;;Using restarts we can handle the error
;;appropriately at the point it occurred
(defn lucky-numbers? []
(binding [*unlucky-number-error*
(fn [msg info]
(*skip*))]
(mapv check-for-lucky-number (range 20))))
(lucky-numbers?)
Exceptions are side effects. Makes it difficult to compose functions when you have a backchannel result. Using special return values makes the error conditions we expect to handle more explicit.
(ns error-handling-test.monads
(require [cats.core :as m])
(require [cats.builtin])
(require [cats.monad.either :as either])
(require [cats.monad.maybe :as maybe]))
(defn value-set [value]
(if (nil? value)
(either/left "Required value")
(either/right value)))
(defn valid-email [value]
(if (re-matches #"\S+@\S+\.\S+" value)
(either/right value)
(either/left "invalid email")))
(defn valid-zip-code [zipCode]
(if (re-matches #"\d{5}" zipCode)
(either/right zipCode)
(either/left "invalid zip code")))
(let [contact {:name "batman"
:email "batman99@gmail.com"
:zipCode "12345"}]
(pr-str
(m/mlet [ valueSet (value-set (:name contact))
email (valid-email (:email contact))
zip (valid-zip-code (:zipCode contact))]
contact)))
(let [contact {:name nil
:email "batman99@gmail.com"
:zipCode "12345678"}]
(pr-str
(m/mlet [ valueSet (value-set (:name contact))
email (valid-email (:email contact))
zip (valid-zip-code (:zipCode contact))]
contact)))
(ns error-handling-test.monads
(require [cats.core :as m])
(require [cats.builtin])
(require [cats.applicative.validation :as v]))
(defn value-set [value]
(if (nil? value)
(v/fail {:required "Required value"})
(v/ok value)))
(defn valid-email [value]
(if (re-matches #"\S+@\S+\.\S+" value)
(v/ok value)
(v/fail {:email "invalid email"})))
(defn valid-zip-code [zipCode]
(if (re-matches #"\d{5}" zipCode)
(v/ok zipCode)
(v/fail {:zipCode "invalid zip code"})))
(let [contact {:name "batman"
:email "batman99@gmail.com"
:zipCode "12345"}]
(pr-str
(m/alet [ valueSet (value-set (:name contact))
email (valid-email (:email contact))
zip (valid-zip-code (:zipCode contact))]
contact)))
;;unlike Either, aggregates failure values
(let [contact {:name nil
:email "batman99@gmail.com"
:zipCode "123456"}]
(pr-str
(m/alet [ valueSet (value-set (:name contact))
email (valid-email (:email contact))
zip (valid-zip-code (:zipCode contact))]
contact)))
Questions?