(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.
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.
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.
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.
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.
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.
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.
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.
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 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.
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!
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! ;)