Search code examples
htmlhaskellbootstrap-4haskell-lucid

The true meaning of Lucid's "Term" type


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.


Solution

  • 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.