Pragmatic Haskell: Simple servant webĀ server []

  1. Pragmatic Haskell: Simple servant web server
  2. Pragmatic Haskell II: IO Webservant
  3. Pragmatic Haskell III: Beam Postgres DB

There are many guides available for learning Haskell. Setting up a something simple like a web server isn’t so straight forward. Perhaps choosing one of the 14 libraries is a bit much.

Type level hell: Haskell sucks

This guide will give opinionated web server start. This guide assumes no experience with Haskell, and will get you up to speed with a (REST) web server called Servant. Servant is a good choice as it can describe both a server and client API. In the future this guide may be used as a foundation to create something more meaningful than just a very basic REST API, this will provide a good starting point however. Basic UNIX (command line) skills are assumed.

From nothing, start with build tools

Install stack:

curl -sSL https://get.haskellstack.org/ | sh

Only attempt shortly to install it trough a package manager. There are other Haskell build tools, they will be more difficult in use. There is also the possibility for fully reproducible builds at a system level (nix). Which is out of the scope of this guide.

Now setup a new project:

stack new awesome-project-name 
cd awesome-project-name

Hello world with stack

Appreciate what happens when this is build:

stack build && stack exec awesome-project-name-exe

This should build successfully and output someFunc. Open up src/Lib.hs with one’s favorite editor. This contains a few lines, created by stack:

module Lib
    ( someFunc
    ) where

someFunc :: IO ()
someFunc = putStrLn "someFunc"

This is where the someFunc output came from when the program was ran. Change it to something a bit more appropriate, and rename the function too:

module Lib
    ( webAppEntry
    ) where

webAppEntry :: IO ()
webAppEntry = putStrLn "This is the beginning of my greetings to world"

Does it compile?

stack build && stack exec awesome-project-name-exe

/home/jappie/projects/haskell/awesome-project-name/app/Main.hs:6:8: error: Variable not in scope: someFunc :: IO ()
  \|
6 \| main = someFunc
  \|        ^^^^^^^^

It does not compile. There is an app folder where by default all the executable reside (which is where the error occurs), and a src folder where the library code lives (the modified file is in there). One can future proving themselves by putting as much code in the library as is reasonable.

Fix the error in app/Main.hs:

module Main where

import Lib

main :: IO ()
main = webAppEntry

It builds! Functions can be renamed, simple compile errors can be solved, and strings can be changed. Progress!

Servant: Your first dependencies

For the impatient, there is a minimal example already available by the library author. This guide will explain how to get there step by step. In ./package.yaml, on line 22 there is a dependencies key, add servant-server, aeson, wai and warp to it like this:

dependencies:
- base >= 4.7 && < 5
- servant-server
- aeson
- wai
- warp 

It may seem strange to immediately add four new dependencies, however this is because Haskell libraries are setup to be flexible. Even small projects grow quickly to have into the twenties of dependencies. Code reuse is not a myth.

servant-server is the servant web server. aeson is for JSON parsing and producing. wai is a web application interface and warp uses wai to implement a web application (it binds to the port).

Ensure that that this is done at the root of the yaml file (no indentation). Stack provides a way of specifying dependencies of either the executable or library. If its done on line 22, the root of the yaml file, it will be a dependency for everything in the project.

A minimal servant

A good start is going to servants’ Hackage page, which linked to a tutorial. Servant does API definition at type level.

If it’s unknown to the reader what a type is, think of it as describing the shape of a function. Functions of different shapes don’t fit together, and won’t compile. What servant allows us to do is define this shape for a REST API. To gain a deeper understanding of this a concrete example will be inspected line by line. First all lines are listed for a minimal servant (Lib.hs) server:

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE DeriveGeneric #-}

module Lib
    ( webAppEntry
    ) where

import Servant(serve, Proxy(..), Server, JSON, Get, (:>))
import Data.Aeson(ToJSON)
import GHC.Generics(Generic)
import Network.Wai(Application)
import Network.Wai.Handler.Warp(run)

type UserAPI = "users" :> Get '[JSON] [User]

data User = User
  { name :: String
  , email :: String
  } deriving (Eq, Show, Generic)

instance ToJSON User

users :: [User]
users =
  [ User "Isaac Newton"    "isaac@newton.co.uk"
  , User "Albert Einstein" "ae@mc2.org"
  ]

server :: Server UserAPI
server = return users

userAPI :: Proxy UserAPI
userAPI = Proxy

app :: Application
app = serve userAPI server

webAppEntry :: IO ()
webAppEntry = run 6868 app

Language extensions

The first three lines are languages extensions, Haskell behaves different for this module according to these. data kinds Can be temporary deleted to see what happens:

/home/jappie/projects/haskell/awesome-project-name/src/Lib.hs:14:16: error:
    Illegal type: ā€˜"users"ā€™ Perhaps you intended to use DataKinds
   |
14 | type UserAPI = "users" :> Get '[JSON] [User]
   |                ^^^^^^^

/home/jappie/projects/haskell/awesome-project-name/src/Lib.hs:14:31: error:
    Illegal type: ā€˜'[JSON]ā€™ Perhaps you intended to use DataKinds
   |
14 | type UserAPI = "users" :> Get '[JSON] [User]
   |                               ^^^^^^^

Data kinds is needed to insert data into a type. A string being data in this case, it is unclear what '[JSON] is, probably also something data. Temporary breaking a program to see what GHC will say is an effective way of learning more about Haskell.

If TypeOperators is disabled, GHC says it doesn’t like :> in the UserAPI line. Apparently :> is a type operator. Apparently types can have operators.

If DeriveGeneric is disabled, GHC says it needs to derive generic in the data definition of User. Generic is required for serialization (in our case JSON conversion).

Modules

module Lib
    ( webAppEntry
    ) where

import Servant(serve, Proxy(..), Server, JSON, Get, (:>))
import Data.Aeson(ToJSON)
import GHC.Generics(Generic)
import Network.Wai(Application)
import Network.Wai.Handler.Warp(run)

Moving onward, there is the module definition that stack generated, modules are just namespaces, or similar to python modules. Nothing really special about those. Then there are many imports which pull functions into the module namespace.

Type level REST API

type UserAPI = "users" :> Get '[JSON] [User]

This line defines the UserAPI type, which will serve as the REST endpoint. The image at the beginning of the post was about this line. Perhaps reading it as a sentence will give us some insight, without worrying about how it fits together: It’s a Get request, mounted below /user, returning something JSON and of shape/type User. Conveniently what a User is will be discussed in the next section.

Domain data

data User = User
  { name :: String
  , email :: String
  } deriving (Eq, Show, Generic)

instance ToJSON User

User is just a data structure consisting of two strings: Email and name. This declaration method is called record syntax. This data structure derives Show, Eq and Generic. Deriving means that GHC will generate function implementations for this data structure. If one calls show on a User, it will know what to do (show is toString in Haskell). instance ToJSON User allows the User to be converted to JSON (implementation is provided by generic).

Functions

Done with data, time for code!

users :: [User]

Specifies a function that will always return a list of Users. There are no arguments to this function. It can be assumed the list is always the same. This is how immutable constants are specified.

users =
  [ User "Isaac Newton"    "isaac@newton.co.uk"
  , User "Albert Einstein" "ae@mc2.org"
  ]

This is the implementation of the before defined function. There are apparently two users in this list, one Isaac, and another Einstein. Note that positional arguments are used to create the Users.

Servant server

server :: Server UserAPI

server :: Server UserAPI says that there is something called a Server which has a UserAPI. A UserAPI is known, it is defined above. A Server is defined in servant. The type signature is rather complicated: type Server api = ServerT api Handler, looking at the definition of ServerT introduces a lot of complexity: type ServerT api (m :: * -> *) :: *.

There are some clues that can be derived (such as that m), but it’s not that important to make something work. Therefore this guide ignores it. Note that ignoring scary looking things is an important Haskell technique. If one is interested, help can be found here, just in case ā¤.

server = return users

The implementation is very simple however. The reader should be cautious, to think that return is a keyword. It’s a function. What both return does is wrap a value into a container. For example an element can be wrapped in a list: return 2 == [2]. That’s all one needs to know for now (the interested reader may look at monads).

Proxy

userAPI :: Proxy UserAPI
userAPI = Proxy

This is just some type level magic. Library author needed type information for a function, but they didn’t need a value. Proxy does that. It’s useful if you store data at type level, for example with the datakinds language extension, which was seen earlier.

Application

app :: Application
app = serve userAPI server

This combines the proxy and server. A serve function takes a Proxi API, Server API and returns an application. If type Application is inspected one can appreciate what serve does for us better:

type Application = Request -> (Response -> IO ResponseReceived) -> IO ResponseReceived 

The arrows indicate function arguments. An application receives a request, then a callback which expects a Response to produce an IO action which gives the result ResponseReceived. However to return this function must also return a type ResponseReceived wrapped in IO. It may be the case that the only way to obtain this response received is to call that callback. The freedom to do whatever one wants is meanwhile granted with the IO return type. To compile that ResponseReceived has to be obtained however.

Running it!

webAppEntry :: IO ()
webAppEntry = run 6868 app

Our initial function! Rather than saying hello world the app is ran on port 6868 (best port). Now build and run it in one terminal, and in another curl it:

stack build && stack exec awesome-project-name-exe &
curl localhost:6868/users

> [{"email":"isaac@newton.co.uk","name":"Isaac Newton"},{"email":"ae@mc2.org","name":"Albert Einstein"}]

Good job!

In conclusion

A lot of concepts have been treated within this blog post while also moving towards something productive. The reader can now start a new project and add arbitrary dependencies. He knows what language extensions are and how to see them in use. Type level magic has been encountered, and wisely was ignored. In future this post will build on top of this work to extend the API and do something with something within the handlers. However this post has grown to big already.

The complete code can be found here.

haskell programming

Recent stuff

Tags