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.