Tidbit: From Dhall to TOML to Free Monad

You are reading a tidbit which, unlike carefully written articles, are often written in haste. Think of tidbits as positioned somewhere in between tweeting and blogging.

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.


See PR that added Dhall support

A single link entry (called “tidbit”) looked like this Dhall:

[ { date =
  , url =
  , 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.


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:

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 =
    [ first (fromGregorian 2020 1)
        <$> [ neutralN
                "Ate one meal at 8pm today."
                $ commonF 3 <> [costcoStrip, costcoSalmonW],
                $ commonF 3 <> [ffWagyu, costcoShrimpW]
    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
      addFood costcoSalmonW
      setNote "Ate one meal at 8pm today.",
    onDay 2020 1 5 Neutral $ do
      coffee 3
      setNote "Going forward, sticking to **2 regular sized meals**.",
    onDay 2020 1 6 Good $ do
      coffee 3
    onDay 2020 1 7 Good $ do
      coffee 4
      setNote "2nd cup at Caprices d'Alice, then one more at home."
    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)
    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:

  :: 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?