osa1 github gitlab twitter cv rss

Quick digestive-functors and heist tutorial

January 3, 2014 - Tagged as: haskell, en.

EDIT: I just found this awesome “Heist in 60 seconds” post in Hesit’s author’s blog, strongly recommended.

I’m currently learning some web programming related libraries for Haskell and I’m very, very confused because of the need for using more than 20 libraries for even the simplest CRUD webapp. In the end I’ll be using Snap, Heist, Digestive-functors, Persistent, Esqueleto and glue libraries to combine all of this together. After wasting several hours trying to learn all of them at once, I decided to move gradually from simplest parts. In this short tutorial, I’ll explain how to create forms using digestive-functors, render them in HTML and run some validation procedures.

This post is written in Literate Haskell, except for the last part, which contains a Heist template for rendering our forms in HTML.

In my opinition, there are two problems for starters of Haskell web programming in Snap. First, Snap lacks some important web development functionalities and for that you need to use separate libraries. This includes form generation and rendering, database operations and probably many others. (on the other hand, we have very high quality libraries so this part may not be a problem, depending on your point of view)

Second, while using other libraries you realize that most of the time documentation is not very good and some important details for starters are missing, when that happens you end up diving into the source code and open source examples.

Anyway, I hope this post serves as an example for using Digestive-functors and Heist together for handling user inputs.

A note before starting: I don’t understand how compiled splices of Heist works, I tried using them but for some reason I couldn’t make it working. So in this post I’ll be using interpreted splices only.

Let’s start with some langauge extensions. You’ll see why this extension is needed below

{-# LANGUAGE ScopedTypeVariables #-}

This one is required for pattern matching against Text values

{-# LANGUAGE OverloadedStrings   #-}
module Main where

Even though our program doesn’t do anything interesting, we still need to use about 10 libraries. I’m showing the package name when a non-standard(e.g. the ones distributed with Haskell Platform) module is imported.

from `blaze-builder’ package

import           Blaze.ByteString.Builder   (toByteString)
import           Control.Applicative        (Applicative (..), (<$>), (<*>))
import           Control.Monad.IO.Class     (MonadIO)

from `either’ package

import           Control.Monad.Trans.Either (EitherT, runEitherT)

from `bytestring’ package, needed for putStrLn function on bytestrings

import qualified Data.ByteString.Char8      as BS (putStrLn)
import           Data.Maybe                 (isJust)
import qualified Data.Text                  as T

from `heist’ package

import           Heist
import qualified Heist.Interpreted          as HI

from `digestive-functors’ package

import           Text.Digestive

from `digestive-functors-heist’ package

import           Text.Digestive.Heist       (bindDigestiveSplices)

In this program, we’ll have one data type that represents a User in our app. I’m planning to extend this post later on to add CRUD database operations on same data type.

data User = User
    { uUsername :: T.Text
    , uEmail    :: T.Text
    , uKarma    :: Int
    } deriving (Show)

userForm is a digestive-functors form for User type, which is used for creating new User and modifying existing User.

In the return type:

.: operator is used to assign a name to form fields. This names are later used in templates, POST request environments and probably in some other places.

userForm :: Monad m => Form T.Text m User
userForm = User
    <$> "username"  .: text Nothing
    <*> "email"     .: check "invalid email" validateEmail (text Nothing)
    <*> pure 0
  where
    -- in our example, we don't need to use `m` monad for validation. if we
    -- were to need that, we could use `checkM` instead of `check`, and
    -- then use a validation function with type `T.Text -> m Bool` for same m.
    validateEmail :: T.Text -> Bool
    validateEmail = isJust . T.find (== '@')

For generating HTML using Heist, we need to maintain HeistState type, which keeps track of information that is needed for rendering templates.

In the code below, `m’ is called “runtime monad” and represents the monad type that rendering functions operate on. We will see an example use later.

I’m using ScopedTypeVariables extension to share type parameter m with type declarations in where part. This is only optional, since I could always use let or just inline the heistConfig definition.

initHeistState :: forall m. MonadIO m => IO (HeistState m)
initHeistState = do
    st <- runEitherT $ initHeist heistConfig
    case st of
      Left errors -> error $ unlines errors
      Right state -> return state
  where
    heistConfig :: HeistConfig m
    -- In HeistConfig, we need to specify what load-time, compile-time and
    -- run-time splices will be available. We also have options for
    -- attibute splices(QUESTION: why we don't have time distinction in
    -- attribute splices?) and locations of template files.
    heistConfig = HeistConfig
      {
      -- default interpreted splices consists of
      -- `apply`, `bind`, `ignore` and `markdown` splices
        hcInterpretedSplices = defaultInterpretedSplices
      -- this is same as default interpreted splices
      , hcLoadTimeSplices = defaultLoadTimeSplices
      -- I'm not using compiled splices because of the reason explained
      -- in first paragraphs
      , hcCompiledSplices = []
      -- .. also here.
      , hcAttributeSplices = []
      -- list of template locations. A template location is an
      -- IO action that either returns a list of error strings,
      -- or a map from template locations to template files.
      -- We're using `loadTemplates` from Heist package for loading
      -- templates from a folder. Subfolders are also traversed.
      , hcTemplateLocations = [loadTemplates "templates"]
      }

This is our function to render form templates written in Heist template format using digestive-functors forms. Digestive-functors forms are not directly renderable, instead we need a View object generated from Form using getForm or postForm from Text.Digestive.View (digestive-functors package).

renderForm :: HeistState IO -> View T.Text -> IO ()
renderForm hs formView = do

Because of a problem, we can’t use digestiveSplices form to get splices and then bind them manually using bindSplices. I think this is because of a type mismatch caused by current versions of digestive-functors-heist and heist libraries. So we need to use bindDigestiveSplices from Text.Digestive.Heist (digestive-functors-heist package).

    maybeBuilder <- HI.renderTemplate (bindDigestiveSplices formView hs) "user_form"
    case maybeBuilder of
      Nothing ->
        -- This happens when wrong template name is given to `renderTemplate`.
        error "template is not rendered"
      Just (builder, mimeType) -> do
        -- here `builder` has type `Blaze.ByteString.Builder.Builder` from
        -- `blaze-builder` package. It's used to efficiently build
        -- ByteStrings.
        BS.putStrLn (toByteString builder)
        print mimeType

I’m just printing stuff, since this app is mostly for learning purposes.

In main function, I do three things:

  1. Just render the form on empty POST request environment. This just renders the form without filling HTML fields with values.
  2. Render form with invalid email address. This fills HTML fields with values form POST request environment, and renders an error message after email field saying that email is invalid. This validation part was handled in validateEmail function above, and error message was specified in userForm function.
  3. Render the form with valid values.

After rendering the HTML code, I’m just printing it. Also, form rendering function(postForm) returns an optional User object depending on the validness of information from POST request. I’m also printing that User object.

main :: IO ()
main = do
    hs <- initHeistState

    -- we need to dynamically bind splices related with form generation
    -- while rendering `user_form` heist template. for that we need to use
    -- `Heist.Interpreter` functions to modify interpereted splices of our
    -- heist state.
    --
    -- To get form splices, we need to pass some POST or GET request as
    -- ByteString to `Text.Digestive.View.getForm` or `postForm`. Then we
    -- can use `Text.Digestive.Heist.digestiveSplices` to get required
    -- splices to render form.

    -- Here the type T.Text comes from first argument of userForm's return
    -- type
    (formView, maybeUser) <- postForm "userform" userForm (const $ return [])
                               :: IO (View T.Text, Maybe User)
    print maybeUser
    renderForm hs formView


    -- Case 2, POST request with invalid email address
    let env path = return $ case path of
                              ["userform", "username"] -> [TextInput "testuser"]
                              ["userform", "email"] -> [TextInput "invalidemail"]
                              _ -> []
    (formView', maybeUser') <- postForm "userform" userForm env
    print maybeUser'
    renderForm hs formView'

    -- Case 3, POST request with valid email address and username
    let env' path = return $ case path of
                               ["userform", "username"] -> [TextInput "testuser"]
                               ["userform", "email"] -> [TextInput "valid@email.com"]
                               _ -> []
    (formView'', maybeUser'') <- postForm "userform" userForm env'
    print maybeUser''
    renderForm hs formView''

Now our program is almost complete, only detail left is the Heist template file. We specified the template path in initHeistState as templates folder, and we’re rendering Heist template named user_form in renderForm. So the template file we need should be templates/user_form.tpl.

Here’s the template file:

<dfForm>
    <dfLabel ref="username">Username: </dfLabel>
    <dfInputText ref="username" />
    <dfErrorList ref="username" />

    <dfLabel ref="email">Email: </dfLabel>
    <dfInputText ref="email" />
    <dfErrorList ref="email" />

    <dfInputSubmit />
</dfForm>

One problem here is that there’s no way to know which tags to put in template file. I wrote this file mostly by looking to source code of bindDigestiveSplices, trial-and-error, and some open source examples.

Output should be something like: (after creating the template file, see below)

Case 1:

Nothing

because POST request environment is not valid, so it’s not possible to create a User object.

<form method='POST' enctype='application/x-www-form-urlencoded'>
    <label for='userform.username'>Username: </label>
    <input type='text' id='userform.username' name='userform.username' value />


    <label for='userform.email'>Email: </label>
    <input type='text' id='userform.email' name='userform.email' value />
    <ul><li>invalid email</li></ul>

    <input type='submit' />
</form>

User form is generated without filling any values and no error messages.

Case 2:

Nothing

because email information in POST request environment is invalid.

<form method='POST' enctype='application/x-www-form-urlencoded'>
    <label for='userform.username'>Username: </label>
    <input type='text' id='userform.username' name='userform.username' value='testuser' />


    <label for='userform.email'>Email: </label>
    <input type='text' id='userform.email' name='userform.email' value='invalidemail' />
    <ul><li>invalid email</li></ul>

    <input type='submit' />
</form>

User form is generated with fields filled and an error message is rendered.

Case 3:

Just (User {uUsername = "testuser", uEmail = "valid@email.com", uKarma = 0})

Since form data is valid, a User object is created.

<form method='POST' enctype='application/x-www-form-urlencoded'>
    <label for='userform.username'>Username: </label>
    <input type='text' id='userform.username' name='userform.username' value='testuser' />


    <label for='userform.email'>Email: </label>
    <input type='text' id='userform.email' name='userform.email' value='valid@email.com' />


    <input type='submit' />
</form>

.. and for is created with values filled, no error messages is rendered.

Note the form and input ids and names. The name passed to postForm is used as prefix of generated HTML elements, and thus also used in POST request environments.

I hope this post helps starters with digestive-functors and heist.