Monadic combinators for scoped resource management

64 views
Skip to first unread message

Vesa Karvonen

unread,
Jan 26, 2006, 8:00:10 PM1/26/06
to

This article briefly discusses a simple technique that I just discovered,
have not seen previously documented, and I believe might interest others.
I would be interested to know of prior art if it exist.

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

Message has been deleted
Message has been deleted

Tomasz Zielonka

unread,
Jan 27, 2006, 2:14:19 AM1/27/06
to
Vesa Karvonen wrote:
> 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).

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

Vesa Karvonen

unread,
Jan 27, 2006, 2:51:41 AM1/27/06
to
Tomasz Zielonka <tomasz....@gmail.com> wrote:
[...]

> 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.

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

Vesa Karvonen

unread,
Jan 27, 2006, 2:56:36 AM1/27/06
to
Stefan Ram <r...@zedat.fu-berlin.de> wrote:
[...]
> I wrote macros, so that one can write the following
> to open to files and allocate memory:

> 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

Tomasz Zielonka

unread,
Jan 27, 2006, 6:39:13 AM1/27/06
to
Vesa Karvonen wrote:
> Tomasz Zielonka <tomasz....@gmail.com> wrote:
> [...]
>> 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.
>
> That's true. Still, the monad operations allow you to combine such
> "with" -functions easily.

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.

vesa.k...@cs.helsinki.fi

unread,
Jan 27, 2006, 7:41:33 AM1/27/06
to
Tomasz Zielonka kirjoitti:

> Vesa Karvonen wrote:
> > Tomasz Zielonka <tomasz....@gmail.com> wrote:
> > [...]
> >> Haskell's lightweight syntax and the ($) operator can help here.
[...]

> > That's true. Still, the monad operations allow you to combine such
> > "with" -functions easily.
>
> 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

Philippa Cowderoy

unread,
Jan 27, 2006, 2:23:18 PM1/27/06
to
On Fri, 27 Jan 2006 vesa.k...@cs.helsinki.fi wrote:

> 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.

--
fli...@flippac.org

The task of the academic is not to scale great
intellectual mountains, but to flatten them.

Lauri Alanko

unread,
Jan 27, 2006, 3:38:50 PM1/27/06
to
Cool. It's really just a CPS monad, of course, but a neat (and only in
hindsight obvious) application. Here's a sketch in Haskell:

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

Vesa Karvonen

unread,
Jan 29, 2006, 4:16:15 AM1/29/06
to
Philippa Cowderoy <fli...@flippac.org> wrote:
> On Fri, 27 Jan 2006 vesa.k...@cs.helsinki.fi wrote:

> > 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

Reply all
Reply to author
Forward
0 new messages