attribute sets
the central data structure of nix: what they are and how to use them.
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.
{ 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.
{ 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.
// 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; }
// 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:
// 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...inlocal 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.
recattribute 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; }
withbrings 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.
inheritcopies 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;.
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.