我正在为面向对象的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的工作;我没有足够的经验与monads知道它们是否在这里是有用的工具。
答案 0 :(得分:1)
我要向with-foo
添加错误处理程序。通过这种方式,宏可以专注于应该做的事情。但是,只有在少数错误处理程序处理所有错误情况时,才会简化代码。如果每次调用with-foo
时都必须定义自定义错误处理程序,则此解决方案会使可读性比if-else结构更差。
我添加了copy-to-map
。 copy-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
if-with-foo
with-foo
宏不同,此with-foo
宏允许用户提供任何init
值,而不是强制拨打create-foo
,不会转换返回值最基本的用例只是将符号绑定到某些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])