如何优雅地结合资源和异常处理?
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-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
宏,原因如下:
- 它与 return 由
create-foo
编辑的地图的特定结构紧密相关
- 与
if-let
不同,它会导致所有绑定都在两个分支的范围内
- 它丑陋的名字反映了它丑陋的复杂性
这些宏是我在这里能做的最好的吗?或者是否有更优雅的方式来处理可能的资源获取失败的资源处理?也许这是 monads 的工作;我对 monad 没有足够的经验,不知道它们在这里是否有用。
我会向 with-foo
添加一个错误处理程序。这样,宏就专注于应该做什么。但是,仅当所有错误情况都由少数错误处理程序处理时,这才简化了代码。如果每次调用 with-foo
时都必须定义自定义错误处理程序,则此解决方案的可读性比 if-else 结构更差。
我添加了copy-to-map
。 copy-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-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
只绑定在mainbody
,not在:error
handler中,反之,::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])
我正在为面向对象 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-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
宏,原因如下:
- 它与 return 由
create-foo
编辑的地图的特定结构紧密相关
- 与
if-let
不同,它会导致所有绑定都在两个分支的范围内 - 它丑陋的名字反映了它丑陋的复杂性
这些宏是我在这里能做的最好的吗?或者是否有更优雅的方式来处理可能的资源获取失败的资源处理?也许这是 monads 的工作;我对 monad 没有足够的经验,不知道它们在这里是否有用。
我会向 with-foo
添加一个错误处理程序。这样,宏就专注于应该做什么。但是,仅当所有错误情况都由少数错误处理程序处理时,这才简化了代码。如果每次调用 with-foo
时都必须定义自定义错误处理程序,则此解决方案的可读性比 if-else 结构更差。
我添加了copy-to-map
。 copy-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-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
只绑定在mainbody
,not在:error
handler中,反之,::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])