The MonadPrompt package is tricky to understand, as it uses a continuation-based representation. This post explains the Prompt monad with a clearer (though less efficient) representation.
Introduction to Prompt
When programming in Haskell, you want to keep code as pure as possible. This may be dogmatic, but it's valuable, and doesn't come automatically. Consider a simple chat server, where the code to handle a client has type:
serve :: ServerEnv -> ClientInfo -> IO ()
serve, and using it correctly, can be quite daunting, as several factors come into play:
Concurrency - When the client sends us a message, we have to notify all the other server threads. When someone else sends us a message, we have to be notified.
Exception safety - When the connection dies, we have to remember to remove our client's entry from the server's list of clients. If another thread might kill this one (e.g. a user connects from somewhere else), we have to make sure that we are prepared to receive an exception at any point.
Windows - GHC currently does not have good IO support on Windows. If our server is running on Windows, and a client connection hangs, using
killThreadon the thread blocked on IO will not kill it. Quite the opposite: the thread calling
killThreadwill block, too!
All of these problems are IO-related. Now consider a simpler example, a web server whose main function is:
serve :: Request -> Response
This is much easier to deal with. When implementing
serve, we don't have to worry about concurrency, exception safety, or Windows. When calling
serve, we don't have to worry about it hanging due to a bad network connection 1.
But what if
serve needs to read a file from disk?
The idea behind the Prompt monad is to have a pure function return an action it would like the caller to perform. To illustrate the concept, let's extend
serve so it can ask to download a file:
serve :: Request -> Prompt data Prompt = ReadFile FilePath (String -> Prompt) | Answer Response
serve is allowed to "perform" a carefully-chosen set of IO actions, and the caller has full control over how it happens.
The Prompt monad
A Prompt monad is described in The Monad.Reader Issue 15, and implemented in the MonadPrompt package. It handles the plumbing for us, and lets us define a type for requests. Each request may have a different return type, so we'll want to define our request type as a GADT:
data Request a where GetLine :: Request String GetChar :: Request Char PutStrLn :: String -> Request () Print :: Show a => a -> Request ()
If you are not familiar with GADTs, see the Monad.Reader article for more information.
Here's one possible implementation of the Prompt monad:
data Prompt p r = forall a. Ask (p a) (a -> Prompt p r) | Answer r instance Monad (Prompt p) where return = Answer Ask req cont >>= k = Ask req (\ans -> cont ans >>= k) Answer x >>= k = k x prompt :: p a -> Prompt p a prompt req = Ask req Answer runPromptM :: Monad m => (forall a. p a -> m a) -> Prompt p r -> m r runPromptM perform (Ask req cont) = perform req >>= runPromptM perform . cont runPromptM _ (Answer r) = return r
Prompt p r computation either has a value
r, or needs some action
p a to be performed before it can continue:
This is in fact the original formulation of Prompt. Unfortunately, left recursion of >>= takes quadratic time to evaluate (thanks for pointing this out, ryani).
Let's compare the formulation above to MonadPrompt's
runPromptC function, and see how we'd implement it in terms of
runPromptC :: (r -> b) -> (forall a . p a -> (a -> b) -> b) -> Prompt p r -> b runPromptC onAnswer onAsk p = case p of Ask req cont -> onAsk req $ runPromptC onAnswer onAsk . cont Answer r -> onAnswer r
The interesting wrinkle is that we embed a recursive call inside of the continuation passed to
onAsk. Thus, the caller does not have to do its own recursion. This simplifies the implementation of
runPromptM, for example:
runPromptM :: Monad m => (forall a . p a -> m a) -> Prompt p r -> m r runPromptM prm = runPromptC return (\p cont -> prm p >>= cont)
So while MonadPrompt's continuation-based representation may be harder to understand at first, it is more efficient, and perhaps even easier to use.
MonadPrompt uses a continuation-based representation to avoid a performance issue. The Ask/Answer formulation, however, may be easier to understand.
A library implementing a network protocol should export a pure API (perhaps in addition to impure convenience functions), so the user can deal with concurrency, exception safety, and Windows on their own terms.
serve :: Request -> Responsemight hang if the
Requestcontains lazy IO. In this case, it's not
serve's fault, but the fault of whatever is producing the data.↩