The Snap Framework for web applications in Haskell

Gregory Collins
Google Switzerland

CUFP 2011, Tokyo, Japan

Friday, September 23, 2011

Introduction

In this workshop, we’ll be creating a web application in Haskell using the Snap Framework.

Introduction

We’ll be writing a simple chat room application using long-polling, XMLHttpRequest, and JSON.

The data model/chat room logic and the front-end HTML/JavaScript have been provided for you.

Introduction

You’ll be writing:

Introduction

There will be some lecture in this tutorial session, but I’m intending for this workshop to be pretty hands-on:

A quick survey

Prerequisites

Install GHC and the Haskell Platform:

www.haskell.org/platform

Prerequisites

Install the Snap Framework:

$ cabal update
$ cabal install snap

Prerequisites

Download and install the tutorial code:

github.com/snapframework/cufp2011

Introduction, HTTP

First, a quick refresher about HTTP: Clients connect to the server’s listening port, and send an HTTP request:

POST / HTTP/1.1
User-Agent: curl/7.19.7 ....
Host: localhost:8000
Accept: */*
Content-Length: 5
Content-Type: text/plain

Hello

Introduction, HTTP

The server responds with a response:

HTTP/1.1 200 OK
Accept-Ranges: bytes
Content-Type: text/plain
Content-Length: 11
Date: Sun, 18 Sep 2011 23:49:05 GMT
Server: Snap/0.5.4

...data...

Parts of HTTP Requests

Parts of HTTP Responses

Exploring the Snap API

OK, so now let’s take a look at the Snap API. The Snap API docs are available on Hackage (http://hackage.haskell.org/package/snap-core), with most of the core stuff in the Snap.Types module.

So what’s in a Request?

Well, basically the stuff we mentioned earlier.

And what’s in a Response?

The Snap Monad

The Snap monad is essentially a state monad over (Request,Response) (although there are some other things in there too).

Some things you can do in the Snap Monad:

State for Requests and Responses

Statefully fetch or modify a Request or Response:

foo :: Snap ()
foo = do
rq <- getRequest
modifyResponse $ setHeader "foo" "bar"
putRequest $ rqSetParam "param" ["value"]

“Alternative” semantics

Get failure/Alternative/MonadPlus semantics: a Snap handler can choose not to handle a given request, using empty or its synonym pass, and you can try alternative handlers with the <|> operator:

a :: Snap Int
a = pass

b :: Snap Int
b = return 7

c :: Snap Int
c = a <|> b -- try running a, if it fails then try b

“Alternative” semantics

These semantics are really useful for guarding against certain preconditions in your handlers, for example:

foo :: Snap ()
foo = method GET getFoo <|>
method POST postFoo <|>
method PUT putFoo

Early termination

The Snap monad also supports early termination using finishWith; if you know you want to respond with a specific request, terminating all other processing, you can do that. See, for example, this function from the Snap core library:

redirect' :: MonadSnap m => ByteString -> Int -> m a
redirect' target status = do
r <- getResponse

finishWith
$ setResponseCode status
$ setContentLength 0
$ modifyResponseBody (const $ enumBuilder mempty)
$ setHeader "Location" target r

Access to IO

The Snap monad is an instance of MonadIO, so you can use liftIO to run things from the IO monad:

foo :: Snap ()
foo = liftIO fireZeMissiles

Setting timeouts

You can set a timeout which will kill the handler thread after n seconds of inactivity:

foo :: Snap ()
foo = setTimeout 30

Writing output

The Snap core library also gives you convenience functions for queueing output to be written to the ‘Response’:

foo :: (forall a . Enumerator a) -> Snap ()
foo someEnumerator = do
writeBS "I'm a strict bytestring"
writeLBS "I'm a lazy bytestring"
writeText "I'm strict text"
addToOutput someEnumerator

Speaking of input/output: the Snap HTTP lifecycle

Snap HTTP lifecycle

Handing an HTTP transaction in Snap is broken up into phases:

Understanding when data is sent

Important: data is not sent right away!

Although you might call writeText to ask for some text to be written to the output, this data is not written immediately.

Instead, you’re building up a function that will be run to generate the response body after the user handler finishes.

(We use the enumerator/iteratee model to do this.)

Request URI routing

Snap gives you a function called dir that matches a prefix of the request URI (per directory/path component, i.e. parts delimited by “/”), and fails if the path doesn’t match:

foo :: Snap ()
foo = dir "foo/bar" doSomething

Request URI routing

Snap also gives you a function called route that takes a list of (path, handler) pairs and gives you an efficient routing table. If you prefix a part of the routed path with a colon, it “captures” this part of the URI as a query parameter:

foo :: Snap ()
foo = route [ ("article", renderIndex)
, ("article/:id", renderArticle)
, ("login", method POST doLogin) ]

Request URI routing

Paths in Snap Requests are broken up into parts depending on how you have matched or processed them:

Request URI routing: example

Let’s say the user requested /foo/bar/baz?q=hello+world, and control passes from the toplevel into this handler:

foo :: Snap ()
foo = dir "foo/bar" doSomething

Request URI routing: example

Within doSomething:

Other core Snap facilities

You can serve files from the disk using Snap.Util.FileServe, and you can handle file uploads using Snap.Util.FileUploads.

Exercise 1

Let’s make sure our Snap installations are working and we understand Snap’s path routing. First, initialize a barebones project:

$ mkdir snap-test-project
$ cd snap-test-project
$ snap init -b

Exercise 1

This command should generate some files in the snap-test-project directory:

snap-test-project.cabal
src/Main.hs

Running cabal install in this directory should produce a binary called dist/build/snap-test-project/snap-test-project which you can run to get a working web server.

Edit src/Main.hs to add a handler to the routing table to do something simple (up to you).

Part 2: JSON serialization/deserialization

The application we’ll be building uses JSON apis on the backend to handle the “business logic” for the chat room.

We’ll be using Bryan O’Sullivan’s excellent aeson (http://hackage.haskell.org/package/aeson) library to encode/decode JSON to/from Haskell datatypes.

Let’s take a look at the aeson API now, starting with Data.Aeson.

Representation of JSON Values

JSON has six different data types:

/* null */
null

/* Booleans */
true
false

/* String literals */
"foo"

Representation of JSON Values

/* Numbers (integer and floating point) */
1
1.2

/* Arrays */
[ 1, 2, 3, 4 ]

/* objects (key-value maps) */
{
"foo": "bar",
"baz": "quux"
}

Aeson representation of the JSON types

-- A JSON "object" (key/value map).
type Object = Map Text Value

-- A JSON "array" (sequence).
type Array = Vector Value

-- A JSON value represented as a Haskell value.
data Value = Object Object
| Array Array
| String Text
| Number Number
| Bool !Bool
| Null

Serializing to/from Haskell datatypes

OK, so we can parse a JSON string and produce a Haskell value in JSON’s abstract syntax. What we want, however, is to go to/from user-defined datatypes and the JSON representation. How do we do this?

class ToJSON a where
toJSON :: a -> Value

class FromJSON a where
parseJSON :: Value -> Parser a

Example: ToJSON instance

Consider the following Haskell datatype:

data Coord = Coord { x :: Double, y :: Double }

We can use the “object :: [Pair] -> Value” helper function (where type Pair = (Text,Value)) to produce a JSON object in abstract syntax from this:

instance ToJSON Coord where
toJSON (Coord x y) = object ["x" .= x, "y" .= y]

Example: FromJSON instance

For parsing from JSON abstract syntax into a Haskell datatype, aeson provides you with a Parser type with Applicative/Monad/MonadPlus instances. We can use it with some of the aeson convenience functions to easily convert JSON to our datatype:

-- Note: (.:) :: FromJSON a => Object -> Text -> Parser a
instance FromJSON Coord where
parseJSON (Object v) = Coord <$>
v .: "x" <*>
v .: "y"

-- A non-Object value is of the wrong type, so use mzero to fail.
parseJSON _ = mzero

Example: putting it together

See examples/JsonExample.hs in the CUFP 2011 tutorial code for a working example of converting to/from ByteStrings:

example1 :: ByteString -> Either String Coord
example1 bs = parseOnly json bs >>= convert
where
convert value = case fromJSON value of
(Error e) -> Left e
(Success a) -> Right a

example2 :: Coord -> ByteString
example2 c = S.concat $ L.toChunks $ encode c

A tour of the chat room code

To follow along with the source code, visit https://github.com/snapframework/cufp2011 (or open it in your text editor).

You can also build the API docs using cabal haddock, which will put them in dist/doc/html/snap-chat/index.html.

If you get stuck…

Exercise 2: Implement ToJSON / FromJSON instances

The specifications for the JSON API you’ll need to implement are included in the cufp2011 repository’s README.md file (see also the github project page).

The ToJSON/FromJSON instances for the chat app have been stubbed out for you; you just need to implement them according to the specification. The places where you need to do so are marked with “toBeImplemented”:

$ find src -name '*hs' | xargs grep -l toBeImplemented
src/Snap/Chat/API/Handlers.hs
src/Snap/Chat/Internal/API/Types.hs
src/Snap/Chat/Internal/Types.hs

Exercise 2: Implement ToJSON / FromJSON instances

The tutorial has a test suite that will check that your JSON instances are correct; to run it:

$ cd test
$ cabal install
$ ./runTestsAndCoverage.sh

Part 3: Handling the JSON API

The Snap Chat API works by having clients POST JSON messages in the pre-determined schema to the URL endpoints /api/join, /api/write, etc.

Once authenticated, users are identified by an encrypted token which gets passed back to the API verbatim by the Javascript frontend. To accomplish this cleanly, we’ll use the clientsession (http://hackage.haskell.org/package/clientsession) library to encrypt/decrypt some JSON-encoded session data.

Exercise 3: Finish the project

Now that you’ve (hopefully) correctly implemented the JSON instances, the only thing that remains is to complete the code in src/Snap/Chat/API/Handlers.hs.

Again, the places where you need to write code are marked by toBeImplemented.

Thank you!

Gregory Collins
Google Switzerland

CUFP 2011, Tokyo, Japan

Friday, September 23, 2011