Thursday, January 31, 2008

Generic resource disposal

One approach is the Java approach. It is pretty much the worst:
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:

Anonymous said...

Very nice!

Anonymous said...

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.