Why would you use ContT?

The ContT transformer is one of the more exotic and less used transformers provided by mtl. The Control.Monad.Cont module now even includes the following warning:

Before using the Continuation monad, be sure that you have a firm understanding of continuation-passing style and that continuations represent the best solution to your particular design problem. Many algorithms which require continuations in other languages do not require them in Haskell, due to Haskell’s lazy semantics. Abuse of the Continuation monad can produce code that is impossible to understand and maintain.

So what is ContT, and when does it represent the best solution to a problem?

Consider the following three functions from the base library:

openFile :: FilePath -> IOMode -> IO Handle takeMVar :: MVar a -> IO a newArray :: Storable a => [a] -> IO ( Ptr a) [a]a)

They are all “unsafe” in the sense that they return something to you but don’t clean up after you: don’t close the file you’ve just opened, put back the MVar you’ve just taken, or free the array’s memory. If you forget to perform the cleanup, it’s on you.

Indeed, a function that closed the file right after opening it would be rather useless. Therefore, a “safer” function should know what you intend to do with the file handle and insert that action between opening and closing the file.

The base library provides the following safer versions of the above functions:

{-~~~~~~~~~~~~~~~~~~~~~~~~-} withFile :: FilePath -> IOMode -> ( Handle -> IO r) -> IO r r) withMVar :: MVar a -> (a -> IO r) -> IO r (ar) withArray :: Storable a => [a] -> ( Ptr a -> IO r) -> IO r [a]r) {-~~~~~~~~~~~~~~~~~~~~~~~~-}

Notice how these functions follow the same pattern and how they relate to their unsafe versions: if the unsafe function returned IO a , then the corresponding safe function takes an additional argument of the form (a -> IO r) and returns IO r . This style of writing functions is called the continuation-passing style (CPS), and the argument a -> IO r is called the continuation.

ContT gives this pattern its own type:

newtype ContT r m a = ContT { runContT :: (a -> m r) -> m r } r m a(am r)m r }

(In this article, the underlying monad m will always be IO .)

You can construct ContT functions from the functions written in the CPS:

-> ContT (withMVar v) :: MVar a -> ContT r IO a \v(withMVar v) -> ContT (withFile f m) :: FilePath -> IOMode -> ContT r IO Handle \f m(withFile f m) -> ContT (withArray l) :: Storable a => [a] -> ContT r IO ( Ptr a) \l(withArray l)[a]a)

Notice how the types of the safe functions, once wrapped in ContT , become even more similar to the types of their non-safe versions: the only difference is ContT r IO instead of IO in the return type. And because ContT turns out to be a monad transformer, we can use and combine the safe function in all the same way as we could with the unsafe functions—getting the added safety essentially for free. Once you no longer need the resources you’ve allocated inside ContT , you turn it to IO using runContT , and when you exit a ContT block, all the cleanup actions are run—in the reverse order, of course.

Let’s consider some practical examples.

llvm-hs

In the llvm-hs package’s API, the continuation-passing style is used to reliably deallocate the allocated LLVM structures and classes, which are written in C++.

Here’s an example from llvm-hs’s test suite, reformatted such that each nesting level gets its own indentation level:

"eager compilation" $ do testCase <- newIORef Map.empty resolversnewIORef Map.empty $ \ mod -> withTestModule $ \tm -> withHostTargetMachine\tm $ \es -> withExecutionSession\es -> fmap (\rs -> rs Map.! k) (readIORef resolvers)) $ \linkingLayer -> withObjectLinkingLayer es (\k(\rsrsk) (readIORef resolvers))\linkingLayer $ \compileLayer -> do withIRCompileLayer linkingLayer tm\compileLayer <- mangleSymbol compileLayer "testFunc" testFuncmangleSymbol compileLayer $ \k -> withModuleKey es\k SymbolResolver (resolver testFunc compileLayer)) $ \resolver -> do withSymbolResolver es ((resolver testFunc compileLayer))\resolver modifyIORef' resolvers (Map.insert k resolver) mod $ do withModule compileLayer k <- mangleSymbol compileLayer "main" mainSymbolmangleSymbol compileLayer Right ( JITSymbol mainFn _) <- CL.findSymbol compileLayer mainSymbol True mainFn _)CL.findSymbol compileLayer mainSymbol <- mkMain (castPtrToFunPtr (wordPtrToPtr mainFn)) resultmkMain (castPtrToFunPtr (wordPtrToPtr mainFn)) @?= 42 result Right ( JITSymbol mainFn _) <- CL.findSymbolIn compileLayer k mainSymbol True mainFn _)CL.findSymbolIn compileLayer k mainSymbol <- mkMain (castPtrToFunPtr (wordPtrToPtr mainFn)) resultmkMain (castPtrToFunPtr (wordPtrToPtr mainFn)) @?= 42 result <- mangleSymbol compileLayer "unknownSymbol" unknownSymbolmangleSymbol compileLayer <- CL.findSymbol compileLayer unknownSymbol True unknownSymbolResCL.findSymbol compileLayer unknownSymbol @?= Left ( JITSymbolError mempty ), unknownSymbolRes),

And here’s the same test rewritten with ContT :

"eager compilation" $ testCase flip runContT return $ do runContT <- liftIO $ newIORef Map.empty resolversliftIOnewIORef Map.empty mod <- ContT withTestModule withTestModule <- ContT withHostTargetMachine tmwithHostTargetMachine <- ContT withExecutionSession eswithExecutionSession <- ContT $ withObjectLinkingLayer es (\k -> fmap (\rs -> rs Map.! k) (readIORef resolvers)) linkingLayerwithObjectLinkingLayer es (\k(\rsrsk) (readIORef resolvers)) <- ContT $ withIRCompileLayer linkingLayer tm compileLayerwithIRCompileLayer linkingLayer tm <- liftIO $ mangleSymbol compileLayer "testFunc" testFuncliftIOmangleSymbol compileLayer k <- ContT $ withModuleKey es withModuleKey es <- ContT $ withSymbolResolver es ( SymbolResolver (resolver testFunc compileLayer)) resolverwithSymbolResolver es ((resolver testFunc compileLayer)) $ modifyIORef' resolvers (Map.insert k resolver) liftIOmodifyIORef' resolvers (Map.insert k resolver) ContT $ \c -> withModule compileLayer k mod (c ()) \cwithModule compileLayer k(c ()) -- All functions below are non-CPS, so we combine them into a single IO block -- instead of applying liftIO on every line. $ do liftIO <- mangleSymbol compileLayer "main" mainSymbolmangleSymbol compileLayer Right ( JITSymbol mainFn _) <- CL.findSymbol compileLayer mainSymbol True mainFn _)CL.findSymbol compileLayer mainSymbol <- mkMain (castPtrToFunPtr (wordPtrToPtr mainFn)) resultmkMain (castPtrToFunPtr (wordPtrToPtr mainFn)) @?= 42 result Right ( JITSymbol mainFn _) <- CL.findSymbolIn compileLayer k mainSymbol True mainFn _)CL.findSymbolIn compileLayer k mainSymbol <- mkMain (castPtrToFunPtr (wordPtrToPtr mainFn)) resultmkMain (castPtrToFunPtr (wordPtrToPtr mainFn)) @?= 42 result <- mangleSymbol compileLayer "unknownSymbol" unknownSymbolmangleSymbol compileLayer <- CL.findSymbol compileLayer unknownSymbol True unknownSymbolResCL.findSymbol compileLayer unknownSymbol @?= Left ( JITSymbolError mempty ), unknownSymbolRes),

Most of the nesting and indentation is gone. The code is completely linear now, just as if we were using the unsafe, non-CPS functions. Applying ContT on every line is a bit awkward, but that would be gone if the library’s API was designed so that the CPS functions were already in the ContT monad.

Reading from a list of files

In the above example, we had merely a high nesting level. But what if the nesting level is unknown in advance?

Consider a program that takes a list of files and prints them interleaved—a line from the first time, then a line from the second, and so on, printing the line m+1 of the first file after the line m of the last file.

To implement such a program, we need to keep all the n files open all the time. If we want to use the safe withFile function, we need to nest it n times, where n is not known until the program is run. How is that even possible?

Well, it is possible with ContT .

import Control.Monad.Cont import System.IO import System.Environment = flip runContT return $ do mainrunContT <- liftIO getArgs argsliftIO getArgs <- forM args $ \arg -> handlesforM args\arg ContT $ withFile arg ReadMode withFile arg $ print_interleaved_from_handles handles liftIOprint_interleaved_from_handles handles print_interleaved_from_handles :: [ Handle ] -> IO () () = ... print_interleaved_from_handles

Allocating a linked list

This example is similar to the previous one, except it actually occurred in my practice. See Way 2 in 6 ways to manage allocated memory in Haskell.