简体   繁体   中英

Evaluation and macros in clojure

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.

eg 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")))

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.

When handlers vector is known at compile time

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.

When handlers vector is dynamically provided during runtime

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"

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 )

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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM