JeffTP

Squandering a perfectly good opportunity to shut up and listen.

Understanding the Nix Language

Published: 2024-05-28 • Reading time: 7 min

#linux #nix #nixos #sysadmin

It's no mistake that there's a large time gap since my last post about NixOS and using Disko to automatically partition disks. Understanding what NixOS is doing has been a challenge. Over the last month I've been making myself more familiar with NixOS by using a NixOS flake called ZaneyOS on my primary home system.

ZaneyOS provides a great configuration to jump into NixOS with the Hyprland window manager. After a few configuration file edits I booted up into NixOS and rebuilt into the ZaneyOS flake. Another reboot and my system was ready for me to log in and go.

I spend several days perusing the nix configuration files that make up ZaneyOS attempting to understand how they work. I wanted to know what was getting installed and how those packages were being configured. Mostly, I was lost. I realized it was time to learn more about this Nix language...

NixOS has gained popularity over the last 12 months. There's been new efforts from the NixOS community to pull together easier to understand documentation. Just 8 months ago, while looking into NixOS, I was running into documentation that would unironically say things like, "A monad is just a monoid in the category of endofunctors."

The Nix Language

The Nix language is a functional programming language. Understanding the Nix language has been challenging because I am not a ~skilled~ functional programmer. I've used functional programming patterns in both Python and Rust. I agree with the basic functional philosophy that avoiding mutability and side-effects is a good idea. Let's see how far that gets me.

I started with the Nix Language section of the Nix Reference Guide. The reference guide provides a terse overview of the Nix language itself. I usually like to find common ground when learning new concepts so I gravitated to the basic concepts of how strings, booleans, integers, floating numbers, and paths are represented.

ValueDescription
"string"A string value
true/falseBoolean values
1An integer
1.1A floating point number
/nixA path

The lists are a little different in Nix than other languages. A Nix list uses square-brackets, and each list element is separated by white-space. Lists can contain any combination of data types.

["string" true false 1 1.1 /nix]

Attribute sets remind me of JSON. I've seen Nix called JSON with functions in many different blogs and reddit posts. I'm going to assume this is because of attribute sets. An attribute set is denoted with curly brackets, and then a list of name-value pairs (called attributes). For example, creating an attribute set named attrs is shown below.

let attrs = {
  number = 1;
  text = "string";
  path = /nix;
};

You can access the values of an attribute set using the . operator. For example, on the above attribute set, attrs.text would resolve to "string".

There's more in the language reference that I encourage you to read for yourself, but overall the Nix language is a pretty simple. I think I spent maybe 30 minutes reading the entire reference. Oh I didn't immediately absorb it all, but I probably understood most of it.

So why am I still completely lost when I start trying to understand a NixOS flake?

Nix Language Basics explains it best:

You may quickly encounter Nix language expressions that look very complicated. As with any programming language, the required amount of Nix language code closely matches the complexity of the problem it is supposed to solve...

The Nix Language Tutorial You've Been Looking For

Nix Language Basics is a relatively new tutorial for the Nix language. This tutorial is the first resource I've stumbled across that describes the nix repl command for interactively experimenting with Nix. I think most people learn better with a little hands-on experimentation than through reading alone.

With nix installed (instructions here) I read through the tutorial and experimented on the command line. Thrilling questions such as:

  • Why does this expression evaluate that way?
    • Because it does.
  • Wait, what happens if you do this?
    • You get a cryptic error.
  • Does it matter which order I do things?
    • It depends.

All answered through my own hands-on experimentation. I could have blogged about the whole experimentation session, but about midway through the tutorial I realized if you're interested in Nix you should do it yourself. If you're not interested in Nix, I'm not sure why you're still reading.

Nix Quirks

Here's a few of the quirks of the Nix language that I found while going through the tutorial.

Let ... in makes assignments without order

In a let ... in ... expression, the assignments following let can occur in any order. The two let ... in ... expressions below produce the same output.

let
  x = 1 + y;
  y = 2;
in x + y
let
  y = 2;
  x = 1 + y;
in x + y

Because of the lack of order, it's easier to accidentally cause an infinite recursion error. I think I'll make sure my attribute assignments go in a logical order even if the Nix interpreter doesn't care.

Inherit is overloaded

The inherit command can be used to pull an attribute (both the name and value) from an outer scope to the local scope.

let
  x = 1;
  y = 2;
in {
  inherit x y; # equivalent to x = x; y = y;
}

But there's a second form of inherit that allows you to inherit nested attributes. I say it's overloaded because inherit has two different behaviors based on whether or not parentheses are present in the arguments.

let
  a = {x = 1; y = 2;};
in {
  inherit (a) x y; # equivalent to x = a.x; y = a.y;
}

I dislike the second syntax; it seems to be inconsistent with the rest of the Nix language. Still, learning this second syntax opened up new understanding for Nix files that I've been hitherto confused by.

Local and parent file paths must have a slash

In path attributes, you can use relative paths. Thus you can use ./subdirectory to refer to a directory named subdirectory located in the current directory. You can also refer to the parent directories using the familiar ../siblingdirectory.

If you want to refer to the current directory, you need to use ./.. And likewise when you want to refer to the parent directory you would use ../..

An unquoted string containing a slash is parsed as a path, so it makes sense that you can have lone . or .. just hanging out in the middle of Nix files.

Wrapping up

The Nix Language Basics tutorial is an invaluable resource for learning the Nix language. It's a practical description of several critical Nix language features. Towards the end the tutorial you are walked through a few snippets of Nix configuration. In each of these final examples a detailed explanation is listed out with clear explanations of the results from the configuration.

I'm much closer to understanding what these Nix flakes are doing. Which is important because the only one who is going to make the perfect Linux distribution for me is apparently me.