Latest news about Bitcoin and all cryptocurrencies. Your daily crypto news habit.
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!
Getting Started
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 appspecification 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.
The SpockMÂ Monad
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.
Composing Routes
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.
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 /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:
- 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 IORef with atomicModifyIORef. And now if we visit /hello/michael twice in a row, we'll get the same output both times!
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 Haskell Web Series. Also, you can download our Production Checklist for more ideas!
Simple Web Routing with Spock! was originally published in Hacker Noon on Medium, where people are continuing the conversation by highlighting and responding to this story.
Disclaimer
The views and opinions expressed in this article are solely those of the authors and do not reflect the views of Bitcoin Insider. Every investment and trading move involves risk - this is especially true for cryptocurrencies given their volatility. We strongly advise our readers to conduct their own research when making a decision.