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:
- Pandoc itself is implemented in Haskell, so they will surely have a nice API
- It has an established community with a mature collection of third party packages (all automatically packaged in nix)
- I already had some experience in Haskell from Advent of Code
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! ;)