loading...
Cover image for Haskell for madmen: Hello, web server!

Haskell for madmen: Hello, web server!

drbearhands profile image DrBearhands Updated on ・7 min read

Post from my blog. If you're enjoying the series, please consider buying me a coffee.
Feedback welcome!

Getting a 3rd party library

Now that we know how to interact with the real world from a functional context, it's time to get a library to help us set up a web server.

First, let us determine what library we need. First we'll go to hackage at https://hackage.haskell.org/. We can search for "web server". Generally, you would select a library according to whatever criteria you deem correct, here we will be using Warp, because I say so and it was popular and well maintained at the time of writing this tutorial.

All right, since we've cut our decision process short here, we will be looking at what functions the package exposes later and add warp to our project. We know we require the package "warp", so we add the dependency to the latest version (3.3.0 at the time of writing)

In package.yaml, under dependencies, let's add the following line:

- warp >= 3.3.0 && < 4

Now, we rebuild the project with stack build and oh no! A compile error! (you might not get this error, but it's good to know how to handle it, so read on!)

Error: While constructing the build plan, the following exceptions were encountered: In the dependencies for haskell-tutorial-0.1.0.0: warp-3.2.28 from stack configuration does not match >=3.3.0 && <4  (latest matching version is 3.3.0) needed since haskell-tutorial is a build target. Some different approaches to resolving this: Set 'allow-newer: true'

What is happening here is that your resolver - the thingy that tries to get all the dependencies of the right version - does not have the package at the version you specified. In this case, we have requested a version newer than the one the resolver has. Stack already suggests 2 options to fix this issue, but there's a caveat: the newer version may be incompatible with other packages! Say you have package A, and A requires package B of version 2.1, and you want to use package B of version 3.0, using the newer B might break A!

So, what we're going to do instead is downgrade warp (note that this is a bad idea if the newer version has security fixes, you should generally check that). Alternatively, we could try to upgrade the resolver, but that can also affect other packages.

Dependency management is a complicated affair anywhere side-effects exist, which includes Haskell through the IO monad and certain unsafe functions. Haskell throws the problems in your face rather than hiding it and potentially failing silently.

So, with the minimum version of warp set to 3.2.28, let's continue.

stack build

Importing new modules

Now that we have installed a new package, let's find out how we can use it.

Back on the hackage page, navigate to the version you have selected for use in your project, 3.2.28 in my case. We see 2 modules: Network.Wai.Handler.Warp and Network.Wai.Handler.Warp.Internal. We don't care about the internals so open the first one. This will take you to the haddock documentation of the relevant module.

The documentation tells you what functions, types, etc. are exported and is auto-generated by haddock based on code and annotations. Types are a sort of documentation entirely by themselves, which makes generated documentation very useful for Haskell. Humans being what they are, this leads to people putting in less effort. All things considered, I've found docs for Haskell library to be far easier than docs for say, Python and JavaScript. So let's take a look at how to interpret a typical haddock page.

Looking at the top function:

run :: Port -> Application -> IO ()

The "return type" of the function is an IO (), that's an impure computation, which is what we would expect for a web server. What about the arguments? Following Port immediately tells us it's an alias for Int. Okay, fair enough, we need to pass in a port number to start a server, makes sense. Application turns out to be a bit more complicated:

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

Okay, so an Application must be a function that takes a Request - make sense so far, a server should handle requests and give responses - and a function that generates an impure computation of a ResponseReceived from a Response, to then generate an impure computation of a ResponseReceived... right...

Luckily, we are given a code snippet in the documentation to help us:

app :: Application
app req respond = bracket_
    (putStrLn "Allocating scarce resource")
    (putStrLn "Cleaning up")
    (respond $ responseLBS status200 [] "Hello World")

From the definition of Application, we know:

  • app is a Request -> (Response -> IO ResponseReceived) -> IO ResponseReceived
  • so req must be a Request
  • and respond must be a Response -> IO ResponseReceived.

We can then look at how respond is used, but there's this weird $ operator there.

The $ operator is a common operator in Haskell that is just used to reduce parentheses. It binds very weakly and applies the right argument to the left one. E.g. f $ g a is the same as f (g a) (we first apply a to g and then apply the result to f), whereas and f g a is the same as (f g) a (we first apply g to f and then apply a to the result).

Now we can see that respond takes a response, and does something statefull that yields a response received. Because we know we want to send a response (an inherently impure operation, we can make an educated guess about what respond does. It sends an http response and returns a "proof" that we have indeed responded. Because we must also provide this proof as the return type of our application, and we have no other way of generating one, the type checker will force us to always provide a response to a request. Neat huh?

All right, all right, there are ways around this restriction by using special functions, but it's a lot harder to accidentally forget to respond.

You may have noticed at this point that when we navigated to the definition of
Application, we entered in the documentation for one of Warp's dependencies,
namely Wai. We need to add it to the dependencies too. In my case:

- wai >= 3.2.2.1 && < 4

With this in mind, we go back to src/Lib.hs and modify it:

module Lib
    ( someFunc
    ) where

import Network.Wai.Handler.Warp (run)
import Network.Wai (Request, Response, ResponseReceived)

someFunc :: IO ()
someFunc = run 8080 requestHandler

requestHandler :: Request -> (Response -> IO ResponseReceived) -> IO ResponseReceived
requestHandler request respond = error "unimplemented"

(note: you can replace a value of any type with an error in Haskell, which you may or may not like. It's one of the ways of getting around the type check I mentioned above. A full discussion about bottoms and total functional programming is a bit out of scope of this tutorial)

We can now run the server (stack run), but if we attempt to curl to it, it
will throw an error. It will not crash however. By default, warp catches any
error, which is what you'd want in production.

Finally, we're going to create a response.

Responding

Response is an abstract data type, meaning we are given no information about
its internal structure and must rely on the provided functions to create a value
with that type. We look in the documentation for functions with Response as
the return type, and find a few:

responseFile :: Status -> ResponseHeaders -> FilePath -> Maybe FilePart -> Response
responseBuilder :: Status -> ResponseHeaders -> Builder -> Response
responseLBS :: Status -> ResponseHeaders -> ByteString -> Response
responseStream :: Status -> ResponseHeaders -> StreamingBody -> Response
responseRaw :: (IO ByteString -> (ByteString -> IO ()) -> IO ()) -> Response -> Response

That's quite a lot of options. For now, let's keep it simple and use a Lazy ByteString with responseLBS.

Again, let's look at the argument types one by one. I skipped the explanation for searching through the documentation as it's no different than what we did earlier.

Status is a regular http status code. It's in the Network.HTTP.Types.Status module in the http-types package. We find the value status200 in the documentation.

ResponseHeaders turns out to be a list of headers. We don't care about headers for now so let's pass the empty list [].

ByteString is a small pain in the butt. There are many "strings" in Haskell, but by default we can only write Strings directly. Luckily we can change that with the OverloadedStrings language extension by adding {-# LANGUAGE OverloadedStrings #-} at the top of our module.

We're also going to use a let ... in block, which declares values in the let part than can be used at the in part. E.g.:

foo :: Int -> Int
foo x =
  let
    bar :: Int
    bar = x + 1
  in
    x + bar

We can now combine our knowledge into the final code for this chapter:

Lib.hs:
{-# LANGUAGE OverloadedStrings #-}

module Lib
    ( someFunc
    ) where

import Network.Wai.Handler.Warp (run)
import Network.Wai (Request, Response, ResponseReceived, responseLBS)
import Network.HTTP.Types.Status (status200)

someFunc :: IO ()
someFunc = run 8080 requestHandler

requestHandler :: Request -> (Response -> IO ResponseReceived) -> IO ResponseReceived
requestHandler request respond =
  let
    response = responseLBS status200 [] "Hello, client!"
  in
    do
      putStrLn "Received an HTTP request!"
      respond response
package.yaml (only the dependencies part, versions may differ):
dependencies:
- base >= 4.7 && < 5
- warp >= 3.2.28 && < 4
- wai >= 3.2.2.1 && < 4
- http-types >= 0.12.3 && < 0.13

Voila, your first web server in Haskell.

Final code branch for this chapter

Posted on by:

Discussion

markdown guide