【问题标题】:Evaluation and macros in clojureclojure 中的求值和宏
【发布时间】:2023-03-19 05:45:01
【问题描述】:

我想做的事:

创建一个宏,它可以采用向量向量(异常处理逻辑,在我的底部示例中称为 handlers),一些其他数据(易发生异常的主体,称为 body 在底部的示例中),并生成弹弓 try/catch 逻辑。

例如我要转

(cp 
  ;; vector of vectors  (exception handling logic)
  [[Exception println]]
  ;;the "other data" exception prone body
  (throw+ (ex-info :crash "and burn")))

进入

(try+
  (throw+ (ex-info :crash "and burn"))
  (catch Exception e (println e)))

我想这样做是因为我相信正常的 try/catch 语法总是很冗长,尤其是在捕获多个错误时。

我可以非常接近,但我不知道如何正确评估宏中的符号以获得我想要的。我相信下面的示例 2 是最有趣的。

我目前的尝试:

1) 将适当数据作为列表返回的宏,但我不想返回它我想评估它。在结果上调用 eval 而不是 pprint 会给出

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) 适用于硬编码数据而非符号的宏

下面不工作的宏调用给出错误:

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) 确实有效的函数如果我引用了我真的想避免的处理程序和主体

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

【问题讨论】:

  • 如果这不仅仅是编写宏的练习,那么我想问一下你想要实现什么?摆脱多个catch 块重复?还是别的什么?
  • 是的,我的目标是可读性。我认为将潜在的多个 catch 块组合成一些我可以在其他地方定义的变量。最好是包裹弹弓。
  • 你不认为在异常发生的地方准确地捕捉异常比把这个逻辑移到别处更可读吗?为什么不在一个 catch 块中捕获任何异常,然后将其传递给某个处理函数,该函数知道如何处理每种异常类型?也许是多方法之类的。

标签: clojure macros


【解决方案1】:

我注意到您尝试需要执行一些代码来生成要在宏中使用的表单,并且您在引用内部执行此操作。正如@leetwinski 评论的那样,这可能是因为您的处理程序在编译时可能不为人所知。让我考虑这两种情况。

当处理程序向量在编译时已知时

通过创建一些辅助函数然后在宏中使用它们会更容易编写和测试。

我认为最好为给定的异常处理程序对定义一个生成catch 表单的函数:

(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__))

现在我们可以访问您的宏了。 macroexpand-1macroexpand 在编写宏时非常方便。您可以通过调用它们提供使用您的宏的表单来查看您的宏产生的内容。例如:

(macroexpand-1 '(when true (println "T")))
;; => (if true (do (println "T")))

让我们先生成所有的 catch 形式,然后在宏返回的带引号的形式中使用它们:

(defmacro cp [handlers & body]
  (let [catch-handlers (map catch-handler handlers)]
    `(try
       ~@body
       ~@catch-handlers)))

现在我们可以看到宏产生了什么:

(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__)))

看起来宏生成了预期的代码。

在运行时动态提供处理程序向量时

在这种情况下,我不会使用 eval 生成代码,而是使用一个函数来处理异常 (handle-exception) 并在通用 catch Throwable 块中使用它:

(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"

【讨论】:

  • 虽然这是一个显而易见的解决方案,但我认为这不是 op 想要的:如果这个宏只接受一个处理程序向量并将它们拼接为 catch 块,那么它有什么需要?似乎目的是允许将在其他地方定义的处理程序集合传递给宏。但它会引发错误:(let [handlers [[Exception println] [RuntimeException str]]] (cp handlers (throw (RuntimeException. "Error"))))。这就是他想用eval 管理的东西..
  • 是的,可能是这样 - 我已经为这种情况添加了解决方案。
  • 不错,但是 op 使用 slingshot,它不仅可以按类型捕获异常,还可以按其他类型捕获异常,matching-handler 找不到问题中的这个处理程序:@987654337 @
  • 就我而言,我真的不认为用一个宏替换trytry+ 宏根本不是一个好主意。它失去了可读性,并增加了一系列可能的错误。
  • 由于我不知道制作这种设计的背景,很难评论另一种设计是否会更好,但我强烈同意这种方法会带来很多复杂性。
【解决方案2】:

根据我对你目标的理解,我会这样做:

首先我会为所有异常使用一个处理程序,这将是多方法,因为它可以轻松确定如何处理不同类型的参数(包括继承和自定义层次结构)。

(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))

在回复中:

(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

好的,它就像我们想要的那样工作。现在我们可以将多方法定义包装到一个宏中,让它更易于管理:

(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#))))

所以你可以像这样使用它:

(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 %))

(您可以将处理程序作为地图传递,或者像在您的示例中那样作为向量的向量)

它扩展为:

(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__)))

对于更多的糖,让我们为try+.. 添加宏。假设try-handle

(defmacro try-handle [handler & body]
  `(slingshot/try+
    ~@body
    (catch Object err# (~handler err#))))

在回复中:

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

注意它成功处理了IllegalAccessError,因为我们的多方法知道继承,并执行正确的函数(在我们的案例处理程序中为Error

【讨论】:

  • 这太棒了!我已经尽可能避免编写宏,所以在这个答案和 Piotrek 的(也很棒)响应中,我看到了我不认识的语法“~@”和函数“macroexpand-1”。是否有一个全面的博客/书籍/等我可以阅读以获取更多关于宏的信息?
  • macroexpand-1(连同macroexpandclojure.walk/macroexpand-all,是检查宏在编译时如何转换为常规clojure 核心的便捷工具。至于~@ 的东西,试试谷歌“unquote splicing”。这里有一个很好的宏基本介绍:braveclojure.com/writing-macros(它很好很有趣,就像整本书一样)。我从 Emerick 的“Clojure Programming”中获得了宏的基础知识/ Carper/Grand。但我想任何一本书都有适当的介绍。还有一本书“掌握 clojure 宏”,但我从未读过。
  • 如果你使用苹果酒进行开发,它有一些很好的宏扩展内置命令:查看cider-macroexpand-* 命令。
【解决方案3】:

在写这个问题的过程中,我找到了解决方案……

查看第一次尝试的错误:一个 java 类被调用,好像它是一个函数。

经过一番玩弄后,我发现引用 Exception 类会起作用,但在宏中引用它们却不行。使用macroexpand 更好地了解正在发生的事情,我发现我需要检查 java 类并将它们转换回 try/catch 所期望的符号。

固定代码:

(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#)))))

我还在宏中添加了 eval 以获取实际评估的结果,我认为在这种情况下这不是一个坏习惯,但我不确定。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2023-02-04
    • 2011-08-09
    • 2015-11-16
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-04-01
    相关资源
    最近更新 更多