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

标签 clojure macros resources lisp

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

(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 宏来消除,类似于 with-open clojure.core 中的宏:

(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) 样板没有任何作用,现在需要两个单独的绑定(bind)形式来实现期望的结果:

(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-let 的行为和 with-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
  • 返回的 map 的特定结构紧密耦合
  • if-let 不同,它导致所有绑定(bind)都在两个分支的范围内
  • 它丑陋的名字反射(reflect)了它丑陋的复杂性

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

最佳答案

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

我添加了copy-to-mapcopy-to-map 应该将所有相关信息从对象复制到 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 - 如何优雅地结合资源和异常处理?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/38928689/

相关文章:

java - 在作为代理工作的 Nginx Ring 处理程序中读取服务器名称

unit-testing - 如何针对参数序列测试谓词?

c - Eclipse C宏自动格式化

java - 如何从构建路径中的可运行jar文件中获取资源文件

C# 字符串资源值作为枚举字符串部分值?

clojure - 为什么减少这个惰性序列会使这个 Clojure 程序减慢 20 倍?

file - 如何将文件中的数据读入 Clojure 中的 HashMap (或其他数据结构)?

macros - SPSS宏命令

c++ - 可以做 "#ifdef DEBUG( ... ) __VA_ARGS__"吗?

c# - 如何在 C# 中使用 XAML 中定义的画笔资源