如何优雅地结合资源和异常处理?

How can I elegantly combine resource and exception handling?

我正在为面向对象 API 编写一个 Clojure 包装器,它大量涉及资源处理。例如,对于 Foo 对象,我编写了三个基本函数:foo?,其中 returns true iff something is a Foo; create-foo,它试图获取创建 Foo 的资源,然后 return 是一个包含 return 代码和(如果构建成功)新创建的 Foo 的映射;和 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 并成功时,必须使用 returned 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 宏,原因如下:

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

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

我添加了copy-to-mapcopy-to-map 应将所有相关信息从对象复制到地图。这样宏的用户就不会意外 return 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)])

基于@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只绑定在mainbodynot:errorhandler中,反之,::result只是在 :error 处理程序中绑定,在主 body
  • 就像@murphy 的解决方案一样,这个宏有一个漂亮、合适的名字,而不是像 if-with-foo
  • 这样丑陋的名字
  • 与@murphy 的 with-foo 宏不同,此 with-foo 宏允许用户提供任何 init 值,而不是强制调用 create-foo,并且不会t 转换返回值

最基本的用例简单地将一个符号绑定到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])