Shubham Singh

Nix Language Overview

Oct 17, 2025 (2m ago)31 views

These are my notes while learning the Nix expression language and understanding how it powers reproducible package management.

Understanding the Nix Language

The Nix language is a pure, lazy, functional programming language designed specifically for describing packages and system configurations. Unlike imperative languages, Nix expressions describe what to build, not how to build it.

Core Philosophy

Basic Data Types and Values

Primitive Types

Nix supports several fundamental data types:

# Numbers (integers and floats)
t = 10 * 2 / 3;

# Booleans
a = toString false;  # "0"
b = toString true;   # "1"
e = ! true;          # false

# Null
foo = null;

# Strings
inter = let name = "shubh"; in "Hello ${name}";

String Interpolation

Nix provides powerful string interpolation using ${} syntax:

int = let
  a = 10;
  b = 20;
in "Hi ${toString (a + b)}";  # "Hi 30"

Important: Non-string values must be converted using toString before interpolation.

Multi-line Strings

Nix supports indented multi-line strings using '':

x = let
  a = ''
    fuck my
    life
    hahaha
  '';
in a;

The indentation is automatically stripped based on the least indented line.

Attribute Sets: The Foundation of Nix

Attribute sets are Nix's primary data structure - similar to dictionaries or objects in other languages.

Basic Syntax

{
  foo = null;
  "foo.bar" = null;  # quoted keys allow special characters
  fooz.bar = null;   # nested attribute
}

Nested Attributes

fooz.bar = null;

# Equivalent to:
fooz = {
  bar = null;
};

Accessing Attributes

c = let
  foo = {
    bar.a = 10;
    bar.b = 20;
  };
in foo.bar.a;  # 10

Default Values with or

Provide fallback values when attributes don't exist:

d = let
  foo = {
    bar.a = 10;
    bar.b = 20;
  };
in foo.bar.e or 50;  # 50 (since 'e' doesn't exist)

Checking Attribute Existence

Use the ? operator to test if an attribute exists:

k = let
  rd = {x, y, ...}@attrs:
    x + y + (if attrs ? ignored then attrs.ignored else 0);
in rd {x = 1; y = 5; ignored = 4;};  # 10

Let Expressions: Local Scope

Let expressions create local bindings that are only available within their scope.

Basic Syntax

variables = let
  a = 10;
in a + 1;  # 11

Multiple Bindings

int = let
  a = 10;
  b = 20;
in "Hi ${toString (a + b)}";

Key Insight: Order doesn't matter in let expressions due to lazy evaluation:

m = 10;
n = toString o;  # Can reference 'o' before it's defined
o = 4;

Functions: The Heart of Nix

Functions in Nix are first-class values and follow a curried pattern.

Basic Function Syntax

somefn = x: x + 1;

Currying and Multiple Arguments

f = let
  sfn = x: y: x + y;
in sfn 1 2;  # 3

How it works: sfn is actually a function that returns another function:

Pattern Matching with Attribute Sets

Functions can destructure attribute sets in their parameters:

g = let
  rd = {x, y}: x + y;
in rd {x = 1; y = 2;};  # 3

Default Arguments

Provide default values for missing attributes:

h = let
  rd = {x, y ? 5, ...}: x + y;
in rd {x = 1; ignore = 4;};  # 6 (y defaults to 5)

The ... allows extra attributes to be passed without error.

The @ Pattern

Capture the entire attribute set while also destructuring:

i = let
  rd = attrs@{x, y}: x + attrs.y;
in rd {x = 1; y = 2;};  # 3

This gives you both the individual attributes AND the whole set.

Complete Pattern Example

l = let
  rd = {x, y, ...}@attrs:
    x + y + attrs.ignored or 0;
in rd {x = 1; y = 5; ignored = 4};  # 10

This pattern:

Conditional Expressions

Nix supports standard if-then-else expressions:

j = if 1 + 1 == 3 then "10" else false;  # false

Important: Both branches must be present - there's no standalone if statement.

The with Expression: Scope Extension

The with expression brings all attributes of a set into scope:

p = let
  a = {
    x = 1;
    y = 2;
    z = 3;
  };
in with a; [x y z];  # [1 2 3]

Practical Usage

ee = let
  names = {fn = "shubham"; ln = "singh";};
in with names; {
  a = fn;  # No need for names.fn
  b = ln;  # No need for names.ln
};

String Interpolation with with

u = let
  a = {name = "shubham"; greeting = "How are you?";};
in with a; "Hello ${name}! ${greeting}";

The inherit Keyword: Reducing Boilerplate

The inherit keyword copies attributes from one set to another or from the surrounding scope.

Basic Inheritance

q = let
  x = 1;
  y = 2;
in {
  inherit x y;  # Same as: x = x; y = y;
};

Inheriting from Specific Sets

r = let
  a = {b = 1; c = 2;};
  m = {n = 3; o = 4;};
in {
  inherit (a) b;  # Same as: b = a.b;
  inherit (m) n;  # Same as: n = m.n;
};

Inherit in Let Expressions

s = let
  inherit ({x = 1; y = 3;}) x y;
  # Shorthand for:
  # a = {x = 1; y = 3};
  # inherit (a) x y;
in [x y];  # [1 3]

Lists: Ordered Collections

Lists in Nix are space-separated values enclosed in square brackets:

builtfns = let
  a = builtins.map (x: x + 1) [1 2 3];
in a;  # [2 3 4]

Lists are commonly used with with to destructure attribute sets:

p = let
  a = {x = 1; y = 2; z = 3;};
in with a; [x y z];  # [1 2 3]

Paths: File System References

Paths in Nix are first-class values that reference files and directories.

Path Types

# Absolute paths (start with /)
/etc/nixos/configuration.nix

# Relative paths (contain / but don't start with it)
./default.nix
../parent/file.nix

# Path interpolation
v = let
  x = "result";
in ./${x};  # ./result

Paths and the Nix Store

When you use a path in a string context, Nix copies it to the store:

# Creates a path in the nix store using content hash + filename
cc = "${./data.nix}";  # "/nix/store/hash-data.nix"

# Directories are copied entirely
dd = "${./result}";  # "/nix/store/hash-result"

Why this matters: This ensures reproducibility - the exact file contents are captured by the hash.

Built-in Functions

Nix provides many built-in functions accessible via builtins:

Common Built-ins

builtins.map (x: x + 1) [1 2 3]  # [2 3 4]
builtins.toString 123            # "123"
builtins.fetchTarball "url"      # Downloads and unpacks
builtins.currentSystem           # "x86_64-linux"

Fetchers

Nix includes several functions for downloading content:

zz = let
  idk = fetchTarball "https://github.com/NixOS/nixpkgs/archive/master.tar.gz";
  pkgs = import idk {};
in pkgs.lib.strings.toUpper "i wish you loved me";

Nixpkgs: The Package Repository

Nixpkgs is a massive repository of package definitions written in Nix.

Importing Nixpkgs

# Using angle bracket syntax (searches NIX_PATH)
pkgs = import <nixpkgs> {};

# Using fetchTarball for pinned versions
nixpkgs = builtins.fetchTarball "https://github.com/NixOS/nixpkgs/archive/master.tar.gz";
pkgs = import nixpkgs {
  system = builtins.currentSystem;
  config = {};
  overlays = [];
};

Using Nixpkgs Libraries

z = let
  pkgs = import <nixpkgs> {};
in pkgs.lib.strings.toUpper "i wish you loved me";

Function Arguments Pattern

Common pattern for functions that depend on nixpkgs:

bb = let
  lib = (import <nixpkgs> {}).lib;
  upperCase = {lib, lower, ...}: lib.strings.toUpper lower;
in upperCase {inherit lib; lower = "hi babies";};

Derivations: The Build Instructions

Derivations are the fundamental building blocks in Nix - they describe how to build something.

Basic Derivation

derivation = let
  smder = builtins.derivation {
    name = "test";
    builder = "/bin/sh";
    system = "x86_64-linux";
  };
in smder;

Standard Derivation with stdenv

The standard environment (stdenv) provides common build tools:

{lib, stdenv, fetchurl}:

let
  pname = "hello";
  version = "2.12";
in
stdenv.mkDerivation {
  pname = pname;
  version = version;
  
  src = fetchurl {
    url = "mirror://gnu/${pname}/${pname}-${version}.tar.gz";
    sha256 = "1ayhp9v4m4rdhjmnl2bq3cibrbqqkgjbl3s7yk2nhlh8vj3ay16g";
  };
  
  meta = with lib; {
    license = licenses.gpl3Plus;
  };
}

Key Components:

Shell Environments: Development Shells

Nix can create isolated development environments with specific tools.

Creating a Shell Environment

{ pkgs ? import <nixpkgs> {} }:

let
  message = "hello world";
in
pkgs.mkShellNoCC {
  packages = with pkgs; [ cowsay ];
  
  shellHook = ''
    cowsay ${message}
  '';
  
  # Custom environment variables
  SHUBHAM = "SHUBHAM";
}

Using Shell Environments

# Using specific file
nix-shell shellenv.nix

# Default (looks for shell.nix)
nix-shell

What happens: Nix creates a shell with:

Importing Files: Code Organization

Nix files can import other Nix files to organize code.

Basic Import

# function.nix
let
  f = x: y: x + y;
in f

# default.nix
y = import ./function.nix 1 2;  # 3

The imported file's final value becomes the import's value.

Importing Attribute Sets

# honey.nix
let
  a = 4;
  b = {
    fn = "Prasoon";
    ln = "Kumar";
    md = "";
  };
in with b; "${fn} ${md} ${ln}"

# Using in another file
result = import ./honey.nix;

Recursive Attribute Sets

By default, attributes in a set can't reference each other. The rec keyword enables this:

# This FAILS:
{
  a = 1;
  b = a + 1;  # Error: 'a' not in scope
}

# This WORKS:
rec {
  a = 1;
  b = a + 1;  # OK
}

Best Practice: Prefer let expressions over rec when possible for clearer scoping.

Evaluation Commands

Nix provides several commands for working with expressions:

nix-instantiate

Evaluates expressions without building:

# Basic evaluation
nix-instantiate --eval default.nix

# Strict evaluation (forces all lazy values)
nix-instantiate --eval --strict default.nix

# With function arguments
nix-instantiate --eval f.nix --arg a "hello"

nix-build

Builds derivations:

nix-build  # Builds default.nix
nix-build -A myPackage  # Builds specific attribute

Process:

  1. Instantiates the derivation (creates .drv file)
  2. Realizes the derivation (performs actual build)
  3. Creates result symlink to output

Purity and Impurity

Understanding purity is crucial for understanding Nix's guarantees.

Pure Functions

Nix expressions are pure - they don't interact with the outside world during evaluation:

# This is pure
let
  a = 10;
  b = 20;
in a + b

Impure Operations

Impurity happens when:

# These introduce controlled impurity:
builtins.currentSystem      # Depends on host system
builtins.fetchurl "..."     # Network access
import <nixpkgs> {}         # Depends on NIX_PATH

Nix's approach: Isolate impurity to well-defined boundaries (derivation builds, fetchers) while keeping expression evaluation pure.

Common Patterns and Idioms

Package Definition Pattern

{lib, stdenv, dependency1, dependency2}:

stdenv.mkDerivation {
  pname = "mypackage";
  version = "1.0.0";
  
  src = ./.;
  
  buildInputs = [ dependency1 dependency2 ];
  
  meta = with lib; {
    description = "My package";
    license = licenses.mit;
    maintainers = with maintainers; [ myname ];
  };
}

Configuration Pattern

{ config, pkgs, ... }:

{
  # System configuration
  environment.systemPackages = with pkgs; [
    vim
    git
  ];
  
  # Service configuration
  services.nginx.enable = true;
}

Overlay Pattern

Overlays modify nixpkgs:

self: super: {
  myPackage = super.myPackage.overrideAttrs (old: {
    version = "2.0.0";
  });
}

Key Takeaways

Understanding these concepts provides the foundation for working with Nix's package management, NixOS configuration, and building reproducible systems.