Abusing Haskell: Executable Blog Posts

Vehbi Sinan Tunalioglu - Aug 4 - - Dev Community

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
Enter fullscreen mode Exit fullscreen mode

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";
  };

  # ...
}
Enter fullscreen mode Exit fullscreen mode

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 #-}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
        }
Enter fullscreen mode Exit fullscreen mode

Now, we can implement our entrypoint function:

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

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .