Hello, web server!
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!)
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
= bracket_
app req respond putStrLn "Allocating scarce resource")
(putStrLn "Cleaning up")
($ responseLBS status200 [] "Hello World") (respond
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 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 stateful 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
( someFuncwhere
)
import Network.Wai.Handler.Warp (run)
import Network.Wai (Request, Response, ResponseReceived)
someFunc :: IO ()
= run 8080 requestHandler
someFunc
requestHandler :: Request -> (Response -> IO ResponseReceived) -> IO ResponseReceived
= error "unimplemented" requestHandler request respond
(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
String
s 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
= x + 1
bar in
+ bar x
We can now combine our knowledge into the final code for this chapter:
Lib.hs
:
{-# LANGUAGE OverloadedStrings #-}
module Lib
( someFuncwhere
)
import Network.Wai.Handler.Warp (run)
import Network.Wai (Request, Response, ResponseReceived, responseLBS)
import Network.HTTP.Types.Status (status200)
someFunc :: IO ()
= run 8080 requestHandler
someFunc
requestHandler :: Request -> (Response -> IO ResponseReceived) -> IO ResponseReceived
=
requestHandler request respond let
= responseLBS status200 [] "Hello, client!"
response 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.