From Dhall to TOML to Free Monad
Lately I wanted to add a “link blog” (I named it tidbits) to this statically generated personal website. Unlike regular pages, each with a corresponding Markdown file on disk, link items are best represented in structured data of some form. Traditionally we used XML, but being a Haskeller I wanted to use this as an excuse to explore Dhall.
Dhall
See PR that added Dhall support
A single link entry (called “tidbit”) looked like this Dhall:
[ { date =
"2019/12/31"
, url =
"https://qz.com/1279371/"
, title =
"This physicist’s ideas of time will blow your mind"
, note =
''
Introduces us to the theoretical physicist Carol Rovelli's
book *The Order of Time*, wherein he essentially argues for
time being an illusion.
''
}
]
: List ./Tidbit.dhall
Repeat that ad-nausean for every link to be added. The main problem with Dhall is ergonomics. I found myself to be no fan of meticulously writing syntax when all I want to do is add a link to my site. The other pain point was having to keep the Haskell type definition in sync with the Dhall type, however as of this writing Dhall very recently gained the ability to generate Haskell types from Dhall types.
TOML
See PR that switched from Dhall to tomland
Wanting better ergonomics I stumbled across TOML, via the tomland library.
tomland
does bidirection serialization, which allows me to generate the TOML syntax from a Haskell value. This was attractive to me, because then I can just write a subcommand to append a new link (auto-fetching title, prompting other properties) to the tidbits.toml
file, which looks like this:
[[tidbit]]
date = 2019-12-31
title = "This physicist’s ideas of time will blow your mind"
url = "https://qz.com/1279371"
tags = ["Humanity"]
note = """
An article about the theoretical physicist Carol Rovelli's book *The Order
of Time*, wherein he essentially argues for time being an illusion. What I
found most remarkable about his explanation is the seeing of *identity*
itself as a product of perception of time.
"""
I find this to be a little more easier to work with than Dhall.
Free Monad
I am satisfied with TOML for describing structured data like links. In another static site of mine, which tracks data for quantified self (in particular: skin health and foods consumed, tracked every day), however, I soon hit the limitation of having to use a serialization library. I wanted the flexibility of Haskell types, without having to wrestle with tomland’s encoding/ decoding layers. Since rib (the software used to generated this static site) already uses ghcid
, I figured - why not just keep the data in Haskell source?
The first implementation turned out to be too ugly to bother to maintain for foreseable future:
entries :: [(Day, Entry)]
entries =
mconcat
[ first (fromGregorian 2020 1)
<$> [ neutralN
4
"Ate one meal at 8pm today."
$ commonF 3 <> [costcoStrip, costcoSalmonW],
neutral
5
$ commonF 3 <> [ffWagyu, costcoShrimpW]
]
]
where
neutral day fs = neutral' day Nothing fs
neutralN day s fs = neutral' day (Just $ Markdown s) fs
neutral' day s fs = (day, Entry Neutral fs s)
commonF c = [Coffee c, fwTallow, pepper]
Ideally, I just want to give high-level “commands” when defining each entry, like “Set note for today to this: …” and “Record that I consumed this food”. Moreover I also wanted to “compose” these commands, and invoke them normally. Evaluating all these commands should ultimately result in the creation of the “Entry” Haskell record.
I was reminded of free monad being used for DSL, so why not use this as an excuse to learn to use it?
As a result, I arrived at a much more ergonic way to define my data:
entries :: [(Day, Entry)]
entries =
[ onDay 2020 1 4 Neutral $ do
coffee 3
nyStrip
addFood costcoSalmonW
setNote "Ate one meal at 8pm today.",
onDay 2020 1 5 Neutral $ do
coffee 3
cheapSurfNTurf
nyStrip
setNote "Going forward, sticking to **2 regular sized meals**.",
onDay 2020 1 6 Good $ do
coffee 3
cheapSurfNTurf
nyStrip,
onDay 2020 1 7 Good $ do
coffee 4
cheapSurfNTurf
nyStrip
setNote "2nd cup at Caprices d'Alice, then one more at home."
]
where
onDay y m d s e = (fromGregorian y m d, runEntry $ setSkin s >> e)
-- Food combinators
coffee = addFood . Coffee
-- | GB + shrimps (with pepper only), cooked in tallow.
cheapSurfNTurf = do
addFood fwTallow
addFood pepper
addFood cumin
addFood ffWagyu -- Wagyu ground beef
addFood costcoShrimpW
-- | Costco NY strips cooked with pepper in tallow
nyStrip = do
addFood fwTallow
addFood pepper
addFood costcoStrip
Looks much better, and I can compose commands to create new ones (here, cheapSurfNTurf
and nyStrip
).
The free monad type is here:
data EntryProgramF a
= SetNote Text a
| Skin Mood a
| AddFood F a
deriving (Functor)
type EntryProgram = Free EntryProgramF
Using Free Monads to incrementally construct a (barbie) record
The end goal of this DSL is to build a simple Haskell record. As such, the record must represent “partially filled” states. To that end, I used the barbies library, specifically its strippable HKDs feature.
The definition of the record is:
-- Not shown here is the `F` ADT which delineates into
-- a bunch of other ADTs (mainly to distinguish between
-- food groups and their sources)
declareBareB
[d|
data Entry'
= Entry'
{ skin :: Mood,
food :: Set F,
note :: Maybe Markdown
}
|]
Finally the free monad interpreter uses State
to incrementally build this record, based on the commands issued. Something like this:
interpretEntry
:: EntryProgramF a -> State (Entry' Covered Maybe) a
interpretEntry = \case
SetNote s x -> do
modify $ \e -> e {note = Just (Just (Markdown s))}
pure x
Skin mood x -> do
modify $ \e -> e {skin = Just mood}
pure x
AddFood fx x -> do
modify $ \e -> e
{ food = Just $
maybe (Set.singleton fx) (Set.insert fx) (food e)
}
pure x
Better alternatives?
What do you think, dear reader? Are their simpler ways to achieve the same goal of ergonomics and flexibility?