This post is part of the Series: Nix can .
Here’s a few features of the language I wish I discovered a little earlier on. Some of these features are part of the module system more than the language itself, but I’ll include them anyways as they are nescessary to show some useful examples.
A functional language
The language is functional. It is focused around creating object structures, called attribute sets , declaratively expressing what software should be installed and what its configuration should look like.
However, the language can do a lot more. And like much of what you find in the Nix ecosystem, it is.. different, in many aspects, to languages you might be used to. Its also dynamically typed, which means you won’t catch type errors until you evaluate it.
Attribute sets
It excels at creating attribute sets - or objects, maps or dictionaries as they’re often called in other languages.
An attribute set looks like this.
{ key = 123; }
# => { key = 123; }
Lookup attributes
You can lookup attributes with a .
{ a = 42; }.a
# => 42
Nested attribute sets
Nested attribute sets can be created like you always wanted to do with json!
{ a.b.c = 42; }
# => { a = { ... }; }
Nix is actually a great language for producing json from these attribute sets, which is something you can do with some of the builtin functions.
Join attribute sets
You can join multiple attribute sets with the //
, like this.
{ a = 1; b = 2; } // { c = 3; }
# => { a = 1; b = 2; c = 3; }
A REPL (read eval print loop)
You can launch a repl to experiment with the language. This one also imports nixpkgs
, so you can dig around, inspect and build existing derivations. It is useful for learning and experimenting with the language.
nix repl --expr 'import <nixpkgs> {}'
You’ll find packages under pkgs
, so e.g. try pkgs.node<tab>
to see what exists on https://github.com/NixOS/nixpkgs/
starting with the name node
.
nix-repl> pkgs.node
pkgs.node-glob pkgs.nodejs-14_x pkgs.nodejs-slim_14 pkgs.nodejs_18
pkgs.node-manta pkgs.nodejs-16_x pkgs.nodejs-slim_16 pkgs.nodejs_20
pkgs.node-problem-detector pkgs.nodejs-16_x-openssl_1_1 pkgs.nodejs-slim_18 pkgs.nodejs_21
pkgs.node2nix pkgs.nodejs-18_x pkgs.nodejs-slim_20 pkgs.nodejs_latest
pkgs.nodePackages pkgs.nodejs-slim pkgs.nodejs-slim_21 pkgs.nodenv
pkgs.nodePackages_latest pkgs.nodejs-slim-14_x pkgs.nodejs-slim_latest pkgs.nodepy-runtime
pkgs.nodehun pkgs.nodejs-slim-16_x pkgs.nodejs_14
pkgs.nodejs pkgs.nodejs-slim-18_x pkgs.nodejs_16
Let expressions
let
is an expression, like in many functional languages (lisp comes to mind), and the variables you define are scoped to the expression. This means the bound variables are available only after the in ...
part of the expression. What comes after it ends up as the result when it is evaluated..
let
result =
let
nested = { a = 42; };
evil = 666;
in { object = nested; value = evil; };
in result
# => { object = { ... }; value = 666; }
Weird REPL commands
There are lots of these weird commands starting with :
.
Want to make sure you see the whole result of the let
expression above? Put a :p
in front of it.
:p let
result =
let
nested = { a = 42; };
evil = 666;
in { object = nested; value = evil; };
in result
# => { object = { a = 42; }; value = 666; }
(Notice { ... }
has now become { a = 42; }
)
I have to dig more into these repl commands later. Look them up with another weird command :?
Semicolons
The placement of semicolons feel kinda arbitrary at first ;
You need them in attribute set key-value pairs and when binding variables in a let
block, before the in
-part.
Also, the auto-formatters are strict, so you need them in the correct places to please it. Or else it won’t format your code.
Multiline strings
You can do multiline strings with 2x single quotes at the start and end of the string, and you can interpolate code using ${code here}
.
let
content = ''
muli
line
this content has ${2 + 1} lines
'';
in
...
Need to actually type a $
? You escape it with.. another two ''
, cause why not. Here’s the same thing with the expression escaped, meaning it will be part of the content literally.
let
content = ''
muli
line
this content has ''${2 + 1} lines
'';
in
...
Functions
A lambda that takes a single parameter and increases it by 1
looks like this.
first: first + 1
# => «lambda @ «string»:1:1»
Call it by putting its args just after the function. You don’t need parens to call it.
let f = first: first + 1;
in f 1
# => 2
Or call it directly - yeah, this one’s a little weird - and you need parens to let it know where the function ends, or you could bind it to a variable.
(first: first + 1) 1
# => 2
A function that takes an attribute set looks like this. It destructures the attribute set into local variables a
and b
in one go.
{ a, b }:
let result = a + b;
in result
# => «lambda @ «string»:1:1»
Call it by putting an attribute set after it.
({ a, b }:
let result = a + b;
in result) { a = 1; b = 1; }
# => 2
Domain specific
The language is a domain specific one, which means it also comes with a bunch of builtins - functions - to talk to the nix store, manage files and derivations.
This shows an example that can be used to create a config file with the provided content. The file will be stored in the /nix/store/
and the called function will return the file path.
let
gh-config =
builtins.toFile "config-file.yml" ''
aliases:
co: pr checkout
editor: vim
git_protocol: https
version: '1'
'';
in
...
Use of these builtins are often found in other pkgs’ derivations, in more specialized forms, or in modules like those provided by home-manager
.
One such example is pkgs.writeShellScript
, that can be used to wire together runnable shell scripts that references pkgs
that nix pulls in.
The following results in two aliases available for all users (using the environment
module), that reference a shell script with the content provided, that use the installed packages curl
and jq
. The aliases call the shell script with a parameter $1
denoting the city.
environment.systemPackages = with pkgs; [curl jq];
environment.shellAliases = let
script = pkgs.writeShellScript "weather.sh" ''
${pkgs.curl}/bin/curl -s "https://wttr.in/$1?format=j1" |\
${pkgs.jq}/bin/jq ".current_condition | \"$1: \(.[].temp_C)°C, feels like \(.[].FeelsLikeC)°C\""
'';
in {
wo = "${script} oslo";
wt = "${script} trondheim";
};
The shell script referenced by the aliases is installed in the /nix/store/
, with its name taken from a hash of the script contents combined with the provided filename. The script is readonly and not editable after it has been installed by nix, like everything in else the /nix/store/
.
> alias wt wo
wt='/nix/store/vkz9vsxyqc1nx2zyaharxcw715i4f2zx-weather.sh trondheim'
wo='/nix/store/vkz9vsxyqc1nx2zyaharxcw715i4f2zx-weather.sh oslo'
Things to note;
${code}
in curly brackets, reference nix-code, when nix runs.$1
references the bash variable, when the shell script runs.
> wt
"trondheim: -5°C, feels like -8°C"
> wo
"oslo: -20°C, feels like -24°C"
Lazy
The language is also lazy, which means the computation of the values of an attribute set are not evaluated until they are needed.
So something like this works fine, instantly.
{
a = 1;
b = thisReallySlowFunction 2;
}.a
# => 1
Imports and shared code
If you put the function in a file, you can pass parameters to it as you import it. This function takes an attribute set with the keys a
and b
as its parameters.
# function.nix
{ a, b }:
let result = a + b;
in result + 2
Then import it and all it in one go, like this.
1 + import ./function.nix {
a = 1;
b = 1;
}
# => 5
With
Careful with this one; the keyword with
is pretty much like the one from javascript. It prevents repetition where you need to repeatedly look stuff up from the same attribute set - and makes it really hard to see where stuff comes from, so keep variables you refer from it close to its use.
It is nice e.g. when installing packages with home-manager
.
Instead of this.
home.packages = [
pkgs.git
pkgs.curl
pkgs.wget
];
You can do this.
home.packages = with pkgs; [
git
curl
wget
];
Everything is merged!
Because you like small, self-contained modules, reusable across systems, you prefer splitting code out into separate modules! Nix handles modification of the same paths of an attribute set across modules by merging them when it is producing the final result.
(This is probably more a property of modules
and home-manager
, than the language itself)
E.g. with two home-manager
modules git.nix
and terminal.nix
.
# home/git.nix
{
home.packages = with pkgs; [ git ];
}
# home/terminal.nix
{
home.packages = with pkgs; [ alacritty foot kitty ];
}
And the modules registered with home-manager
.
# home/default.nix
{ inputs, pkgs, ... }: {
imports = [
./git.nix
./terminal.nix
];
...
}
Their content will be merged, such that all the packages from both modules are installed. You can think about it ending up something like this.
{
home = {
packages = [ pkgs.git pkgs.alacritty pkgs.foot pkgs.kitty ];
};
}
..alongside a bunch of other stuff. This merged result in particular would probably be a tiny part of the resulting attribute set once all modules are combined into a single result.
And this is really what the language Nix is all about - building an attribute set that explains what the resulting system should look like. How it is done? That is up to the rest of Nix to decide.