In this guide dedicated to Nix language syntax, we will take a step-by-step look at how to get started with the REPL and understand the core pillars of functional programming.
We have already discussed our infrastructure in previous posts, and we already mentioned how Nix is an incredibly useful tool for managing system configurations (or your home directory) in a declarative way.
This post is aimed at anyone who has no prior experience with the Nix language but wants to learn the basics, those with a bit of Nix experience looking to dive deeper, or anyone unfamiliar with functional programming in general, which, it’s worth noting, is far more than just using functions within a program.
The goal here is not to be exhaustive, but rather to introduce the core concepts of functional and declarative programming. We will walk through key elements of the Nix language syntax and its tooling, and then move on to building a few simple programs.
We highly recommend a hands-on approach: grab your computer, install Nix, and follow along by typing the code examples into your terminal. Experiment and try new things as you read. This will help you understand the differences between Nix and the languages you are more familiar with, allowing you to build a solid intuition for how it works.
Declarative Languages? Functional Languages?
Nix is often considered an excellent way to declaratively express a system configuration. But what does “declarative” actually mean?
In computer science, “declarative programming” refers to a way of writing a program without explicitly defining the sequence of commands to execute, but rather describing the desired state instead.
To use a non-programming analogy, if you wanted to express how to bake a carrot cake, the “classic” (imperative) approach would look something like this:
1. prendi le uova, rompile e separa gli albumi dai tuorli,
2. aggiungi la farina e lo zucchero ai tuorli,
3. mescola finché non ottieni una crema uniforme,
4. aggiungi le carote tritate,
5. mescola fino ad avere un composto omogeneo,
6. monta gli albumi a neve,
7. aggiungi al composto senza smontarli, mescolando dal basso in alto,
8. inforna a 180 gradi per 35 minuti
on the other hand, with a declarative approach, you might say:
una torta di carote è fatta cuocendo l'impasto in forno, l'impasto si fa con albumi montati e composto di farina, tuorli, zucchero e carote tritate.
The difference lies in the fact that a declarative approach defines what objects are, rather than how they are built: we don’t specify how long the cake needs to bake, or how to fold the egg whites into the batter, and so on.
That being said, underneath it all, execution still happens more or less the same way (a hand will still have to grab the eggs and separate the whites from the yolks, baking will still take place in an oven at around 180°C, etc.). The difference is in how the recipe is expressed, and potentially in how much the context allows us to make assumptions.
Clearly, there are many different ways to program a system: different languages, different styles, different paradigms. Two famous paradigms that fall under the “declarative” umbrella are the functional and the logical ones. Both are widely considered declarative approaches to programming. Lisp and Prolog are two particularly famous programming languages that represent functional and logical approaches, respectively. However, this doesn’t stop other languages from adopting different styles: even in JavaScript, which is not a “functional language,” you can still write in a “functional style.”
Now, let’s eat the carrot cake and move on with Nix.
Nix is a “functional language” because it facilitates writing programs (and configurations!) using a functional style. In other words, the program (or configuration) is defined through a sequence of applications of “pure” functions—meaning functions that have no side effects. This implies that evaluating the same function twice with the exact same inputs will always yield the exact same outputs.
For example, a time() function that returns the current time as a string is not pure if it returns a different timestamp every time we call it. If it were to return a fixed time instead (e.g., 12:15:59), it might not be very useful, but it would be pure.
There are excellent reasons why a functional language should be “pure.” One of them, which is particularly vital for Nix, is reproducibility: if evaluating a program always leads to the same output given the same input, it means we can always reproduce the result. Furthermore, we can replace the execution of that program with the value it produces—just like writing time() in our previous example could be replaced directly with 12:15:59.
This property, where a part of a program can always be replaced by its resulting value without changing the final outcome, is called referential transparency. It is a property that is almost always desired and maintained in functional languages.
Of course, there are situations (like time() above) where we actually want impure functions, but this comes with a set of drawbacks. Because of this, the functional programming community has worked hard in various ways to isolate these situations as much as possible. But we won’t be diving into that here, because in Nix, time stands still—and as we will see, there are very good reasons for that.
Program or Configuration?
In the previous section, we used the terms “program” and “configuration” almost interchangeably. But is that really the case? What is a program? What is a configuration? Answering these questions is not as straightforward as it might seem.
A very common interpretation is that a program is the definition of something that must be executed to transform input data into output data, whereas a configuration is simply data, and as such, it does not need to be executed (at most, it just needs to be read from a specific format).
Another common interpretation is that a configuration is something a program needs in order to function, and it determines that program’s behavior.
And yet, the source code of a program is just a series of strings, typically defined in a formal language, and we use software like text editors to write this data. Therefore, source code is data!
Furthermore, source code is typically data provided to another program, a compiler or an interpreter, so that it can function and determine its behavior (e.g., creating an executable or manipulating pixels on a screen). It is clear, then, that the line between them is not so sharp: source code can become a configuration itself, depending on the abstraction layer we are looking at.
So, we won’t try to definitively answer those questions. For some people, there is no difference; for others, there is, depending on convention, contextual semantics, or the pragmatics of the current communication.
What matters to us is that in Nix, a configuration is a program: it is not a simple data declaration that achieves its ultimate purpose directly; rather, it must be evaluated by an interpreter before producing the final data.
Nix: The Language and the Package Manager
Nix is actually a duo of two distinct entities: a functional programming language and a package manager. Its purpose is to provide the computer with instructions to take the source code of other applications (a “package”) and run it—typically after compiling it (building it).
This is where several elements we described earlier come together:
- The steps indicating how to build a package are inherently imperative: first you must compile the source code, then link it;
- Building a package depends on the state of the computer: if a piece of source code uses the system date to produce the executable file, then two different builds will produce two different executables;
Therefore, it is clear that if we want reproducible builds (which is highly desirable for tracking bugs, team development, etc.), we must isolate the compilation of a program from the environment in which it is built.
This is where the need for a pure and reproducible language to build packages arises. If a program were to contain code like:
#ifdef BUILD_YEAR_2025
printf("Computing statistics\n");
// etc
#else
printf("License expired!\n");
exit(0);
#endif
it is obvious that two builds of the code performed at two different points in time could have very different outcomes.
Nix, therefore, was created as a combination of a package manager and a pure functional language that aims to completely isolate the package build process in order to produce fully reproducible artifacts. And indeed, Nix runs builds inside an isolated environment (sandbox) where the date never changes.
So, how does compiling a program actually work in Nix? Typically, you write a configuration using the Nix language. This configuration can be executed and interpreted to produce a final, “static” object that defines a derivation, meaning a reproducible blueprint of how to build a specific package. The Nix package manager then takes this derivation and follows its instructions to produce an artifact: the actual build of that package.
In this article, we will focus strictly on the Nix language level: we won’t be concerned with building packages or creating derivations, but simply with writing a program (or configuration) with Nix that takes some data and transforms it in some way.
Syntax Basics
Let’s dive straight into the action by exploring the main commands for manipulating lists and handling strings in the Nix REPL.
Let’s dive into the practical side: after installing Nix, we can launch a REPL (Read-Evaluate-Print Loop)—a program that waits for our input, evaluates it, prints the result, and goes back to waiting for our next input:
$ nix repl
Nix 2.31.2
Type :? for help.
nix-repl>
Here nix-repl> indicates the program’s prompt, which is waiting for our input. Let’s start taking a look at how Nix actually behaves with dates and times:
nix-repl> builtins.currentTime
1781171288
This function returns a timestamp in seconds, and if we check what it corresponds to, we will see that it matches the exact moment the REPL was started. For instance, we can use Python to verify this (in another terminal, without exiting the REPL):
Python 3.13.12 (main, Feb 3 2026, 17:53:27) [GCC 14.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from datetime import datetime
>>> datetime.fromtimestamp(1781171288)
datetime.datetime(2026, 6, 11, 11, 48, 8)
Ok, the timestamp 1781171288 corrisponds to 2026-06-11 at 11:48:08. Let’s ignore the timezone for now.
Let’s try asking Nix for the time again:
nix-repl> builtins.currentTime
1781171288
(dopo qualche secondo...)
nix-repl> builtins.currentTime
1781171288
There you go—we can see that the result doesn’t change. This is because Nix is pure and aims to isolate us from side effects—meaning changes to the environment that we didn’t cause ourselves. If we wanted a 100% reproducible environment where, for example, the date and time are always constant, we would just need to start the Nix interpreter by specifying and pinning all these “boundary” variables. This is exactly what Nix does when it needs to build packages reproducibly; in fact, when building a package via Nix, the date is locked to January 1st, 1970, even though this constraint is not strictly enforced in the REPL.
Of course, the REPL itself has side effects (reading and printing strings), but these are not supposed to impact the commands being executed.
Now let’s see how to print a string. Nix supports strings enclosed in both double quotes and double single quotes, depending on your needs:
nix-repl> "Hello, World!"
"Hello, World!"
nix-repl> ''Hello, World!''
"Hello, World!"
nix-repl> "Hello,
> World!"
"Hello,\nWorld!"
nix-repl> ''Hello,
> World!''
"Hello,\nWorld!"
Double single quotes are typically used when we want to include double quotes inside the string:
nix-repl> ''Hello, "World!"''
"Hello, \"World!\""
nix-repl> '''Hello', "World!"''
"'Hello', \"World!\""
which is very useful when defining packages, as it is quite common to need `”` inside scripts and build procedures.
How do we escape characters if we need both `”` and `”` inside the string? Escaping is done by adding an extra `”` before `”`:
nix-repl> ''Hello, '''World!'''''
"Hello, ''World!''"
while `”` is escaped using\”`.
Nix also supports string interpolation—meaning the expansion of a variable inside a string. This is done using the ${expr}syntax, for example:
nix-repl> foo = "World"
nix-repl> "Hello, ${foo}!"
"Hello, World!"
Here we also saw that it is possible to declare variables in the REPL. In a Nix script, the syntax is slightly different. Let’s take this opportunity to show how, instead of using a REPL, you could use an editor to write a source file and then evaluate it.
Let’s write the following content into the file hello_world.nix :
let
who = "World";
in
"Hello, ${who}!"
and then let’s evaluate it:
$ nix eval --file hello_world.nix "Hello, World!"
Since we are talking about files .nix, it’s also worth mentioning that you can use import to pull in a file, which might come in handy while experimenting with the REPL:
nix-repl> import hello_world.nix
"Hello, World!"
Perfect, we have seen how to declare a variable in Nix using let ... in ... and how to evaluate a file.
At this point, it is worth introducing an important concept that is typical of many functional languages, but often unfamiliar to people coming from imperative backgrounds: everything in the Nix language is an expression.
Statements vs. Expressions
In languages like C, C++, Java, and JavaScript, you typically see structures like these:
if (true) {
printf("Hello, World!");
} else {
printf("Hi, World!");
}
or, using ternary operators, constructions similar to the following:
printf(true ? "Hello, World!" : "Hi, World!");
What is the fundamental difference? While if ... else ... is a statement, the ternary operator is an expression. In other words, an expression always evaluates to a value, whereas a statement does not. In those languages, it is actually impossible to do something like:
printf(if (true) { "Hello, World!" } else { "Hi, World!" });
This is because we can write certain “blocks” of code that do not have an associated value, meaning we cannot use them in places where a value is required (such as a function call or an addition).
In many functional languages, however, almost everything is an expression—meaning it evaluates to a value—and code blocks have an associated value.
In Nix, writing
if true then "Hello, World!" else "Hi, World!"
is exactly equivalent to writing "Hello, World!":
nix-repl> if true then "Hello, World!" else "Hi, World!"
"Hello, World!"
quindi possiamo fare cose come questa:
nix-repl> "Hello, " + (if true then "World!" else "World!")
"Hello, World!"
where the entire expression inside the parentheses evaluates to a value. In fact, in Nix there is no way to write if without else: if it were possible, we would have to accept that the program might not return a value:
nix-repl> "Hello, " + (if true then "World!")
error: syntax error, unexpected ')', expecting ELSE
at «string»:1:35:
1| "Hello, " + (if true then "World!")
| ^
Instead, Nix requires expressions to always evaluate to a value. Therefore, for instance, if statements must always be accompanied by an else.
This might seem absurd to anyone who isn’t used to this programming style, but it isn’t.
In fact, in Nix and other languages, the entire program becomes an expression: since blocks are expressions and thus have a value, running the program produces a value, and there is no way not to produce one (other than hitting a runtime error). From a certain point of view, this is not only logical but also liberating: all elements of the language fall into a single category (expressions), making them much easier to combine.
Going back to our previous example where we used the let ... in ...; construct: in this case as well, the entire block is an expression and evaluates to a value: let allows us to create associations between a value and an identifier (“bindings”, i.e., creating variables) that are valid and usable inside the body.
While of questionable utility, this code is perfectly valid (the parentheses can be removed; they were included just to make it clear which parts are blocks):
# L'intera espressione ha valore "FooBar"
(
let
foo = (
let
f = "F";
o = "o";
in
( f + o + o ) # "F" + "o" + "o"
);
bar = "Bar";
in
( foo + bar ) # "Foo" + "Bar"
)
Therefore, let ... in ... evaluates to a value as well. We also saw that # allows us to create inline comments. The let ... in ... construct is not that different from creating a scope within a C++ or Rust block, where we can define variables that are only valid for that specific scope.
Unlike C++, however, rust is similar to Nix in that a block can evaluate to a value, just like in Nix and many other functional languages.
Functions in the Nix Language
Understanding how to structure functions and variables in Nix is the fundamental step toward writing clean, modular, and side-effect-free configurations.
Some readers might notice that, despite discussing a functional language, we haven’t actually seen any functions yet.
And yet, we actually have seen one: import.
Unlike the well-known syntax used in C-like languages (C, C++, Java, JavaScript, C#, Rust, etc.), where parentheses are used to denote a function application (call), Nix—along with a variety of functional languages, typically those from the ML-like family—does not use parentheses. Instead, you simply follow the function with its arguments, separated by spaces.
In this case, the import function requires only a single argument—a path (which is a native data type in the Nix language):
nix-repl> import /path/to/file.nix
As another example, if we had a function double that doubles its argument, we could have invoked it like this:
nix-repl> double 21
42
Before moving on, it is worth explaining why Nix is a “functional” language. After all, functions are a widely used construct in most programming languages (though not all). What makes “functional” languages different from others? C, C++, and JavaScript, for instance, are not functional languages in the strict sense.
Purity, that is, the absence of side effects, is an aspect we have already discussed. In particular, functions in functional languages are usually free of side effects, but this is not a universal rule.
Another important aspect of functional programming is that functions are a primitive data type. In other words, you can create and manipulate them much like other primitive types in the language, such as numbers or strings. In fact, you can assign functions to variables, pass them as arguments, or return them as values from another function.
Unfortunately, operations like “checking the equality of two functions” or “chaining functions” are not always straightforward, but this depends on the language and its properties. In Nix, for example, comparing two different functions always returns false false, even if they perform the exact same task.
Assuming we have a double function and a timesTwo function, both of which double their argument:
nix-repl> double == timesTwo
false
But this applies to the exact same function as well:
nix-repl> import == import
false
But the crucial point of a functional programming language is that programs are built by applying and composing functions.
Usually, and in Nix this is definitely the case, a function application (invocation, call) is an expression; that is, the function must evaluate to a value.
This should bring everything full circle: in Nix, expressions compose together, blocks are expressions, and applying a function to a value is an expression. Therefore, all these factors come together to define a program as a single expression obtained by applying functions to data.
So, how do we define functions in Nix? Like this:
parametro: espressione
For example, the double function from earlier is defined like this:
nix-repl> double = x: x + x
nix-repl> double 21
42
nix-repl> double == double # le funzioni non si possono comparare
false
I used the singular on purpose: in Nix, functions can have exactly one argument. Not zero. Not two.
How is it possible then to work with multiple values? There are at least three ways:
- Leverage the fact that functions can access values that were available at the time of their definition—in other words, creating a closure;
- Return another function that handles part of the work (currying);
- Pass a composite value (such as a list) as the function’s argument.
Let’s look at all three of these possibilities.
Closures
In general and loosely speaking, we talk about a “closure” when a function is able to “capture” values defined outside of it and use them within its own definition.
There are different variations and definitions of what this means, but we don’t need to be strictly rigorous at the moment.
However, we can look at an illustrative example in Nix:
nix-repl> foo = 21
nix-repl> bar = x: x * foo
nix-repl> bar 2
42
Herebar, bar was able to access foo because it was available at the time the function was defined. Notice how the “captured” value offoo inside the function cannot be changed now:
nix-repl> foo = 123
nix-repl> bar 2 # il valore di foo all'interno di bar è ancora 21
42
Someone might object and point out that this doesn’t really solve the problem: if we had to define a function every time we needed to pass more than one argument, then the concept of a function would make little sense.
That is true. But this technique paves the way for another approach: currying.
Currying
nix-repl> sum = x: y: x + y
nix-repl> sum 20 22
42
There you have it. The sum function now seems to take two values, but it actually doesn’t: the sum function has only a single argument, x, and returns an (anonymous) function that adds the captured value of x to the argument y. Therefore, we are leveraging the closures we saw earlier to simulate a function with multiple arguments.
If we write only sum 20, we get a function in return:
nix-repl> sum 20
«lambda @ «string»:1:5»
lambda lambda is the term used in functional programming to refer to functions. sum sum itself is a “lambda”:
nix-repl> sum
«lambda @ «string»:1:2»
So: sum is a function, but the result of its application sum 20is a function as well. We could write the exact same thing in different ways:
nix-repl> sum 20 22
42
nix-repl> (sum 20) 22
42
nix-repl> (((sum) 20) 22)
42
nix-repl> let sum-twenty = sum 20; in sum-twenty 22
42
In the first three cases, we simply made the values and function applications explicit, whereas in the last case, we gave a name to a function that would otherwise have remained anonymous.
The attentive reader will have noticed that there is no specific syntax for naming functions in Nix: all Nix functions are born “anonymous.” In the syntax I showed earlier, parametro: espressione, there is no “function name.” This function is indeed created anonymous, and it is up to us to decide whether we want to give it a name.
For instance, it is perfectly legitimate not to assign a name to functions and do something like this:
nix-repl> (x: 20 + x) 22
42
At this point, it should be quite clear what happened earlier when we used iimport: it is not a language construct like, say, import in Python, but rather a function that loads a file and evaluates it, returning the value of the expression inside that file:
# nel file test.nix
"Hello, World!"
In the same directory as the test.nix file, we invoke nix repl:
nix-repl> import ./test.nix # attenzione al ./ davanti al nome del file
"Hello, World!"
It is not uncommon in Nix to have files that contain a single function and use import to bring that function in and evaluate it with the desired arguments:
# nel file concatena.nix
s1: s2: s1 + s2
nix-repl> import ./concatena.nix "Foo" "Bar"
"FooBar"
nix-repl> (import ./concatena.nix) "Foo" "Bar"
"FooBar"
nix-repl> ((import ./concatena.nix) "Foo") "Bar"
"FooBar"
The import function loaded the concatena.nixfile and returned the function contained within it; this function was then invoked with the argument "Foo", returning an anonymous function which was in turn invoked with the argument "Bar", ultimately producing the concatenated string "FooBar".
This brings us to the final pattern: using more complex data structures as a single function argument, which also opens up a new chapter.
Composite Data Types: attrset
In Nix, it is not at all uncommon to come across files that look like the following:
# somefile.nix
{ first, second, third }: {
foo = first + second;
bar = someFunction third;
}
How should this code be interpreted? The presence of : should hint that this program is nothing but a function. This function takes an argument { first, second, third } and returns an object of some kind: { foo = ...; bar = ...; }.
These objects are very important in Nix and vaguely resemble similar objects in other languages: structures in C and C++, objects in JavaScript, or even JSON objects.
In Nix, these objects have a different, somewhat high-sounding name: attribute sets, abbreviated as attrset. They are collections of attributes; attributes always evaluate to a value and, being sets, attributes are unique and cannot be duplicated. The order of the attributes is irrelevant. In general, an attrset is defined like this (notice the = and ;):
{
attr-name = value;
attr-name2 = value;
# etc
}
Attribute names can be any arbitrary string. Typically, separatiDaMaiuscole, or separati_da_underscore or separati-da-dash names are used, but they can also be other strings if enclosed in quotes:
nix-repl> {
> name = "Alex";
> "date of birth" = "1970-01-01";
> current-country = "Italy";
> petName = "Axel";
> }
{
current-country = "Italy";
"date of birth" = "1970-01-01";
name = "Alex";
petName = "Axel";
}
This should clarify the body of the function we saw in somefile.nix, though the argument { first, second, third } is slightly different. In this case, Nix supports the use of “set patterns” as function arguments. That is, you can provide a function with an argument structured like an attrset but containing only the names: the function must then be invoked with an attrset that has precisely those attributes.
Let’s look at this syntax with an example. We will see how it is mandatory to specify all, and only, the indicated arguments:
nix-repl> test = { foo, bar } : foo + bar # definiamo una funzione che prende un attrset con attributi foo e bar
nix-repl> test { foo = 1; bar = 2; } # possiamo invocarla passando un attrset
3
nix-repl> test { foo = 1; } # non si possono passare attrset incompleti
error:
… from call site
at «string»:1:1:
1| test { foo = 1 ; }
| ^
error: function 'anonymous lambda' called without required argument 'bar'
at «string»:1:2:
1| { foo, bar } : foo + bar
| ^
nix-repl> test { foo = 1; bar = 2; baz = 3; } # non si possono passare attrset con più attributi
error:
… from call site
at «string»:1:1:
1| test { foo = 1 ; bar = 2 ; baz = 3; }
| ^
error: function 'anonymous lambda' called with unexpected argument 'baz'
at «string»:1:2:
1| { foo, bar } : foo + bar
| ^
Did you mean bar?
As you can see, Nix is strict regarding the number of required arguments. (The sharp-eyed observer will also have noticed that the data type is not specified; indeed, Nix is not a statically typed language.)
However, we have two alternatives regarding the number of arguments:
- It is possible to provide a default value for an argument in case it is omitted, using the
parametro ? valoresyntax; - It is possible to ignore additional arguments, should they be provided, using the
...syntax.
Here is an example:
nix-repl> concat = { first, second ? "", ... }: first + second
nix-repl> concat { first = "foo"; }
"foo"
nix-repl> concat { first = "foo"; second = "bar"; }
"foobar"
nix-repl> concat { first = "foo"; second = "bar"; third = "baz"; }
"foobar"
This explains the previous example: the content of somefile.nix somefile.nix is a function that takes exactly three arguments first, second and third returns an attrset with two attributes foo and bar.
Composite Data Types: Lists
We now need to look at another very important data type: lists.
Lists in Nix are defined within square brackets and without commas or other separators between elements. The elements do not all have to be of the same type; we can mix integers, strings, attrsets, and lists without any issues:
nix-repl> [ 1 2 3 4 ]
[
1
2
3
4
]
nix-repl> [ "one" "two" 3 ]
[
"one"
"two"
3
]
nix-repl> [ "one" [ "two" 2 ] { three = 3; } ]
[
"one"
[ ... ]
{ ... }
]
In the last example, the Nix REPL does not show us the contents of the data from root to leaf. This is not a bug, but rather a characteristic of the Nix language, which is a “lazy” language. In other words, it avoids evaluating an expression if it can avoid doing so.
This characteristic has both pros and cons, like all design choices. A benefit is, for example, that we can work with very, very large attrsets (with tens of thousands of elements) very quickly; a drawback is, for example, that we do not see the full value inside the REPL.
To view it inside the REPL, we can use the :p or :printcommand, followed by the expression:
nix-repl> :p [ "one" ["two" 2] { three = 3; } ]
[
"one"
[
"two"
2
]
{ three = 3; }
]
You can try typing :? in the REPL to see other useful commands.
Returning to lists, a clever reader might wonder: if a function application is performed by following a function with an argument (with a space in between), and the sequence of values in a list is also separated by spaces, what happens if we want to put a function inside a list? The answer is that we must use parentheses:
nix-repl> double = x: x + x
nix-repl> [ double double 21 (double 21) ] # ovvero: [(double) (double) (21) (double 21)]
[
«lambda @ «string»:1:2» # double
«lambda @ «string»:1:2» # double
21 # 21
42 # double 21
]
A quirk of the language that sometimes emerges when working with lists is how Nix handles negative numbers: they exist, but they are actually calculated by subtracting a number from zero. This leads to some bizarre behaviors like the following:
nix-repl> -42 # sembra Ok...
-42
nix-repl> [ -42 ] # dentro una lista, qualcosa rivela che c'è un problema!
error: syntax error, unexpected '-'
at «string»:1:3:
1| [ -42 ]
| ^
nix-repl> [ 10-42 ] # sottrarre numeri dentro una lista, non si può fare!
error: syntax error, unexpected '-'
at «string»:1:5:
1| [ 10-42 ]
| ^
nix-repl> [ (-42) ] # dobbiamo mettere delle parentesi
[ -42 ]
To be fair, positive numbers cannot start with a +sign either, as it is reserved for additions:
nix-repl> 12 # numero positivo
12
nix-repl> +12 # niente segno positivo
error: syntax error, unexpected '+'
at «string»:1:1:
1| +12
| ^
nix-repl> (+12) # nemmeno tra parentesi!
error: syntax error, unexpected '+'
at «string»:1:2:
1| (+12)
| ^
nix-repl> [ +12 ] # nemmeno nelle liste!
error: syntax error, unexpected '+'
at «string»:1:3:
1| [ +12 ]
| ^
nix-repl> [ (+12) ]
error: syntax error, unexpected '+'
at «string»:1:4:
1| [ (+12) ]
| ^
This deviation from other languages, along with the asymmetry between positive and negative numbers, can be surprising, and it is something to pay attention to at times.
Loops and Recursion
We now have many useful tools to work on more serious tasks: we know that Nix is a functional language featuring integers (spoiler: floats exist too), strings, functions, lists, and attrsets. We have seen the syntax for defining a function, creating bindings using let, and using function closures, currying, and attrsets with pattern matching to pass multiple arguments at once.
We have also seen the if ... then ... construct, so anyone coming from other C-like languages might wonder: how do I write a loop? How do I process all elements of an attrset or a list?
Well, in Nix, there are no constructs for loops, such as for and while. In fact, such constructs do not exist in several functional languages.
This task is delegated to functions through recursion: a function that invokes itself provides the mechanism by which we can execute the same code multiple times.
Let’s look at an example of recursion in Nix by implementing a function that calculates the triangular summation 1 + 2 + 3 + ... + n n for a given argument n. We write it in a file named triangular.nix:
# triangular.nix
let
triangular = n:
if n > 0
then n + triangular (n - 1)
else 0;
in
triangular
(Notice, first of all, how we had to name the function using a let let; otherwise, we wouldn’t have been able to call it inside itself.)
And then we import it:
nix-repl> tri = import ./triangular.nix
nix-repl> tri 4
10
nix-repl> tri 5
15
nix-repl> tri (-1)
0
If we wanted to understand what this function does, we could simply expand the expression:
= triangular 4
# ora "espandiamo" la funzione, applicando tutti i parametri
# espandiamo triangular 4
= if 4 > 0 then 4 + triangular 3 else 0
# espandiamo triangular 3
= if 4 > 0 then 4 + (if 3 > 0 then 3 + triangular 2 else 0) else 0
# espandiamo triangular 2
= if 4 > 0 then 4 + (if 3 > 0 then 3 + (if 2 > 0 then 2 + triangular 1 else 0) else 0) else 0
# espandiamo triangular 1
= if 4 > 0 then 4 + (if 3 > 0 then 3 + (if 2 > 0 then 2 + (if 1 > 0 then 1 + triangular 0 else 0) else 0) else 0) else 0
# espandiamo triangular 0
= if 4 > 0 then 4 + (if 3 > 0 then 3 + (if 2 > 0 then 2 + (if 1 > 0 then 1 + (if 0 > 0 then 0 + triangular (-1) else 0) else 0) else 0) else 0) else 0
# ora "comprimiamo" la funzione, calcolando i valori
# calcoliamo 0 > 0 -> false
= if 4 > 0 then 4 + (if 3 > 0 then 3 + (if 2 > 0 then 2 + (if 1 > 0 then 1 + (if false then 0 + triangular (-1) else 0) else 0) else 0) else 0) else 0
# calcoliamo if false then ... else 0 -> 0
= if 4 > 0 then 4 + (if 3 > 0 then 3 + (if 2 > 0 then 2 + (if 1 > 0 then 1 + (0) else 0) else 0) else 0) else 0
# calcoliamo 1 > 0 -> true
= if 4 > 0 then 4 + (if 3 > 0 then 3 + (if 2 > 0 then 2 + (if true then 1 + (0) else 0) else 0) else 0) else 0
# calcoliamo if true then 1 + (0) else ... -> 1
= if 4 > 0 then 4 + (if 3 > 0 then 3 + (if 2 > 0 then 2 + (1) else 0) else 0) else 0
# calcoliamo 2 > 0 -> true
= if 4 > 0 then 4 + (if 3 > 0 then 3 + (if true then 2 + (1) else 0) else 0) else 0
# calcoliamo if true then 2 + (1) else ... -> 3
= if 4 > 0 then 4 + (if 3 > 0 then 3 + (3) else 0) else 0
# calcoliamo 3 > 0 -> true
= if 4 > 0 then 4 + (if true then 3 + (3) else 0) else 0
# calcoliamo if true then 3 + 3 else ... -> 6
= if 4 > 0 then 4 + (6) else 0
# calcoliamo 4 > 0 -> true
= if true then 4 + (6) else 0
# calcoliamo if true then 4 + 6 else ... -> 10
= 10
And this is, with a reasonable approximation, what happens when the function is evaluated recursively. This process is used in many functional languages and is such a common and established approach that functional language interpreters and compilers are highly optimized to perform these types of operations.
Typically, however, it is not necessary to write custom recursive functions.
This is because it is very common to have a few “primitive” functions, such as map, filter, and fold (or reduce), which allow us to work with data sequences. Let’s look at them briefly.
map map is very common in many languages: it allows us to take a sequence and transform each value within it using a function, “mapping” the values of the starting sequence to those resulting from the function application. Among the functions built into the language (built-ins), Nix provides map f list f list, which takes a function f and a list, and applies f to every element of the list.
For example:
nix-repl> double = x: x + x
nix-repl> map double [1 2 3]
[
2
4
6
]
Another very useful function is filter f list, which filters the elements of a list based on the result of a function: if the function f returns true for a given element of the list, that element is kept.
For example:
nix-repl> is-negative = x: x < 0
nix-repl> builtins.filter is-negative [1 (-1) 2 (-2) 3 (-3)]
[
-1
-2
-3
]
The last important function is fold, which is used to reduce a sequence to a single value. Since there are various ways to reduce a sequence to a single value, multiple versions of this function exist.
In builtins, Nix provides foldl' op nul list, which uses a binary function op (meaning it takes two arguments) to compress the list into a single value, starting from the left element (the beginning) of the list. Specifically, the three arguments are as follows:
op, a function that takes two arguments—an accumulator and the current value—and produces a new value to be used as the accumulator for the next element in the list;nul, the initial value of the accumulator, assigned before the first invocation ofop;- The list to iterate over.
If we were to write it in pseudo-code (Python), foldl would look something like this:
def foldl(op, nul, list):
# inizializza l'accumulatore con nul
acc = nul
# scorri tutta la lista, da sinistra a destra
for elem in list:
# applica la funzione passando l'accumulatore e l'elemento
# poi aggiorna l'accumulatore col risultato
acc = op(acc, elem)
# ritorna il valore accumulato
return acc
Let’s look at a practical example:
nix-repl> sum = acc: elem: acc + elem
nix-repl> builtins.foldl' sum 0 [ 1 2 3 4 ]
10
What happens behind the scenes is essentially this:
foldl' sum 0 [ 1 2 3 4 ] =
# acc = 0
= foldl' sum 1 [ 2 3 4 ]
# acc = sum 0 1 = 1
= foldl' sum 3 [ 3 4 ]
# acc = sum acc 2 = sum 1 2 = 3
= foldl' sum 6 [ 4 ]
# acc = sum acc 3 = sum 3 3 = 6
= foldl' sum 10 [ ]
# acc = sum acc 4 = sum 6 4 = 10
= 10
map, filter, and foldl'allow us to calculate essentially whatever we want starting from lists. Lists can be created by writing them literally, combining them from other lists using the ++operator, or using functions like builtins.genList generator length:
nix-repl> [1 2 3 4]
[ 1 2 3 4 ]
nix-repl> [1 2] ++ [3 4]
[ 1 2 3 4 ]
nix-repl> builtins.genList (x: x+1) 4
[ 1 2 3 4 ]
Operations on attrsets
We have discussed map, filter, and foldl' on lists, but how do we process attrsets?
Well, not only does Nix provide utility functions to work in a similar fashion on attrsets, such as builtins.mapAttrs, but it also provides two much simpler primitives that allow us to transition from a list to an attrset and vice versa:
nix-repl> builtins.listToAttrs [ { name = "Foo"; value = "Bar"; } { name = "Baz"; value = 123; } ]
{
Baz = 123;
Foo = "Bar";
}
nix-repl> builtins.attrNames { Foo = "Bar"; Baz = 123; }
[
"Baz"
"Foo"
]
nix-repl> builtins.attrValues { Foo = "Bar"; Baz = 123; }
[
123
"Bar"
]
The attributes are returned in alphabetical order, while the values are returned in the order corresponding to their respective attributes.
Conclusion and References
And with that, we have concluded our tour of Nix as a functional language!
There are other important aspects of the Nix language that we have glossed over (e.g., we didn’t delve into the implications of Nix being a “lazy” language), we haven’t analyzed many of the built-in utility functions that definitely make life easier (but we refer you to noogle to search for available functions), and we haven’t discussed all the aspects related to package management.
The goal, however, was to introduce the main concepts of functional programming through a simple and focused language, which is useful for a very specific and immediately practical use case.
Now, all that’s left is to practice. Interested readers can try implementing commonly used functions or simple exercises; here are a few options:
- Python’s
enumerate()function, which, given a list, returns a list of index-value pairs, e.g.,.enumerate(["foo", "bar"]) == [(0, "foo"), (1, "bar")]; - Python’s
zipfunction, which, given two lists, returns a list that pairs the values of the first and second list, e.g.,zip([1, 2, 3], ["a", "b", "c"]) == [(1, "a"), (2, "b"), (3, "c")]; - Recursive calculation of the factorial;
- Recursive calculation of the Fibonacci sequence;
- The classic FizzBuzz game for a parametric N.
For those who want to challenge themselves further with the Nix language—keeping in mind that it wasn’t designed as a general-purpose language, but rather as a language for a package manager—we recommend:
- The builtins documentation;
- The Nix language reference documentation;
- The Nix Language Quirks page on the official NixOS wiki;
- The Nix language tutorial on the official Nix documentation;
- For those who want to try using Nix for package management and configurations, the Nix Pills are an excellent resource.
