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 5
1 + 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.nix
The 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 27
In addition, the /* comment */
syntax can be used for a multi-line comment:
/*
Multi-line
comment
*/
5
let
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 * mySum
The 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 70
The 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) * mySum
Here 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 6
We 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 true
Other 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.nix
The 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 5
Nested 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 true
Note: 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
inherit
keyword can also be used inlet
bindings. In that case, it is not exactly syntactic sugar fora = a
, but rather thea
on the right is taken from outside of thelet
binding (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 + 5
This 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 11
As 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 11
It’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 3
Note 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 3
Another 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 3
Since 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 3
You 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 3
In 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 6
which 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
args
in 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 9
The 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 example
Another 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
:
1
And this could be the content of sum.nix
:
import ./number.nix + 3
We can run the following command to ensure it all works as expected:
❯ nix-instantiate --eval --strict sum.nix
4
let
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.