Chapter 2
✔️ Click to Expand Table of Contents

Understanding Nix Functions
Functions are the building blocks of Nix, appearing everywhere in Nix expressions and configurations. Mastering them is essential for writing effective Nix code and understanding tools like NixOS and Home Manager. This chapter explores how Nix functions work, focusing on their single-argument nature, currying, partial application, and their role in modules.
What are Nix Functions?
A Nix Function is a rule that takes an input (called an argument) and produces an output based on that input. Unlike many programming languages, Nix functions are designed to take exactly one argument at a time. This unique approach, combined with a technique called currying, allows Nix to simulate multi-argument functions in a flexible and reusable way.
Builtins
✔️ Nix Builtin Functions (Click to Expand)
The Nix expression evaluator has a bunch of functions and constants built in:
-
toString e
: (Convert the expressione
to a string) -
import path
: (Load, parse and return the Nix expression in the filepath
) -
throw x
: (Throw an error messagex
. Usually stops evaluation) -
map f list
: (Apply the functionf
to each element in thelist
)
Lamdas
Nix functions are anonymous (lambdas) (e.g., x: x + 2
), and technically take a
single parameter. However, that single parameter is very often an attribute set,
allowing you to effectively pass multiple named inputs by destructuring (e.g.,
{ arg1, arg2 }: arg1 + arg2
).
Type the parameter name, followed by a colon, and finally the body of the function.
nix-repl> param: param * 2
<<lambda @ <<string>>:1:1>>
nix-repl> (param: param * 2) 2
4
The above example shows that everything in Nix returns a value. When you call a function directly (without first assigning the function itself to a variable), the result of that call is immediately evaluated and displayed/used.
In order to make our function reusable and be able to pass different values at different times we have to assign our function to a variable:
nix-repl> twoTimes = param: param * 2
Now, we can reference our function by it's name and pass our required parameter:
nix-repl> twoTimes
«lambda @ «string»:1:2»
nix-repl> twoTimes 2
4
nix-repl> twoTimes 4
8
We defined a function param: param * 2
takes one parameter param
, and
returns param * 2
. We then assigned this function to the variable twoTimes
.
Lastly, we called the function with a few different arguments showing it's
reusability.
Understanding Function Structure: The Role of the Colon
The colon (:
) acts as a clear separator within a function definition:
-
Left of the Colon: This is the function's argument. It's a placeholder name for a value that will be provided when the function is called.
-
Right of the Colon: This is the function body. It's the expression that will be evaluated when the function is invoked.
Think of function arguments as naming values that aren't known in advance. These names are placeholders that get filled with specific values when the function is used.
Example:
greet = personName: "Hello, ${personName}!";
-
Here,
personName
is the argument (the placeholder). -
"Hello, ${personName}!"
, is the function body (which uses the placeholder to create the greeting).
When you call the function, (click to see Output):
greet "Anonymous"
"Hello, Anonymous!"
-
The value
"Anonymous"
is substituted for thepersonName
placeholder within the function body. -
This structure is the foundation of all Nix functions, whether simple or complex.
Single-Argument Functions: The Basics
The simplest form of a Nix function takes a single argument. In Nix, function
definitions like x: x + 1
or personName: "Hello, ${personName}!";
are
anonymous lambda functions. They exist as values until they are assigned to
a variable.
- Click to see Output:
# This is an anonymous lambda function value:
# x: x + 1
inc = x: x + 1; # here we assigned our lambda to a variable `inc`
inc 5
6
-
x
is the argument. -
x + 1
is the function body.
This straightforward design makes single-argument functions easy to understand and use. But what if you need a function that seems to take multiple arguments? That's where currying comes in.
Simulating Multiple Arguments: Currying
To create functions that appear to take multiple arguments, Nix uses currying. This involves nesting single-argument functions, where each function takes one argument and returns another function that takes the next argument, and so on.
nix-repl> multiply = x: (y: x*y)
nix-repl> multiply
«lambda»
nix-repl> multiply 4
«lambda»
nix-repl> (mul 4) 5
20
We defined a function that takes the parameter x
, the body returns another
function. This other function takes a parameter y
and returns x*y
.
Therefore, calling multiply 4
returns a function like: x: 4*y
. In turn, we
call the returned function with 5
, and get the expected result.
Currying example 2
# concat is equivalent to:
# concat = x: (y: x + y);
concat = x: y: x + y;
concat 6 6 # Evaluates to 12
12
Here, concat
is actually two nested functions
-
The first function takes
x
and returns another function. -
The second function takes
y
and performsx + y
Nix interprets the colons (:
) as separators for this chain of single-argument
functions.
Here's how it works step by step:
-
When you call
concat 6
, the outer function bindsx
to6
and returns a new function:y: 6 + y
. -
When you call that function with
6
(i.e.,concat 6 6
), it computes6 + 6
, resulting in12
.
This chaining is why Nix functions are so powerful—it allows you to build flexible, reusable functions.
Currying is a powerful feature in Nix that enables you to partially apply arguments to functions, leading to increased reusability. This behavior is a direct consequence of Nix functions being "first-class citizens" (a concept we'll delve into later), and it proves invaluable for decomposing intricate logic into a series of smaller, more focused functions.
Key Insight: Every colon in a function definition separates a single argument from its function body, even if that body is another function definition.
Greeting Example
Let's explore currying with a more relatable example in the nix repl
:
nix repl
nix-repl> greeting = prefix: name: "${prefix}, ${name}!";
nix-repl> greeting "Hello"
<<lambda @ <<string>>:1:10>> # partial application returns a lambda
nix-repl> greeting "Hello" "Alice"
"Hello, Alice!" # providing both arguments returns the expected result
This function is a chain of two single-argument functions:
-
The outer function takes
prefix
(e.g."Hello"
) and returns a function that expectsname
. -
The inner function takes
name
(e.g."Alice"
) and combines it withprefix
to produce the final string.
Thanks to lexical scope (where inner functions can access variables from
outer functions), the inner function "remembers" the prefix
value.
Partial Application: Using Functions Incrementally
Because of currying, you can apply arguments to a Nix function one at a time. This is called partial application. When you provide only some of the expected arguments, you get a new function that "remembers" the provided arguments and waits for the rest.
Example:
Using our greeting
function again:
nix repl
nix-repl> greeting = prefix: name: "${prefix}, ${name}!";
nix-repl> helloGreeting = greeting "Hello";
nix-repl> helloGreeting "Alice"
"Hello, Alice"
helloGreeting
is now a new function. It has already received theprefix
argument ("Hello"
), when we provide the second argument we get"Hello, Alice!"
Benefits of Partial Application:
Partial application provides significant benefits by enabling you to derive
specialized functions from more general ones through the process of fixing
certain parameters. Additionally, it serves as a powerful tool for adapting
existing functions to fit the precise argument requirements of higher-order
functions like map
and filter
.
Nix Functions being "first class citizens"
In the context of Nix, the phrase "Nix treats functions as first-class citizens" means that functions in Nix are treated as values, just like numbers, strings, or lists. They can be manipulated, passed around, and used in the same flexible ways as other data types. This concept comes from functional programming and has specific implications in Nix.
What It Means in Nix
- Functions Can Be Assigned to Variables:
-
You can store a function in a variable, just like you would store a number or string.
-
Example:
greet = name: "Hello, ${name}!";
- Here, greet is a variable that holds a function.
- Functions Can Be Passed as Arguments:
-
You can pass a function to another function as an argument, allowing for higher-order functions (functions that operate on other functions).
-
Example:
applyTwice = f: x: f (f x);
inc = x: x + 1;
applyTwice inc 5 # Output: 7 (increments 5 twice: 5 → 6 → 7)
7
- Here, applyTwice takes a function
f
(in this case,inc
) and applies it tox
twice.
- Functions Can Be Returned from Functions:
-
Functions can produce other functions as their output, which is key to currying in Nix.
-
Example:
greeting = prefix: name: "${prefix}, ${name}!";
helloGreeting = greeting "Hello"; # Returns a function
helloGreeting "Alice" # Output: "Hello, Alice!"
"Hello, Alice!"
- The greeting function returns another function when partially applied with prefix.
- Functions Are Values in Expressions:
-
Functions can be used anywhere a value is expected, such as in attribute sets or lists.
-
Example:
myFuncs = {
add = x: y: x + y;
multiply = x: y: x * y;
};
myFuncs.add 3 4 # Output: 7
7
-
Here, functions are stored as values in an attribute set.
-
To try this in the
repl
just remove the semi-colon (;
)
Why This Matters in Nix:
This functional approach is fundamental to Nix's unique build system. In Nix, package builds (called derivations) are essentially functions. They take specific inputs (source code, dependencies, build scripts) and deterministically produce outputs (a built package).
This design ensures atomicity: if a build does not succeed completely and perfectly, it produces no output at all. This prevents situations common in other package managers where partial updates or corrupted builds can leave your system in an inconsistent or broken state.
Many NixOS and Home Manager modules are functions, and their first-class status means they can be combined, reused, or passed to other parts of the configuration system.
Now that we understand the "first-class" nature of Nix Functions let's see how they fit into NixOS and Home Manager modules.
The Function Nature of NixOS and Home Manager Modules
It's crucial to understand that most NixOS and Home Manager modules are fundamentally functions.
These module functions typically accept a single argument: an attribute set (remember this, it's important to understand).
Example:
A practical NixOS module example for Thunar with plugins:
# thunar.nix
{pkgs, ...}: {
programs = {
thunar = {
enable = true;
plugins = with pkgs.xfce; [
thunar-archive-plugin
thunar-volman
];
};
};
}
- To use this module I would need to import it into my
configuration.nix
or equivalent, shown here for completeness.
# configuration.nix
# ... snip ...
imports = [ ../nixos/thunar.nix ];
# ... snip ...
-
This is actually a pretty good example of
with
making it a bit harder to reason where the plugins are from. You might instinctively try to trace a path likeprograms.thunar.plugins.pkgs.xfce
because you sawpkgs.xfce
in thewith
statement. But that's now howwith
works. Thepkgs.xfce
path exists outside theplugins
list, defining the source of the items, not their nested structure within the list. -
To follow best practices you could write the above plugins section as:
plugins = [
pkgs.xfce.thunar-archive-plugin
pkgs.xfce.thunar-volman
];
-
Now it's clear that each plugin comes directly from
pkgs
and each will resolve to a derivation.- To be clear either way is fine, especially in such a small self contained
module. If it were in a single file
configuration.nix
it would be a bit more confusing to trace. Explicitness is your friend with Nix and maintaining reproducability.with
isn't always bad but should be avoided at the top of a file for example to bringnixpkgs
into scope, uselet
instead.
- To be clear either way is fine, especially in such a small self contained
module. If it were in a single file
The entire module definition is a function that takes one argument (an attribute
set):{ pkgs, ... }
. When this module is included in your configuration, the
NixOS module system calls this function with a specific attribute set. This
attribute set contains the available packages (pkgs
), and other relevant
information. The module then uses these values to define parts of your system.
Understanding passing and getting back arguments
For this example we will build the Hello derivation from the Nix Pills series.
Create an autotools.nix
with the following contents:
pkgs: attrs: let
defaultAttrs = {
builder = "${pkgs.bash}/bin/bash";
args = [./builder.sh];
baseInputs = with pkgs; [
gnutar
gzip
gnumake
gcc
coreutils
gawk
gnused
gnugrep
binutils.bintools
];
buildInputs = [];
system = builtins.currentSystem;
};
in
derivation (defaultAttrs // attrs)
Let's create the hello derivation:
let
pkgs = import <nixpkgs> {};
mkDerivation = import ./autotools.nix pkgs;
in
mkDerivation {
name = "hello";
src = ./hello-2.12.1.tar.gz;
}
- You can get the tarball
here, place it in the
same directory as
autotools.nix
And finally the builder.sh
that autotools.nix
declares for the args
attribute:
!#/bin/bash
set -e
unset PATH
for p in $buildInputs $baseInputs; do
export PATH=$p/bin${PATH:+:}$PATH
done
tar -xf $src
for d in *; do
if [ -d "$d" ]; then
cd "$d"
break
fi
done
./configure --prefix=$out
make
make install
When you write:
mkDerivation = import ./autotools.nix pkgs;
-
import ./autotools.nix
: This evaluates theautotools.nix
file. Because it starts withpkgs: attrs: ...
, it means thatautotools.nix
evaluates to a function that expects one argument namedpkgs
. -
... pkgs
: We are immediately calling that function (the one returned byimport ./autotools.nix
) and passing it ourpkgs
variable (which is the result ofimport <nixpkgs> {}
).
This illustrates the concept of Currying in Nix:
The function defined in autotools.nix
(pkgs: attrs: ...
) is a curried
function. It's a function that, when given its first argument (pkgs
), returns
another function (which then expects attrs
).
The result of import ./autotools.nix pkgs
is that second, inner function:
attrs: derivation (defaultAttrs // attrs)
. This inner function is then bound
to the mkDerivation
variable, making it ready to be called with just the
specific attributes for your package (like name
and src
).
Understanding the attrs
Argument
Now let's focus on the second argument of our autotools.nix
function: attrs
.
Recall the full function signature in autotools.nix
:
pkgs: attrs: let
# ... defaultAttrs definition ...
in
derivation (defaultAttrs // attrs)
- What
attrs
Represents:
-
Once
autotools.nix
has received itspkgs
argument (and returned the inner function), this inner function is waiting for its final argument, which we callattrs
. -
attrs
is simply an attribute set (a key-value map in Nix). It's designed to receive all the specific properties of the individual package you want to build using this helper.
- How
attrs
is Used:
-
Look at the final line of
autotools.nix
:derivation (defaultAttrs // attrs)
. -
The
//
operator in Nix performs an attribute set merge. It takes all attributes fromdefaultAttrs
and combines them with all attributes fromattrs
. -
Crucially, if an attribute exists in both
defaultAttrs
andattrs
, the value fromattrs
(the second operand) takes precedence and overrides the default value.
- Applying attrs in the hello Derivation:
- In the
hello
derivation, we callmkDerivation
like this:
mkDerivation {
name = "hello";
src = ./hello-2.12.1.tar.gz;
}
-
The attribute set
{ name = "hello"; src = ./hello-2.12.1.tar.gz; }
is what gets passed as theattrs
argument to themkDerivation
function (which, remember, is the inner function returned byautotools.nix
). -
When derivation
(defaultAttrs // attrs)
is evaluated for "hello", thename
andsrc
provided in theattrs
set will be merged with all thedefaultAttrs
(likebuilder
,args
,baseInputs
, etc.).
In summary:
-
The
pkgs
argument configures the general environment and available tools for the builder. -
The
attrs
argument is where you provide the unique details for each specific package you intend to build using thisautotools.nix
helper. It allows you to specify things like the package's name, source code, version, and any custom build flags, while still benefiting from all the sensible defaults provided byautotools.nix
. This separation makesautotools.nix
a reusable and flexible "template" for creating derivations.
Conclusion
Having explored the fundamental nature of functions in Nix, we can now see this concept applies to more complex areas like NixOS configuration and derivations. In the next chapter, NixOS Modules Explained. We will learn about NixOS Modules which are themselves functions most of the time.