Custom Forms and the Form Monad

October 20, 2010

GravatarBy Michael Snoyman

I think it's fair to say that forms are one of the most complicated parts of web development. A huge part of this complication simply comes from the tedious, boilerplate style code we need to write. A library such as formlets can do wonders to clean this up. Yesod 0.5 follows the philosophy behind formlets.

formlets is great because it lets you forget about so many of the boring details of your form: names, ids, layout. This can all be handled in an automated manner. For example (using Yesod):

data Person = Person { name :: String, age :: Int }
personForm = fieldsToTable $ Person
    <$> stringField "Name" Nothing
    <*> intField "Age" Nothing
myHandler = do
    (res, form, enctype) <- runFormPost personForm
    defaultLayout [$hamlet|^form^|]

Sometimes, however, you do want to deal with some of those pesky details. Yesod already allows overriding the automatically generated ids and names, eg:

<$> stringField "Name" { ffsId = Just "name-field" } Nothing

But Yesod 0.5 doesn't really have a simply way to produce arbitrarily laid out forms. For example, we might want to get fancy with our Person form and produce:

Hi, my name is <input type="text" name="name"> and I am <input type="number" name="age"> years old.

You see, the forms library keeps all of the information on the view of the form locked up, and only returns the whole thing once you run the form. So in theory, with Yesod 0.5, we could do something like:

personForm = Person -- notice no fieldsToTable
    <$> stringField "Name" Nothing
    <*> intField "Age" Nothing
myHandler = do
    (res, [nameFieldInfo, ageFieldInfo], enctype) <- runFormPost personForm
    defaultLayout [$hamlet|
    Hi, my name is ^fiInput.nameFieldInfo^ and I am ^ageFieldInfo^ years old.

But this is an absolute recipe for disaster: we've completely lost so many of our type-safety benefits by forcing a pattern match on a specific size of a list, if you change the order of our fields the fields will be backwards, we need to remember to update multiple places when the Person datatype changes, and so on. What we'd like is to have to stringField and intField functions give us the HTML directly, something like:

personForm = do
    (name, nameHtml) <- stringField "Name" Nothing
    (age, ageHtml) <- intField "Age" Nothing
    return (Person name age, [$hamlet|
    Hi, my name is ^nameHtml^ and I am ^ageHtml^ years old.

This doesn't work. The GForm datatype doesn't have a monadic instance. It's easy enough to add one, but that would result in one of two things:

  • We would have an inconsistent definition of our Monad and Applicative instances. You see, Applicatives cannot use the result from a previous action in determining the next course of action, which allows them to collect multiple error values. (I know this is vague, please forgive me, a fully fleshed out explanation would not fit here well.)

  • We would need to cut out the true power of the forms library, but defining a weaker Applicative instance which can't collect multiple failures. Imagine a form validation that only tells you the first validation error.

Additionally, the code above will only really return HTML if the stringField and intField functions succeed. It turns out that we need a different approach to handle this problem.


Yesod 0.6 will be adding a new datatype, GFormMonad, to complement the GForm datatype. As a high-level overview: GForm automatically handles keeping track of validation failures and HTML for you; GFormMonad gives you direct access to these. As an example:

personForm = do
    (name, nameField) <- stringField "Name" Nothing
    (age, ageField) <- intField "Age" Nothing
    return (Person <$> name <*> age, [$hamlet|
    Hi, my name is ^fiInput.nameField^ and I am ^fiInput.ageField^ years old.

This is almost identical to what we wrote as our ideal code above, but not quite. The name variable above has a datatype of FormResult String. In other words, when using GFormMonad, you have to deal with the possibility of validation errors directly. Fortunately, you are still free to use the Applicative instance of FormResult, as we did here.

Also notice how I'm still using stringField and intField; field functions are all now polymorphic, using the IsForm typeclass. In order to unwrap a GFormMonad, we use runFormMonadGet and runFormMonadPost, like so:

myHandler = do
    ((personResult, form), enctype) <- runFormMonadPost personForm
    case personResult of
        FormSuccess person -> ...
    defaultLayout [$hamlet|^form^|]

Making the run functions non-polymorphic helps the type system figure out what you're trying to do.

Other changes

As I had anticipated, there are not going to be many other changes in Yesod 0.6. discovered a bug in MonadCatchIO which screwed up the database connection pool, so I had to yank all of the MonadCatchIO code out and replace it with something else. I've moved the Yesod.Mail code to a separate package, as well as Yesod.Helpers.Auth.

I'm going to spend a few more days going through the Haddocks and make sure names are consistent and the docs are comprehendable, and will probably make the Yesod 0.6 and Persistent 0.3 release some time early next week. It's already powering and the Hackage dependency monitor, so I think it's solid enough. This is a last call for change requests!

Given the API stability between 0.5 and 0.6, I feel pretty confident that the next release of Yesod after this will be 1.0. My goal for that release is all about documentation: I want to get as much content as possible into the book, and have it polished. If you see something in the book you don't understand, please ask, and if you see a topic I've skipped, bring it up. We're really building a great community for Yesod, and I need everyone's help to make it the best it can be.


comments powered by Disqus