You are viewing a preview of this lesson. Sign in to start learning
Back to Hermetic Builds

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

TypeExampleDescription
Integer42Whole numbers
Float3.14Decimal numbers
String"hello"Text in quotes
Path./file.txtFilesystem paths
Booleantrue / falseLogical values
NullnullAbsence 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 let block
  • Create attribute set with package metadata
  • Use inherit to copy version (shorthand for version = version;)
  • String interpolation in src path
  • 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 🎯

  1. Everything is an expression that evaluates to a value
  2. No side effects: Pure functional language for reproducible builds
  3. Lists use spaces, not commas: [ 1 2 3 ]
  4. Attribute sets are the primary data structure, like JSON objects
  5. Functions take one argument, use currying for multiple parameters
  6. let...in creates local bindings for the in expression
  7. rec enables self-referencing in attribute sets
  8. inherit copies attributes from outer scope
  9. Lazy evaluation means code only runs when values are needed
  10. String interpolation uses ${} syntax

📋 Quick Reference Card

SyntaxMeaningExample
let x = 1; in xLocal bindingResult: 1
{ a = 1; b = 2; }Attribute setObject/dict
[ 1 2 3 ]ListSpace-separated
x: x + 1FunctionLambda
"${x}"InterpolationInsert value
rec { x = 1; y = x; }Recursive setSelf-reference
inherit x;Copy attributex = x;
if c then a else bConditionalBoth branches required
a // bMerge setsb overwrites a
with x; [ a b ]Bring into scopeAccess x.a, x.b

📚 Further Study

  1. Official Nix Language Guide: https://nix.dev/tutorials/nix-language
  2. Nix Pills Series (deep dive into Nix concepts): https://nixos.org/guides/nix-pills/
  3. 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!