Request Handling in Snap and Happstack: An Interesting Observation

The way this story turns out was surprising to me, so I’m writing to share it.

I’ve written a fair number of web applications using Happstack. In Happstack, to oversimplify a bit, a handler for an HTTP request has type

myHandler :: ServerPart Response

If I want to make use of information from the request URI, a common way to do that (at least the one I always do) is to use the following function (again, simplifying a bit)

withDataFn :: RqData a -> (a -> ServerPart r) -> ServerPart r

The first parameter is actually a monad that can be used to build up fair complex ways of getting data from the parameters and cookies in the request.

type RqData a = ReaderT ([(String, Input)], [(String, Cookie)]) Maybe a

So given access to a list of all the request parameters and all the cookies, this might be able to get a value (or if the desired cookies or parameters are missing, the result is just Nothing). Now, most of the time I don’t build really complicated things in this monad. Instead, there are the “primitives”

look :: String -> RqData String lookCoookie :: String -> RqData Cookie

so my handler might look something like

profileHandler = withDataFn (look "pid") $ \pid -> ...

And voila, I’m done. The monad could be helpful, though, if there is a set of fields that often go together. Suppose I have address forms in several places around my web application. Great, I can build this as follows

data Address = Address { street :: String, city :: String, country :: String, postalCode :: String } addressData :: RqData Address addressData = do street <- look "street" city <- look "city" country <- look "country" postalCode <- look "postalCode" return (Address street city country postalCode)

Now my handlers are all just

addAddrHandler = withDataFn addressData $ \addr -> ...

This is rather convenient for handling form submissions and the like. So now that I’m looking at building an application in Snap, I looked for something similar. Well, it doesn’t seem to exist. Fine, I said to myself, I’ll write it myself.

I noticed from the very beginning (from past experience in Happstack) that I wanted to make at least one change. Namely, I wanted to give my new RqData access to the entire request, including the request headers. I’ve needed this in the past for implementing a replacement for a web service where the way data was stored in the parameters was described by request headers. So a first attempt was this.

type RqData a = ReaderT Request Maybe a withDataFn :: RqData a -> (a -> Snap b) -> Snap b withDataFn rqd fn = maybe mzero fn . runReaderT rqd =<< getRequest look :: ByteString -> RqData ByteString look name = maybe mzero (return . head) . M.lookup name =<< fmap rqParams ask

That does it. Implementation of equivalent cookie and header primitives is left as an exercise for the reader. (Yes, I’m golfing these functions a bit to turn them into one-liners in the interest of making a point… but they still aren’t all that unreadable.)

An interesting insight, though, is that the Snap monad (and Happstack’s ServerPart monad, too) already acts as a reader monad for the request, and also provides for failure a la the Maybe monad. So we can eliminate a type, at the expense of allowing people to write pathological “RqData” values that modify the response, do I/O, or finish early with a given response. If we are willing to just trust the user not to do these things (or, in the case of I/O, perhaps to know what they are doing and decide to do it anyway — now looking up a code in the database can be part of looking up request data), we can proceed. The simplification looks like this for our venerable withDataFn:

withDataFn :: Snap a -> (a -> Snap b) -> Snap b

Wait a second, that type signature sure looks familiar! Indeed, the correct implementation is

withDataFn = (>>=)

The other “look” style primitives are not quite so straightforward, just because Snap made arbitrary choices that don’t agree with the way we’ve defined things. But here’s one:

look :: ByteString -> Snap ByteString look name = getParam name >>= maybe mzero return

The only difference between look and getParam from the Snap core library is that getParam uses an explicit Maybe, while we want to just pass on handling the request in the Snap monad.

To be fair, this isn’t all there is to the use of RqData in Happstack. There’s also a typeclass, and a default RqData object that can be defined per type. At the same time, though, I see no reason that can’t be done using the ServerPart monad instead of RqData (or, in the Snap world, the Snap monad). The lack of compile-time enforcement that data extraction is free from side effects could also be useful. But in my mind, eliminating a commonly used data type and turning a commonly used function into the (>>=) operator is a simplification worth that cost.

I’m suddenly a lot more comfortable with Snap’s lack of copious numbers of combinators and structures for handling requests. When I can do the same things with fewer operations, that makes me happy.