JAX London 2014: A retrospective
This should function as a good introduction

Tutorial: Developing Web Applications in Haskell

PatrickBrisbin
haskell-logo1

Patrick Brisbin on the benefits of using a pure functional language for creating real-world web sites.

There’s much more to Haskell than just buzz-words like laziness and parallelism – which are completely deserved, by the way. Having pure computations defined as side-effect-free morphisms that take and return immutable datatypes allows the compiler to do amazing optimizations. This frees you to write elegant, readable code and get near-C performance at the same time.

Runtime errors. Grepping through source to find a method you just rewrote to ensure it’s not incorrectly called somewhere. Wondering if that expression represents a String or a Boolean. Determining how a template behaves when user is nil. Unit tests. Hitting deploy and frantically browsing the site to make sure things still work. These are the hazards of a dynamic language. These are things that all go away when you use a language like Haskell.

It’s been my experience that when developing in Haskell: if it compiles, it works. I’d say, conservatively, that 93% of every bug I’ve ever written in Haskell has been caught immediately by the compiler. That’s a testament to both the compiler and the number of bugs I’m able to produce in Haskell code. It’s liberating to gain such a level of confidence in the correctness of your code simply by seeing its successful compilation.

My hope for this article is to illustrate this experience by building out a simple site in the Haskell web framework Yesod. Yesod is just one of many web frameworks in Haskell, but it’s the one I’m most comfortable with. I encourage you to check it out at yesodweb.com as there are many features and considerations that I won’t be touching on here.

Yesod Development

In order to develop a Yesod site, you’ll need the Glasglow Haskell Compiler, along with some additional build tools. These can all be installed by setting up the Haskell Platform. There are installers for Windows, OSX, and most Linux distributions have it in their repositories.

Once the Haskell Platform is setup, type:

$ cabal update
$ cabal install yesod-platform

This one time installation of the framework can take while.

The Lemonstand

The blog example feels a bit overdone, doesn’t it? Instead, let’s build a lemonade stand (which I’ll refer to as “The Lemonstand” from now on). We won’t go too crazy with features, we just want a few pages and some database interaction so you can see how this framework can be used.

Much of the site will be provided by the code-generating tool called the Yesod Scaffold.

Yesod Init

The Yesod scaffolding tool will build out a sample site showing some of the more common and useful patterns used in Yesod sites. It will build you a simple “hello world” site with important features like persistence authentication and static file serving already coded out. You can then edit and extend this site to quickly build out features.

It’s important to note that this is not the way to structure a Yesod application, is just one way to do it. That said, this organisational structure has been refined over a long period of time and comes with many benefits.

To start our project, we do the following:

$ yesod init

We’ll answer a couple of questions about ourselves and our project. I’m calling it “lemonstand” and choosing the sqlite database type since it does not require a separate server.

The first thing we have to do is pull in any additional dependencies (like the driver for the type of database we chose to use).

$ cd lemonstand
$ cabal install

In order for authentication via Google to work (a feature we’ll use down the line), we need to make one small change:

config/settings.yml.

Please update the development block like so:

Development:
  <<: *defaults
  approot: http://localhost:3000

With that bit of housekeeping out of the way, go ahead and fire up the development server:

$ yesod devel

You should see lots of output about compilation, database migrations, etc. The most important is “Devel application launched: http://localhost:3000”. Go ahead and checkout the sample site by visiting that URL in your browser.

Models

In a “production” lemonade stand app, we may get a little more complex with the data modelling, but to keep this demo simple, I’m going to model the system uncomplicatedly as well.

The scaffold already comes with the concept of a User and authentication, so we’ll keep that as it is. The second most important concept will be Orders which our users can create through a typical check-out flow.

Orders will have many Lemonades which have size, price, and quantity.

Open up Model.hs. This is where we’ll place our core data type definitions. You should notice a line about persistFile. What this does is parse the text file “config/models” and generate some Haskell datatypes for us. This line also adds the required boilerplate to persist these types to the database as well as the initial migration code. This is where your User model comes from.

We’ll get to this file in a second, but first we’re going to define some data types that won’t be persisted.

Go ahead and add the following after the import lines but before the share line:

type Price = Double
type Qty   = Int

What these are, are type aliases. They just allow you to refer to one type as another ([Char] is aliased to String in the standard Prelude, for instance).

If and when we later make functions that deal with Lemonades, we’ll see type signatures like this:

-- | Calculate the total price of multiple lemonades
totalPrice :: [Lemonade] -> Price
totalPrice = ...

And not like this:

-- | Calculate the total price of multiple lemonades
totalPrice :: [Lemonade] -> Double
totalPrice = ...

…which is not as descriptive. It’s a little thing, but goes a long way.

We’re also going to create an additional data type that won’t (itself) be stored as a database record.

data Size = Small
          | Medium
          | Large
          deriving (Show, Read, Eq, Ord, Enum, Bounded)

You might be familiar with this concept as an enum. In Haskell, the concept of enumeration types are just a degenerative form of algebraic data types where the constructors take no arguments.

Don’t worry about the deriving line. That just tells Haskell to go ahead and use the same defaults when performing common operations with this type, like converting it to string or comparing two values for equality. With this deriving in place, Haskell knows that Small can be shown as “Small” and that Medium == Medium.

Even though we don’t want to store Sizes directly in the database as records, we do plan to have fields of other records be of the type Size. To allow this, we just have to ask Yesod to generate some boilerplate on this type:

derivePersistField "Size"

Easy.

When you hit save on this file, you should see in your terminal that it is still running yesod devel which has recompiled your sources and restarted your development server. The important thing is that it does this successfully every time you make a change. When you introduce a bug, you’ll get a compiler error directing you to the problem. This immediate and accurate feedback is important to the development process as we’ll see later on.

Next, we’ll go ahead and add some database models. Open up config/models.

You’ll see some models are already present, we’ll just add more to the bottom of the file:

Order
    user UserId

Lemonade
    order OrderId Maybe
    size  Size
    price Price
    qty   Qty

This is exactly as if you had handwritten the Haskell data types:

data Order = Order
    { orderUser :: UserId
    }

data Lemonade = Lemonade
    { lemonadeOrder :: Maybe OrderId
    , lemonadeSize  :: Size
    , lemonadePrice :: Price
    , lemonadeQty   :: Qty
    }

In addition to the above declarations, Yesod will add all of the boilerplate needed for values of these types to be (de)serialized and persisted to or restored from the database.

Again, save the file and make sure it compiles.

Notice that I used the Maybe type on lemonadeOrder. In Haskell, this type is defined as:

data Maybe a = Just a | Nothing

This allows you to have a function which can return some a or Nothing at all. This is how Haskell can maintain type safety even when you need the concept of an optional parameter or return value.

I’m assuming here that we might want to describe Lemonades that aren’t yet associated with an Order. We’ll see if that turns out to be the case.

Route Handling

Before we start making further changes, let me provide some context on how the current homepage is rendered. We’ll be mimicking this pattern for our other pages.

Every URL that your app responds to, will be listed in config/routes, so go ahead and open that file.

You’ll see some scaffold-provided routes already. /static and /auth use a concept called Subsites to provide additional functionality to your app (namely static file serving and user authentication). We’ll not go into this any further as it can get hairy quickly and for the purposes of this article, we can treat these as black boxes.

The rest of the entries are normal routes. For these, you provide:

  1. The relative URL you answer to (we’ll get to variable pieces later)
  2. The data type of the route (again, more later)
  3. The supported methods (GET, POST, etc)

Let’s look at HomeR.

In your Foundation.hs file there’s another line similar to the persistFile line in Model.hs. It works much the same way in that it will parse this flat file (config/routes) and generate some Haskell code for us.

When the parser comes across this HomeR line, it’s going to do a number of things. Conceptually, it’s something like the following:

  • HomeR is made a valid constructor for values of type Route which is used by the framework to route requests to your handler functions.
  • The functions in charge of rendering and parsing URLs can now translate to and from this HomeR type.

In order to accomplish this, two functions need to be in scope: getHomeR and postHomeR. This is because we’ve specified GET and POST as supported methods.

So, whenever a GET request comes in for “/”, Yesod will now translate that URL into the data type HomeR and know to call getHomeR which is a function that returns an HTML response (RepHtml).

If you were to define a route like “/users/#UserId UsersR GET”, then your required function getUsersR would have the type UserId -> RepHtml. Since your URL has a variable in it, that piece will match as a UserId and it will be given as the first argument to your handler function – all in an entirely type safe way.

Let’s add a route for buying some lemonade:

/checkout CheckoutR GET POST

While we’re here, remove the POST from HomeR since we’ll no longer be using that.

When you save this file you should see some problems in your compiler window:

[7 of 7] Compiling Application      ( Application.hs, 
dist/build/Application.o )

Application.hs:30:1: Not in scope: `getCheckoutR'

Application.hs:30:1: Not in scope: `postCheckoutR'

Well, look at that. We’ve introduced a bug, and it was caught immediately.

Since the app now needs to answer requests for “/checkout” by calling your handler functions, they need to be there or you’d have runtime errors. There is very little potential for runtime errors in Haskell, and this is just our first example of why: the compiler catches us ahead of time.

So let’s fix it. The following steps might feel a bit tedious, and in Yesod version 1.1 there is a tool to do them for you, however I think that doing things like this manually at least once is useful.

Add the following around line 36 of lemonstand.cabal:

Handler.Checkout

This tells the build system to include this new source file we’ll create.

Add the following around line 26 of Application.hs:

import Handler.Checkout

This imports that module (still not written) into the scope where these functions are needed.

Finally, create the file Handler/Checkout.hs:

module Handler.Checkout where

import Import

getCheckoutR :: Handler RepHtml
getCheckoutR = undefined

postCheckoutR :: Handler RepHtml
postCheckoutR = undefined

We’ve really just traded one runtime error for another as visiting that page will result in the app calling undefined which will fail. However, we’ve made the compiler happy and can move onto other things and come back to these later.

Templates

Let’s open up Handler/Home.hs and see how our current home page is rendered.

We’re going to strip out just about everything here. Similar code will be added later in other handlers, and I’d like you see those concepts then rather than now.

Rewrite the file so it looks like this:

-- leave everything up to and including the import line as-is.

getHomeR :: Handler RepHtml
getHomeR = do
    -- Use the default overall layout, you'll almost always do this.
    defaultLayout $ do

        -- The page title.
        setTitle "Lemonade Stand"

        -- The template to render.
        $(widgetFile "homepage")

You may notice, you’ve triggered another compiler error, quite a few actually: not in scope: aDomId.

Our templates reference is a variable which we’ve just removed. Please, take a moment to appreciate type-safe templates. No runtime error, no silent nil-handling, we get an up-front compiler error indicating exactly where the problem is. How cool is that?

In the process of fixing this, I’ll also try to provide a little more context.

$(widgetFile “homepage”) is a very useful function. What it does is to look in your templates directory for any HTML, CSS and JavaScript templates for your “homepage”. These templates will be combined into a Widget. Widgets can be nested and combined quite naturally throughout your application. In the end, they will all be rolled up into one final Widget and served as a single response. All style sheets and scripts will be concatenated, minified (when configured to do so) and ordered correctly – all without you having to think about it.

For us, this means templates/homepage.{hamlet,lucius,julius} are being found and compiled.

Julius is JavaScript templating; it’s essentially a straight pass through except with variable interpolation. You can go ahead and remove it now; we won’t use it on this page.

$ rm templates/homepage.julius

Lucius is a superset of CSS. It was designed to allow existing CSS to be pasted directly in and have it still compile and work. On top of this, it allows for variable interpolation and some Less-like extensions like nesting and mix-ins. Open up the template and remove the style block referencing aDomId.

Hamlet is the most complex of Yesod’s templaters. Open up the template and fill it with the following content:

<h1>_{MsgHello}

<p>
  Click 
  <a href=@{CheckoutR}>here
   to buy some Lemonade!

We’re going to leave _{MsgHello} in place. The _{ } interpolation will check your messages file for translations and show different content based on the user’s preferred language.

@{ } is a route interpolation. As you might’ve guessed, it’s used to show internal links in a type safe way. Now that we’ve removed the aDomId references things are compiling, but it’s important to realize that if we had added this link to CheckoutR in here before actually adding that route to our app, we’d get a similar compiler error. No more dead links in your application, any URLs that don’t resolve will immediately show up as compiler errors.

If we had a route as mentioned before for users (“/users/#UserId”) we’d have to use something like @{UsersR aUserId} and the compiler would infer and enforce that aUserId is, in fact, a UserId.

There is a lot of functionality in Hamlet templates, some of which we’ll get to when we build out our next page. What you can do right now is refresh your browser to see the changes.

Forms

Let’s head back to Handler/Checkout.hs. We’re going to add a very simple form where the user can pick the size of their lemonade and checkout.

First we’ll declare a form:

lemonadeForm :: Form Lemonade
lemonadeForm = renderTable $ Lemonade
    <$> pure Nothing
    <*> areq (selectField optionsEnum) "Size" Nothing
    <*> pure 0.0
    <*> areq intField "Quantity" Nothing

There are a few things going on here worth looking at. First of all, each line represents a record of the Lemonade data type. When shown, this form will have fields according to what’s listed which will then map those values back to a value of type Lemonade when the form is processed. The lines that use pure provide values when processed, but don’t not actually show any fields.

Where going to cheat here and completely ignore Price. Dealing with dependent fields (setting price based on size, for example) can get tricky, so we’re just going to set the price server-side after the size and quantity have been submitted.

Before we can test out this form, there’s one thing we need to change about our Foundation.hs. We’re going to use the function requireAuthId to force users to authenticate before checking out. This function also gives us the ID of the current user.

To allow this, we’ve got to change the module exports of Foundation.hs like so:

module Foundation
    ( App (..)
    , Route (..)
    , AppMessage (..)
    , resourcesApp
    , Handler
    , Widget
    , Form
    , maybeAuth
    , requireAuth
    , requireAuthId -- <- add this
    , module Settings
    , module Model
    ) where

With that in place, we can sketch out the Handler now:

getCheckoutR :: Handler RepHtml
getCheckoutR = do
    -- force authentication and tell us who they are
    uid <- requireAuthId

    -- run the defined form. give us a result, the html and an encoding 
    -- type
    ((res,form), enctype) <- runFormPost $ lemonadeForm

    case res of
        -- if a form was posted we get a Lemonade
        FormSuccess l -> do
            -- process it and give us the order id
            oid <- processOrder uid l

            -- TODO: redirect to Thank You page here
            return ()

        -- in all other cases just "fall through"
        _ -> return ()

    -- and display the page
    defaultLayout $ do
        setTitle "Checkout"
        $(widgetFile "checkout")

postCheckoutR :: Handler RepHtml
postCheckoutR = getCheckoutR

processOrder :: UserId -> Lemonade -> OrderId
processOrder = undefined

When requireAuthId is encountered for an unauthenticated user, they will be redirected to login. The scaffold site uses the GoogleEmail plug-in which allows users to login using their Gmail accounts via Open Id. This authentication system can of course be changed, extended or removed, but we’re going to just use it as is.

We’re also using a common idiom here: the same Handler handles both GET and POST requests. In the case of a GET, the form result (res) will be FormMissing, that case statement will fall through and the form will be displayed. In the case of a POST, the form result will be FormSuccess, we’ll execute processOrder (which we’ve left undefined for now) and redirect to a “Thank You” page.

Upon saving this, we should have another compiler error. We’ve told Yesod to look for “checkout” templates, but there are none. So let’s create templates/checkout.hamlet:

<h1>Checkout
<p>What size lemonade would you like?
<form enctype="#{enctype}" method="post">
  <table>
    ^{form}
    <tr>
      <td>&nbsp;
      <td>
        <button type="submit">Checkout

Simple variable interpolation is done via #{ }, while embedding one template (like form) into another is done via ^{ }.

Now that we’ve got the form showing, we can replace our undefined business logic with some actual updates:

-- | Take a constructed Lemonade and store it as part of a new order in 
--   the database, return the id of the created order.
processOrder :: UserId -> Lemonade -> Handler OrderId
processOrder uid l = runDB $ do
    oid <- insert $ Order uid
    _   <- insert $ l { lemonadeOrder = Just oid
                      , lemonadePrice = priceForSize $ lemonadeSize l
                      }

    return oid

    where
        priceForSize :: Size -> Price
        priceForSize Small  = 0.99
        priceForSize Medium = 1.99
        priceForSize Large  = 2.99

Make sure that compiles, then add in the actual redirect:

getCheckoutR :: Handler RepHtml
getCheckoutR = do
    ((res,form), enctype) <- runFormPost $ lemonadeForm

    case res of
        FormSuccess l -> do
            oid <- processOrder l

            -- redirect to a "Thank You" page which takes an order id as 
            -- a parameter.
            redirect $ ThankYouR oid

        _ -> return ()

    defaultLayout $ do
        setTitle "Checkout"
        $(widgetFile "checkout")

Hopefully, you’ve noticed the compiler error this introduces. Can you guess how to fix it? We’ve told our Application to redirect to ThankYouR but that route does not exist. Again, with no runtime error, there is just a clear compiler error.

So, follow the advice of the compiler and add the route declaration to config/routes:

/thank_you/#OrderId ThankYouR GET

Again we get the expected compiler error that getThankYouR is not in scope. In the interest of time and variety, we’ll not create an entirely different module, or template for the thank you page, we’ll inline everything right here in Handler/Checkout.hs:

getThankYouR :: OrderId -> Handler RepHtml
getThankYouR oid = defaultLayout $ do
    setTitle "Thanks!"

    [whamlet|
        <h1>Thank You!
        <p>Your order is ##{toPathPiece oid}
        |]

Thank you page

Conclusion

Obviously, The Lemonstand is lacking a few things: the user never gets to see price, there’s no concept of buying multiple lemonades of varying Sizes, and the overall UI/UX is pretty terrible.

These are all things that can be fixed, but this article is already getting quite long, so I’ll have to leave them for another time. Hopefully you’ve seen a good enough mix of theory and practice to agree that there are benefits to working on web applications (or any software) in a purely functional language like Haskell.

Patrick Brisbin currently works as a Ruby on Rails developer for ideeli.com. He has a number of side projects as pbrisbin on github.com and also blogs about them at pbrisbin.com. He has a degree in Aerospace Engineering from Boston University which he doesn’t use at all.

Author
Comments
comments powered by Disqus