Resource r = acquire(...);
try {
doSomething(r);
} finally {
r.dispose();
}
This is bad because it leads to code duplication, and forgetting to pair acquisition with disposal, or forgetting to do it with try/finally, can result in resource leaks which are hard to track down.
Idiomatic C++ combines resource acquisition with memory management, so you have something like:
void foo(...) {
Resource r;
doSomething(&r);
/* r's dtor called here */
}
This is somewhat cleaner than the Java approach but it depends on value types and deterministic memory management, which is not a trait that most high-level languages share.
Languages with first-class functions such as Lisp, Haskell and Factor take another approach, which is my personal favorite; you pass a block of code to a higher order function, which encapsulates the
try/finally
boilerplate for you:[ do-something ] with-stream
This approach works very well; it composes naturally for acquiring multiple resources at once, and there is no boilerplate on the caller side.
However, with our growing library we're also growing the set of disposable resources. Right now, we have:
- Streams
- Memory mapped files
- File system change monitors
- Database connections
- Database queries
With more on the way. This creates a problem because each
with-foo
word does essentially the same thing:: with-foo ( foo quot -- )
over [ close-foo ] curry [ ] cleanup ; inline
There were some variations on the theme, but it was all essentially the same.
Instead of having different words to close each type of resource, it makes sense to have a single word which does it. So now the
continuations
vocabulary has two new words, dispose
and with-disposal
. The dispose
word is generic, and it replaces stream-close
, close-mapped-file
, close-monitor
, cleanup-statement
(databases), etc. The with-disposal
word is the cleanup combinator.The
with-stream
word is there, it binds stdio
in addition to doing the cleanup. However it is simpler because it now uses with-disposal
.Simple and sweet, and now all the boilerplate is gone, on the side of the caller as well as the implementation side.
2 comments:
Very nice!
I'd like to see more OO languages adopt this pattern. My general feel is that 80/20 rule applies here. You should be able to solve about 80% of your resource lifecycle problems by only reserving the resource for exactly the lifespan of a function call, without having to mangle your code terribly to get there. For the other 20%, you can engage in a little mangling, or simply concentrate your vigilance on those particular parts.
Post a Comment