Before you can begin properly configuring NixOS, you must learn various things, the most basic of which is the syntax of Nix.
Nix is a programming language (aside from being a package manager), but it differs a lot from traditional programming languages. One of the most important differences is that a Nix file is not executed from top to bottom. Actually, it’s not “executed” at all, but rather evaluated.
A Nix file must evaluate to a value. In other words, a Nix file contains an expression that can be “simplified” to a single value. The following snippets are all valid examples of Nix files:
5 # This file evaluates to the value 51 + 1 # This file evaluates to 2"Hello, " + "World!" # This file evaluates to the string "Hello, World!"If you have Nix installed, you can check what these files evaluate to using the following command:
nix-instantiate --eval --strict file-name.nixThe important concept to understand from these examples is that Nix is not evaluated sequentially, but rather evaluated by “simplifying” the expression contained in the file. Note that all values in Nix are constant! There is no modifying a value, there is only “simplifying” the expression further.
I will now explain most (but not all) of the syntax of Nix. While the next sections might be boring, they are required to fully understand the next chapters, so I recommend reading them.
Looking at the previous examples, we can learn a few more basics about the Nix language:
# symbol can be used to begin a comment which won’t
end until the next line.+ symbol is an operation that can add two numbers
or concatenate two strings. Other basic math symbols, such as
* for multiplication won’t be explained as they are
intuitive.Just like in other programming languages, parentheses can be used to indicate priority:
(5 + 4) * 3 # Evaluates to 27In addition, the /* comment */
syntax can be used for a multi-line comment:
/*
Multi-line
comment
*/
5let bindingsWhile evaluating basic expressions using Nix is certainly cool, it can sometimes be useful to temporarily save an intermediate result. For example, take a look at the following expression:
5 * (1 + 1) * (1 + 1)It seems we have to repeat (1 + 1)
twice. It would instead be more appropriate to assign the value to a
name, which we can later reference twice. That is where let
bindings come into play:
let
mySum = 1 + 1;
in 5 * mySum * mySumThe whole file evaluates to 20.
The let ... in ...
syntax allows you to assign (bind) values to names after the
let keyword and use them after the in keyword.
The result of the expression is only what comes after
the in keyword:
let
mySum = 1 + 2;
in 70The whole file in this case evaluates to 70, meaning that
what you write between the let and in keywords
is only useful if you reference it after the in
keyword.
In addition, the bindings defined in a let binding are
only available in the let ... in ...
expression, as can be seen in the following example, which does
not evaluate successfully:
(let
mySum = 1 + 2;
in 5 * mySum) * mySumHere is the error we receive when attempting the evaluation of the above:
❯ nix-instantiate --eval --strict let-in-not-working.nix
error: undefined variable 'mySum'
at /home/tobor/let-in-not-working.nix:7:3:
6| )
7| * mySum
| ^
8|Nix indeed complains about mySum not being defined, as
it’s only valid in the let binding.
Assignments in let bindings can also refer to themselves
or other assignments:
let
a = 1;
b = a + 2;
in b + 3 # This evaluates to 6We can use the if <cond> then <value-true> else <value-false>
syntax to define conditionals. If the condition is true, then the first
value will be returned, otherwise the second will. Unlike traditional
languages, there must always be an else case. This is
because pretty much everything in Nix is an expression, meaning that it
must evaluate to something. If there was no else case, then
Nix wouldn’t know what to put as the output of the evaluated expression
if the condition was false.
The condition must evaluate to a boolean, meaning either true or false. Here is an
example:
if 5 == 2 + 3 then "yes" else "no" # This evaluates to "yes"We’ve now learned a few Nix keywords, but we still only manipulated numbers and basic strings. There are other data types in Nix and they are listed below together with some of their quirks.
We’ve already seen numbers in the previous examples. Integers are
restricted between -9223372036854775808
and 9223372036854775807
(i64), while floats (rational numbers that are not
integers) are f64. Numbers can also be written in the
following form: 1.2e5, which is
the same as 120000.
The 4 basic operations +, -, *
and / can be used with numbers. More advanced operations
are available through builtins, which we
will look at later.
Strings can be defined by surrounding text with double quotation
marks. Those types of strings can use escape characters, such as
\n, but they can only span across one line:
"Hello\nthere on a new line"For multi-line strings, the following syntax can be used:
''
In computer science, functional programming is a
programming paradigm where programs are constructed
by applying and composing functions.
- Wikipedia
''Notice how there are extra spaces preceding each line in the string.
These spaces will be ignored by Nix: the smallest number of spaces
preceding a line will be taken and subtracted from all lines. Escape
characters such as \n cannot be used in this type of
string.
If we want to interpolate a string inside of another, we can use the
${}
syntax:
let
inside = "string";
in "My first ${inside} interpolation" # Evaluates to "My first string interpolation"Note: The
${}string can be escaped in a multi-line string by writing''${}.
As previously mentioned, a boolean can either be true or
false. Booleans can be negated with the !
operator:
!false # This evaluates to trueOther boolean operators are && (logical
conjunction, meaning “and”), || (logical disjunction,
meaning “or”) and -> (logical implication).
Due to its native purpose of composing derivations and configuring
systems, Nix has first-class support for paths. Paths in Nix can begin
with / (absolute path), . (relative path) or
.. (relative path from the previous folder), for
example:
./my-relative/path/to/my-file.nixThe type of the value null.
Lists in Nix are ordered groups of values. Lists can be created using
the [ item1 item2 item3 ]
syntax, where items are separated by spaces. Items in lists can be of
any type:
[ 1 2 3 "Nix" ]There is an operator to concatenate lists: ++. It can be
used like this:
[ 1 2 ] ++ [ 3 4 ] # This evaluates to [ 1 2 3 4 ]An attribute set is a set of key-value pairs defined with the following syntax:
{
name = "Tobor";
github = "ToborWinner";
madeNixGuide = true;
}The keys are called attributes. The values of these attributes can be
accessed by using a .:
{ a = "hey"; }.a # This evaluates to "hey"A default value if the attribute doesn’t exist can be specified with
or:
{ }.a or 5 # Evaluates to 5Nested attribute sets can be set through a shortcut using
.:
{
a.b.c = 5;
}is the same as
{
a = {
b = {
c = 5;
};
};
}I can check if an attribute set has an attribute by using
?:
{ a = 4; } ? a # Evaluates to trueNote: The name “attribute set” is often shortened to “attrs”.
rec keywordThe rec keyword can be used to make an attribute set
“recursive”, meaning it can define its attributes based on other
attributes it defines:
rec {
a = 5;
b = a + 6;
}This example evaluates to the following attribute set:
{
a = 5;
b = 11;
}inherit keywordThe inherit keyword, when used in attribute sets, is
syntactic sugar for a = a:
let
a = 5;
in {
inherit a; # This is the same as writing a = a;
}Attributes can be added in parentheses to specify the path:
let
a = {
b = 5;
};
in {
inherit (a) b; # This is the same as writing b = a.b;
}In addition, multiple attributes can be inherited with only one
inherit keyword:
let
a = {
b = 5;
c = 6;
};
in {
inherit (a) b c; # This is the same as writing b = a.b; c = a.c;
}Note: The
inheritkeyword can also be used inletbindings. In that case, it is not exactly syntactic sugar fora = a, but rather theaon the right is taken from outside of theletbinding (or from where it’s supposed to come from). You should not worry about this and might understand it better in the next chapter.
// is the update operator for attribute sets. It will
overwrite the attributes of the attribute set on the left with the
attributes of the attribute set on the right:
{ a = 5; b = 1; } // { a = 2; c = 6; } # This evaluates to { a = 2; b = 1; c = 6; }With Nix being a functional programming language, functions are one of the core constructs of the Nix language.
Functions take a single argument as input and produce an output. It’s important to understand that a function itself is also a value! They can be defined with the following syntax:
argument: argument + 5This file now contains a function as its value. It’s a valid Nix file because it evaluates to a value. In fact, if we run the command, we get the following output:
❯ nix-instantiate --eval --strict simple-function.nix
<LAMBDA><LAMBDA> here means function!
In this case, the argument to this function is called
argument and it produces the output argument + 5.
To call a function, we can add a space after it and pass it an
argument:
(argument: argument + 5) 6 # This evaluates to 11As a function is just like any other value, we could assign it to a
name using a let binding for example:
let
myFunc = x: x + 5;
in myFunc 6 # This evaluates to 11It’s very important to understand that functions in Nix always take one argument. If we need a function to take two arguments, we can “chain” two single-argument functions together, which is also called currying:
(arg1: arg2: arg1 + arg2) 1 2 # This evaluates to 3Note that we did not create a function that takes two arguments, but
rather combined two single-argument functions. The first function takes
arg1 as argument and outputs another function. This other
function takes arg2 as argument and outputs arg1 + arg2. We
then call the first function with the number 1, which gives us
a function in return. We then call this returned function with the
number 2,
which gives us 3 back.
We could separate the two function calls:
let
myFunc = arg1: arg2: arg1 + arg2;
firstOut = myFunc 1; # firstOut is now a function
in firstOut 2 # We call the function with the number 2, obtaining 3Another way to somehow pass multiple arguments to a function that takes a single argument is by passing it a compound type such as an attribute set. For example:
(x: x.a + x.b) {
a = 1;
b = 2;
} # This evaluates to 3Since this is a pattern that is very commonly used in Nix (as we’ll see in future chapters about nixpkgs and NixOS configurations), there is a language feature that makes it easier:
({ a, b }: a + b) {
a = 1;
b = 2;
} # This evaluates to 3You can destructure the attribute set passed as argument into its
attributes! This is almost the same as the previous
example, but Nix is strict when checking the arguments, meaning
that the attribute set must have exactly and only the attributes
a and b. You can allow for other attributes in
the attribute set by writing { a, b, ... }:
instead.
Default values can also be provided:
({ a ? 1, b }: a + b) { b = 2; } # Evaluates to 3In addition, a name can be assigned to the attribute set using the
@-pattern:
(args@{ a, b, ... }: a + b + args.c) {
a = 1;
b = 2;
c = 3;
} # This evaluates to 6which can come on either side (meaning { a, b, ...}@args:
can also be used).
Note: Default values specified in the argument destructuring are not applied to
argsin this case.
with expressionsA with expression can be used to make the attributes of
an attribute set available for use in an expression. For example:
with {
a = 4;
}; 5 + a # Here I can use a, because it's available from the with expression
# This file evaluates to 9The general format for a with expression is the
following:
with <attribute-set>; <expression>Often the normal operators provided by the Nix language are not
enough to achieve your goals easily. That’s where builtins comes in.
builtins is
an attribute set where most of the attributes contain functions. These
functions are not coded in Nix, but are instead a part of Nix.
An example is builtins.attrNames
(attrNames stands for attribute names):
builtins.attrNames {
a = 4;
b = 6;
} # Evaluates to [ "a" "b" ]builtins.attrNames,
when passed an attribute set as argument, returns a list of strings
containing the names of the attributes.
There are many builtins
functions, but some are used so often that they can be called without
adding builtins. An
example is the toString
builtin:
toString 5 # Evaluates to "5". Useful for string interpolation for exampleAnother example is the map builtin:
map (x: x + 5) [ 1 2 3 ] # Evaluates to [ 6 7 8 ]The map
builtin takes a function as argument and returns a function that takes a
list as argument. That function then returns the “mapped” list when
called. This is currying, as previously explained, which also works with
functions in builtins!
For example I can make a partially applied priomp function like this:
let
# This is now a function that takes a list as input and returns a mapped list as output
# Because `map` comes from `builtins`, it is called a partially applied priomp
add5ToEachElement = map (x: x + 5);
in add5ToEachElement [ 1 2 ] # Evaluates to [ 6 7 ]To find other builtins, you can
use the unofficial website noogle.dev. Note that for now you should ignore all
functions that don’t begin with builtins.
The assert keyword and the throw builtin can
be used to cause errors during the evaluation of an expression. The
syntax for assert is the following:
assert <condition>; <expression>If the condition is false, then Nix will throw an error. If the condition is true, then the expression will be returned.
The throw
builtin (one of the builtins that you don’t need to write builtins for) can
be used to throw an error with an error message:
throw "This is an error!"If you want to split your Nix expression across multiple files, you
can use the import builtin
(which is also a builtin for which you don’t need to write builtins for). The
import
builtin takes a path as argument and returns the expression contained in
the imported Nix file. For example this could be the content of
number.nix:
1And this could be the content of sum.nix:
import ./number.nix + 3We can run the following command to ensure it all works as expected:
❯ nix-instantiate --eval --strict sum.nix
4let binding can be used to assign values to
names.if keyword, which
always requires an else case.builtins
is an attribute set that contains various native functions that you can
use.What was discussed in this chapter was most of the Nix syntax commonly used, but not all of it. In the next chapter we are going to discuss the evaluation of Nix code, how it’s lazy and how that allows you to do incredible things.