digestive-functors 0.3
Published on March 21, 2012 under the tag haskell
I’ve just released digestive-functors 0.3, which is a major rehaul of my formlets library. It has a number of great features which (as far as I know) have never been implemented for any formlets library.
This blogpost is very general, so some users might want to jump directly to the
tutorial. Installation is through cabal: cabal install digestive-functors
.
What are formlets?
In 2008, a paper was published, called “The Essence of Form Abstraction”. The paper applied a well-known functional design pattern (Applicative Functors) to have clean way of creating HTML forms, which are inherently composable.
Let’s have a quick look at how this typically works. The following code is based on the initial Haskell implementation of formlets by the paper authors, later maintained by Chris Eidhof and others. We usually have a data structure we want to create a type for:
data Date = Date {month :: Integer, day :: Integer} deriving (Show)
…for which we create a form…
validDate :: Date -> Bool
Date m d) = m `elem` [1..12] && d `elem` [1..31]
validDate (
dateComponent :: FailingForm Date
= Date <$> inputIntegerF (Just 1) <*> inputIntegerF (Just 16)
dateComponent
dateFull :: FailingForm Date
= dateComponent `check` ensure validDate "This is not a valid date" dateFull
And to illustrate the power of any formlets library, it’s composability, let’s
see how you can easily reuse the dateFull
form anywhere:
data User = User {name :: String, password :: String, birthdate :: Date}
deriving (Show)
userFull :: FailingForm User
= User <$> inputF Nothing <*> passwordF Nothing <*> dateFull userFull
digestive-functors
While I was working on a few patches for the formlets library back in 2010, I noticed a few things were impossible to do using this library.
For example, For usability reasons, you might want to add <label>
tags in your
forms. These labels need to be associated with an input element using the for
attribute – when this is the case, the user will be able to click the label
instead of the (relatively small) checkbox. Demo:
Another thing is error handling. When evaluating a form using the original formlets library, you would get a list of errors, and the user would see something like this:
While this is usually pretty clear, it is nicer when the library can actually associate the errors with input field(s), so you are able to show something like:
Along with some other things, these were the practical improvements the digestive-functors library made in comparison to formlets.
digestive-functors 0.3
However, one serious issue remained. When you write down a form in a formlets library, you specify the HTML layout as well as the validation rules. This is really a bad thing: sepataration of model and view is a well known goal in programming.
Separating the HTML layout and the validation rules would lead to a number of benefits:
It would be possible to create multiple representations for a single form (a good example is a login form in the site header, and a login form in the page body, which you see find on many websites).
The code to specify the validation rules becomes smaller and easier to read.
It becomes easier to insert custom HTML code in between your form HTML.
You can rewrite the form using another HTML templating engine, e.g. blaze-html, Hamlet or Heist, without touching the validation rules.
However, it does seem to come with a serious disadvantage as well:
- It seems impossible 1 to have a type-safe coupling between validation rules and HTML layout without losing flexibility or ease-of-use.
In order to make the coupling between the validation rules and the HTML layout,
digestive-functors-0.3 uses simple Text
values. An example of some validation
rules:
= check "This is not a valid date" validDate $ Date
dateForm <$> "month" .: stringRead "Could not parse month"
<*> "day" .: stringRead "Could not parse day"
A major difference you can immediately notice is that we use Text
labels
("month"
, "day"
) and use the custom .:
operator to assign these to parts
of our form. We will use these labels to refer to fields in the HTML layout.
Form composition is obviously still very important. Let’s give an example by
implemening userForm
using dateForm
:
= User
userForm <$> "name" .: string Nothing
<*> "password" .: string Nothing
<*> "birthdate" .: dateForm
One important difference between userFull
and userForm
is that we used
string
twice here – where the original one used inputF
and passwordF
.
This is a result of the separation of concerns. After all, a password is just a
string: it is up to the view to represent it as a password box.
Let’s look at the views now and write an HTML layout using e.g. blaze-html. The code for this is a bit verbose (HTML always is), but clear, and it’s possible to add some utility combinators for it:
= do
dateView view "month" view
errorList "month" view "Month: "
label "month" view
inputText
H.br
"day" view
errorList "day" view "Day: "
label "day" view
inputText H.br
Views are composable as well, which is very important. A developer might want to inline a view, e.g.:
= do
userView view "name" view
errorList "name" view "Name: "
label "name" view
inputText
H.br
"password" view
errorList "password" view "Password: "
label "password" view
inputPassword
H.br
"birthdate.month" view
errorList "birthdate.month" view "Month: "
label "birthdate.month" view
inputText
H.br
"birthdate.day" view
errorList "birthdate.day" view "Day: "
label "birthdate.day" view
inputText H.br
A few things to note. While "name"
and "password"
are both of the same type
(String
) we chose to use a textbox for the former and a password box for the
latter: this is a possibility we gain because of the separation we made. A
(probably more useful) example is when the user has to choose between a number
of options (e.g. Apples, Oranges or Bananas), we can decide in the view code
whether we want to use a combobox or a set of radio buttons.
We use a "foo.bar"
notation to refer to fields of “subforms”. This is useful
when a designer wants a custom form layout, but it leads to duplication of code.
To counter this, views are composable, just like forms!
= do
userView view -- Name, password...
$ subView "birthdate" view dateView
This concludes this blogpost about the digestive-functors 0.3 release.
Note that I have omitted types and other details – you can find everything in this tutorial. The digestive-functors library provides a very easy interface for writing these view libraries: you can basically query the previous input, errors, etc. for each field. This makes it very easy to add libraries for e.g. Hamlet or Heist (but I haven’t done so yet, if anyone is interested, contact me!).
I’ve thought about this for some time, and haven’t found a way to do it, and discovered many problems with the different approaches one could take. The reasoning behind these is outside of the scope of this blogpost, but I’d be happy to elaborate if anyone is interested.↩︎