This blog is powered by the Jekyll static site generator, hosted on GitHub Pages. With Jekyll, you write posts in Markdown-formatted files, which are then rendered into HTML using some templates. When writing technical articles including code examples, as an author I want to make sure these examples are correct, i.e., they compile cleanly and, ideally, some tests pass. I assume the reader appreciates this as well (I know I do!).

One way to tackle this is keeping the code in some source file(s), ensure these compile, and copy-paste snippets into the Markdown source of an article. This works, but copy-paste leaves room for error, and updating source-code while writing the article, then making sure all snippets are updated correctly, is definitely error-prone.

Wouldn’t it be nice if the article file itself would be executable (or, at least, checked by the compiler)? Luckily this is possible, without all too much hassle! Given a working Jekyll installation, here’s what we want:

  • A way to turn an article, including code blocks, into input the compiler can work with.
  • A way to run the article file, or load it in a REPL, including any dependencies the code relies on.
  • A way for Jekyll to include such articles in the rendered website.

Compiling Markdown

The GHC Haskell compiler has built-in support for Literate Haskell, which allows to interleave code blocks with text. The native Literate Haskell support doesn’t know about Markdown formatting, but with some flags, GHC can be instructed to use some custom preprocessor which knows how to extract code from some literate source file.

The markdown-unlit package does exactly this for Markdown-formatted files: it extracts all Haskell code blocks and passes these to the compiler. So Markdown-formatted input like

Here's how to print the string "Hello, world" on the console:

```haskell
printHelloWorld :: IO ()
printHelloWorld = putStrLn "Hello, world"
```

Simple!

becomes

printHelloWorld :: IO ()
printHelloWorld = putStrLn "Hello, world"

For GHC to use the markdown-unlit preprocessor, the -pgmL option must be used, and the source file must have the .lhs file extension.

Aside: Testing README.md

Next to running Markdown articles, markdown-unlit can be used to test your project’s README.md, as I do in the landlock package. See this example for more information!

Running Articles

Now we know how to extract code blocks from article source files for the compiler to process them, we need a way to invoke the compiler (or launch it in REPL mode) with some source file. Of course the ghc binary can be invoked as-is, passing the necessary -pgmL option and any others, but this has several drawbacks, especially when the code in the article depends on some other packages:

  • markdown-unlit must be pre-installed
  • We must not forget the -pgmL option, including the path to markdown-unlit unless it’s installed in $PATH
  • Any dependencies must be installed in a way for GHC to find them
  • Dependencies may require -package ... or other options to be passed to GHC

In the Haskell ecosystem, the cabal tool is used to install dependencies, invoke the compiler with the right options to find these dependencies, etc. In regular Haskell packages, a Cabal project description file (packagename.cabal) contains all information needed for cabal to do its job. A Jekyll article Markdown source file doesn’t come with a project description file, so how can the cabal-install functionality be reused? I recently found out cabal nowadays has a scripting mode: it can read metadata, enclosed in a comment block, from a source file, and use it accordingly. So, including the following in the Markdown source of an article will instruct cabal to provide all dependencies (none but base for this article), ensure markdown-unlit is available, and invoke ghc with the right options:

{- cabal:
build-depends: base ^>=4.17
default-language: Haskell2010
build-tool-depends: markdown-unlit:markdown-unlit
ghc-options: -pgmL markdown-unlit -Wall -Werror
-}

The above snippet could be put in any kind of section, but I prefer to put it in a haskell code-block, so it’s retained in the output of markdown-unlit.

Next to build-depends and similar fields in a cabal section, configuration used in a cabal.project file can be put in a project section. See the documentation for more details.

With all this in place, articles can be executed using cabal run article.lhs, or even better, loaded in a REPL session, including all dependencies, using cabal repl article.lhs.

Rendering Literate Haskell Articles

By default, Jekyll won’t read .lhs files in the _drafts or _posts folders as Markdown files. By default, it will only read files with the .md extension (and several others) as Markdown. To treat .lhs files the same way, add the following to your site’s _config.yml:

markdown_ext: "markdown,mkdown,mkdn,mkd,md,lhs"

Any .lhs files in the _drafts, _posts and other folders will now be rendered as Markdown sources.

Finishing Up

Finally, let’s wrap things up for this particular article:

main :: IO ()
main = printHelloWorld

and indeed:

$ cabal run _drafts/jekyll-literate-haskell.lhs
Hello, world

Of course, this could launch a tasty test-suite or similar, see here for an example.

Happy hacking^Wwriting!

Categories:

Updated: