【问题标题】:How can I elegantly combine resource and exception handling?如何优雅地结合资源和异常处理?
【发布时间】:2016-08-13 02:43:19
【问题描述】:

我正在为一个大量涉及资源处理的面向对象的 API 编写 Clojure 包装器。例如,对于 Foo 对象,我编写了三个基本函数:foo?,如果某物是 Foo,则返回 truecreate-foo,尝试获取资源来创建一个Foo,然后返回一个包含返回码和(如果构造成功)新创建的Foo的map;和destroy-foo,它接受一个Foo并释放它的资源。以下是这三个函数的一些存根:

(def foo? (comp boolean #{:placeholder}))

(defn create-foo []
  (let [result (rand-nth [::success ::bar-too-full ::baz-not-available])]
    (merge {::result result}
           (when (= ::success result)
             {::foo :placeholder}))))

(defn destroy-foo [foo] {:pre [(foo? foo)]} nil)

显然,每次调用 create-foo 并成功时,都必须使用返回的 Foo 调用 destroy-foo。这是一个不使用任何自定义宏的简单示例:

(let [{:keys [::result ::foo]} (create-foo)]
  (if (= ::success result)
    (try
      (println "Got a Foo:")
      (prn foo)
      (finally
        (destroy-foo foo)))
    (do
      (println "Got an error:")
      (prn result))))

这里有很多样板:必须存在 try-finally-destroy-foo 构造以确保释放所有 Foo 资源,并且必须存在 (= ::success result) 测试以确保没有任何东西运行当没有 Foo 时假设为 Foo。

with-foo 宏可以消除其中的一些样板,类似于clojure.core 中的with-open 宏:

(defmacro with-foo [bindings & body]
  {:pre [(vector? bindings)
         (= 2 (count bindings))
         (symbol? (bindings 0))]}
  `(let ~bindings
     (try
       ~@body
       (finally
         (destroy-foo ~(bindings 0))))))

虽然这确实有所帮助,但它对 (= ::success result) 样板没有任何作用,现在需要两个单独的绑定表单来实现所需的结果:

(let [{:keys [::result] :as m} (create-foo)]
  (if (= ::success result)
    (with-foo [foo (::foo m)]
      (println "Got a Foo:")
      (prn foo))
    (do
      (println "Got an error:")
      (prn result))))

我根本想不出一个好的方法来处理这个问题。我的意思是,我可以将if-letwith-foo 的行为组合成某种if-with-foo 宏:

(defmacro if-with-foo [bindings then else]
  {:pre [(vector? bindings)
         (= 2 (count bindings))]}
  `(let [{result# ::result foo# ::foo :as m#} ~(bindings 1)
         ~(bindings 0) m#]
     (if (= ::success result#)
       (try
         ~then
         (finally
           (destroy-foo foo#)))
       ~else)))

这确实消除了更多样板:

(if-with-foo [{:keys [::result ::foo]} (create-foo)]
  (do
    (println "Got a Foo:")
    (prn foo))
  (do
    (println "Got a result:")
    (prn result)))

但是,我不喜欢这个if-with-foo 宏有几个原因:

  • 它与create-foo 返回的映射的特定结构紧密耦合
  • if-let 不同,它会导致所有绑定都在两个分支的范围内
  • 它丑陋的名字反映了它丑陋的复杂性

这些宏是我能做的最好的吗?或者有没有更优雅的方式来处理可能资源获取失败的资源处理?也许这是 的工作;我对 monad 没有足够的经验,不知道它们在这里是否有用。

【问题讨论】:

    标签: monads clojure macros resources lisp


    【解决方案1】:

    我会向with-foo 添加一个错误处理程序。这样,宏就专注于应该做什么。但是,只有当所有错误情况都由少数错误处理程序处理时,这才简化了代码。如果您每次调用 with-foo 时都必须定义一个自定义错误处理程序,则此解决方案会使可读性比 if-else 构造更差。

    我添加了copy-to-mapcopy-to-map 应该将所有相关信息从对象复制到地图。这样宏的用户不会意外返回 foo 对象,因为它在宏内部被销毁

    (defn foo? [foo]
      (= ::success (:result foo)))
    
    (defn create-foo [param-one param-two]
      (rand-nth (map #(merge {:obj :foo-obj :result %} {:params [param-one param-two]})
                     [::success ::bar-too-full ::baz-not-available])))
    
    (defn destroy-foo [foo]
          nil)
    
    (defn err-handler [foo]
          [:error foo])
    
    (defn copy-to-map [foo]
          ;; pseudo code here
          (into {} foo))
    
    (defmacro with-foo [[f-sym foo-params & {:keys [on-error]}] & body]
      `(let [foo# (apply ~create-foo [~@foo-params])
             ~f-sym (copy-to-map foo#)]
         (if (foo? foo#)
           (try ~@body
                (finally (destroy-foo foo#)))
           (when ~on-error
             (apply ~on-error [~f-sym])))))
    

    现在你叫它

    (with-foo [f [:param-one :param-two] :on-error err-handler]
        [:success (str "i made it: " f)])
    

    【讨论】:

    • 我无法遵循您的解决方案;另外,当我运行您的示例代码时,我得到一个clojure.lang.ArityException: Wrong number of args (-1) passed to: create-foo
    • 对不起,我稍微重写了你的例子来自己试验一下。我用与你的不同但有效的版本对其进行了更新。
    • @SamEstep 现在它可以与剪切和粘贴一起使用还有什么不清楚的?我很乐意提供帮助
    • 感谢您的澄清!您的 copy-to-map 函数无关紧要,因为我正在使用的真实对象是不透明的。 with-foo 宏中有一些令人困惑的不一致之处,但我想我现在明白了。我喜欢它如何将重点放在正常情况下,这使它可以保持更好的with-foo 名称。但是,您的代码似乎已将“Foo”重新定义为“包含来自create-foo 调用的:result:params:obj 的地图”(甚至直接包含create-foowith-foo) 中,这是我试图避免的相当紧密的耦合。
    • 如果我没记错的话,您的不透明对象将无济于事。在您的if-with-foo 版本中,您做的最后一件事是(prn foo)。但是,如果您只是“返回” foo:... (with-foo [foo (::foo m)] foo) ... 那么用户会返回一个 foo 对象,该对象在您的宏的 finally 块中被销毁。如果你在你的 destroy_foo 函数中添加一个 (prn :destroy_foo),你会看到,在打印结果之前调用了析构函数。
    【解决方案2】:

    基于@murphy 的绝妙想法,将错误处理程序放入with-foobindings 以保持对正常情况的关注,我最终得到了一个我非常喜欢的解决方案:

    (defmacro with-foo [bindings & body]
      {:pre [(vector? bindings)
             (even? (count bindings))]}
      (if-let [[sym init temp error] (not-empty bindings)]
        (let [error? (= :error temp)]
          `(let [{result# ::result foo# ::foo :as m#} ~init]
             (if (contains? m# ::foo)
               (try
                 (let [~sym foo#]
                   (with-foo ~(subvec bindings (if error? 4 2))
                     ~@body))
                 (finally
                   (destroy-foo foo#)))
               (let [f# ~(if error? error `(constantly nil))]
                 (f# result#)))))
        `(do
           ~@body)))
    
    • 就像我在问题中的if-with-foo 宏一样,这个with-foo 宏仍然与create-foo 返回的结构相关联; 不同于我的if-with-foo 宏和@murphy 的with-foo 宏,它消除了用户手动拆开该结构的需要
    • 所有名称都有适当的范围;用户的sym 只绑定在主body 中,not:error 处理程序中,相反,::result 只绑定在:error 处理程序中,不是主要的body
    • 就像@murphy 的解决方案一样,这个宏有一个漂亮、合适的名字,而不是像if-with-foo 这样丑陋的名字
    • 与@murphy 的with-foo 宏不同,此with-foo 宏允许用户提供任何init 值,而不是强制调用create-foo,并且不会转换返回值

    最基本的用例只是将符号绑定到create-foo在某些body中返回的Foo,如果构造失败则返回nil

    (with-foo [foo (create-foo)]
      ["Got a Foo!" foo])
    

    要处理异常情况,可以将:error 处理程序添加到绑定中:

    (with-foo [foo (create-foo)
               :error (partial vector "Got an error!")]
      ["Got a Foo!" foo])
    

    可以使用任意数量的 Foo 绑定:

    (with-foo [foo1 (create-foo)
               foo2 (create-foo)]
      ["Got some Foos!" foo1 foo2])
    

    每个绑定都可以有自己的:error 处理程序;任何缺失的错误处理程序都将替换为 (constantly nil):

    (with-foo [foo1 (create-foo)
               :error (partial vector "Got an error!")
               foo2 (create-foo)]
      ["Got some Foos!" foo1 foo2])
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2014-07-10
      • 1970-01-01
      • 2015-12-14
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2023-03-21
      • 2012-09-19
      相关资源
      最近更新 更多