我如何优雅地结合资源和异常处理? [英] How can I elegantly combine resource and exception handling?
问题描述
我正在为一个面向对象的API编写一个Clojure包装,它涉及资源处理。例如,对于Foo对象,我写了三个基本函数: foo?
,它返回 true
iff something是Foo; create-foo
,它尝试获取创建Foo的资源,然后返回一个包含返回码的映射,并且(如果构造成功)新创建的Foo;和 destroy-foo
,它需要一个Foo并释放其资源。下面是这三个函数的一些存根:
(def foo? 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
(printlnGot a Foo:)
(prn foo)
(最后
(destroy-foo foo)))
(do
(printlnGot a error: )))
这里有很多样板: try
- finally
- destroy-foo
构造必须存在,才能确保释放所有Foo资源, (= :: success result)
测试必须存在,以确保没有运行假设Foo时没有Foo。
某些模板可以通过 with-foo
宏消除,类似于 with-open
宏 clojure.core
:
(defmacro with-foo [bindings& body]
pre>
{:pre [(vector?bindings)
(= 2(count bindings))
(symbol?(bindings 0))]}
`
(try
〜@ body
(finally
(destroy-foo〜(bindings 0))))))
虽然这有帮助,但它不会对
(= :: success result)
样板,现在需要两个单独的绑定表单来实现所需的结果:(let [{:keys [:: result]:as m}(create-foo)]
(if(= :: success result)
(with-foo [ :foo m)]
(printlnGot a Foo:)
(prn foo))
(do
(printlnGot a error: (prn result))))
我根本无法找出一个很好的方法来处理。我的意思是,我可以考虑
if-let
和with-foo
转换为某种if-with-foo
宏:(defmacro if-with-foo [bindings then else]
{:pre [ ?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)))
p>
(if-with-foo [{:keys [:: result :: foo]} -foo)]
(do
(printlnGot a Foo:)
(prn foo))
(do
(printlnGot a result: )
(prn result)))
但是,我不喜欢这个
if-with-foo
宏有几个原因:
-
if-let
返回的映射结构create-foo
- 其丑陋的名称反映了其丑陋的复杂性
<
这些宏是我能做的最好的吗?或者有一个更优雅的方式来处理资源处理与可能的资源获取失败?也许这是 monads 的工作;
建立从@ murphy的好主意,把错误处理程序放入 with-foo
的绑定
保持焦点在正常情况下,我最终得到了一个我非常喜欢的解决方案:
(b)
$像我的
(if-let [[sym初始化循环错误](非空绑定)]
(let [error?(=:error temp)]
`(let [{result#:: result foo#:: foo:as m# }〜init]
(if(contains?m#:: foo)
(try
(let [〜sym foo#]
(with-foo〜(subvec bindings如果错误?4 2))
〜@ body))
(finally
(destroy-foo foo#)))
(let [f#〜 (constant nil))]
(f#result#)))))
`(do
〜@ body)))
if-with-foo
宏在问题中,这个
- 所有名称都有适当的范围;用户的
sym
只绑定在主体正文
中, $ c>:error 处理程序,相反,:: result
只绑定在:error
处理程序,不是在正文
- 一个漂亮,合适的名字,而不是像
if-with-foo
- 丑陋的东西,而不像@ murphy的
-foo
宏,此with-foo
宏允许用户提供任何init
值,而不是强制调用create-foo
,并且不会转换返回的值
< c $ c> with-foo 宏仍然绑定到
create-foo
返回的结构;不同于我的 if-with-foo
宏和@ murphy的 with-foo
消除了用户手动拆分该结构的需要 最基本的用例只是将一个符号绑定到一些正文中
create-foo
>,如果构造失败,返回 nil
:
(with-foo [foo(create-foo)]
[Got a Foo!foo])
为了处理异常情况,可以将:error
处理程序添加到绑定中:
(with-foo [foo(create-foo)
:error(partial vectorGot an error! b [Got a Foo!foo])
可以使用任意数量的Foo绑定: / p>
(with-foo [foo1(create-foo)
foo2(create-foo )]
[Got some Foos!foo1 foo2])
它自己的:error
handler;任何丢失的错误处理程序都替换为(constants nil)
:
(with-foo [foo1(create-foo)
:错误(部分向量有一个错误!)
foo2(create-foo)]
[有一些Foos!foo1 foo2])
I'm writing a Clojure wrapper for an object-oriented API that heavily involves resource handling. For instance, for the Foo object, I've written three basic functions: foo?
, which returns true
iff something is a Foo; create-foo
, which attempts to obtain the resources to create a Foo, then returns a map containing a return code and (if the construction succeeded) the newly created Foo; and destroy-foo
, which takes a Foo and releases its resources. Here are some stubs for those three functions:
(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)
Obviously, every time create-foo
is called and succeeds, destroy-foo
must be called with the returned Foo. Here's a simple example that doesn't use any custom macros:
(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))))
There's a lot of boilerplate here: the try
-finally
-destroy-foo
construct must be present to ensure that all Foo resources are released, and the (= ::success result)
test must be present to ensure that nothing gets run assuming a Foo when there is no Foo.
Some of that boilerplate can be eliminated by a with-foo
macro, similar to the with-open
macro in 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))))))
While this does help somewhat, it doesn't do anything about the (= ::success result)
boilerplate, and now two separate binding forms are required to achieve the desired 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))))
I simply can't figure out a good way to handle this. I mean, I could complect the behaviors of if-let
and with-foo
into some sort of if-with-foo
macro:
(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)))
This does eliminate even more boilerplate:
(if-with-foo [{:keys [::result ::foo]} (create-foo)]
(do
(println "Got a Foo:")
(prn foo))
(do
(println "Got a result:")
(prn result)))
However, I don't like this if-with-foo
macro for several reasons:
- it's very tightly coupled to the specific structure of the map returned by
create-foo
- unlike
if-let
, it causes all bindings to be in scope in both branches - its ugly name reflects its ugly complexity
Are these macros the best I can do here? Or is there a more elegant way to handle resource handling with possible resource obtainment failure? Perhaps this is a job for monads; I don't have enough experience with monads to know whether they would be useful tool here.
Building from @murphy's excellent idea to put the error handler into with-foo
's bindings
to keep the focus on the normal case, I've ended up with a solution that I like quite a lot:
(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)))
- like my
if-with-foo
macro in the question, thiswith-foo
macro is still tied to the structure returned bycreate-foo
; unlike myif-with-foo
macro and @murphy'swith-foo
macro, it eliminates the need for the user to manually take apart that structure - all names are properly scoped; the user's
sym
is only bound in the mainbody
, not in the:error
handler, and conversely, the::result
is only bound in the:error
handler, not in the mainbody
- like @murphy's solution, this macro has a nice, fitting name, instead of something ugly like
if-with-foo
- unlike @murphy's
with-foo
macro, thiswith-foo
macro allows the user to provide anyinit
value, rather than forcing a call tocreate-foo
, and doesn't transform the returned value
The most basic use case simply binds a symbol to a Foo returned by create-foo
in some body
, returning nil
if the construction fails:
(with-foo [foo (create-foo)]
["Got a Foo!" foo])
To handle the exceptional case, an :error
handler can be added to the binding:
(with-foo [foo (create-foo)
:error (partial vector "Got an error!")]
["Got a Foo!" foo])
Any number of Foo bindings can be used:
(with-foo [foo1 (create-foo)
foo2 (create-foo)]
["Got some Foos!" foo1 foo2])
Each binding can have its own :error
handler; any missing error handlers are replaced with (constantly nil)
:
(with-foo [foo1 (create-foo)
:error (partial vector "Got an error!")
foo2 (create-foo)]
["Got some Foos!" foo1 foo2])
这篇关于我如何优雅地结合资源和异常处理?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!