Monday, March 5, 2007

Design Patterns in Haskell: bracket

Last week, Pascal Costanza wrote up a nice explanation of Lisp (and Scheme) macros.

It starts with a block of code like this (in Java, in this example):
FileInputStream in = new FileInputStream(filename);
try {
} finally {
The two important things to note are creating the input stream, and doing something with the input stream. Everything else is required scaffolding to make error handling work properly. This example is particularly clean, hiding all of the file operations behind a doSomething method. However, the operation could be written inline, so that instead of writing 6 lines of code to get 2 lines of work done, you see 24 lines of code to get 20 lines of work done.

The problem isn't that, in this example, the scaffolding adds 200% more code (as measured in lines, using typical Java style). The problem is that every time you want to open a file (or connect to a database, or ....), you need to write at least 4 extra lines of scaffolding to handle exceptions.

A common response in the functional programming world is to factor out this common scaffolding into reusable code. Pascal describes how to create a with-open-file macro that lets you write this:
(with-input-file (in filename)
(do-something in))
...which expands into the Lisp equivalent of the Java code above:
(let ((in (open filename)))
(do-something in)
(close in)))
This should be familiar if you've seen Lisp macros before, or come across a discussion of (with-open-file ...). However, the real power is in abstracting the required exception scaffolding, not in using macros to achieve that goal.

The power of (with-open-file ...) is undeniable, and has long since leaked out of the Lisp world. It's a common idiom in Ruby, available to all IO objects:"testing", "w") {|fh|
fh.puts "Hi!"
It's interesting that Ruby offers the same idiom using blocks (er, closures) and not macros. In fact, it's the same technique used in Haskell. You can find it in Control.Exception.bracket:
bracket :: IO a -> (a -> IO b) -> (a -> IO c) -> IO c
As I was writing this post, I went looking through the Haskell Hierarchical Libraries index for a definition of withOpenFile, but I didn't find it. Instead, I found a definition lurking in the documentation for bracket:
withOpenFile:: FileName -> FileMode -> (Handle -> IO a) -> IO a
withOpenFile name mode = bracket (openFile name mode) hClose
If you didn't have a function like bracket available, it would be trivial to write:
bracket :: IO a -> (a -> IO b) -> (a -> IO c) -> IO c
bracket acquire release action =
do h <- acquire
result <- action h
release h
return result
Interestingly, bracket is a completely general function that abstracts the idea of wrapping IO operations with an acquire and release action. Due to how errors are handled within the IO monad, processing within the action function terminates at the first error, so no explicit try/finally block is necessary.

It's not absolutely necessary to require a function like withOpenFile appear within the standard library. Instead, you can define as many wrappers as you want, using the precise combination of IO actions you need. For example, you may want to define withInputFile and withOutputFile separately. You may want to use openBinaryFile, openTempFile, openFd, runInteractiveCommand or createPipe instead of openFile. It doesn't really matter; each of these functions are just one-line partial applications of bracket.

Note that bracket only works on IO operations, but it's an implementation of a much more generic idea. Looking over the library index, you'll find many functions in the standard libraries named with...: withArgs, withArray, withCString, withMVar, and withSocketsDo are some interesting examples.

Now, I don't want this to be a "Haskell is better than Lisp" (or "Lisp is better than Haskell," etc.) kind of post. Quite the contrary actually. Lisp offers a set of tools to simplify code, and Lispers have taken to use macros to solve this kind of problem. Haskell doesn't have macros, but still enables these 'with...' bracketing patterns, and makes them downright trivial to write, even if you have to write them from scratch. What matters is identifying this kind of repeated scaffolding is a design pattern, giving it a name, and making it easy to use without writing it out longhand each and every time it's needed.


Tom Moertel said...

To ensure that your version of bracket releases the acquired resource in the event the action fails, you'll need to add code to catch any exception that occurs, release the resource, and then re-throw the exception. In effect, you do need the Haskell equivalent of a try/finally block.

Here's how the libraries do it:


Audrey said...

But Haskell does have macros, with support for early and late bindings, type checking for the macro AST, quasiquoting and splicing. It's very useful -- we even use some of them in Pugs. :-)

Don Stewart said...

Looks a lot like withFile, available in the darcs version of the base library:

withFile name mode =
bracket (openFile name mode) hClose

Eelis said...

C++ bases all of its resource management on this idea, in the form of RAII ("Resource Acquisition Is Initialization"), a very awkwardly named yet powerful resource management idiom based on the idea of acquiring resources in constructors and releasing them in destructors. C++ guarantees that destructors are automatically called for variables when they go out of scope, thereby transparently ensuring proper cleanup - even in the face of exceptions.

One thing that is annoying about the Haskell "withFile"-style solutions is that it isn't transparent: the user must remember to use them. Another thing that is annoying is that every resource type comes with its own "withFOO" function. This latter annoyance, however, we can easily get rid of using a type class:

> class IOResource a where dealloc :: a -> IO ()
> withResource :: IOResource a => IO a -> (a -> IO b) -> IO b
> withResource x = bracket x dealloc

Now, we can say for example:

> instance IOResource Socket where dealloc = sClose

This way a user needs to remember neither that Sockets need to be closed with sClose, nor what "with..." function is to be used with Sockets. All he needs to remember is that a Socket is an IOResource, and that he can therefore use withResource:

> withResource (socket AF_INET Stream 6) $ \sock -> do ...

The reason we can apply this generalization is that resource deallocation functions normally only take a single argument, namely the object representing the resource. This corresponds to the fixed signature for destructors in C++.

"withResource" is still not as nice as C++'s destructors, but it's getting closer.

Anonymous said...

The first commentor wasn't paying attention apparently. The OP wasn't saying it isn't needed at all. He was saying that you don't have to write the exception code as in Java. It has been generalized to a function 'bracket'.