nomisiv.com / blog / holy-shit-haskell

Holy Shit, Haskell

(Or, “How I Re-rebuilt This Website”)

In the last blog post about my website, I ended up using Nix to generate the content for my website. This might seem like a strange choice at first, surely this is just a post-rationalized and nix-evangelized desicion? No, I still believe it was a great choice, just not quite good enough.

What was good?

Nix provides easy access to several useful programs. I quickly moved on from using cmark-gfm to pandoc instead, as the latter allowed me to use better templates and converting documents to PDF. I also made use of image-magick to convert the favicon from SVG to ICO, and adding that extra dependency was incredibly easy to do. Leveraging nixpkgs I have quick access to basically any program, ready to do whatever I want. Next up, the builtins-functions and the nixpkgs.lib-functions make it trivial to abstract away repetitive patterns when writing similar lines of code, keeping only what actually differs between them. This makes the code cleaner and more concise. Another thing is that nix is basically just a build system at its core, which (surprise) makes it ideal for building things. Everything is a “derivation”, which allows for incremental builds where only the updated pages are regenerated. Templating is also something that comes baked into the language, which is key for inserting automated content into the otherwise static pages.

…and what was bad?

I tried touching the nix code to introduce a new feature, and I was immediately reminded of how fragile this house of cards I had built was. Having just started working at a company using an ML-based programming language, I was used to being treated better than being served hundreds of lines of incomprehensible stack traces ultimately not leading anywhere, whenever I tried to poke the code. Another thing is their lacking standard library. Yes it exists, and yes it has mostly the functions you expect, but it’s not a particularily enjoyable experience. (Shoutout to noogle tho)

Nix, you’re great, but you also fucking suck sometimes.

Discovering old solutions to old problems

I needed types and proper error messages and a real programming language, and I needed it fast. So I asked myself wikipedia what the best purely functional programming language is, and the best alternative seemed to be Haskell:

Haskell it is, then.

Building Haskell with Nix

Haskell and nix have a rather intimate relationship. I’m not quite experienced enough to understand why, but I’m not complaining.

The “traditional” ways to build a Haskell project is to use either cabal or stack. Both these build tools can use nix in the background, but I want nix in control in the foreground. And nix actually uses cabal in their official way to build haskell projects. However, I belive to have found a better way, using nix and only nix:

pkgs.haskellPackages.ghcWithPackages (
pkgs:
with pkgs; [
# Haskell packages go here
]
)

This will wrap the ghc binary, so the included packages always will be available.

pkgs.stdenv.mkDerivation {
name = "haskell-with-nix";
src = ./src;
buildInputs = [haskellWithPackages];
buildPhase = "ghc -O Main -o haskell-with-nix";
installPhase = "install -D haskell-with-nix $out/bin/haskell-with-nix";
}

And this uses the wrapped ghc to build an entire project, with dependencies and modules and everything. ghc will automatically find any extra modules located in the src folder, but the -i<dir>-flag can be used to specify additional paths to search in.

Some other cool things about Haskell

I think it’s rather amazing that to a zoomer like me, Haskell and C feel approximately equally ancient, age wise. But one of them feels significantly more polished and developed and progressive compared to the other. Not gonna say any names or point any fingers, but here are some cool things that Haskell can do that kinda blows my mind:

Is compiling too slow for testing, or do you just want to hack together a small script? runghc allows you to run a haskell program just like you would with python. I think it’s using some JIT magic in the background, but whatever. This is invaluable for developing or just prototyping as you don’t have to mess around with build scripts or wait for the compilation to finish. Just one command.

Following the theme of interpreted-language-vibes is ghci, an interactive mode for Haskell. This is not a very big thing, but it’s helpful when you just want to quickly check the behavior of a function or do some quick prototyping.

Lastly, I want to mention the type system. When I was learning Rust, I was having my mind blown by Option and Result. Finally no more BS exceptions that I forgot to catch. The type system wouldn’t let me not handle the Error (by slapping .unwrap() on everything, as you do). I like this because it makes it extremely explicit where the program is allowed to crash. Haskell’s type system has taken one step further, by not only making crashing the program a type-level thing, but also things like reading and writing to files and sockets. It is impossible for a function to print to stdout if it doesn’t return an IO-monad.

I am absolutely blown away by how modern such an old language feels.

But how did I make my website???

I’ll try not to go too far into the details here. On a high level, I decided to combine the part serving the content with the part generating the content. This would allow me to store the final form of the documents in RAM, which will be infinitely (unnoticeably) faster than accessing the page from disk. So the program reads the Markdown-files and converts them to HTML (more on that later) which is then stored in a map from file name to file data.

pages:
  "index.html"      -> HTML content
  "favicon.png"     -> PNG data
  "blog/index.html" -> HTML content

When the server gets a request it will then try to find the requested file in the map. If it does, it serves the content, but if it doesn’t, it servers 404 instead.

Serving the files

warp appeared to be the most low-level HTTP server for Haskell. I don’t want an entire framework, I just want to define a function taking a HTTP request and returning a HTTP response, and the library doing the rest. And warp does just this, and nothing more.

Converting the documents

I’m still be using pandoc to convert from Markdown to HTML. Now that I’m using the library directly I get more fine grained control over everything. I can for instance parse the list of blog posts, and extract their title and creation date to dynamically generate the list of blog posts in the blog index page.

Generating the images

Generating the favicon.ico file from SVG proved difficult in Haskell. The only package that could export ICO-files didn’t support SVG, and it was also marked broken in nixpkgs. So that was a no-go. At last I found three packages svg-tree, rasterific-svg and JuicyPixels that when used together allowed me to convert the SVG-file to a PNG-file, which could also be used for favicons.

Parsing command line arguments

Instead of implementing my own solution for printing a helpful usage guide and parsing the command line arguments, I found a package called optparse-applicative, which is very similar to Rust’s clap-crate. On top of being easy to use, it can even generate bash/fish/zsh-completions!

Closing thoughts

The Haskell implementation as it stands today is not in any way simpler or smaller than the one I had before in Nix (multiple hundred lines of code and almost 200MB final binary…) but at least I have error messages and types, and I have scratched the re-write itch. At least for now.

We’ll see how long it takes until I rewrite it all from scratch again! ;)