Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Nix Language

✔️ Click to Expand Table of Contents

lambda1

Nix Expression Language Syntax Overview

The Nix language is designed for conveniently creating and composing derivations precise descriptions of how contents of files are used to derive new files. --Nix Reference Manual

Nix is often described as “JSON with functions.” It’s a declarative language where you define outcomes, not step-by-step instructions. Instead of writing sequential code, you create expressions that describe data structures, functions, and dependencies. These expressions are evaluated lazily, meaning Nix computes values only when needed, making it efficient for managing large systems.

You can plug most of the following into the nix repl I'm showing it in a single code block here for brevity:

# Comments Look Like This!

# Strings
"This is a string"          # String literal

''
one
two                        # multi-line String
three
''

("foo" + "bar")           # => "foobar"

"foo" != "bar"   # Inequality test  # => true

!false      # => true

("Home dir is ${builtins.getEnv "HOME"}")  # String Interpolation
# => "Home dir is /home/jr"

"3 6 ${builtins.toString 9}"
# => "3 6 9"

"goodbye ${ { d = "world";}.d}"
# => "goodbye world"

# Booleans

(false && true)    # AND         # => false

(true || false)    # OR         # => true

(if 6 < 9 then "yay" else "nay")  # => "yay"

null      # Null Value

679       # Integer

(6 + 7 + 9) # => 22   # Addition

(9 - 3  - 2) # => 4   # Subtraction

(6 / 3)  # => 2       # Division
6.79      # Floating Point

/etc/nixos      # Absolute Path

../modules/nixos/boot.nix    # relative

# Let expressions

(let a = "2"; in                   # Let expressions are a way to create variables
a + a + builtins.toString "4")
# => "224"

(let first = "firstname"; in
"lastname " first)
# => "lastname firstname"

# Lists

[ 1 2 "three" "bar" "baz" ]   # lists are whitespace separated

builtins.elemAt [ 1 2 3 4 5 ] 3
# => 4

builtins.length [ 1 2 3 4 ]
# => 4

# Attrsets

{ first = "Jim"; last = "Bo"; }.last # Attribute selection
# => "Bo"

{ a = 1; b = 3; } // { c = 4; b = 2; }   # Attribute Set merging
# => { a = 1; b = 2; c = 4; }               # Right Side takes precedence

builtins.listToAttrs [ { name = "Jr"; value = "Jr Juniorville"; } {name = "$"; value = "JR"; } { name = "jr"; value = "jr
ville"; }]
# => { "$" = "JR"; Jr = "Jr Juniorville"; jr = "jrville"; }

# Control Flow

if 2 * 2 == 4
then "yes!"
else "no!"
# => "yes!"

assert 2 * 2
== 4; "yes!"
# => "yes!"

with builtins;
head [ 5 6 7 ]
# => 5

# or

builtins.head[ 5 6 7 ]

inherit pkgs     # pkgs = pkgs;
src;             # src = src;

Understanding Laziness

Nix expressions are evaluated lazily, meaning Nix computes values only when needed. This is a powerful feature that makes Nix efficient for managing large systems, as it avoids unnecessary computations.

For example, observe how a is never evaluated in the following nix-repl session:

nix-repl> let a = builtins.div 4 0; b = 6; in b
6
  • Since a isn't used in the final result, there's no division by zero error.

Strings and String Interpolation

Strings: Strings are enclosed in double quotes (") or two single quotes ('').

nix-repl> "stringDaddy"
"stringDaddy"
nix-repl> ''
  This is a
  multi-line
  string
''
"This is a\nmulti-line\nstring.\n"

string interpolation. is a language feature where a string, path, or attribute name can contain an expressions enclosed in ${ }. This construct is called an interpolated string, and the expression inside is an interpolated expression.

Rather than writing:

let path = "/usr/local"; in "--prefix=${path}"

This evaluates to "--prefix=/usr/local". Interpolated expressions must evaluate to a string, path, or an attribute set with an outPath or __toString attribute.

Attribute Sets

Attribute sets are all over Nix code and deserve their own section, they are name-value pairs wrapped in curly braces, where the names must be unique:

{
  string = "hello";
  int = 8;
}

Attribute names usually don't need quotes.

You can access attributes using dot notation:

let person = { name = "Alice"; age = 30; }; in person.name
"Alice"

You will sometimes see attribute sets with rec prepended. This allows access to attributes within the set:

rec {
  x = y;
  y = 123;
}.x

Output: 123

or

rec {
  one = 1;
  two = one + 1;
  three = two + 1;
}

Output:

 {
  one = 1;
  three = 3;
  two = 2;
 }
# This would fail:
{
  one = 1;
  two = one + 1;  # Error: undefined variable 'one'
  three = two + 1;
}

Recursive sets introduce the danger of infinite recursion For example:

rec {
  x = y;
  y = x;
}.x

Will crash with an infinite recursion encountered error message.

The attribute set update operator merges two attribute sets.

Example:

{ a = 1; b = 2; } // { b = 3; c = 4; }

Output:

{ a = 1; b = 3; c = 4; }

However, names on the right take precedence, and updates are shallow.

Example:

{ a = { b = 1; }; } // { a = { c = 3; }; }

Output:

{ a = { c = 3; }; }

Above, key b was completely removed, because the whole a value was replaced.

Inheriting Attributes

  • Click to see Output:
let x = 123; in
{
  inherit x;
  y = 456;
}

is equivalent to

let x = 123; in
{
  x = x;
  y = 456;
}

which are both equivalent to

{
  x = 123;
  y = 456;
}

❗: This works because x is added to the lexical scope by the let construct.

Now that we understand attribute sets lets move on to functions, a powerful feature of the Nix language that gives you the ability to reuse and share logical pieces of code.

Functions(lambdas):

Functions in Nix help you build reusable components and are the the building blocks of Nix. In the next chapter we'll go even further into Nix functions and how to use them but I will touch on them here.

Nix functions have this form:

pattern: body

The following is a function that expects an integer and returns it increased by 1:

x: x + 1   # lambda function, not bound to a variable

The pattern tells us what the argument of the function has to look like, and binds variables in the body to (parts of) the argument.

(x: x + 5) 200
205

They are all lambdas (i.e. anonymous functions without names) until we assign them to a variable like the following example.

Functions are defined using this syntax, where x and y are attributes passed into the function:

{
  my_function = x: y: x + y;
}

The code below calls a function called my_function with the parameters 2 and 3, and assigns its output to the my_value field:

{
  my_value = my_function 2 3;
}
my_value
5

The body of the function automatically returns the result of the function. Functions are called by spaces between it and its parameters. No commas are needed to separate parameters.

The following is a function that expects an attribute set with required attributes a and b and concatenates them:

{ a, b }: a + b

Default Values in Functions:

Functions in Nix can define default values for their arguments. This allows for more flexible function calls where some arguments are optional.

{ x, y ? "foo", z ? "bar" }: z + y + x
  • Specifies a function that only requires an attribute named x, but optionally accepts y and z.

@-patterns in functions:

An @-pattern provides a means of referring to the whole value being matched by the function's argument pattern, in addition to destructuring it. This is especially useful when you want to access attributes that are not explicitly destructured in the pattern:

args@{ x, y, z, ... }: z + y + x + args.a
# or
{ x, y, z, ... } @ args: z + y + x + args.a
  • Here, args is bound to the argument as passed, which is further matched against the pattern { x, y, z, ... }. The @-pattern makes mainly sense with an ellipsis(...) as you can access attribute names as a, using args.a, which was given as an additional attribute to the function.

  • We will expand on Functions in This Chapter

If, Let, and With Expressions

Nix is a pure expression language, meaning every construct evaluates to a value — there are no statements. Because of this, if expressions in Nix work differently than in imperative languages, where conditional logic often relies on statements (if, elsif, etc.).

If expressions in Nix:

Since everything in Nix is an expression, an if expression must always produce a value:

nix-repl> a = 6
nix-repl> b = 10
nix-repl> if a > b then "yes" else "no"
"no"

Here, "no" is the result because a(6) is not greater than b(10). Notice that there's no separate conditional statement -- the entire construct evaluates to a value.

Another example, integrating built-in functions:

{
  key = if builtins.pathExists ./path then "YES" else "NO!";
}

If ./path exists it will evaluate to the value "YES" or else it will evaluate to "NO!".

Thus, the final result of the expression would be:

{ key = "YES"; }
# or
{ key = "NO!"; }

Since Nix does not have statements, Nix's if statements behave more like ternary operators (condition ? value_if_true : value_if_false) in other languages.

Let expressions:

Let expressions in Nix is primarily a mechanism for local variable binding and scoping. It allows you to define named values that are only accessible within the in block of the let expression. This is useful for keeping code clean and avoiding repitition.

For example:

let
  a = "foo";
  b = "fighter";
in a + b
"foofighter"

Here, a and b are defined inside the let block, and their values are used in the in expression. Since everything in Nix is an expression, a + b evaluates to "foofighter"

Using Let Expressions Inside Attribute Sets

Let expressions are commonly used when defining attribute sets (Click for output):

let
  appName = "nix-app";
  version = "1.0";
in {
  name = appName;
  fullName = appName + "-" + version;
}
{
  name = "nix-app";
  fullName = "nix-app-1.0";
}

This allows you to reuse values within an attribute set, making the code more modular and preventing duplication.

Let Expressions in Function Arguments

You can also use let expressions within function arguments to define intermediate values before returning an output:

{ pkgs, lib }:
let
  someVar = "hello";
  otherVar = "world";
in
{ inherit pkgs lib someVar otherVar; }

Result:

{
  pkgs = <value>;
  lib = <value>;
  someVar = "hello";
  otherVar = "world";
}

Here, inherit brings pkgs and lib into the resulting attribute set, alongside the locally defined variables someVar and otherVar.

Key Takeaways:

  • Let expressions allow local variable bindings that are only visible inside the in block. They also help avoid repitition and improve readability.

  • Commonly used inside attribute sets or function arguments.

  • Their scope is limited to the expression in which they are declared.

With expressions:

A with expression in Nix is primarily used to simplify access to attributes within an attribute set. Instead of repeatedly referring to a long attribute path, with temporarily brings the attributes into scope, allowing direct access without prefixing them.

Basic Example: Reducing Attribute Path Usage

Consider the following expressions:

nix-repl> longName = { a = 3; b = 4; }
nix-repl> longName.a + longName.b
7

Here, we must explicitly reference longName.a and longName.b. Using a with expression simplifies this:

nix-repl> with longName; a + b
7

Now, within the scope of the with expression, a and b are accessible without prefixing them with longName.

Practical Use Case: Working with pkgs

One of the most common uses of with that you'll see is when dealing with packages from nixpkgs is writing the following:

{ pkgs }:
with pkgs; {
  myPackages = [ vim git neofetch ];
}

Instead of writing this:

{ pkgs }:
{
  myPackages = [ pkgs.vim pkgs.git pkgs.neofetch ];
}

Tip: Overusing with lib; or with pkgs; can reduce clarity, it may be fine for smaller modules where the scope is limited. For larger configurations, explicit references (pkgs.something) often make dependencies clearer and prevent ambiguity.

Nix Language Quirks

  1. with gets less priority than let. This can be confusing, especially if you like to write with pkgs;:
nix-repl> pkgs = { x = 2; }

nix-repl> with pkgs; x
2

nix-repl> with pkgs; let x = 4; in x
4

This shows us that the let binding overrides the with binding.

let x = 4; in with pkgs; x
4

Still returns 4, but the reasoning is different. The with expression doesn't define new bindings; it simply makes attributes from pkgs available as unqualified names. However, because let x = 4 is outside the with, it already extablished x = 4, so when with pkgs; x is evaluated inside, x still refers to the outer let binding, not the one from pkgs.

  1. Default values aren't bound in @-patterns

In the following example, calling a function that binds a default value "baz" to the argument's attribute b with an empty attribute set as argument will produce an empty attribute set inputs rather than the desired { b = "baz"; }:

(inputs@(b ? "baz"): inputs) {}

Output:

{}
  1. Destructuring function arguments:
nix-repl> f = { x ? 2, y ? 4 }: x + y

nix-repl> f { }
6

The function f takes an attribute set with default values (x = 2, y = 4)

When called with {} (an empty set), it falls back to the default values (2 + 4 -> 6)

Using @args to capture the entire input set:

The @args syntax allows us to retain access to the full attribute set, even after destructuring:

nix-repl> f = { x ? 1, y ? 2, ... }@args: with args; x + y + z

nix-repl> f { z = 3; }
6

The { x ? 1, y ? 2, ... } syntax means x and y have defaults, while ... allows additional attributes.

@args binds the entire attribute set (args) so that we can access z, which wouldn't be destructured by default.

When calling f { z = 3; }, we pass an extra attribute (z = 3), making x + y + z1 + 2 + 3 = 6.

  1. Imports and namespaces

There is a keyword import, but it's equivalent in other languages is eval. It can be used for namespacing too:

let
  pkgs = import <nixpkgs> {};
  lib = import <nixpkgs/lib>;
in
  pkgs.runCommand (lib.strings.removePrefix "....

consider using import here as using qualified import ... in Haskell or import ... in Python.

Another way of importing is with import ...;, which corresponds to Python from ... import *.

But because of not very great IDE support in Nix, with import ...; is discouraged. Rather use inherit, especially if you are targeting source code for Nix newcomers:

let
  lib = import <nixpkgs/lib>;
  inherit (lib.strings)
    removePrefix removeSuffix
  ;
  inherit (lib.lists)
    isList init drop
  ;
in
  removePrefix ...

inherit has higher priority than with, and conflicts with let

nix-repl> let pkgs = { x = 1; }; x = 2; x = 3; inherit (pkgs) x; in x
error: attribute ‘x’ at (string):1:31 already defined at (string):1:24

This makes it a sane citizen of Nix lanugage... except it has a twin, called { inherit ...; }. They DON'T do the same - let inherit ... adds let-bindings, and { inherit ...; } adds attributes to a record. --https://nixos.wiki/wiki/Nix_Language_Quirks

  1. Only attribute names can be interpolated, not Nix code:
nix-repl> let ${"y"} = 4; in y
4

nix-repl> with { ${"y"} = 4; }; y
4

let y = 1; x = ${y}; in x
error: syntax error, unexpected DOLLAR_CURLY

Conclusion

  • let bindings introduce new local values and override anything from with.

  • with doesn't create bindings - it only makes attributes available within its scope.

  • The order matters: If let x = 4 is outside with, then x = 4 already exists before with runs, so with pkgs; x resolves to 4, not the value from pkgs.

Resources

✔️ Resources (Click to Expand)

A few resources to help get you started with the Nix Language, I have actually grown to love the language. I find it fairly simple but powerful!