Nix Language Primer
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.
Nix REPL
Playing with expressions in the nix command-line interpreter (Read-Evaluate-Print-Loop) is encouraged.
In nix 1:
nix-env -ibA nixpkgs.nix-repl
nix-repl
Nix 2 includes the repl by default. You can access with:
nix repl
Whitespace
Whitespace generally does not matter in nix.
Comments
# trailing comment
/* multiline comment */
Expressions
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.
Boolean Values
true
false
Integers
1
20
Strings
"hello, world!"
"a multiline
string"
''a string containing the " symbol''
Interpolation (“Anti-quotation”)
"a string with a ${nix_expression}"
Files
./filename.txt
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
http://domain.com/path
URLs evaluate to strings, but have their own literal format. They do not require quotes.
Lists
[ "one" "two" "three" ]
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
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”.
{ key1 = "value1"; key2 = "value2"; }
The final semicolon is required.
Accessing values in a set:
{ key = "value"; }.key
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:
do_something { }
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:
# pattern: function body
arg: do_something_with arg # do_something_with is a made-up function
“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 pattern }: function body
{ arg1, arg2 ? "default_value" }: do_something_with (arg1 && arg2)
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).
Function Invocation
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):
# (function definition ) <string argument>
(arg: do_something_with arg) "value"
Functions specified with a set pattern must receive a set with exactly the required keys and nothing more (minus any defaults, if desired):
# (function definition ) <set argument>
({ arg }: do_something_with arg) { arg = "value"; }
Functions with “Multiple” Arguments
Functions can only take one argument, but they can return a function which then takes the next argument:
arg1: arg2: do_something_with (arg1 && arg2)
Which syntactically breaks down to:
# (function 1 (function 2 ))
(arg1: (arg2: do_something_with (arg1 && arg2)))
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.
Scoping
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
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.
Recursive Sets
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:
{ a = 1; b = a; }
Recursive sets make the set’s own keys available as variables within the
scope of the set, including its subexpressions. Recursive sets employ
the rec
keyword, followed by the usual set notation:
rec { a = 1; b = a; }
Note that rec is a keyword, not a variable containing a function. You cannot have a variable named “rec”.
Let
There are no assignment statements in nix, but you can create a new
scope that has “bindings” (another name for assignment) with the let
expression:
let
a = 1;
b = 2;
<...>;
in
do_something_with a
Remember that whitespace doesn’t matter so you don’t need the above indentation.
The final semicolon in the let
portion is required.
The in
portion of the expression has access to the variables defined
in the let
portion.
The variables are not available to anything outside the let
expression, only to the in
portion.
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
the expression.
Note that because there are no assignment statements in nix, you cannot modify the global scope. You can only create bindings in subscopes.
Lazy Evaluation
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 in
expression:
let
a = 1;
b = 1 / 0;
in
do_something_with a
Bindings may refer to other bindings within the same let
, but again,
since they are only evaluated when referenced by the in
expression,
order doesn’t matter. For example, the following is fine, even though
a refers to b before b is defined:
let
a = b;
b = 1;
in
do_something_with a
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.
With
As a convenience, you can extract the keys of a set into variables via
the with
expression:
with { a = 1; b = 2; }; do_something_with a
The variables a and b will be in scope for the do_something_with a
expression, similar to the in
portion of a let
expression.
Further with
expressions inside the second portion of an outer with
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:
with { builtins = "hello"; }; builtins
There is no way to extract only a subset of a set’s keys via a simple
with
.
Builtins
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:
builtins.attrNames builtins
A few builtins are also available directly in the global namespace, such
as toString
:
toString ./filename.txt
Derivations
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:
derivation { name = "myname"; builder = "mybuilder"; system = "mysystem"; }
The result of this function call is a special set (type: derivation) which looks like the following:
{
all = [ «derivation /nix/store/z3hhlxbckx4g3n9sw91nnvlkjvyw754p-myname.drv» ] ;
builder = "mybuilder" ;
drvAttrs = { builder = "mybuilder"; name = "myname"; system = "mysystem"; } ;
drvPath = "/nix/store/z3hhlxbckx4g3n9sw91nnvlkjvyw754p-myname.drv" ;
name = "myname" ;
out = «derivation /nix/store/z3hhlxbckx4g3n9sw91nnvlkjvyw754p-myname.drv» ;
outPath = "/nix/store/40s0qmrfb45vlh6610rk29ym318dswdr-myname" ;
outputName = "out" ;
system = "mysystem" ;
type = "derivation" ;
}
While it is its own type, it can still be treated as a normal set.
Inherit
The inherit
keyword in a set definition creates a key with the same
value as the variable of the same name:
let
a = 1;
in
{ inherit a; }
This is the same as:
let
a = 1;
in
{ a = a; }
inherit
can take multiple arguments:
let
a = 1;
b = 2;
in
{ inherit a b; }
Packages
A package is typically a function which produces a derivation:
{ system ? builtins.currentSystem }:
derivation {
name = "myname" ;
builder = "mybuilder" ;
inherit system ;
}
Channels
A channel is a function which produces a set of derivations:
{ system ? builtin.currentSystem }:
let
builder = "mybuilder";
in
{
package1 = derivation { name = "package1"; inherit builder system; };
package2 = derivation { name = "package2"; inherit builder system; };
}
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.
Import
The import keyword expression loads the expression in the given file:
import ./expression.nix
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 default.nix
from
the same directory in which the expression’s own file is located:
import ./.
Nix Path
In addition to files, you can use nix path references with import:
import <nixpkgs>
The angle brackets are literal and instruct nix to consult the NIX_PATH environment variable for resolution.
NIX_PATH contains a colon-delimited string of key=path pairs. The keys are available as expansions to nix using the angle brackets, and the values are the paths to which those keys expand.
Per usual, if the path is a directory, then it references default.nix in that directory.
Resources
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
more.
Some good sources of information include:
-
the nix pills series
-
the nix expression language documentation
-
the nix wiki
-
this tutorial blog post
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.