With MonadIO

January 13, 2010

GravatarMichael Snoyman

I've been bitten by this one before: I'd like to use a monad transformer, and I'd like to use a "with" function. In this case, we'll give the example of withCString for creating C-style strings from [Char]s. The function signature is:

withCString :: String -> (CString -> IO a) -> IO a

However, I don't have an IO monad. In this case, let's say I have a "ReaderT Parser IO". No problem, we'll just apply a liftIO... somewhere... and...

This is a little tricky in fact. What we want here is a function of type signature:

MonadIO m => String -> (CString -> m a) -> m a

Unfortunately, there's no way to create it automatically. Now, for the withCString function, we could go ahead and implement things ourselves. But what about allocaBytes, which does some fancy magic under the hood?

Dealing with ReaderT

Before dealing with a generic solution, let's first play around with ReaderT and see what we come up with. Let's assume that we'll curry away the first argument to withCString, so we're left with a function with the type signature:

(CString -> IO a) -> IO a

We need to produce a function with the type signature:

(CString -> ReaderT Parser IO a) -> ReaderT Parser IO a

Turns out this function isn't too bad. We'll call it withReader (I wouldn't mind name suggestions):

withReader :: ((CString -> IO a) -> IO a) -> (CString -> ReaderT Parser IO a) -> ReaderT Parser IO a
withReader orig f = ReaderT $ \parser -> do
    let f' a = (runReaderT $ f a) parser
    orig f'

You can call this function like such:

someOtherFunction :: CString -> ReaderT Parser IO SomeOutput
...
withReader (withCString "foobar") someOtherFunction :: ReaderT Parser IO SomeOutput

More general

Well, that solved the problem. That is, until we want to be able to stack our monads. For example, let's say (for simplicity) that I wanted to have the following monad:

ReaderT Parser (ReaderT Emitter IO)

(In case you're wondering, this is code used for interleaving parsing and emitting of a YAML document. See my previous blog post for more context.)

In any event, we could write a withReaderReader function, but instead I smell a great opportunity for a typeclass:

class With m where
    with :: ((a -> IO b) -> IO b) -> (a -> m b) -> m b

I currently only have two instances of this class, but I'm sure other monads could be easily added:

instance With IO where -- obvious instance
    with = id
instance With m => With (ReaderT r m) where
    with orig f = ReaderT $ \r -> do
        let f' a = (runReaderT $ f a) r
        with orig f'

Improvements

The number one improvement I need is a better name for this beast. After that, I'd like to add some more methods to handle even more useful functions, such as finally. I know I've been caught in the past wanting to ensure resource release when dealing with a monad stack.

This code (with slightly different names) is part of my yaml github repo. If there's demand, I'll split it into its own package.

Comments

comments powered by Disqus

Archives