Developing Web apps with Haskell and Yesod

July 22, 2016

These are my notes from working through Yesod Web Framework, 2nd edition.

Chapter 2

Tools

We're instructed to install Yesod and several libraries with cabal install. I prefer to do this in a sandbox, so normally I would begin with cabal sandbox init:

$ cabal sandbox init

Then proceed with the book's instructions:

$ cabal update
$ cabal install yesod yesod-bin persistent-sqlite yesod-static

Then wait roughly forever for everything to build.

Since I'm doing this from NixOS, I'll just spin up a Nix shell with all of the Yesod packages, and avoid the above cabal install entirely:

$ PACKAGES="yesod yesod-bin persistent-sqlite yesod-static"
$ nix-shell -p "haskellPackages.ghcWithPackages (pkgs: with pkgs; [$PACKAGES])"

To make this more reusable, I'll make it a function in my ~/.bashrc file:

function nix-haskell() {
  EXPR="haskellPackages.ghcWithPackages (pkgs: with pkgs; [$@])"
  nix-shell -p "$EXPR"
}

Now I can simply run nix-haskell <pkg1> <pkg2> ... <pkg2>:

$ nix-haskell yesod yesod-bin persistent-sqlite yesod-static

Overloaded strings

Here's a nice example of when the compiler can't figure out a type because of ambiguous type class instances:

{-# LANGUAGE OverloadedStrings
           , TypeSynonymInstances
           , FlexibleInstances
  #-}

import Data.Text (Text, unpack)

class Printer a where
  println :: a -> IO ()

instance Printer String where
  println x = putStrLn x

instance Printer Text where
  println x = putStrLn $ unpack x

If we try to call println with a string-like input "Hello, world!", the compiler can't figure out whether that value has the type String or Text, so can't choose a Printer instance to use:

main :: IO ()
main = println "Hello, world!" -- fails to compile
$ curl -sL earldouglas.com/notes/yesod-book/chapter-2/OverloadedStringsBad.hs | runghc

/run/user/1000/runghcXXXX1804289383846930886.hs:18:8:
    No instance for (Printer a0) arising from a use of ‘println’
    The type variable ‘a0’ is ambiguous
    Note: there are several potential instances:
      instance Printer Text
        -- Defined at
/run/user/1000/runghcXXXX1804289383846930886.hs:14:10
      instance Printer String
        -- Defined at
/run/user/1000/runghcXXXX1804289383846930886.hs:11:10
    In the expression: println "Hello, world!"
    In an equation for ‘main’: main = println "Hello, world!"

/run/user/1000/runghcXXXX1804289383846930886.hs:18:16:
    No instance for (Data.String.IsString a0)
      arising from the literal ‘"Hello, world!"’
    The type variable ‘a0’ is ambiguous
    Note: there are several potential instances:
      instance Data.String.IsString
                 Data.ByteString.Builder.Internal.Builder
        -- Defined in ‘Data.ByteString.Builder’
      instance Data.String.IsString Text -- Defined in ‘Data.Text’
      instance Data.String.IsString [Char] -- Defined in ‘Data.String’
    In the first argument of ‘println’, namely ‘"Hello, world!"’
    In the expression: println "Hello, world!"
    In an equation for ‘main’: main = println "Hello, world!"

We have to tell the compiler which type we want to use:

main :: IO ()
main = println ("Hello, world!" :: String) -- compiles
$ curl -sL earldouglas.com/notes/yesod-book/chapter-2/OverloadedStringsGood.hs | runghc
Hello, world!

Chapter 3

In this chapter, we get to get our hands dirty with a bare-bones Warp/Yesod application.

Hello, world

We can start the server on localhost:3000 with curl and runghc:

$ curl -sL earldouglas.com/notes/yesod-book/chapter-3/HelloWorld.hs | runghc

And test it with curl:

$ curl localhost:3000
<!DOCTYPE html>
<html><head><title></title></head><body>Hello, world!</body></html>

Routing

Yesod uses a front controller to handle routing.

The HelloWorld example uses Yesod's declarative route specification style:

mkYesod "HelloWorld" [parseRoutes|
/ HomeR GET
|]

In Chapter 7 we'll dig into the mkYesod TH function and the various bits it generates: a route data type, parser/render functions, a dispatch function, and some helper types.

It can be useful to compile with --ddump-splices and to generate Haddock documentation for an application to see what functions and data types are generated.

Resources and type-safe URLs

$ curl -sL earldouglas.com/notes/yesod-book/chapter-3/Links.hs | runghc

Non-HTML responses

$ curl -sL earldouglas.com/notes/yesod-book/chapter-3/App.hs | runghc
$ curl localhost:3000
HTTP/1.1 200 OK
Date: Fri, 22 Jul 2016 17:54:50 GMT
Server: Warp/3.2.3 + Yesod/1.4.19 (core)
Content-Length: 23
Content-Type: application/json; charset=utf-8
Set-Cookie:
_SESSION=UWC5A5Fnw5ruVsv8HJmjOkaaGIQS9x0Fj0xcokFkw0yCPPmLmTIg9+dU53CP+cqIVK8/WFvvPlReUIQy+UmUIy84SccmQzmScXVOYR9PXk5xoa3dQXb/i3GY6QR814ogcw3dBVOV9VY=;
Path=/; Expires=Fri, 22-Jul-2016 19:54:50 GMT; HttpOnly
Vary: Accept, Accept-Language

{"msg":"Hello, world!"}
$ curl -s localhost:3000 | jq .
{
  "msg": "Hello, world!"
}

Chapter 4

Syntax

Hamlet syntax

$ curl -sL earldouglas.com/notes/yesod-book/chapter-4/Home.hs | runghc
<body><p>This is my page.</p>
<footer>Return to <a href="/home">Homepage</a>
.</footer>
</body>
$ curl -sL earldouglas.com/notes/yesod-book/chapter-4/SomePage.hs | runghc
<p>You are currently on page 2.
<a href="/home?page=1">Previous</a>
<a href="/home?page=3">Next</a>
</p>

Lucius syntax

$ curl -sL earldouglas.com/notes/yesod-book/chapter-4/Lucius.hs | runghc
.sme-class{-webkit-transition:all 4s ease;-moz-transition:all 4s ease;-ms-transition:all 4s ease;-o-transition:all 4s ease;transition:all 4s ease}