When we know that a function might (or might not) throw an exception we need to prepare for the error condition in advance. Clojure inter-operates with Java and supports catching and throwing of exceptions:
(try .. (catch FooException e ..))
However, in practice we often need to catch an exception and store it temporarily and probably look for few more such error conditions so that we can treat them collectively. For an example, validating user input with multiple data elements. Trying to scale try-catch to such scenarios is hard and clunky, so let's try an alternative approach - non-breaking error handling.
(defmacro maybe "Assuming that the body of code returns X, this macro returns [X nil] in the case of no error and [nil E] in event of an exception object E." [& body] `(try [(do ~@body) nil] (catch Exception e# [nil e#])))
Let's see how to use it:
;; a function that might throw IllegalArgumentException as per input (defn valid-name "Check if name is valid non-empty string and return it, throw exception otherwise." [name] (let [iarg #(throw (IllegalArgumentException. "Bad name"))] (if (not (string? name)) (iarg)) (let [vname (clojure.string/trim name)] (if (empty? vname) (iarg)) vname))) (defn say-hello [name] (let [[vname error] (maybe (valid-name name))] (if error (println "Error: " (.getMessage error)) (println "Hello " vname))))
Now trying them out:
user=> (valid-name "John") "John" user=> (maybe (valid-name "John")) ["John" nil] user=> (say-hello "John") Hello John nil user=> (valid-name "") java.lang.IllegalArgumentException: Bad name (NO_SOURCE_FILE:0) user=> (maybe (valid-name "")) [nil #<IllegalArgumentException java.lang.IllegalArgumentException: Bad name>] user=> (say-hello "") Error: Bad name nil
So a possible error-condition got conveniently folded into a vector at a predictable index, without breaking the flow of control at the consumer end. Now let us see another example where the underlying functionality might throw an exception.
(defn safe-slurp "Slurp content of given filename. Return nil on error reading the file." [filename] (let [[text ex] (maybe (slurp filename))] (if ex nil text)))
Such a function can be used to read a configuration file and fall back on defaults if safe-slurp returns nil:
(or (safe-slurp "config-filename.properties") (system-defaults))
Summarily, I would like to note that a try-catch block forces one to think imperatively. Fortunately, there is a solution -- use 'maybe'. Please post your comments about it. You may like to follow me on Twitter.
No comments:
Post a Comment