The code examples in this article are written in Standard ML, but the
technique should be applicable to other similar languages.
Implementing resource allocation and deallocation correctly in a language
with exceptions can be quite a pain. Fortunately, in many cases resources
can be allocated and deallocated in a scoped or nested fashion simplifying
the management considerably.
First of all, a function such as
fun finally (thunk, effect) =
(((fn x => fn () => x) (thunk ())
handle e => fn () => raise e) o effect) ()
can be provided to make sure that a given effect will be performed after a
given thunk returns - whether normally or by raising an exception.
It is customary to provide functions that allocate a resource, pass the
resource to a given body function, and take care of deallocating the
resource. For example, one could provide the function
fun withInputString string =
fn body =>
let
val instream = TextIO.openString string
in
finally (fn () => body instream,
fn () => TextIO.closeIn instream)
end
for executing a body with an instream that reads from a string.
Another example of such a scoped resource management function could be
fun withDirStream path =
fn body =>
let
val dirstream = OS.FileSys.openDir path
in
finally (fn () => body dirstream,
fn () => OS.FileSys.closeDir dirstream)
end
for executing a body with a directory stream. As you can see, there is
some duplication in structure between the above two functions that could
easily be factored out, but I'll ignore that aspect in this article.
With scoped resource management functions like the above, using resources
in scoped fashion is greatly simplified. However, when multiple resources
need to be acquired, straightforward use of the above style of combinators
leads to deeply nested code of the form
withA aArgs
(fn a =>
withB bArgs
(fn b =>
...))
that can be a bit hard to read (and write).
Instead of writing such deeply nested code, let's invent a couple of
combinators for combining scoped resource management functions. To cut
the story short (it is late), it turns out that we can view scoped
resource management monadically. Here are implementations of the monad
operators:
infix >>=
fun return x =
fn body => body x
fun withA >>= aToWithB =
fn body => withA (fn a => aToWithB a body)
With a basic knowledge of monads we can then write (or reuse) a bunch of
combinators for combining scoped resource management functions. For
example, a monad product combinator:
infix >>*
fun withA >>* withB =
withA >>= (fn a => withB >>= (fn b => return (a, b)))
Using the above, we can rewrite nested code of the form
withA aArgs
(fn a =>
withB bArgs
(fn b =>
...))
as non-nested code of the form
(withA aArgs >>* withB bArgs)
(fn (a, b) =>
...)
which is arguably easier to read.
Much more could be said, but I'll end with a simple test case that
demonstrates that effects are indeed performed in the desired way (for
all allocated resources and in reverse order of allocation).
infix >>
fun withA >> withB =
withA >>= (fn () => withB)
let
val counter = ref 0
val result = ref ""
fun verify b = if b then () else raise Fail "verify failed"
fun `name =
fn () =>
result := !result ^ name
val fail0 =
fn () =>
if !counter = 0 then
raise Fail "counter"
else
counter := !counter - 1
fun invoke atEntry atExit =
fn body => (atEntry () ; finally (body, atExit))
in
app (fn (n, expected) =>
(counter := n
; result := ""
; (invoke (`"+A" o fail0) (`"-A") >>
invoke (`"+B" o fail0) (`"-B")) (`"*C" o fail0)
handle Fail _ => ()
; verify (!result = expected)))
[(0, ""),
(1, "+A-A"),
(2, "+A+B-B-A"),
(3, "+A+B*C-B-A")]
end
-Vesa Karvonen
Haskell's lightweight syntax and the ($) operator can help here. For
example, your code in Haskell could look like this:
f ... =
withA aArgs $ \a ->
withB bArgs $ \b ->
...
There are no opening parentheses, so no need for closing ones too.
Best regards
Tomasz
--
I am searching for programmers who are good at least in
(Haskell || ML) && (Linux || FreeBSD || math)
for work in Warsaw, Poland
> f ... =
> withA aArgs $ \a ->
> withB bArgs $ \b ->
> ...
> There are no opening parentheses, so no need for closing ones too.
That's true. Still, the monad operations allow you to combine such
"with" -functions easily. Indeed, the possibility of combining such
actions was actually one of the main reasons for me to come up with
the technique. I'm currently thinking about writing (and probably
soon writing) lots of unit test code in Scheme for code that isn't so
straightforward to test. In order to test many of the (effectful)
procedures, the execution environment needs to be modified for the
duration of a test case and then put back to the original state before
the next test. In order to do that conveniently and efficiently, I'd
like to be able combine such environment restoring functions easily in
order to reduce the boiler plate required for testing particular
procedures. The monadic combinators are the second way of doing it
that I have so far come up with.
-Vesa Karvonen
> FOPEN( source, "source", "r" )
> FOPEN( target, "target", "w" )
> MALLOC( buff, 1024 )
> copy( target, source, buff );
[...]
> So, what the above sequence really does will be like:
> if( source = fopen( "source", "r" ))
> { if( target = fopen( "target", "w" ))
> { if( buffer = malloc( 1024 ))
> { result = buffcopy( target, source, buffer );
> free( buffer ); }
> fclose( target ); }
> fclose( source ); }
Yep, this looks very similar. Nice idea! I'll have to mention this
to some C folks... ;)
I guess I've never stumbled upon this technique due to having mostly
used C++ (rather than C), where similar structuring of code can be
achieved easily using (destructors and specifically) the RAII-idiom.
-Vesa Karvonen
Well, yes, but IMO in Haskell this approach adds more problems than
it solves. For example, the "with"s and variable bindings are
interspersed, so it's easier to confuse their order. You have
withA aArgs ... withB bArgs ... withC cArgs ... a ... b ... c
instead of
withA aArgs ... a ... withB bArgs ... b ... withC cArgs ... c
But of course, this technique may be useful in some circumstances and/or
in other languages.
Yes, I guess it wasn't clear from my earlier post, but most of the
"with" -procedure I will likely be using will not pass any parameters
to the body. They will simply (imperatively) change the environment
for running a test and then restore the environment. So, matching
"with" -procedures to the parameters they introduce is not going to be
a problem.
In either case, I think that the bigger problem is ensuring that
resources get deallocated properly. I'm not personally aware of any
universal and completely satisfactory type safe way (or type system) to
ensure that. However, I've seen and likely will continue to see a lot
of code (including code written in functional languages) that simply
fails to deallocate resources properly in exceptional situations.
"with" -functions are certainly a good technique. Using the kind of
monadic combinators introduced in this thread you can start treating
"with" -functions as units that can be combined to produce more complex
"with" -functions. This could help you see more opportunites for
organizing code. But, no, it is obviously not a panacea.
One idea, that I haven't yet examined, is to introduce a choice
combinator. The idea being that you could write
(withA aArgs >>| withA' aArgs')
meaning that you could attempt to obtain a resource in two alternative
ways. This could be quite useful in some situations. Further
combinators along these lines might also be possible. In general,
using a combinator approach to scoped resource management would allow
you to separate the resource management logic from the other logic.
-Vesa Karvonen
> One idea, that I haven't yet examined, is to introduce a choice
> combinator. The idea being that you could write
>
> (withA aArgs >>| withA' aArgs')
>
> meaning that you could attempt to obtain a resource in two alternative
> ways. This could be quite useful in some situations. Further
> combinators along these lines might also be possible. In general,
> using a combinator approach to scoped resource management would allow
> you to separate the resource management logic from the other logic.
>
This should be reasonably easy to implement in the right monad - it's not
that dissimilar to the equivalent in a parsing monad, for example. You
might need more than one kind of failure, I guess.
The task of the academic is not to scale great
intellectual mountains, but to flatten them.
import Control.Monad
import Control.Monad.Cont
import Control.Monad.Trans
import IO
type RMT m a = ContT () m a
with :: Monad m => m a -> (a -> m ()) -> RMT m a
with alloc dealloc = do res <- lift alloc
ContT (\k -> k res >> dealloc res)
runRMT :: Monad m => RMT m () -> m ()
runRMT rmt = runContT rmt return
-- run rmt and all its finalizers before proceeding
syncRMT :: Monad m => RMT m () -> RMT m ()
syncRMT rmt = lift (runRmt rmt)
withFile :: FilePath -> IOMode -> RMT IO Handle
withFile path mode = with (openFile path mode) hClose
To get a MonadPlus, i.e. multiple ways of attempting to allocate a
resource, a standard hinzean two-continuation backtracking monad
transformer should do the trick (though you have to add your
finalizers to the failure continuation, too).
Lauri
> > One idea, that I haven't yet examined, is to introduce a choice
> > combinator. The idea being that you could write
> >
> > (withA aArgs >>| withA' aArgs')
> >
> > meaning that you could attempt to obtain a resource in two alternative
> > ways.
> This should be reasonably easy to implement in the right monad - it's not
> that dissimilar to the equivalent in a parsing monad, for example. You
> might need more than one kind of failure, I guess.
Possibly, yes. In Haskell, that would probably be the way to go. SML
(and the Scheme dialect I'm working with) already have a concept of
exception handling. I would like to be able to use existing "with"
-procedures with the framework, as long as it doesn't cause too much
ugliness. Adding two kinds of failure continuations, which is what I
think you mean above, seems to require modifying the interface of the
"with" -procedures, but I haven't given it much thought. One
possibility might be to introduce lifting functions to lift existing
"with" -procedures to the more flexible framework (or monad).
Anyway, I implemented two variations of the choice combinator. The
simpler one tries the second alternative regardless of where the failure
came from:
infix >>|*
fun withA >>|* withB =
fn body => withA body handle _ => withB body
The more complicated one tries the second alternative only if the
failure came from the first alternative:
infix >>|
fun withA >>| withB =
let
exception Body of exn
in
fn body =>
withA (fn a =>
body a
handle e => raise Body e)
handle Body e => raise e
| _ => withB body
end
Other variations might also be useful. For instance, it would
probably make sense to introduce a choice combinator that allows one
to specify (using a predicate) whether to try the alternative or not
based on the failure exception.
-Vesa Karvonen