Skip to content
October 12, 2010 / cdsmith

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.

One Comment

Leave a Comment
  1. Jeremy Shaw / Oct 13 2010 6:58 pm

    Yeah, I always thought that requiring the use of RqData was a bit wonky. So I removed that requirement :) But RqData does still have its uses. I wrote a blog post about it here:

    Back in the early days of HAppS/happstack, routing was done in a very structured manner.

    1. use the ‘dir’, ‘path’, etc, combinators to route based on the pathInfo portion of the URL
    2. use ‘withDataFn’ to extract the query parameters / form data
    3. use ‘method’ to specify which request method you are matching on (GET, POST, etc)
    4. generate your Response in the WebT monad

    These days that structure is not enforced, except we still had RqData hanging around. Now RqData is around, but optional. You can just do everything in the ServerPart monad. If you want to generate your Response, then look at the query parameters, then check the request method, and finally examine the pathInfo, you can do it that way. ;)

    One thing that is a little wonky to implement in ServerPart monad (but not RqData) is the localRqEnv function:

    localRqEnv :: (RqEnv -> RqEnv) -> m a -> m a

    This is used with (new) filters like ‘body’ and ‘queryString’ which limit the scope of where look searches. But, as a library user, you don’t need to know that it is wonky on the inside :)

    – jeremy

Leave a Reply

Fill in your details below or click an icon to log in: Logo

You are commenting using your account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: