In our Haskell Web Series, we go over the basics of how we can build a web application with Haskell. That includes using Persistent for our database layer, and Servant for our HTTP layer. But these aren’t the only libraries for those tasks in the Haskell ecosystem.
We’ve already looked at how to use Beam as another potential database library. In these next two articles, we’ll examine Spock, another HTTP library. We’ll compare it to Servant and see what the different design decisions are. We’ll start this week by looking at the basics of routing. We’ll also see how to use a global application state to coordinate information on our server. Next week, we’ll see how to hook up a database and use sessions.
For some useful libraries, make sure to download our Production Checklist. It will give you some more ideas for libraries you can use even beyond these! Also, you can follow along the code here by looking at our Github repository!
Spock gives us a helpful starting point for making a basic server. We’ll begin by taking a look at the starter code on their homepage. Here’s our initial adaptation of it:
data MySession = EmptySessiondata MyAppState = DummyAppState (IORef Int)
main :: IO ()main = do ref <- newIORef 0 spockConfig <- defaultSpockCfg EmptySession PCNoDatabase (DummyAppState ref) runSpock 8080 (spock spockConfig app)
app :: SpockM () MySession MyAppState ()app = do get root $ text "Hello World!" get ("hello" <//> var) $ \name -> do (DummyAppState ref) <- getState visitorNumber <- liftIO $ atomicModifyIORef' ref $ \i -> (i+1, i+1) text ("Hello " <> name <> ", you are visitor number " <> T.pack (show visitorNumber))
In our main function, we initialize an IO ref that we’ll use as the only “state” of our application. Then we’ll create a configuration object for our server. Last, we’ll run our server using our app
specification of the actual routes.
The configuration has a few important fields attached to it. For now, we’re using dummy values for all these. Our config wants a Session
, which we've defined as EmptySession
. It also wants some kind of a database, which we'll add later. Finally, it includes an application state, and for now we'll only supply our pointer to an integer. We'll see later how we can add a bit more flavor to each of these parameters. But for the moment, let's dig a bit deeper into the app
expression that defines the routing for our Server.
Our router lives in the SpockM
monad. We can see this has three different type parameters. Remember the defaultSpockConfig
had three comparable arguments! We have the empty session as MySession
and the IORef
app state as MyAppState
. Finally, there's an extra ()
parameter corresponding to our empty database. (The return value of our router is also ()
).
Now each element of this monad is a path component. These path components use HTTP verbs, as you might expect. At the moment, our router only has a couple get
routes. The first lies at the root
of our path, and outputs Hello World!
. The second lies at hello/{name}
. It will print a message specifying the input name while keeping track of how many visitors we've had.
Now let’s talk a little bit now about the structure of our router code. The SpockM
monad works like a Writer
monad. Each action we take adds a new route to the application. In this case, we take two actions, each responding to get
requests (we'll see an example of a post
request next week).
For any of our HTTP verbs, the first argument will be a representation of the path. On our first route, we use the hard-coded root
expression to refer to the /
path. For our second expression, we have a couple different components that we combine with <//>
.
First, we have a string path component hello
. We could combine other strings as well. Let's suppose we wanted the route /api/hello/world
. We'd use the expression:
"api" <//> "hello" <//> "world"
In our original code though, the second part of the path is a var
. This allows us to substitute information into the path. When we visit /hello/james
, we'll be able to get the path component james
as a variable. Spock passes this argument to the function we have as the second argument of the get
combinator.
This argument has a rather complicated type RouteSpec
. We don't need to go into the details here. But the simplest thing we can return is some raw text by using the text
combinator. (We could also use html
if we have our own template). We conclude both our route definitions by doing this.
Notice that the expression for our first route has no parameters, while the second has one parameter. As you might guess, the parameter in the second route refers to the variable we can pull out of the path thanks to var
. We have the same number of var
elements in the path as we do arguments to the function. Spock uses dependent types to ensure these match.
Now that we know the basics, let’s start using some of Spock’s more advanced features. This week, we’ll see how to use the App State.
Currently, we bump the visitor count each time we visit the route with a name, even if that name is the same. So visiting /hello/michael
the first time results in:
Hello michael, you are visitor number 1
Then we’ll visit again and see:
Hello michael, you are visitor number 2
Instead, let’s make it so we assign each name to a particular number. This way, when a user visits the same route again, they’ll see what number they originally were.
Making this change is rather easy. Instead of using an IORef
on an Int
for our state, we'll use a mapping from Text
to Int
:
data AppState = AppState (IORef (M.Map Text Int))
Now we’ll initialize our ref with an empty map and pass it to our config:
main :: IO ()main = do ref <- newIORef M.empty spockConfig <- defaultSpockCfg EmptySession PCNoDatabase (AppState ref) runSpock 8080 (spock spockConfig app)
And for our hello/{name}
route, we'll update it to follow this process:
IORef
This process is pretty straightforward. Let’s see what it looks like:
app :: SpockM () MySession AppState ()app = do get root $ text "Hello World!" get ("hello" <//> var) $ \name -> do (AppState mapRef) <- getState visitorNumber <- liftIO $ atomicModifyIORef' mapRef $ updateMapWithName name text ("Hello " <> name <> ", you are visitor number " <> T.pack (show visitorNumber))
updateMapWithName :: T.Text -> M.Map T.Text Int -> (M.Map T.Text Int, Int)updateMapWithName name nameMap = case M.lookup name nameMap of Nothing -> (M.insert name (mapSize + 1) nameMap, mapSize + 1) Just i -> (nameMap, i) where mapSize = M.size nameMap
We create a function to update the map every time our app encounters a new name. The we update our IORef
with atomicModifyIORef
. And now if we visit /hello/michael
twice in a row, we'll get the same output both times!
That’s as far as we’ll go this week! We covered the basics of how to make a basic application in Spock. We saw the basics of composing routes. Then we saw how we could use the app state to keep track of information across requests. Next week, we’ll improve this process by adding a database to our application. We’ll also use sessions to keep track of users.
For more cool libraries, read up on our Haskell Web Series. Also, you can download our Production Checklist for more ideas!