I've been playing with Haskell, trying to create a very simple website using Servant and Lucid. At the moment I reached the stage "My code works, I have no idea why". I tried creating Bootstrap button. According the the doc, it should be defined as:
<button type="button" class="btn btn-primary">Primary</button>
So I found Lucid.Html5 doc: https://hackage.haskell.org/package/lucid-2.9.11/docs/Lucid-Html5.html and worked out the function that creates a button:
button_ :: Term arg result => arg -> result
After spending some time trying to work out the correct syntax, I came up with this:
-- correctly replicates the html pasted above
button_ [type_ "button", class_ "btn btn-primary"] "Primary"
Typically I would have called it a victory and focused on other tasks, but this one looks like a true piece of magic to me.
The doc says "button_" is a function that takes an argument "arg" and returns a value of a generic type "result". However, in my application "button_" clearly takes two arguments and returns "Html ()".
-- f arg arg again ??
button_ [type_ "button", class_ "btn btn-primary"] "Primary"
It must do something with the "Term" typeclass, but I'm not sure how to understand it. Can someone help me with this ? I tried loading the module into ghci and inspecting types with ":t", but that didn't help me too much.
The Term
typeclass is very convenient—we don't need different term
functions for creating elements with or without attributes—but can be a bit difficult to understand.
The definition of button_
is
-- | @button@ element
button_ :: Term arg result => arg -> result
button_ = term "button"
term
is a method of the Term
typeclass, and has type:
term :: Text -> arg -> result
That is: you give it the name of the element, some argument whose type depends on the particular instance, and it returns some result whose type depends on the particular instance. But what instances are available? There are three:
Term Text Attribute
-- here, term :: Text -> Text -> Attribute
This one is for creating attributes, not elements.
Applicative m => Term (HtmlT m a) (HtmlT m a)
-- here, term :: Text -> HtmlT m a -> HtmlT m a
This one is for creating elements without attributes. The arg
we pass as argument to term
is some piece of html representing the children, and we get another piece of html in return.
(Applicative m, f ~ HtmlT m a) => Term [Attribute] (f -> HtmlT m a)
-- here, term :: Text -> [Attribute] -> HtmlT m a -> HtmlT m a
This one is the most confusing, and the one that is being used in your code. Here, arg
is a list of Attribute
values. That much is clear. But result
is the type of a function! After passing the attributes, we are left with another function HtmlT m a
-> HtmlT m a
which allows us to supply the contents of the button ("Primary" in your case).
The f ~ HtmlT m a
is another wrinkle that isn't really relevant for this answer. It simply says that f
is equal to HtmlT m a
. Why not put it directly then? Well, in certain cases it can help drive type inference in desirable ways.