Abusing Haskell: Executable Blog Posts

Why? Because I can, and it is a rainy Sunday.

I post my notes on my blog, Hashnode and dev.to, which require slightly different markdown formats. I have been doing the sane thing to fix formats so far. But it is a rainy Sunday and I am bored, so I decided to make this blog post an executable Haskell program to do the same.

The Non-Problem

I am blogging on https://thenegation.com. It is a static site generated by Zola in a Nix shell. I write my blog posts in Markdown.

After I publish a post on my blog, I cross-post it to my dev.to and Hashnode blogs with the hope of reaching a wider audience. This is a manual process. One of the steps in this process is to copy the post content in Markdown format that is compatible with the target platform.

The Friendly Solution

I do not care much about the specifics of the original and target Markdown formats. Following assumptions worked so far: The original format uses newlines as soft-wraps, and the target format may use them as hard-wraps. Also, the output format does not need a front-matter.

So, following was sufficient and convenient:

pandoc \
  --from markdown \
  --to markdown \
  --wrap=none \
  --strip-comments=true \
  content/posts/2024-08-04_abuse-haskell.md

What is wrong with this solution? It is too boring to my liking.

Alternative Solutions

There are a few alternatives to this solution:

  1. Stop blogging

  2. Do not cross-post

  3. Use standard Markdown everywhere

  4. Write a Visual Basic script to do the conversion

Clearly, none of these solutions would offend anyone enough to be interesting. So, pass!

The Ultimate Solution

What if I would write a blog post that would be an executable that I can use to convert my blog posts to the target format?

This is a terrible idea. So, let’s do it!

The Plan

This very blog post is going to be a literate Haskell program that I can compile with ghc. Then, I will use the compiled executable to convert the format of my Markdown files removing soft line-wraps, front-matter and HTML comments.

Since I am using a Nix shell for my blog’s codebase, I will provision a GHC inside it:

{
  ##...

  ghc = pkgs.haskellPackages.ghcWithPackages (hpkgs: [
    hpkgs.markdown-unlit
    hpkgs.pandoc
  ]);

  thisShell = pkgs.mkShell {
    buildInputs = [
      ## ...

      ghc

      ## ...
    ];

    NIX_GHC = "${ghc}/bin/ghc";
    NIX_GHCPKG = "${ghc}/bin/ghc-pkg";
    NIX_GHC_DOCDIR = "${ghc}/share/doc/ghc/html";
    NIX_GHC_LIBDIR = "${ghc}/lib/ghc-9.6.5/lib";
  };

  # ...
}

I am so ready for the implementation…

The Implementation

Let’s start with adding some MSG for tastier Haskell, also known as GHC Language Extensions:

{-# LANGUAGE OverloadedStrings #-}

We need some imports:

import System.Environment (getArgs)
import qualified Data.Text as T
import qualified Data.Text.IO as TIO
import qualified Text.Pandoc as P

Our workhorse function is unfortunately quite simple:

convert :: T.Text -> IO T.Text
convert txt = P.runIOorExplode $ do
  md <- P.readMarkdown readerOptions txt
  P.writeMarkdown writerOptions md
  where
    readerOptions = P.def
        { P.readerExtensions = P.enableExtension P.Ext_yaml_metadata_block $ P.getDefaultExtensions "markdown"
        , P.readerStripComments = True
        }
    writerOptions = P.def
        { P.writerExtensions = P.githubMarkdownExtensions
        , P.writerWrapText = P.WrapNone
        }

Now, we can implement our entrypoint function:

main :: IO ()
main = do
  path <- head <$> getArgs
  iTxt <- TIO.readFile path
  oTxt <- convert iTxt
  TIO.putStrLn oTxt

We are done with the program. We can run our blog post via runhaskell on our blog post. But first, we need to symlink our Markdown file (.md) with a literate Haskell file extension (.lhs) so that GHC is not upset:

ln -sr \
  content/posts/2024-08-04_abuse-haskell.md \
  content/posts/2024-08-04_abuse-haskell.lhs

Then, we can run the blog post on the blog post itself:

runhaskell \
  -pgmLmarkdown-unlit \
  content/posts/2024-08-04_abuse-haskell.lhs \
  content/posts/2024-08-04_abuse-haskell.md

We can even compile our blog post into an executable and use it later:

$ ghc -pgmLmarkdown-unlit content/posts/2024-08-04_abuse-haskell.lhs
[1 of 2] Compiling Main             ( content/posts/2024-08-04_abuse-haskell.lhs, content/posts/2024-08-04_abuse-haskell.o )
[2 of 2] Linking content/posts/2024-08-04_abuse-haskell
$ content/posts/2024-08-04_abuse-haskell content/posts/2024-08-04_abuse-haskell.md

I even added a script in my Nix shell that aliases above so that I can use it all my life:

dev-md-format content/posts/2024-08-04_abuse-haskell.md

Wrap-Up

This solution will help me continue abusing Haskell in the future. It was stupid enough, as well, to deserve its own GitHub template repository:

https://github.com/vst/literate-haskell-nix-example

You can play with it, or see it in action in my blog’s source code.

Joke aside; rain is over.

0
Subscribe to my newsletter

Read articles from Vehbi Sinan Tunalioglu directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Vehbi Sinan Tunalioglu
Vehbi Sinan Tunalioglu

My name is Sinan. I am a computer programmer and a life-style entrepreneur. You can check my LinkedIn and GitHub profile pages for more information, and send an email to vst@vsthost.com to contact me. I am re-publishing my technical blog posts on hashnode. My website is available on thenegation.com, and its source code is available on GitHub.