A Quick Intro to Snap

So, what's Snap?

Snap is a web framework for the Haskell programming language. Nice!

By the way, what's Haskell?

Snap leverages Haskell's strength

Easy to get running with snap

Install

$ cabal update
$ cabal install snap

Start a project

$ mkdir myproject; cd myproject
$ snap init

Make and run

$ cabal install
$ myproject -p 8000

Snap's no slouch

It comes with a built-in fast HTTP server with an optional high-concurrency backend using the libev event loop library.

Dynamic recompilation

Snap is flexible

It's also modular

Snap goes well with some Heist

A simple Heist template

<artists>
    <div>
        <a href="${artisturl}"><name/></a> - born on <birthdate/>
    </div>
    <ul>
        <albums>
            <li><year/> - <albumtitle/></li>
        </albums>
    </ul>
</artists>

Snap is actively developed

Taking a Closer Look at Snap

What makes Snap

Handling a request

The Snap monad

The monad that web handlers run in. It gives you:

xmlhtml

<div class="bear-info">
    <h1>The grizzly bear</h1>
    The grizzly bear is a very
    <em>fearsome</em>
    beast. Avoid at all costs.
</div>

Heist takes a template ...

<person>
    Hi I'm <name/>, these are my favorites:
    <ul>
        <flavors>
            <li><flavor/></li>
        </flavors>
    </ul>
</person>

... and renders it

Hi I'm Andy, these are my favorites:
<ul>
    <li>Vanilla</li>
    <li>Strawberry</li>
    <li>Pistachio</li>
</ul>

How does Heist do it?

The flavors node ...

    <flavors>
        <li><flavor></li>
    </flavors>

.. gets substituted by three li nodes.

    <li>Vanilla</li>
    <li>Strawberry</li>
    <li>Pistachio</li>

A splice that reverses text

reverseSplice :: Splice AppHandler
reverseSplice = do
    node <- getParamNode
    let text = nodeText node
    return [TextNode $ reverse text]
<div>
  <reverse>Hello</reverse>
</div>
<div>
  olleH
</div>

Applying templates

The apply splice is bound to the apply tag. It loads a template in the node's place.

...
<div class="navbar">
    <apply template="navigation"/>
</div>
...

navigation.tpl:

<ul>
    <li><a href="/">Home</a></li>
    <li><a href="/art">Art</a></li>
    <li><a href="/family">My beautiful family</a></li>
    <li><a href="/rugcollection">Rug collection</a></li>
</ul>

Leaving holes in templates

index.tpl

<html>
  <head>
    <title>My home page! Welcome</title>
  </head>
  <body>
    <div id="main">
        <maincontent/>
    </div>
    <div id="sidebar">
        <sidebar/>
    </div>
  </body>
</html>

mainpage.tpl

<apply template="index">
  <bind tag="maincontent">
    <h1>Welcome to my cool site! I'm 12!</h1>
    <p>
        Hi, I like sloths, they're cool
    </p>
  </bind>
  <bind tag="sidebar">
    <ul>
      <li><a href="/cartoons">Cartoons</a></li>
      <li><a href="/toys">My toys</a></li>
      <li><a href="/superpowers">My superpowers</a></li>
    </ul>
  </bind>
</apply>

A Trivial Snap Application

Getting a project set up

Install Snap for development

$ cabal install snap -fhint

Starting our project

$ mkdir sample-project; cd sample-project
$ snap init
$ cabal install -fdevelopment

Installing Snap with -fhint and installing your project with -fdevelopment enables dynamic recompilation.

Initializing our web app

routes :: [(ByteString, Handler App App ())]
routes = [ ("/", index)
         , ("/echo/:word",  echo)]

app :: SnapletInit App App
app = makeSnaplet "app" "my cool app." Nothing $ do
    h <- nestSnaplet "heist" heist $ heistInit "templates"
    addRoutes routes
    return $ App h

Adding some handlers

index :: Handler App App ()
index = ifTop $ writeBS "hey i'm in your browser"

echo :: Handler App App ()
echo = do
    (Just word) <- getParam "word"
    writeBS $ "Here's a word: " `mappend` word

A Starcraft 2 Blog

What is Starcraft 2?

This is what it looks like

Let's make a site about Starcraft 2

The database

The type for posts

data Post = Post
    { postID :: Maybe Integer
    , title :: Text
    , body :: Text
    , slug :: Text
    , timePosted :: UTCTime
    } deriving (Eq, Show, Read)

Players

data Player = Player
    { playerID :: Maybe Integer
    , name :: Text
    , race :: Race
    , rating :: Integer
    } deriving (Eq, Show, Read)

data Race = Terran | Protoss | Zerg
    deriving (Eq, Show, Read, Enum, Ord)

Matches

data Game = Game
    { gameID :: Maybe Integer
    , player1 :: Player
    , player2 :: Player
    , winner :: Player
    , timePlayed :: UTCTime
    , videoUrl :: ByteString
    } deriving (Eq, Show, Read)

Db.hs

allPosts :: Connection -> IO [Post]

playerByName :: Connection -> Text -> IO (Maybe Player)
allPlayers :: Connection -> IO [Player]
putPlayer :: Connection -> Player -> IO ()

games :: Connection -> IO [Game]
game :: Connection -> Integer -> IO (Maybe Game)
gamesByPlayer :: Connection -> Player -> IO [Game]

base.tpl

    <div id="navigation">
        <a href="/">Home</a>
        <a href="/players">Players</a>
        <a href="/games">Games</a>
        <a href="/about">About</a>
    </div>
    <div id="blurb">
        This is a blog about StarCraft 2
    </div>
    <div id="left-column">
        <main/>
    </div>
    <div id="right-column">
        <sidebar/>
    </div>

Splicing posts

Let's make a splice for displaying post info. It should take something like this ...

<posts>
    <h1><posttitle/></h1>
    <p><postbody/></p>
    <span>Posted on: <timeposted/></span>
</posts>

And turn it into something like this.

<h1>Thorzain wins Dreamhack</h1>
<p>The Swedish Terran Thorzain has won the Dreamhack tournament!</p>
<span>Posted on: 2012-05-25 11:11:11 UTC</span>
<h1>Oz switches to team Fnatic</h1>
<p>The Korean Protoss Oz is now on the team Fnatic.</p>
<span>Posted on: 2012-05-24 11:11:11 UTC</span>
<h1>MVP wins his fourth GSL!</h1>
<p>He's so dreamy.</p>
<span>Posted on: 2012-05-21 11:11:11 UTC</span>

The splice will interact with the database!

Splicing a single post

mkPostSplice :: Post -> Splice AppHandler
mkPostSplice p = runChildrenWithText
    [("posttitle", DB.title p)
    ,("postbody", DB.body p)
    ,("timeposted", showAsText $ DB.timePosted p)
    ]

This function takes a Post and returns a splice which will substitute relevant tags with data from the Post.

If our mkPostSplice function gets this Post

Post { postID = Nothing
     , title = "Thing happens"
     , body = "Wow, a thing happened, this is amazing"
     , slug = "thing-happens"
     , timePosted = someTime
     }

and we bind the resulting splice to the tag examplepost, Heist will transform each occurence of this

<examplepost>
    <h1><posttitle></h1>
    <p><postbody></p>
</examplepost>

into this:

<h1>Thing happens</h1>
<p>Wow, a thing happened, this is amazing</p>

Splicing in all the posts

postsSplice :: Splice AppHandler
postsSplice = do
    conn <- lift $ gets _conn
    posts <- liftIO $ DB.allPosts conn
    mapSplices mkPostSplice posts

 

<posts>
    <h1><posttitle/></h1>
    <p><postbody/></p>
    <span>Posted on: <timeposted/></span>
</posts>
<h1>Thorzain wins Dreamhack</h1>
<p>The Swedish Terran Thorzain has won the Dreamhack tournament!</p>
<span>Posted on: 2012-05-25 11:11:11 UTC</span>
<h1>Oz switches to team Fnatic</h1>
<p>The Korean Protoss Oz is now on the team Fnatic.</p>
<span>Posted on: 2012-05-24 11:11:11 UTC</span>
<h1>MVP wins his fourth GSL!</h1>
<p>He's so dreamy.</p>
<span>Posted on: 2012-05-21 11:11:11 UTC</span>

Binding the posts splice

app :: SnapletInit App App
app = makeSnaplet "app" "our sc2 site" Nothing $ do
    h <- nestSnaplet "heist" heist $ heistInit "templates"
    conn <- liftIO $ connectSqlite3 "data.db"
    addSplices $ map (second liftHeist)
        [ ("posts", postsSplice)
        ]
    addRoutes routes
    wrapHandlers (<|> the404)
    return $ App h conn

Because we bind it when our app is initialized, it's usable from anywhere within our app!

Adding a handler and a route

We don't have to do anything special in the handler. We just render index.tpl! That's because our splice was bound at initialization.

index :: Handler App App ()
index = ifTop $ render "index"

routes :: [(ByteString, Handler App App ())]
routes = [ ("/", index)
         , ("", serveDirectory "static")
         ]

index.tpl

<apply template="base">
  <bind tag="main">
    <posts>
      <h1><posttitle/></h1>
        <p>
        <postbody/>
        <br>
        <span class="date">Posted on: <timeposted/></span>
      </p>
    </posts>
  </bind>
  <bind tag="sidebar">
  </bind>
</apply>

Displaying players

mkPlayerSplice :: Player -> Splice AppHandler
mkPlayerSplice p = runChildrenWithText
    [("name", DB.name p)
    ,("race", showAsText $ DB.race p)
    ,("r", T.take 1 . showAsText $ DB.race p)
    ,("rating", showAsText $ DB.rating p)
    ]
playersSplice :: Splice AppHandler
playersSplice = do
    conn <- lift $ gets _conn
    players <- liftIO $ DB.allPlayers conn
    mapSplices mkPlayerSplice players

players.tpl

<apply template="base">
  <bind tag="main">
    ...
      <tbody>
        <players>
          <tr>
            <td><a href="/players/${name}"><name/></a></td>
            <td><race/></td>
            <td><rating/></td>
          </tr>
        </players>
      </tbody>
      ...
  </bind>
  <bind tag="sidebar">
  </bind>
</apply>

Binding the players splice

app :: SnapletInit App App
app = makeSnaplet "app" "our sc2 site" Nothing $ do
    h <- nestSnaplet "heist" heist $ heistInit "templates"
    conn <- liftIO $ connectSqlite3 "data.db"
    addSplices $ map (second liftHeist)
        [ ("posts", postsSplice)
        , ("players", playersSplice)
        ]
    addRoutes routes
    wrapHandlers (<|> the404)
    return $ App h conn

Adding a handler and a route

index :: Handler App App ()
index = ifTop $ render "index"

players :: Handler App App ()
players = render "players"

routes :: [(ByteString, Handler App App ())]
routes = [ ("/", index)
         , ("/players", players)
         , ("", serveDirectory "static")
         ]

Displaying games

mkGameSplice :: Game -> Splice AppHandler
mkGameSplice game = runChildrenWith
    [("id", textSplice $ maybe "invalid" showAsText $ DB.gameID game)
    ,("player1", mkPlayerSplice $ DB.player1 game)
    ,("player2", mkPlayerSplice $ DB.player2 game)
    ,("winner", mkPlayerSplice $ DB.winner game)
    ,("timeplayed", textSplice $ showAsText $ DB.timePlayed game)
    ,("videourl", textSplice $ bs2text $ DB.videoUrl game)
    ]
gamesSplice :: Splice AppHandler
gamesSplice = do
    conn <- lift $ gets _conn
    games <- liftIO $ DB.games conn
    mapSplices mkGameSplice games

games.tpl

<apply template="base">
  <bind tag="main">
    <ul>
      <games>
        <li>
          <a href="/games/${id}">
            <player1><name/>(<r/>)</player1> vs. 
            <player2><name/>(<r/>)</player2>
          </a>
          <br>
          <span class="date"><timeplayed/></span>
        </li>
      </games>
    </ul>
  </bind>
  <bind tag="sidebar">
  </bind>
</apply>

Binding the games splice

app :: SnapletInit App App
app = makeSnaplet "app" "our sc2 blog" Nothing $ do
    h <- nestSnaplet "heist" heist $ heistInit "templates"
    conn <- liftIO $ connectSqlite3 "data.db"
    addSplices $ map (second liftHeist)
        [ ("posts", postsSplice)
        , ("players", playersSplice)
        , ("games", gamesSplice)
        ]
    addRoutes routes
    wrapHandlers (<|> the404)
    return $ App h conn

Adding the route and handler

index :: Handler App App ()
index = ifTop $ render "index"

players :: Handler App App ()
players = render "players"

games :: Handler App App ()
games = render "games"

routes :: [(ByteString, Handler App App ())]
routes = [ ("/", index)
         , ("/players", players)
         , ("/games", games)
         , ("", serveDirectory "static")
         ]

A handler to display a specific player

player :: Handler App App ()
player = do
  (Just name) <- getParam "name"
  conn <- gets _conn
  mplayer <- liftIO $ DB.playerByName conn (bs2text name)
  case mplayer of
    Nothing -> the404
    (Just player) -> do
      games <- liftIO $ DB.gamesByPlayer conn player
      let playerSplice = mkPlayerSplice player
      let gamesSplice = mapSplices mkGameSplice games
      heistLocal (bindSplice "player" playerSplice
                 .bindSplice "games" gamesSplice
                 ) $ render "player"

player.tpl

<apply template="base">
  <bind tag="main">
    <player>
      <h1><name/></h1>
      <p>He plays <race/> and his rating is <rating/>.</p>
      <ul>
        <games>
          <li>
            <a href="/games/${id}">
              <player1><name/>(<r/>)</player1> vs. 
              <player2><name/>(<r/>)</player2>
            </a>
            <br>
            <span class="date"><timeplayed/></span>
          </li>
        </games>
      </ul>
    </player>
  </bind>
  <bind tag="sidebar">
  </bind>
</apply>

Ideas for improvement

Get the code

http://github.com/bonus500/sc2blog

In summary

Thanks for listening!

Also thanks a million to

Roman Gonzalez, Jesse Heaslip & Tavis Rudd

#