In our , 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. Haskell Web Series We’ve already looked at how to use Beam as another potential database library. In these next two articles, we’ll examine , 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. Spock For some useful libraries, make sure to download our . 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 ! Production Checklist Github repository Getting Started Spock gives us a helpful starting point for making a basic server. We’ll begin by taking a look at the on their homepage. Here’s our initial adaptation of it: starter code 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 specification of the actual routes. app The configuration has a few important fields attached to it. For now, we’re using dummy values for all these. Our config wants a , which we've defined as . 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 expression that defines the routing for our Server. Session EmptySession app The SpockM Monad Our router lives in the monad. We can see this has three different type parameters. Remember the had three comparable arguments! We have the empty session as and the app state as . Finally, there's an extra parameter corresponding to our empty database. (The return value of our router is also ). SpockM defaultSpockConfig MySession IORef MyAppState () () 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 routes. The first lies at the of our path, and outputs . The second lies at . It will print a message specifying the input name while keeping track of how many visitors we've had. get root Hello World! hello/{name} Composing Routes Now let’s talk a little bit now about the structure of our router code. The monad works like a monad. Each action we take adds a new route to the application. In this case, we take two actions, each responding to requests (we'll see an example of a request next week). SpockM Writer get post 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 expression to refer to the path. For our second expression, we have a couple different components that we combine with . root / <//> First, we have a string path component . We could combine other strings as well. Let's suppose we wanted the route . We'd use the expression: hello /api/hello/world "api" <//> "hello" <//> "world" In our original code though, the second part of the path is a . This allows us to substitute information into the path. When we visit , we'll be able to get the path component as a variable. Spock passes this argument to the function we have as the second argument of the combinator. var /hello/james james get This argument has a rather complicated type . We don't need to go into the details here. But the simplest thing we can return is some raw text by using the combinator. (We could also use if we have our own template). We conclude both our route definitions by doing this. RouteSpec text html 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 . We have the same number of elements in the path as we do arguments to the function. Spock uses dependent types to ensure these match. var var Using the App State 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 the first time results in: /hello/michael 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 on an for our state, we'll use a mapping from to : IORef Int Text 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 route, we'll update it to follow this process: hello/{name} Get the map reference See if we have an entry for this user yet. If not, insert them with the length of the map, and write this back to our IORef Return the message 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 with . And now if we visit twice in a row, we'll get the same output both times! IORef atomicModifyIORef /hello/michael Conclusion 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 . Also, you can download our for more ideas! Haskell Web Series Production Checklist