attribute sets

the central data structure of nix: what they are and how to use them.

key-value maps. nix uses them for everything: derivation descriptions, function arguments, package collections, configuration modules. nixpkgs itself evaluates to one.

basics

{ name = "curl"; version = "8.18.0"; }
# { name = "curl"; version = "8.18.0"; }

{ name = "curl"; version = "8.18.0"; }.name
# "curl"

every binding ends with a semicolon. miss one and the parser blames the wrong line.

nesting

{ meta = { description = "a url transfer tool"; }; }.meta.description
# "a url transfer tool"

shorthand for nested keys:

{ meta.description = "a url transfer tool"; meta.license = "mit"; }
# { meta = { ... }; }

meta.description = "..." is sugar for meta = { description = "..."; }. same value.

the merge operator

// merges two attribute sets. same key on both sides: right wins.

{ a = 1; b = 2; } // { b = 3; c = 4; }
# { a = 1; b = 3; c = 4; }
left
pname="hello"
version="1.0"
debug=true
right
version="2.0"
src=./src
left // right
result
pname="hello"
no conflict
version="2.0"
right wins
debug=true
no conflict
src=./src
not in left

shallow merge

// does not recurse into nested sets:

{ meta = { a = 1; b = 2; }; } // { meta = { b = 3; }; }
# { meta = { b = 3; }; }

the entire meta value got replaced. meta.a is gone. toggle between shallow and deep to see the difference:

result
name = "curl"
meta.description = "a url transfer tool" gone
meta.license = "mit" gone
meta.homepage = "https://curl.se"

// replaced the entire meta attribute. description and license are gone.

this bites people constantly in nixos configurations and package overrides. lib.recursiveUpdate or the module system handle the deep case.

? and or

{ a = 1; } ? a            # true
{ a = 1; } ? b            # false
{ a = { b = 1; }; } ? a.b # true

or for default access:

{ a = 1; }.b or 0   # 0
{ a = 1; }.a or 0   # 1

let...in

local bindings:

let
  name = "curl";
  version = "8.18.0";
in
  "${name}-${version}"
# "curl-8.18.0"

bindings can reference each other:

let
  x = 1;
  y = x + 1;
in
  y
# 2

let is an expression. it evaluates to whatever follows in. no mutation, no reassignment.

rec

attribute sets cannot normally reference their own keys:

{ x = 1; y = x + 1; }
# error: undefined variable 'x'

rec enables it:

rec { x = 1; y = x + 1; }
# { x = 1; y = 2; }

use it sparingly. rec makes evaluation order less obvious and opens the door to infinite recursion. let...in is almost always clearer:

let x = 1; in { x = x; y = x + 1; }

with

brings all attributes of a set into scope:

with { a = 1; b = 2; }; a + b
# 3

the common use:

buildInputs = with pkgs; [ openssl zlib curl ];

# instead of:
buildInputs = [ pkgs.openssl pkgs.zlib pkgs.curl ];

with does not shadow existing bindings:

let a = 99; in with { a = 1; }; a
# 99

the let binding wins. with is convenience, not an override mechanism.

inherit

copies a binding from the surrounding scope:

let name = "curl"; in { inherit name; version = "8.18.0"; }
# { name = "curl"; version = "8.18.0"; }

inherit name; is sugar for name = name;. inherit from a specific set:

let pkg = { name = "curl"; version = "8.18.0"; }; in { inherit (pkg) name version; }
# { name = "curl"; version = "8.18.0"; }

inherit (pkg) name version; is name = pkg.name; version = pkg.version;.

why this matters

derivations are attribute sets. mkDerivation takes one, adds defaults, returns one. overriding a package means merging with //. the nixpkgs override system (overrideAttrs, override) is built on that.

// as shallow merge. let as the primary binding form. with as a convenience scope opener. those three get you through most real nix code.

functions are the other half: single-argument lambdas, currying, and the pattern matching syntax that shows up in every nix file.