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.