rauhala.info/posts/configs.md

2.7 KiB

title date
Extensible configuration Pt. 1 2019-03-27

This is the first part of a series where I'm going through how to make extensible configuration. There is nothing groundbreaking or new in this series, it's just me going through different implementations and trying to understand them.

The source material for this post is the fixed point implementation for nix.

By extensible configuration, I'm talking about nix style extensible configuration, like overlays, overrides and extensions. Let's see an example of an extensible configuration.

{ haskellPackages, fetchFromGitHub }:

let
  purescript = fetchFromGitHub {
    owner = "purescript";
    repo = "purescript";
    rev = "2cb4a6496052db726e099539be682b87585af494";
    sha256 = "1v4gs08xnqgym6jj3drkzbic7ln3hfmflpbpij3qzwxsmqd2abr7";
  }
  hp = haskellPackages.extend (self: super: {
    purescript = super.callCabal2nix "purescript" purescript {};
  });

in

hp.purescript;

On a high level we are augmenting the haskellPackages attrset by replacing the existing purescript package with a different one. The extension is a function that takes two arguments, self and super. super is the original non-lazy value and self is the lazy value that corresponds to the value at end.

The first step on this journey is done by getting to know fix. Fix is described being the least fixed point of a function. In practice it's a function allowing declaring recursive functions without explicit recursion.

fix = f: let x = f x; in x

With fix you can have access to the lazy self value. It's value is whatever would have been computed in the end. As it is lazy, it is possible to end up in a recursive loop if there is a cyclic dependency.

let recursive = fix (self: {
  foo = 3;
  bar = self.foo + 1;
});
infinite = fix (self: {
  foo = self.bar + 1;
  bar = self.foo + 1;
});

You can try those yourself. The first version is fine and returns an attrset like you would expect. The second one has a cyclic dependency and nix helpfully errors out.

The next step is making a function that has access to the unmodified original values. This is managed through the extends function. It took a while for me to understand what's happening in there, but luckily nix has good documentation for it.

extends = f: rattrs: self: let super = rattrs self; in super // f self super