Reputation: 622
What I'm trying to do:
Create a macro that can take a vector of vectors (exception handling logic, called handlers in my examples at the bottom), some some other data (exception prone body, called body in my examples at the bottom), and generate slingshot try/catch logic.
e.g. I want to turn
(cp
;; vector of vectors (exception handling logic)
[[Exception println]]
;;the "other data" exception prone body
(throw+ (ex-info :crash "and burn")))
into
(try+
(throw+ (ex-info :crash "and burn"))
(catch Exception e (println e)))
I want to do this because I believe the normal try/catch syntax is always verbose, especially so when catching multiple errors.
I'm able to get pretty close but I can't figure out how to properly evaluate the symbols within a macro to get what I want. I believe my example 2 below is the most interesting.
My attempts so far:
1) macro that returns appropriate data as a list, but I don't want to return it I want to evaluate it. Calling eval
instead of pprint
on the result gives
ClassCastException java.lang.Class cannot be cast to clojure.lang.IFn stream-stocks.core/eval27882 (form-init2616933651136754630.clj:1)
.
(defmacro cp "handle exceptions"
[handlers & body]
`(loop [h# ~handlers
acc# (conj '~body 'slingshot.slingshot/try+)]
(if h#
(recur (next h#)
(concat acc# (list (list 'catch (first (first h#)) 'e# (reverse (conj (next (first h#)) 'e#)))) ))
acc#)))
(let [handlers [[Exception println] [java.lang.NullPointerException type]
[:test-throw #(println "Works! Will handle exception: " %)]]]
(pprint (cp [[Exception println] [java.lang.NullPointerException type]
[:test-throw #(println "Works! Will handle exception: " %)]]
(slingshot.slingshot/throw+ {:test-throw "Test-throw error msg"})))
(pprint (cp handlers
(slingshot.slingshot/throw+ {:test-throw "Test-throw error msg"}))))
2) macro that works with hard coded data, but not symbols
The macro call that that Does Not work below gives error:
CompilerException java.lang.IllegalArgumentException: Don't know how to create ISeq from: clojure.lang.Symbol, compiling:(/tmp/form-init2616933651136754630.clj:6:3)
.
(defmacro cp "handle exceptions"
[handlers2 & body]
(loop [h# handlers2
acc# (conj (list (first body)) 'slingshot.slingshot/try+)]
(if h#
(recur (next h#)
(concat acc# (list (list 'catch (first (first h#)) 'e# (reverse (conj (next (first h#)) 'e#))))))
acc#)))
(let [handlers [ [Exception println] [java.lang.NullPointerException type]
[:test-throw #(println "Works! Will handle exception: " %)]]]
;;I work
(cp [ [Exception println] [java.lang.NullPointerException type]
[:test-throw #(println "Works! Will handle exception: " %)]]
(slingshot.slingshot/throw+ {:test-throw "Test-throw error msg"}))
;;I do NOT work
(cp handlers
(slingshot.slingshot/throw+ {:test-throw "Test-throw error msg"})))
3) function that does actually work iff I quote the handlers and body, which I really want to avoid
(defn cpf "handle exceptions" [handlers & body]
(eval (loop [h handlers
acc (conj body 'slingshot.slingshot/try+)]
(if h
(recur (next h)
(concat acc (list (list 'catch (first (first h)) 'e (reverse (conj (next (first h)) 'e))))))
acc))))
(let [handlers [ '[Exception println] '[java.lang.NullPointerException type]
'[:test-throw #(println "Works! Will handle exception: " %)]
]]
(cpf [ '[Exception println]
'[:test-throw println]
]
'(println "Should get called")
'(throw+ {:test-throw "Test-throw error msg"})
'(println "Should not get called")
)
(cpf handlers
'(println "Should get called")
'(throw+ {:test-throw "Test-throw error msg"})
'(println "Should not get called")))
Upvotes: 2
Views: 650
Reputation: 17859
according to my understanding of your goal, that's what i would do:
first of all i would go with one handler for all exceptions, which would be the multimethod, as it can easily determine how to handle different types of parameters (including inhetihance and custom hierarchies).
(require '[slingshot.slingshot :as slingshot])
(defmulti my-exception-handler
#(if (instance? Throwable %)
(.getClass %)
%))
(defmethod my-exception-handler NoSuchFieldError [error]
(println "caught no such field error"))
(defmethod my-exception-handler :my-custom-error [error]
(println "caught custom error"))
(defmethod my-exception-handler Error [error]
(println "caught some error"))
(defmethod my-exception-handler :default [error]
(println "caught something" error))
in repl:
(slingshot/try+
(slingshot/throw+ (Error. "asdd"))
(catch Object o (my-exception-handler o)))
;; => caught some error
(slingshot/try+
(slingshot/throw+ (NoSuchFieldError. "asdd"))
(catch Object o (my-exception-handler o)))
;; => caught no such field error
(slingshot/try+
(slingshot/throw+ :aaaa)
(catch Object o (my-exception-handler o)))
;; => caught something :aaaa
(slingshot/try+
(slingshot/throw+ :my-custom-error)
(catch Object o (my-exception-handler o)))
;; => caught custom error
ok it works just as we want. Now we can wrap multimethod definition to a macro, mo make it more manageable:
(defmacro def-error-catcher [name definitions default-handler]
`(do (defmulti ~name #(if (instance? Throwable %)
(.getClass %) %))
~@(for [[dispatch-val handler] definitions]
`(defmethod ~name ~dispatch-val [v#]
(~handler v#)))
(defmethod ~name :default [v#] (~default-handler v#))))
so you can use it like this:
(def-error-catcher
some-awesome-handler
{NoSuchFieldError #(println :no-such-field (.getMessage %))
NoSuchMethodError #(println :no-such-method (.getMessage %))
Error #(println :error (.getMessage %))
:my-custom-error println}
#(println :unspecified %))
(you can pass handlers as a map, or as a vector of vectors like in your example)
it expands to:
(do
(defmulti
some-awesome-handler
#(if (instance? java.lang.Throwable %) (.getClass %) %))
(defmethod
some-awesome-handler
NoSuchFieldError
[v__20379__auto__]
(#(println :no-such-field (.getMessage %)) v__20379__auto__))
(defmethod
some-awesome-handler
NoSuchMethodError
[v__20379__auto__]
(#(println :no-such-method (.getMessage %)) v__20379__auto__))
(defmethod
some-awesome-handler
Error
[v__20379__auto__]
(#(println :error (.getMessage %)) v__20379__auto__))
(defmethod
some-awesome-handler
:my-custom-error
[v__20379__auto__]
(println v__20379__auto__))
(defmethod
some-awesome-handler
:default
[v__20381__auto__]
(#(println :unspecified %) v__20381__auto__)))
and for more sugar lets add macro for try+
.. let's say try-handle
:
(defmacro try-handle [handler & body]
`(slingshot/try+
~@body
(catch Object err# (~handler err#))))
in repl:
user> (try-handle some-awesome-handler
(slingshot/throw+ :my-custom-error))
:my-custom-error
nil
user> (try-handle some-awesome-handler
(slingshot/throw+ (NoSuchFieldError. "no field")))
:no-such-field no field
nil
user> (try-handle some-awesome-handler
(slingshot/throw+ (NoSuchMethodError. "no method")))
:no-such-method no method
nil
user> (try-handle some-awesome-handler
(slingshot/throw+ (IllegalAccessError. "ill access")))
:error ill access
nil
user> (try-handle some-awesome-handler
(slingshot/throw+ :something-else))
:unspecified :something-else
nil
notice that it successfully handles IllegalAccessError
, since our multimethod knows about inheritance, and executes right function (in our case handler for Error
)
Upvotes: 2
Reputation: 13175
I noticed that you try to need to execute some code to produce forms to be used in the macro and you do it inside of the quoting. As @leetwinski commented, it might be because your handlers are might not be known at compile time. Let me consider both cases.
It would be easier to write and to test by creating some helper functions and then use them in your macro.
I think it would be good to define a function which produces a catch
form for a given exception-handler pair:
(defn catch-handler [[exception handler]]
`(catch ~exception e# (~handler e#)))
(catch-handler [Exception println])
;; => (catch java.lang.Exception e__20006__auto__
;; => (#function[clojure.core/println] e__20006__auto__))
Now we can get to your macro. macroexpand-1
and macroexpand
are quite handy when writing macros. You can see what your macro produces by calling them providing a form which uses your macro. For example:
(macroexpand-1 '(when true (println "T")))
;; => (if true (do (println "T")))
Let's produce all the catch forms first and then use them inside the quoted form returned by the macro:
(defmacro cp [handlers & body]
(let [catch-handlers (map catch-handler handlers)]
`(try
~@body
~@catch-handlers)))
Now we can see what the macro produces:
(macroexpand-1
'(cp [[Exception println] [RuntimeException str]]
(throw (RuntimeException. "Error"))))
;; => (try
;; => (throw (RuntimeException. "Error"))
;; => (catch Exception e__20006__auto__ (println e__20006__auto__))
;; => (catch RuntimeException e__20006__auto__ (str e__20006__auto__)))
It looks that the macro generates the expected code.
In this case instead of generating code with eval
I would just use a function to handle exceptions (handle-exception
) and use it inside a generic catch Throwable
block:
(defn matching-handler [handlers exception]
(->> handlers
(filter (fn [[exception-type handler]]
(instance? exception-type exception)))
(first)
(second)))
(defn handle-exception [handlers exception]
(let [handler (or (matching-handler handlers exception)
#(throw %))]
(handler exception)))
(defmacro cp' [handlers & body]
`(try
~@body
(catch Throwable e#
(handle-exception ~handlers e#))))
(let [handlers [[RuntimeException println] [Exception str]]]
(cp' handlers
(throw (Exception.))))
;; => "java.lang.Exception"
Upvotes: 3
Reputation: 622
In the process of writing this question I found the solution...
Looking at the error from the first attempt: a java class is being called as though it was a function.
After some toying around I found that quoting the Exception classes would work but quoting them inside the macro would not. Using macroexpand
to get a better idea of what was going on I found that I needed to check for java classes and turn them back into the symbols that try/catch is expecting.
Fixed code:
(defmacro cp "handle exceptions"
[handlers & body]
`(eval (loop [h# ~handlers
acc# (conj '~body 'slingshot.slingshot/try+)]
(let [pred# (if (class? (first (first h#)))
(symbol (.getName (first (first h#))))
(first (first h#)))]
(if (not (nil? h#))
(recur (next h#)
(concat acc# (list (list 'catch pred# 'e# (reverse (conj (next (first h#)) 'e#)))) ))
acc#)))))
I also added eval inside the macro to get the results actually evaluated, I think that isn't a bad practice in this case but I'm not certain.
Upvotes: -1