Flake My Life - setting up a minimal flake on NixOS
Tags: nixos, functional, beginners, configuration
Published: 2023-04-18 14:33:51

These days I have been exploring lambda calculus through Nix language which will be the theme of another blog post. I've been trying out `builtins` commands with attribute sets, learning more about flakes, watching some great content on NixOS on YouTube, and playing with `nix repl` and `nix-instantiate --eval`. Overall, I think it's a great time to be learning Nix.

However, I haven't transformed my NixOS configuration into a flake yet. This time, I refuse to simply copy and paste an advanced configuration like I did when I started with NixOS.

The configs I managed to reproduce in the past were from: 

1. [utdemir/dotfile-nix](https://github.com/utdemir/dotfiles-nix) - Utdemir was a great help when I ran into issues with reproducing the config. Our conversation on [GitHub issue #29]( https://github.com/utdemir/dotfiles-nix/issues/29) was my first engagement on GitHub, and it was a such a positive and inspiring experience.

> Your issue was really useful! Always good to know what's the pain points on a fresh install. To be honest, installing this repository from scratch likely won't be a click&forget experience, since I do not routinely test installing from scratch and using other machines other than mine. Let me know if I can help with anything (even if you're not using this repo).

2. [gvolpe.com/blog/xmonad-polybar-nixos](https://gvolpe.com/blog/xmonad-polybar-nixos/) Reproducing this setup took me a couple of days, especially when dealing with the xmonad tiling window manager, which seemed very difficult to set up on NixOS.

3. [Th0rgal/horus-nix-home](https://github.com/Th0rgal/horus-nix-home) This config was another great learning experience with a visually appealing theme. The setup was simple enough, requiring only a few tweaks to the sub-modules.

### Going all over again, this time with a basic minimal setup. 

Although I would love to use any of these three configurations again, I decided to take a more incremental approach. I wanted to find a minimal setup that I could build on with better understanding before attempting more advanced configurations. I discovered the [NixOS-starter-config](https://github.com/Misterio77/nix-starter-configs) repository, which offers minimal and standard flake templates.


> If this is your first trying flakes, or you're attempting to migrate your (simple) config to it; you should use the minimal version.

> If you're here looking for inspiration/tips/good practices (and you already use flakes), or you're migrating a config that already has overlays and custom packages; try the standard version.

OK, let's try the minimal one for starters and then build on that. The goal is to transform an already installed basic NixOS system running the Plasma DE (so no complicated setups with tiling managers yet!) and then build on that.

We are first instructed to create a git directory (because flakes by default work with git):

```nix
cd ~/Documents # this can be anywhere basically
git init nix-config
cd nix-config
```

So now we have a `flake.nix`, a `flake.lock`, and two separate directories `home-manager` and `nixos`. So we can inspect now the files inside of these directories. I recommend not just simply copy pasting one's own `configuration.nix` or `home.nix` because the `configuration.nix` in the `nixos` folder is also a template. Yeah.. I see there are these `overlays` and `overrides` which I have no idea how they work. That's the next other thing I have to learn about. I do remember there is a NixCon 2019 or so, a talk by Graham about overrides and NixOS internals so that's for sure in the next 10 talks I plan to watch on Nix. 

```nix
nix = {
    # This will add each flake input as a registry
    # To make nix3 commands consistent with your flake
    registry = lib.mapAttrs (_: value: { flake = value; }) inputs;

    # This will additionally add your inputs to the system's legacy channels
    # Making legacy nix commands consistent as well, awesome!
    nixPath = lib.mapAttrsToList (key: value: "${key}=${value.to.path}") config.nix.registry;
```

Now here we already face the first obstacle: I do not understand what this means. I read already couple times about this flakes registry. Is this only on my machine? Where is this *registry*? And what are the `nix3` commands? Is this a typo? What is the number `3` next to the `nix` command? 


I will ask GPT4 and copy paste this piece of code into it. 

OK, so this piece of code defines two attributes: `registry` and `nixPath`. That's clear. How are these attributes defined and what do they actually do and what for?

> registry: The purpose of the registry attribute is to create an attribute set that maps each input flake to a registry entry. A registry entry in Nix is a way to map a flake reference to a specific flake. In this case, we are creating a registry entry for each input flake with the same name as the input itself.

So what does this mean. After much talking with GPT it shows me that this registry is not some online package registry of flakes but a local registry simply defined with this piece of code in my `configuration.nix`. So I can input one or many flakes that will be pulled into the rebuild cycle. Now this does remind of sub-modules that we can import into a `configuration.nix` or a `default.nix` but this isn't the same. The flakes are referenced and not simply combined into one flake. Or they are in a way when defined in `flake.nix`?

> The registry is a user-defined mapping of flake references to specific flakes, and it is not a global, system-wide resource like a package registry in other package managers.

> To achieve this, the lib.mapAttrs function is used. lib.mapAttrs is a higher-order function that takes two arguments: a function and an attribute set. It applies the provided function to each key-value pair in the attribute set and returns a new attribute set with the same keys and the resulting values.

Now what and how does this function work? Just by reading this it looks like a general mapping function that maps an operation to a list of things transforming the first list into a new list, or to be precise in our case an attribute set.

```nix
registry = lib.mapAttrs (_: value: { flake = value; }) inputs;
```

Let's check the Nix reference manual at https://nixos.org/manual/nix/stable/language/builtins.html#builtins-mapAttrs  to see the what this `lib.mapAttrs` does:

Apply function *f* to every element of *attrset*. As an example:

```nix
nix-repl> builtins.mapAttrs (name: value: value * 10) { a = 1; b = 2; }
{ a = 10; b = 20; }
```
So here we come to a higher-order *mapping* function which is the bread and butter of functional programming. Nix is a pure functional language based on lambda calculus so these concepts such as *map*, *reduce*, *filter*, and *fold* pop out often. This is the thing I love about Nix. It reminds me on Haskell which is dear to my heart but still conceptually difficult for me. Nix might be actually a nice step towards learning Haskell eventually and also Rust with its functional constructs. So let's simplify this a bit and maybe use a simpler *map* function with numbers and list of numbers to see how it works:

Let's create a function that would double its input. So if we give it a number `2` it will give us back a `4`:

```nix
nix-repl> double = x: x * 2

nix-repl> double 2
4
```

Ok that seems easy enough. Now let's make a list of numbers:

```nix
nix-repl> numbers = [1 2 3 4 5 6]

nix-repl> numbers
[1 2 3 4 5 6]
```
Great! We have a doubling function and a list of numbers. Can we apply our `double` function to a list of numbers as same as we applied it to a single number?

```nix
nix-repl> double numbers
error: value is a list while an integer was expected

       at «string»:1:5:

            1|  x: x * 2
             |     ^
```

Nope. We can't. What do we do now? Now we come to the idea: How could we *map* the `double` function to each number of the list `numbers`? Or in other words, the `map` function takes two arguments: the function to be applied and the list of elements to which the function will be applied. Now is there a `map` function in Nix that we can use? Yes there is! And it's right above the `mapAttrs` function in the Nix reference manual at https://nixos.org/manual/nix/stable/language/builtins.html#builtins-map

So in Nix we have the `map` function that maps over lists and we have the `mapAttrs` function that maps over attribute sets.
So the solution is simple then:

```nix
nix-repl> map double numbers
[ 2 4 6 8 10 12 ]

# same could be done with a square function

nix-repl> square = x: x * x

nix-repl> map square numbers
[ 1 4 9 16 25 36 ]
```

But by observing this behavior we can tell that this will not work with attribute sets because attribute sets have keys and values which is also a kind of map so we are talking about different *data structures*. So our *mapping* function needs to be modified. Mindblowing right? All of this smells like a *Functor* but I won't deep dive into that right now. 

Actually by studying the `mapAttrs` function on our double and numbers list will show the difference:

```nix
let
  lib = import <nixpkgs/lib>;
  doubleValue = _: value: value * 2;
  numbers = { one = 1; two = 2; three = 3; };
  doubledNumbers = lib.mapAttrs doubleValue numbers;
in
  doubledNumbers
```

We need to import the `<nixpkgs/lib>` because our simple `map` is built into nix while the `mapAttrs` is defined in the `lib` library which is a part of `nixpkgs` packages. Save that file as let's say mapAttrs.nix and then:

```nix
❯ nix-instantiate --eval mapAttrs.nix 
{ five = <CODE>; four = <CODE>; one = <CODE>; six = <CODE>; three = <CODE>; two = <CODE>; }
```
Wow! we can see it works! Yes, the `<CODE>` parts are not showing because Nix is a lazy language, it only evaluates what is needed or to put it in better terms:

> Nix only evaluates the function for a specific key-value pair when the corresponding value is accessed in the resulting attribute set. 

If we add `--strict` option to our evaluation then we can see the result and the attribute sets will be evaluated:

```nix
❯ nix-instantiate --eval --strict mapAttrs.nix 
{ five = 10; four = 8; one = 2; six = 12; three = 6; two = 4; }
```

Wonderful! But what else can we see from this? We can see that the objects in this resulting set are unordered. While lists in Nix are ordered collections of elements that preserve the order the attribute sets are unordered because they are designed to be accessed by their keys, rather than the order of the elements.

### So what about our flake?

Let's leave our deep dive into *mapping* functions and look again our flake registry definition.

```nix
registry = lib.mapAttrs (_: value: { flake = value; }) inputs;
```

So the result of our function over the flake attribute set will be stored in the registry attribute set which is basically a *variable* called `registry`. Then it become pretty obvious by now what the `lib.mapAttrs` does - a function that transforms the key-value pairs, and an attribute set to apply the transformation on. But now we come to the second part of the definition which is a lambda function that takes two arguments, `_:` and `values:` Now the underscore is used in cases when the key is not used in the function body same as in our previous example when we doubled the attribute set with `doubleValue = _: value: value * 2;` In this case we are interested in the doubling of the value while the key is empty. This is a concept known in functional language but I still need to see more examples of it.

OK I have talked more with GPT and this quote actually brings things nicely in line:

> When you apply lib.mapAttrs (_: value: { flake = value; }) inputs;, it will iterate over the key-value pairs in the inputs attribute set, and for each key-value pair, it will create a new attribute set with the attribute flake set to the original value. The resulting registry attribute set will have the same keys as the inputs attribute set, but the values will be wrapped in a new attribute set with the flake attribute.

So if our `inputs` attribute set looks like this:

```nix
{
  nixpkgs = ...;
  flake-utils = ...;
}
```
After applying the registry function it will look like this:

```nix
{
  nixpkgs = { flake = ...; };
  flake-utils = { flake = ...; };
}
```
This finally makes so much sense. Now I just need to reread all this exploration couple times, meditate on it, sleep over it, and then again try to install flakes. Flake my life, this isn't easy at all but is super interesting. I never had this much fun configuring the system. I am learning programming and configuring at the same time while on top of it all everything is functional and there are lambda functions everywhere. How amazing is that!

But wait did I setup my flake? No I didn't. But I learned a lot and I have been doing this for hours today whole day. So much from my simple flake setup. But I will flake myself soon! I need to rest and watch some Nix talk and then try out the flake bit again. Then post about it next time. 


![Bill Murray happy](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/339mc5n4un7b4gunu337.jpg)