Dhall support in depth

By @lucasdicioccio, 1048 words, 11 code snippets, 8 links, 0images.

The Dhall support in KitchenSink provides a template engine unlike many static-site generators. In typical static-site generators, the template language serves two purposes: dictating the structure of the HTML files, and generating repetitive content of HTML files like for data tables. In KitchenSink, Layouts dictate the HTML structure and are hard to change. Dhall provides a pre-processor to apply some template on repetititve contents to render Sections.

Dhall as a pre-processor

Why Dhall rather than other languages? The short answer is that Dhall hits a sweet spot for pre-processing sections in static-blog generators. Dhall is at a same time: deterministic, easy to embed in Kitchen-Sink, and expressive. Let’s develop these points.

First, Dhall is a deterministic language. A major benefit here is that Dhall is simple on a conceptual level: you can substitute a Dhall expression with its result and it will work as expected. Further, being deterministic, Dhall disallows user-defined side-effects. Albeit a constraint for people writing Dhall code, such determinism comes handy to write a pre-processor: a pre-processor turns Sections into Sections, since the pre-processor does not do more, for instance, by having no extra side-effects to track. We do not freeze KitchenSink development into a bad-architecture as the Section -> Section function can be moved without surprises.

Second, Dhall is embeddable in Haskell, the programming language in which KitchenSink is written. The immediate benefit is that no external setup is required: KitchenSink embeds the same Dhall interpreter as the official Dhall binary and that’s it. Compare this simplicity (no setup required) to generator sections, whose result vary depending on which tools are available on each setups.

Third, Dhall is expressive enough to have libraries of functions that you import over the network or on the file-system. Such imports open the door to avoiding redundant code (e.g., to re-use the same base:social content in every article). Networked imports open the door to community-based sharing.

Summarizing, the choice of Dhall is not that surprising after analyzis. Dhall fits the problem of pre-processing article sections pretty well, without costing much to implement and without freezing the design of KitchenSink engine.

Examples

Let’s now give some examples.

Rendering a section in CommonMark

example 1

=base:main-content.dhalL

{ contents = ["__generated from dhall__", "\n", kitchensink.file ], format = "cmark" }

example 2

=base:main-content.dhall
    
{ contents = [
    "::: output"
  , "\n"
  , "__this code block is defined in some Dhall__"
  , "\n"
  , "file=", kitchensink.file
  , "\n", "section="
  , Integer/show kitchensink.sectionNum
  , "\n"
  , ":::"
  ]
, format = "cmark"
}

rendered in HTML (via CommonMark)

this code block is defined in some Dhall

file= ./website-src/sections-dhall.cmark

section= +9

some number from a dataset: 42

some list from a dataset:

  • foo
  • bar
  • baz

The same section ported to tramaj. The list has to be built structurally here, in .tramaj-doc mode: expression mode’s contents array has to be a flat list of lines, with no way to splice in one <li> per item and keep the surrounding header lines, so a document built out of real elements is the natural fit.

=base:main-content.tramaj-doc
@d=$ctx.datasets.example
.div(class: "output",
  .p(.strong("this code block is defined in some tramaj")),
  .p("file=`$ctx.file`"),
  .p("section=`$ctx.sectionNum`"),
  .p("some number from a dataset:`$d.answer`"),
  .p("some list from a dataset:"),
  .ul(map($d.items, (i) => .li($i))))

this code block is defined in some tramaj

file=./website-src/sections-dhall.cmark

section=11

some number from a dataset:42

some list from a dataset:

  • foo
  • bar
  • baz

Rendering a section in HTML

You can return HTML directly-formatted.

example

=base:main-content.dhall

{ contents = ["__generated from dhall__", "\n", kitchensink.file ], format = "html" }

Rendering JSON-sections

Dhall can substitute itself to JSON objects as well. Jut replace a .json with .dhall and KitchenSink will interpret a Dhall expression to fill-in the JSON.

A few use cases are envisioned:

  • generating a commands from Dhall expressions
  • generating a command requiring the filename
  • avoiding redundant information like social infos using Dhall’s support for remote includes

example 1

=generator:cmd.dhall
let
  ping = { cmd = "ping"
         , args = ["-c", "3", "8.8.4.4"]
         , target = "latency"
         }
in
{ contents = ping
, format = "json"
}

example 2

This section is handy to show the content in this file. Note that I use kitchensink.file to get the path name.

=generator:cmd.dhall
let
  cmd = { cmd = "cat"
        , args = [kitchensink.file]
        , target = "cat-this-file"
        }
in
{ contents = cmd
, format = "json"
}

once the generated section is evaluated

The same generator in tramaj. This one is a straight port: the result is a JSON value rather than lines of text, so it stays in expression mode, and kitchensink.file becomes $ctx.file.

=generator:cmd.tramaj-json
{ "format": "json"
, "contents": { "cmd": "cat"
              , "args": [$ctx.file]
              , "target": "cat-this-file-templating"
              }
}

once the generated section is evaluated

example 3

You could write the content in ./social.dhall once and import the content.

=base:social.dhall
{ contents = ./social.dhall
, format = "json"
}

example 4

You can also generate JSON datasets if for some reason writing them in JSON is not immediate enough .

=base:dataset.dhall alice-bob
{ contents =
    [ {name = "Alice", posts = 22.0}
    , {name = "Bob", posts = 7.0}
    ]
, format = "json"
}

rendered

And the same dataset in tramaj. A .tramaj-json cell returning format = "json" registers a dataset exactly the way the Dhall one does, so every later section can read it back from $ctx.datasets.

=base:dataset.tramaj-json alice-bob-templating
{ "format": "json"
, "contents": [ {"name": "Alice", "posts": 22}
              , {"name": "Bob", "posts": 7}
              ]
}

rendered

Implementation notes

The following notes are more useful for myself and for people curious about modifying or contributing to KitchenSink.

present

The evaluation chain

Today SiteLoader traverses all sections (i.e., after mapping Article Sections to Article Sections). Upon encountering a .dhall section, SiteLoader evaluates the content as a Dhall expression returning some well-known format.

Further, datasets encountered during this phase are collected in a datasets object. Previous datasets are passed to Dhall sections. Thus Dhall can templatize an in-line dataset.

Thus, unlike generators, Dhall sections are pre-processors. They are evaluated once. When operating Kitchen-Sink as a static-site generator there is no much difference, however if you operate Kitchen-Sink as a live-website, then you cannot use Dhall as a generator-on-steroids.

The kitchensink object

Evaluated Dhall expressions carry information provided by KitchenSink. This information is bound to the kitchensink Dhall value and has the following structure:

{ file : Text -- contains the source file path for the cmark
, sectionNum : Integer -- contains the section number of this file (starting from zero)
, datasets : <data-dependant> -- contains a record<dataset-name, (json-value-to-dhall|text-value)> or an error message if it failed to load
, vars : <data-dependant> -- contains a record<key, value: Text> depending on the --var foo=bar command line arguments
}

Note that missing data-dependant bindings will prevent the site from loading, so be sure to not rely too much on these values.

future work

  • carry-over more context from previous sections
  • allow appending new sections (i.e., preprocess articles over sections)?