【发布时间】:2016-08-13 02:43:19
【问题描述】:
我正在为一个大量涉及资源处理的面向对象的 API 编写 Clojure 包装器。例如,对于 Foo 对象,我编写了三个基本函数:foo?,如果某物是 Foo,则返回 true; create-foo,尝试获取资源来创建一个Foo,然后返回一个包含返回码和(如果构造成功)新创建的Foo的map;和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 资源,并且必须存在 (= ::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 宏有几个原因:
- 它与
create-foo返回的映射的特定结构紧密耦合 - 与
if-let不同,它会导致所有绑定都在两个分支的范围内 - 它丑陋的名字反映了它丑陋的复杂性
这些宏是我能做的最好的吗?或者有没有更优雅的方式来处理可能资源获取失败的资源处理?也许这是monads 的工作;我对 monad 没有足够的经验,不知道它们在这里是否有用。
【问题讨论】:
标签: monads clojure macros resources lisp