Rescuing Errors

Another common use case for macros is to limit the reach of any errors we encounter during evaluation. Unit testing libraries typically have to do this in order to capture failures while continuing to run other tests. In clojure.test, for instance, the is macro uses an internal macro try-expr to catch exceptions and report them to the test-running infrastructure.

context/try_expr.clj
 
(​defmacro​ try-expr [msg form]
 
`(​try​ ~(assert-expr msg form)
 
(​catch​ Throwable t#
 
(do-report {:type :error, :message ~msg,
 
:expected '~form, :actual t#}))))
 
(​defmacro​ is
 
([form] `(is ~form nil))
 
([form msg] `(try-expr ~msg ~form)))

So when the form is actually evaluated within the assert-expr, if an unexpected exception propagates due to some failing code, we definitely want to (a) prevent that failure from propagating, so that tests can continue, and (b) capture information about the failure in order to report it to the user. Wrapping the evaluation in a try-catch block is really the only way to go. This allows us to write test cases like this:

context/test.clj
 
(​require​ '[clojure.test :refer [is]])
 
(is (​=​ 1 1))
 
;=> true
 
 
(is (​=​ 1 (​do​ (​throw​ (Exception.)) 1)))
 
; ERROR in clojure.lang.PersistentList$EmptyList@1 (NO_SOURCE_FILE:1)
 
; expected: (= 1 (throw (Exception.)))
 
; actual: java.lang.Exception: null
 
;=> nil

Here we could almost avoid macros by requiring clients to pass functions rather than expressions. This would require users to pass a thunk instead of just an expression. But there’s a problem with the following function version of try-expr. Can you spot it?

context/try_expr_fn.clj
 
(​require​ '[clojure.test :as ​test​])
 
(​defn​ try-expr [msg f]
 
(​try​ (​eval​ (test/assert-expr msg (f)))
 
(​catch​ Throwable t
 
(test/do-report {:type :error, :message msg,
 
:expected f, :actual t}))))
 
 
(​defn​ our-is
 
([f] (our-is (f) nil))
 
([f msg] (test/try-expr msg f)))
 
 
(our-is (​fn​ [] (​=​ 1 1)))
 
;=> true
 
 
(our-is (​fn​ [] (​=​ 1 2)))
 
; FAIL in clojure.lang.PersistentList$EmptyList@1 (NO_SOURCE_FILE:3)
 
; expected: f
 
; actual: false
 
;=> false

Even assuming we could replace the ugly eval by updating assert-expr and all its dependencies with function versions to avoid the eval, we’re left with the horrible error message you see here. When we stop and think about what the try-expr macro is actually doing for us when it fails, it’s clear that nothing but a macro would do. When we report this failure to the test runner, we’re giving it not only the message from the test and the exception we encountered, but also the complete expression that failed! And with a macro, we have access to that original expression, which we can evaluate (or, in this case, pass down to assert-expr to be evaluated), or use as an expression, depending on our needs. Ironically, while this example shows us a way macros can help us to provide excellent, context-specific error messages, in practice, macros are often a source for more confusing error messages.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.118.28.179