Hello World Web Application in Haskell

So, I’m learning Haskell. I’ve done the Yorgey course and want to write a web app. How do I start? Should I learn Snap or Yesod? Well, the short answer is no.

Here I’m going to outline the creation of the simplest possible Haskell “Hello World” web application.

Wai Wai Pom Pom Pom

Snap and Yesod are both “big” web frameworks. Of the two, Snap aims to be the smaller. Both have their own web server, templating system and so on. Both are sufficiently complex to need a program to set up a starter project. Both have fairly sophisticated monad stacks to understand. They’re also both phenomenal high-performance pieces of engineering.

What this means for a beginner is that you’re going to spend as much time trying to get to grips with the framework as you are learning how to use Haskell. If like me, you’re coming from Clojure, they both feel a bit more like Rails than Compojure.

So, are there simpler to understand models out there? Well, the equivalent of Compojure/Sinatra is Scotty. But I found the next level down again more interesting: Wai.

Wai corresponds most closely to Ring or Rack. It was intended to be a common API that Haskell web servers could expose. In practice, it’s only Warp that really supports it. However, Warp is a damn fine web server so that shouldn’t hold us back too much. Nearly every Ring app runs Jetty and hardly anyone really worries that the “standard” isn’t as portable in practice as it is in theory.

Setting up Hello World

To start, create a new directory. For our purposes we’ll call it “example”. Then we set up a completely blank project.

mkdir example
cd example
cabal sandbox init
wget http://www.stackage.org/lts/1.8/cabal.config
cabal init

The “sandbox” and “wget” lines I’ll gloss over, but they basically constitute the best way I know to avoid what’s known as “cabal hell”. And believe me, you don’t want cabal hell.

When you run the init command, you’ll be asked a whole bunch of questions. The defaults are fine, just make sure you specify you’re creating an executable. It’ll create a file “example.cabal”. You then need to go in and make it look like this:

-- Initial semele.cabal generated by cabal init.  For further
-- documentation, see http://haskell.org/cabal/users-guide/

name:                example
version:             0.1.0.0
-- synopsis:
-- description:
license:             AGPL-3
license-file:        LICENSE
author:              Rainbow Dash
maintainer:          rainbow.dash@gmail.com
-- copyright:
category:            Web
build-type:          Simple
-- extra-source-files:
cabal-version:       >=1.10

executable semele
  hs-source-dirs:      src
  main-is:             Main.hs
  -- other-modules:
  -- other-extensions:
  build-depends:       base >=4.7 && <4.8,
                       wai,
                       warp,
                       http-types
  default-language:    Haskell2010

There’s two import edits here. The first is that we specify hs-source-dirs. The default is that the Haskell files are dumped in the project’s root directory, which is a lousy default. The other is that we set up our dependencies: wai, warp and http-types. Wai and http-types from our API, Warp our implementation. Note that dependencies are case-sensitive.

You may also be wondering why I haven’t specified any version constraints. That’s because we’ve set them up in the cabal.config instead. Welcome to the new world of LTS Haskell.

Writing Hello World

mkdir src
cd src

Now create Main.hs.

{-# LANGUAGE OverloadedStrings #-}

We need this because Wai uses ByteStrings rather than Strings, and overloaded strings makes using them lower friction.

import qualified Network.Wai as W
import qualified Network.HTTP.Types.Status as HS
import qualified Network.Wai.Handler.Warp as Warp

I’m qualifying everything for clarity. In practice, I do this a fair bit even when I’m not writing a blog post.

main :: IO ()
main = main = do
  putStrLn "Starting Web Server..."
  Warp.runSettings Warp.defaultSettings app

So, all we’re saying here is “Run the app with the default settings for a Warp server.” The default port is 3000.

Finally, we need the app itself:

app :: W.Application

Let’s take a huge detour and examine what that actually means.

Understanding W.Application

Now, the type of app is W.Application, but that tells us nothing. So let’s look it up (LTS has a hoogle, search for Wai.Application). You’ll find

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

So, it’s a alias for a type of function. However, the type’s way more complex that we were expecting. What were we expecting? Well, in Ring the type’s more like

type ApplicationRing = Request -> Response

Take a request, return a response. However, in order to allow for correct resource management, it uses a continuation passing style instead. (I’m hoping to expand on that in another post.) So instead, you need a callback. As you see, we called that respond.

What’s respond‘s type? Well, it’s got to take a response. At this point I hit the limits of my understanding. I’d have made the function return (), but instead it returns ResponseReceived which appears to be a placeholder type. Finally, obviously respond is going to have to write to a socket, so it’s going to have to incorporate the IO monad. Now, in most of the more complex APIs, what you find here is a monad transformer stack with IO somewhere in the mix. In Wai, you just get a naked IO ResponseReceived and can build your own later.

To summarize, the type of respond is Response -> IO ResponseReceived and that means “when you call it with a response it will do some IO and return that it’s been processed`.

Finally, Application expects IO ResponseReceived to be returned from the function. I believe this to be practically motivated: nearly every handler is going to want to call respond as the last thing it does, and this means that the types work when you do that.

You Had Me At Hello

So, now we’ve understood the type, let’s write the function

app request respond = respond $ W.responseLBS HS.status200 [] "Hello World"

To unpack this: when you receive a request, respond using status 200 (success), no headers ([]) and byte string “Hello World”.

So, that’s about the simplest thing we can possibly do without writing our own web server.

Let’s Be Frank

So, how does this compare to Sinatra’s famously good home page? Well, for a start we have three files instead of one. However, two of those files are devoted to ensuring that our dependencies don’t mess us around, if you want to do the same in Ruby, you’ll be setting up bundler, using a gemfile.lock in addition to your normal gemfile, so three files again.

Haskell actually comes out slightly ahead here if you’re willing to forgo some flexibility, in that the cabal.config is repeatable and upgrading is a matter of trying a new cabal.config/ reverting if it doesn’t work.

In comparison, bundler generates a lock file dependent on your current gemfile. If you need to add another library later, it’s up to you to figure out which versions are compatible with your code.

On the other hand, if you need more flexibility, you’re going to encounter cabal hell pretty quickly. Good luck.

We’ve got three dependencies instead of one. That’s a pity. But it comes from the two sources:

  • We’ve got to import types declaring interfaces as well as just implementation code.
  • We don’t have the web server appearing by magic.

On the other hand, Sinatra’s actually provided a routing library, and we don’t have one yet. But we could have used Scotty instead.

Keep On Running

So, let’s see it in action. Get back to the root project directory and type

cabal install && dist/*/build/example/example

and navigate to http://localhost:3000/. Hey presto, you’ve served a web page. Looking at the headers, all that it’s specified is a Date, the Server and Transfer-Encoding, so we’ll definitely have a bit more work to do to for a full experience.

FOOTNOTE: I’m quite pleased with the response this article had on reddit. This discussion is quite interesting and I recommend reading it.

FOOTNOTE: Quite a few people have remarked that the comparison section isn’t really fair on Haskell in that I’ve implemented something at the Rack/Wai level, rather than the Sinatra/Scotty level, which is true. However, I wanted to use Wai rather than Scotty to avoid going into monads and monad transformers and ultimately, I think the Haskell one is still quite concise and beautiful in a very precise manner.

EDIT:A number of people have pointed out that modern ruby is indeed capable of precise version locking. I’ve updated and expanded the comparison to reflect that.

EDIT:Originally I believed that Wai’s continuation passing style was due to asynchronous concerns. Instead, it’s driven by resource management concerns. I’ve corrected the line.