Hello! Yes, this blog is still alive. In this post, I want to share a small little pattern that I’ve found to have a surprisingly high quality-of-life improvement, and I call it the list of monoids pattern.
The idea is that whenever we have a monoidal value - a type that is an instance of the Monoid
type class - we can sometimes produce a more ergonomic API if we change our functions to instead to a list of these monoidal values.
I recently proposed an instance of this pattern to lucid
, and it was well received and ultimately merged as part of the new lucid2
package. To motivate this post, I’m going to start by reiterating my proposal.
lucid
is a domain specific language for producing HTML documents. In lucid
we have the Attribute
type which represents a single key-value pairing. When we construct HTML elements, we supply a [Attribute]
list. For example,
div_ :: [Attribute] -> Html a -> Html a
(Note that lucid
has an overloading mechanism, and this is one possible type of div_
).
The problem with this API is that it makes it difficult to abstract groups of attributes and reuse them.
My motivation came from using the fantastic HTMX library, and wanting to group a common set of attributes that are needed whenever you connect an element with an end-point that serves server-sent events. More specifically, I wanted to “tail” an event stream, automatically scrolling the latest element of the stream into view (using Alpine.js). An example of the attributes are:
<div
hx-sse="connect:/stream"
hx-swap="beforeend"
x-on:sse-message.camel="$el.scrollIntoView(false);">
In lucid
, we can express this as:
div_
[ makeAttribute "hx-sse" "connect:/stream"
, makeAttribute "hx-swap" "beforeend"
, makeAttribute "x-on:sse-message.camel" "$el.scrollIntoView(false);" ]
This is great, but my problem is wanting to re-use these attributes. If I have another page that I also want to have a stream in, I could copy these attributes, but the programmer in me is unhappy with that. Instead, I want to try and share this definition.
One option is:
=
tailSSE url "hx-sse" $ "connect:" <> url
[ makeAttribute "hx-swap" "beforeend"
, makeAttribute "x-on:sse-message.camel" "$el.scrollIntoView(false);"
, makeAttribute ]
But look what happens when I use this:
div_concat [ tailSSE "/stream", [ class_ "stream-container" ] ]) (
Urgh! Just using this requires that I call concat
, and to use more attributes I have to nest them in another list, and then I have to surround the whole thing in parenthesis. Worse, look what happens if we consider this code in the context of more “ordinary” HTML:
"page"] do
div_ [class_ "Heading"
h1_
div_"scroll"]
[class_ do
div_concat
( "/stream",
[ tailSSE "stream-container",
[ class_ "stream-1"
id_
]
] )
Our SSE attributes stand out like a sore thumb, ruining the nice DSL that lucid
gives us.
At this point, we need to start thinking about ways to fix this.
Before we get to that, let’s look at one more example
Continuing with lucid
, I’m also a user of Tailwind for styling pages. In Tailwind, we combine primitive classes to style our elements. Sometimes, this styling needs to be conditional. When we we layout a list, we might want to emphasize a particular element:
do
ul_ "p-4" ] "Item 1"
li_ [ class_ "p-4 font-bold" ] "Item 2"
li_ [ class_ "p-4" ] "Item 3" li_ [ class_
Generally this list will come from another container which we want to enumerate over:
do
ul_ ->
for_ items \item $ if active item then "p-4 font-bold" else "p-4" ] li_ [ class_
It’s unfortunate that we’ve had to repeat p-4
here. We could of course factor that out, but what I more generally want to do is define a common attribute for list items, and another attribute that indicates active. Then, for active items I can just conditionally add the “active” element:
do
ul_ ->
for_ items \item
li_"p-4"
[ class_ if active item then class_ "font-bold" else ???
,
] (toHTML (caption item))
But what we are we going to put for ???
? There isn’t really an “identity” attribute. A common hack is to add class_ ""
, but that is definitely a hack.
If you see both of these problems, a natural reaction might be to make Attribute
an instance of Monoid
. We might change the type of
div_ :: Attributes -> Html a -> Html a
However, when we do this we momentarily make things a little worse. Starting with the second example 2:
do
ul_ ->
for_ items \item
li_mconcat
( "p-4",
[ class_ if active item then class_ "font-bold" else mempty
,
]
) (toHTML (caption item))
Our ???
becomes mempty
which literally means “no attributes at all”. This solves our problem, but the cost is that overall the API has got more verbose.
How about our first example?
"page") do
div_ (class_ "Heading"
h1_
div_"scroll")
(class_ do
div_mconcat
( "/stream",
[ tailSSE "stream-container",
, class_ "stream-1"
, id_
] )
The result here is somewhat mixed. Applying a single attribute isn’t too bad, but my main objection to this was that it’s inconsistent, and here it’s even more inconsistent - a single attribute uses parethesis, but multiple attributes need a call to mconcat
. It is nice though that our tailSSE
call no longer sticks out, and just looks like any other attribute.
With that setup, I can now present my solution - the list of monoids pattern. As the name suggests, the trick is to simply change our Attributes
argument to now be a [Attributes]
. This is essentially a list-of-lists of key-value pairs, which is probably not our first instinct when creating this API. However, I think it pays of when we try and lay out HTML using lucid
:
"page" ] do
div_ [ class_ "Heading"
h1_
div_"scroll" ]
[ class_ do
div_"/stream",
[ tailSSE "stream-container",
, class_ "stream-1"
, id_ ]
We’re back to where we started! However, we’ve also retained a solution to the second example:
do
ul_ ->
for_ items \item
li_"p-4",
[ class_ if active item then class_ "font-bold" else mempty
,
] (toHTML (caption item))
lucid
could go furtherInterestingly, once I had this observation I realised that lucid
could actually go further. Html a
is a Monoid
, but notice that when we construct a div_
we supply a single Html a
. This post suggests that an alternative API is instead:
div_ :: [Attributes] -> [Html a] -> Html a
Users of Elm might be getting a sense of déjà vu, as this is very similar to the type that Elm uses! I like this form because I think it makes HTML documents much more regular:
div_"p-4 font-bold" ]
[ class_ "Paragraph 1"
[ p_ "haskell.gif" ]
, img_ [ src_ "More"
, p_ ]
Elm falls short of the pattern advocated in this blog post, as both attributes and html elements lack an identity element, so while Elm uses lists, they aren’t lists of monoidal values.
optparse-applicative
I want to briefly compare this to the API in optparse-applicative
. Gabriella postulated that optparse-applicative
might be more approachable if it used records instead of its current monoidal based API. While I don’t disagree, I want to suggest that the list-of-monoids pattern here might also help.
When we use optparse-applicative
, we often end up with code like:
flagTrue
False
"no-extensions"
( long <> short 'E'
<> help "Don't show the possible extensions for physical files"
)
Here we’re using a <>
like a list. Unfortunately, this has two problems:
Automatic code formatters already have a way to format lists, but they aren’t generally aware that we’re using <>
as if it were constructing a list. This leads to either unexpected formatting, or special-casing within the formatter.
It’s less discoverable to new Haskell users that they can supply multiple values. Even an experienced Haskell user will likely have to look up the type and spot the Monoid
instance.
If optparse-applicative
instead used a list of monoids, the API would be a little more succinct for users, while not losing any functionality:
flagTrue
False
"no-extensions"
[ long 'E'
, short "Don't show the possible extensions for physical files"
, help ]
Modifiers can be grouped and abstracted as before, and if we want to compute modifiers with an option to produce no modifiers at all, we can still return mempty
. However users are no longer burdened with needing to combine modifiers using <>
, and can instead lean on Haskell’s special syntax for lists.
If at this point you’re somewhat underwhelmed by this blog post, don’t worry! This pattern is extremely simple - there are no complex tricks required, it’s just literally wrapping things in a list, moving a call to mconcat
, and you’re done. However, I think the implications are fairly significant, and I highly recommend you give this a try.
You can contact me via email at ollie@ocharles.org.uk or tweet to me @acid2. I share almost all of my work at GitHub. This post is licensed under a Creative Commons Attribution-NonCommercial-NoDerivs 3.0 Unported License.