Nix Language Primer
Nix is a package manager much like apt, yum or homebrew. Nix is also a language by which the nix package manager specifies the packages offered.
While nix (the language, not the package manager) is a complete programming language, it is not general-purpose. Its primary focus is declarative. It is used to create sets of parameters to be fed to external compilation tools such as configure and gcc.
This is a primer for the nix language. It is meant to enable you to read nix expressions. It does not attempt, for example, to teach you about the nix build system, nor how to best compose package definitions.
Since any nix “program” is fundamentally a single nix expression, the primary goal is to enable you to see where the sub-parts of an expression begin and end.
For those coming from other languages, nix can be confusing due to its use of semicolons and braces. In this respect, nix is different than many languages because it does not employ semicolons to separate statements. Instead, semicolons play a role more similar to commas in other languages, separating elements within part of an expression. Likewise, braces do not denote blocks, but rather sets and set patterns.
You will see more on this in the following sections.
Playing with expressions in the nix command-line interpreter (Read-Evaluate-Print-Loop) is encouraged.
Whitespace generally does not matter in nix.
Nix is a pure functional language. There are no statements in nix, only a top-level, single expression which in turn is composed of other expressions. Every expression can be evaluated down to either a function or a value.
Expressions are composed from values, functions (called lambdas) and operations (operators with operands), as well as a handful of special keyword expressions.
You can use parentheses around any expression for clarity or to force precedence.
Files are a basic type (they are not strings, so no quotes). They must always contain a /.
Relative pathnames should always start with ./ to ensure the presence of at least one /.
Relative paths are relative to the file in which they appear.
URLs are a basic type. Again, no quotes.
Lists are a container type for multiple values.
Lists can be empty:
List elements can be of diverse types, including other lists or sets, even within the same list.
Lists are not typically used as frequently as sets are.
Sets are the workhorse of nix.
Sets are a container type for multiple key/value pairs. They are analogous to hashes in other languages.
Nix refers to the keys as “attributes”.
The final semicolon is required.
Accessing values in a set:
Sets can be empty:
Note that, in nix, braces are only used for sets and set patterns (see below). There are no brace-delimited blocks as there are in other languages.
For example, while the following may look like a function definition in another language, a function name followed by braces is actually a function invocation with an empty set as an argument, not a definition:
Function Definition and Patterns
Functions are anonymous, meaning they don’t have names in their definitions. To give a name to a function, you bind it to a key or a variable just as you would with any other value.
Functions start with an argument pattern:
“Argument pattern” is a fancy name for “give me a (single) variable name for the argument”.
There is only ever one argument to a function, however that one argument may be a set, in which case you may use a set pattern in the declaration:
Set patterns consist of the key names (with default values, if desired) separated by commas, which become variables in the function scope.
Note that set patterns are the only construct in nix which uses commas, and they don’t need a final comma before the closing brace (unlike nix’s semicolon-based expressions).
Functions are invoked by passing an argument after a space. Parentheses around the argument(s) are generally not required.
Note that you don’t need to name the function, just pass an argument after its declaration (parentheses for precedence):
Functions specified with a set pattern must receive a set with exactly the required keys and nothing more (minus any defaults, if desired):
Functions with “Multiple” Arguments
Functions can only take one argument, but they can return a function which then takes the next argument:
Which syntactically breaks down to:
Calling the outer function with one argument returns the inner function, curried with arg1 (i.e. a function with a closure containing arg1). That function may be later called just by providing arg2.
Nix is lexically scoped like most languages. Variables always resolve the same way based on the local scope first, then up through parent expression scopes, up to the global scope as necessary. The matching name in the closest scope to the executing code is the value to which the variable is resolved.
In the multi-argument function above, arg1 is available to the inner function because arg1 is in the outer function’s scope. Since it is the parent expression, its scope is available to the child unless the child masks that name with its own variable of the same name.
Variables are names which can hold the result of any expression, usually values or functions.
Functions stored in variables can be called by invoking the variable name with an argument.
The keys of sets are similar to variables, and can be extracted into variables, but they are distinct concepts.
A set’s values can refer to variables, but they can’t refer to other keys in the set. Those are keys, not variables.
For example, this doesn’t work:
Recursive sets make the set’s own keys available as variables within the
scope of the set, including its subexpressions. Recursive sets employ
rec keyword, followed by the usual set notation:
Note that rec is a keyword, not a variable containing a function. You cannot have a variable named “rec”.
There are no assignment statements in nix, but you can create a new
scope that has “bindings” (another name for assignment) with the
Remember that whitespace doesn’t matter so you don’t need the above indentation.
The final semicolon in the
let portion is required.
in portion of the expression has access to the variables defined
The variables are not available to anything outside the
expression, only to the
The variables will mask variables of the same name of an outer scope.
The expression as a whole evaluates to the value of the
in portion of
Note that because there are no assignment statements in nix, you cannot modify the global scope. You can only create bindings in subscopes.
Bindings are only evaluated if they are referenced by the resulting
expression. For example, b does not throw a divide by 0 error in the
following expression because it is not referenced by the
Bindings may refer to other bindings within the same
let, but again,
since they are only evaluated when referenced by the
order doesn’t matter. For example, the following is fine, even though
a refers to b before b is defined:
Within container types (sets and lists), values are only evaluated insofar as they reach another container type and no further. So only functions, operations and basic values are evaluated upon reference.
The unevaluated elements of container types become further evaluated when their key (or list element) is referenced directly.
As a convenience, you can extract the keys of a set into variables via
The variables a and b will be in scope for the
expression, similar to the
in portion of a
with expressions inside the second portion of an outer
can mask variables created by the outer with.
Variables created via
with will not, however, mask variables created
by an outer
let expression, a recursive set (see below), nor the
global namespace. This can lead to unexpected results, depending on the
environment in which the
with expression is evaluated.
As an example, try this expression:
There is no way to extract only a subset of a set’s keys via a simple
I won’t go over each of the builtins, but all of the functions available out-of-the-box with nix are stored in a global set called builtins.
You can examine the names of the available functions with:
A few builtins are also available directly in the global namespace, such
Derivations are the set of information needed to classify and build a package. Derivations are a their own type, layered over a basic set.
Derivations are created with the
derivation function. It takes, at a
minimum, the set of name, builder and system:
The result of this function call is a special set (type: derivation) which looks like the following:
While it is its own type, it can still be treated as a normal set.
inherit keyword in a set definition creates a key with the same
value as the variable of the same name:
This is the same as:
inherit can take multiple arguments:
A package is typically a function which produces a derivation:
A channel is a function which produces a set of derivations:
Typically a channel is constructed with package imports which are then invoked with an argument, since packages are functions that produce derivations, rather than direct derivation calls as shown here.
The import keyword expression loads the expression in the given file:
The expression in the file is returned as if it had been source code in place of the import expression, with one important difference. The imported expression cannot see any outer scopes except for the global scope.
Because of this, most file-based expressions are functions, so anything they need from the outer scope can be explicitly passed to them.
Importing a directory path causes the file
default.nix in that
directory to be loaded.
For example, the following expression loads the file
the same directory in which the expression’s own file is located:
That is a brief survey of the most important facets of the nix language.
There are many other important features and details, such as how
builders work, the
mkDerivation helper, if then else expressions and
Some good sources of information include:
The nix-repl referenced at the beginning of the cheatsheet is an invaluable tool for learning the ins and outs of the language.
Finally, there is a dated but illuminating description of an early form of the nix language grammar.