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

时间:2016-08-13 02:43:20

标签: clojure macros resources lisp

我正在为面向对象的API编写一个Clojure包装器,它主要涉及资源处理。例如,对于Foo对象,我写了三个基本函数:foo?,如果某个东西是Foo则返回true; create-foo尝试获取创建Foo的资源,然后返回包含返回码的映射,并且(如果构造成功)返回新创建的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并成功时,必须使用返回的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资源,{{1}必须存在test以确保在没有Foo时假设Foo没有任何内容运行。

某些样板文件可以通过(= ::success result)宏删除,类似于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)

我无法找到解决这个问题的好方法。我的意思是,我可以将if-let(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)))) 的行为反映到某种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不同,它会导致所有绑定都在两个分支的范围内
  • 它丑陋的名字反映了它难看的复杂性

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

2 个答案:

答案 0 :(得分:1)

我要向with-foo添加错误处理程序。通过这种方式,宏可以专注于应该做的事情。但是,只有在少数错误处理程序处理所有错误情况时,才会简化代码。如果每次调用with-foo时都必须定义自定义错误处理程序,则此解决方案会使可读性比if-else结构更差。

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

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

答案 1 :(得分:0)

建立@phyphy的优秀想法,将错误处理程序放入with-foo bindings以保持对正常情况的关注,我结束了我喜欢的解决方案很多:

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

最基本的用例只是将符号绑定到某些create-foobody返回的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])