Nix Language Basics
Understand the Nix expression language for declaring hermetic build environments.
Nix Language Basics
Master the Nix expression language with free flashcards and hands-on examples. This lesson covers fundamental data types, functions, attribute sets, and lazy evaluation—essential concepts for building hermetic, reproducible software packages and development environments.
Welcome 💻
The Nix language is a purely functional, lazily evaluated programming language designed specifically for package management and system configuration. Unlike imperative build scripts that execute commands step-by-step, Nix describes what you want to build, and the Nix toolchain figures out how to build it reproducibly.
🎯 Why Learn Nix Language?
- Declarative builds: Describe outputs, not processes
- Reproducibility: Same input → same output, always
- Composability: Combine packages like LEGO blocks
- Side-effect free: No hidden dependencies or global state
Think of Nix as JSON with functions—it's not a general-purpose programming language like Python or JavaScript, but a domain-specific language (DSL) for expressing build configurations.
Core Concepts 🧠
1. Everything is an Expression
In Nix, everything evaluates to a value. There are no statements, no loops with side effects, no variable reassignment. Every piece of Nix code produces a result.
## Simple values
42 # integer
3.14 # float
"hello" # string
true # boolean
/path/to/file # path
💡 Tip: Nix files typically end in .nix. The entire file is one expression that gets evaluated.
2. Basic Data Types
| Type | Example | Description |
|---|---|---|
| Integer | 42 | Whole numbers |
| Float | 3.14 | Decimal numbers |
| String | "hello" | Text in quotes |
| Path | ./file.txt | Filesystem paths |
| Boolean | true / false | Logical values |
| Null | null | Absence of value |
| List | [ 1 2 3 ] | Ordered collection |
| Attribute Set | { x = 1; y = 2; } | Key-value pairs |
String Interpolation
Nix supports string interpolation using ${} syntax:
let
name = "World";
in
"Hello, ${name}!" # Result: "Hello, World!"
Multi-line strings use double single-quotes:
''
This is a
multi-line string
with ${name} interpolation
''
💡 Mnemonic: Think '' as "extra strong quotes" for long content.
3. Lists 📋
Lists are space-separated values in square brackets (no commas!):
[ 1 2 3 4 5 ]
[ "apple" "banana" "cherry" ]
[ true false true ]
Lists can contain mixed types:
[ 1 "two" 3.0 true ]
⚠️ Common Mistake: Using commas between list elements (this is wrong!):
[ 1, 2, 3 ] # ❌ WRONG - commas not allowed
[ 1 2 3 ] # ✅ CORRECT
4. Attribute Sets (The Heart of Nix) ❤️
Attribute sets are like JSON objects or Python dictionaries—collections of key-value pairs:
{
name = "my-package";
version = "1.0.0";
description = "A sample package";
}
Access attributes using dot notation:
let
pkg = { name = "foo"; version = "1.0"; };
in
pkg.name # Result: "foo"
Nested attribute sets:
{
package = {
name = "myapp";
meta = {
author = "Jane";
license = "MIT";
};
};
}
Access nested values: set.package.meta.author
Recursive Attribute Sets (rec)
Normally, attributes cannot reference each other. Use rec to enable self-reference:
rec {
x = 10;
y = x + 5; # y can reference x
z = y * 2; # z can reference y
}
## Result: { x = 10; y = 15; z = 30; }
5. Functions (λ Lambda Magic) ⚡
Nix functions are anonymous lambda functions. The basic syntax:
## Pattern: argument: body
x: x + 1
This creates a function that takes x and returns x + 1.
Calling functions (no parentheses for single arguments!):
let
addOne = x: x + 1;
in
addOne 5 # Result: 6
Multiple Arguments (Currying)
Nix functions take exactly one argument. For multiple arguments, use nested functions:
## This function takes x, returns a function that takes y
add = x: y: x + y
## Usage:
add 3 5 # Result: 8
## You can partially apply:
addThree = add 3
addThree 5 # Result: 8
FUNCTION CURRYING VISUALIZATION
add = x: y: x + y
↓
add 3 ──→ (y: 3 + y) ← Partial application
↓
(y: 3 + y) 5 ──→ 8
Attribute Set Arguments (Named Parameters)
The most common pattern: functions that accept attribute sets:
{ name, version }: "${name}-${version}"
## Usage:
myFunc { name = "foo"; version = "1.0"; }
## Result: "foo-1.0"
Default values using ?:
{ name, version ? "1.0.0" }: "${name}-${version}"
## Can omit version:
myFunc { name = "foo"; } # Uses default "1.0.0"
Capturing extra attributes with ...:
{ name, version, ... }: "${name}-${version}"
## Accepts any extra attributes without error:
myFunc { name = "foo"; version = "1.0"; author = "Jane"; }
Binding the entire set with @:
args@{ name, version, ... }: {
package = "${name}-${version}";
allArgs = args; # Access the full attribute set
}
6. Let Bindings 📝
The let...in expression defines local variables:
let
x = 10;
y = 20;
in
x + y # Result: 30
Scope: Variables defined in let are only visible in the in section.
let
firstName = "Jane";
lastName = "Doe";
fullName = "${firstName} ${lastName}";
in
fullName # Result: "Jane Doe"
## firstName is NOT accessible here
🧠 Mental Model: Think of let as a temporary workspace where you prepare values before producing the final result in in.
7. Conditionals (if-then-else) 🔀
Nix has if-then-else expressions (both branches required!):
if condition then value1 else value2
Example:
let
age = 25;
in
if age >= 18 then "adult" else "minor"
# Result: "adult"
⚠️ Critical: The else branch is mandatory (every expression must have a value):
if true then "yes" # ❌ ERROR: missing else
if true then "yes" else "no" # ✅ CORRECT
8. With Expression 🎯
The with expression brings attribute set members into scope:
let
pkgs = { gcc = "11.0"; python = "3.9"; };
in
with pkgs; [ gcc python ]
# Result: [ "11.0" "3.9" ]
Without with, you'd write:
[ pkgs.gcc pkgs.python ]
⚠️ Warning: Overuse of with can make code harder to understand. Use sparingly!
9. Inherit Keyword 🔗
The inherit keyword copies attributes from outer scope:
let
x = 1;
y = 2;
in
{ inherit x y; z = 3; }
# Equivalent to: { x = 1; y = 2; z = 3; }
Inherit from another set:
let
old = { a = 1; b = 2; };
in
{ inherit (old) a; c = 3; }
# Result: { a = 1; c = 3; }
10. Lazy Evaluation 🦥
Nix is lazily evaluated—expressions are only computed when needed:
let
expensive = throw "This would crash!";
result = 42;
in
result # Result: 42 (expensive never evaluated!)
Infinite recursion is fine if you don't access problematic values:
rec {
a = b; # Would be circular...
b = a;
c = 5; # ...but c is safe to access
}.c # Result: 5
💡 Practical Impact: Nix can handle huge package sets (like nixpkgs with 80,000+ packages) efficiently because it only evaluates what you actually use.
Examples 🔬
Example 1: Simple Package Description
Let's build a basic package description:
let
name = "hello";
version = "2.12";
in
{
pname = name;
inherit version;
src = ./hello-${version}.tar.gz;
description = "A program that prints 'Hello, World!'";
buildInputs = [ "gcc" "make" ];
}
Breaking it down:
- Define variables in
letblock - Create attribute set with package metadata
- Use
inheritto copyversion(shorthand forversion = version;) - String interpolation in
srcpath - List of build dependencies
Result (conceptual output):
{
pname = "hello";
version = "2.12";
src = ./hello-2.12.tar.gz;
description = "A program that prints 'Hello, World!'";
buildInputs = [ "gcc" "make" ];
}
Example 2: Function with Default Arguments
A function to create standardized package metadata:
{ name
, version
, license ? "MIT"
, platforms ? [ "linux" "darwin" ]
}:
{
inherit name version license;
meta = {
inherit platforms;
description = "Package ${name} version ${version}";
};
}
Usage:
makePackage {
name = "myapp";
version = "1.0.0";
# license and platforms use defaults
}
Result:
{
name = "myapp";
version = "1.0.0";
license = "MIT";
meta = {
platforms = [ "linux" "darwin" ];
description = "Package myapp version 1.0.0";
};
}
Example 3: Conditional Build Configuration
Adjusting build settings based on conditions:
let
isDevelopment = true;
baseConfig = {
name = "myapp";
version = "1.0.0";
};
in
baseConfig // (if isDevelopment then {
enableDebugging = true;
optimizationLevel = 0;
extraFlags = [ "-g" "-Wall" ];
} else {
enableDebugging = false;
optimizationLevel = 3;
extraFlags = [ "-O3" ];
})
Key concept: The // operator merges attribute sets (right side overwrites left on conflicts).
Development mode result:
{
name = "myapp";
version = "1.0.0";
enableDebugging = true;
optimizationLevel = 0;
extraFlags = [ "-g" "-Wall" ];
}
Example 4: Recursive Package with Dependencies
A package that references other packages in the same set:
rec {
lib = {
version = "1.0";
source = ./lib-src;
};
app = {
version = "2.0";
dependencies = [ lib ]; # References lib from same set
source = ./app-src;
};
allPackages = [ lib app ];
# Computed property
packageCount = builtins.length allPackages; # Result: 2
}
Why rec? Without it, app couldn't reference lib, and allPackages couldn't reference either.
Common Mistakes ⚠️
1. Forgetting Semicolons in Attribute Sets
## ❌ WRONG
{
name = "foo"
version = "1.0"
}
## ✅ CORRECT
{
name = "foo";
version = "1.0";
}
2. Using Commas in Lists
## ❌ WRONG
[ 1, 2, 3 ]
## ✅ CORRECT
[ 1 2 3 ]
3. Forgetting rec for Self-Reference
## ❌ WRONG (x is not in scope for y)
{
x = 10;
y = x + 5; # ERROR: undefined variable 'x'
}
## ✅ CORRECT
rec {
x = 10;
y = x + 5;
}
4. Missing else Branch
## ❌ WRONG
if condition then "yes"
## ✅ CORRECT
if condition then "yes" else "no"
5. Wrong String Interpolation Syntax
## ❌ WRONG
"Hello, #{name}" # Ruby/JavaScript style
"Hello, {name}" # Python f-string style
## ✅ CORRECT
"Hello, ${name}" # Nix style
6. Trying to Reassign Variables
## ❌ WRONG (Nix is immutable)
let
x = 5;
x = 10; # ERROR: attribute 'x' already defined
in
x
## ✅ CORRECT (use different names)
let
x = 5;
y = 10;
in
y
7. Using Parentheses for Single-Argument Functions
let
addOne = x: x + 1;
in
addOne(5) # ❌ WRONG (tries to call addOne with a function argument)
addOne 5 # ✅ CORRECT
Key Takeaways 🎯
- Everything is an expression that evaluates to a value
- No side effects: Pure functional language for reproducible builds
- Lists use spaces, not commas:
[ 1 2 3 ] - Attribute sets are the primary data structure, like JSON objects
- Functions take one argument, use currying for multiple parameters
let...increates local bindings for theinexpressionrecenables self-referencing in attribute setsinheritcopies attributes from outer scope- Lazy evaluation means code only runs when values are needed
- String interpolation uses
${}syntax
📋 Quick Reference Card
| Syntax | Meaning | Example |
|---|---|---|
let x = 1; in x | Local binding | Result: 1 |
{ a = 1; b = 2; } | Attribute set | Object/dict |
[ 1 2 3 ] | List | Space-separated |
x: x + 1 | Function | Lambda |
"${x}" | Interpolation | Insert value |
rec { x = 1; y = x; } | Recursive set | Self-reference |
inherit x; | Copy attribute | x = x; |
if c then a else b | Conditional | Both branches required |
a // b | Merge sets | b overwrites a |
with x; [ a b ] | Bring into scope | Access x.a, x.b |
📚 Further Study
- Official Nix Language Guide: https://nix.dev/tutorials/nix-language
- Nix Pills Series (deep dive into Nix concepts): https://nixos.org/guides/nix-pills/
- NixOS Manual - Nix Expression Language: https://nixos.org/manual/nix/stable/language/
🎓 Next Steps: Practice writing simple Nix expressions, explore the nixpkgs repository, and start building your first derivations!