Attribute Sets and Recursive Definitions
Attribute Sets and Recursive Definitions
Attrsets are how Nix holds config. The boring default is: plain { }, merge with //, read with . and or, and use rec only when siblings need each other and you will not // over them later.
Mental model
{ host = "127.0.0.1"; port = 8080; }| Op | Role |
|---|---|
s.port |
Access; missing → error |
s.timeout or 30 |
Default if missing |
a // b |
Shallow merge; right wins |
rec { x = 1; y = x; } |
Siblings see each other |
desk.db.port = 5432; in a let bind is nested sugar in some positions (nested in rec / let via x.y =). Prefer nested { desk.db = { port = 5432; }; } when learning.
Worked examples
Case 1: Merge and or
Save as attr_operations.nix:
# attr_operations.nix
let
defaultConfig = {
host = "127.0.0.1";
port = 8080;
workers = 4;
};
productionOverrides = {
port = 443;
workers = 16;
};
finalConfig = defaultConfig // productionOverrides;
in
{
inherit finalConfig;
customTimeout = finalConfig.timeout or 30;
}nix eval --file attr_operations.nix --jsonOutput:
{"customTimeout":30,"finalConfig":{"host":"127.0.0.1","port":443,"workers":16}}
Case 2: rec for sibling URLs
Save as rec_demo.nix:
# rec_demo.nix
rec {
domain = "desk.corp";
authService = "https://auth.${domain}";
billingService = "https://billing.${domain}";
endpoints = [ authService billingService ];
}nix eval --file rec_demo.nix --apply 'x: x.authService'Output:
"https://auth.desk.corp"
A let around the same names is often clearer than rec. Use rec when the set is the value you export.
Case 3: // does not update rec dependents
Save as rec_override.nix:
# rec_override.nix
let
base = rec { x = 1; y = x + 1; };
updated = base // { x = 10; };
in
{
inherit (updated) x y;
}nix eval --file rec_override.nix --jsonOutput:
{"x":10,"y":2}
y is still 2. It closed over the original x. If overrides must recompute, use a function:
make = { x }: rec { inherit x; y = x + 1; };
make { x = 10; } # y = 11Case 4: Missing attr without or
nix eval --file attr_operations.nix --apply 'x: x.finalConfig.timeout'Output (shape):
error: attribute 'timeout' missing
Production config: or at the edges, required keys in the module types (part 6).
Case 5: // is shallow (again)
# nest.nix
let
a = { tls = { enable = true; port = 443; }; };
b = { tls = { enable = false; }; };
in
a // bnix eval --file nest.nix --jsonOutput:
{"tls":{"enable":false}}
port vanished. Rebuild tls = a.tls // b.tls or lib.recursiveUpdate.
# nest-fix.nix
let
lib = import <nixpkgs/lib>;
a = { tls = { enable = true; port = 443; }; };
b = { tls = { enable = false; }; };
in
lib.recursiveUpdate a bnix eval --impure --file nest-fix.nix --json{"tls":{"enable":false,"port":443}}
recursiveUpdate is still not the module system (no mkForce). Use it for data. Use modules for NixOS config.
The trap
The trap is rec plus // and expecting dependents to move. Case 3. Prefer let + function.
The other trap is s.foo.bar when foo might be missing — error on foo, not on bar. s.foo.bar or 1 does not save you; or binds the whole s.foo.bar only if the syntax is (s.foo.bar) or 1 vs s.foo.bar or 1 — in Nix, or is tight on the selection. Write (s.foo or {}).bar or 1 when nested.
The boring rule
.andorfor reads.//for shallow overlays of config.recfor small self-contained sets you will not patch.- Overrides that must recompute → function, not
rec //. - Nested data: merge one level at a time or
recursiveUpdate. - Missing attributes should error in internal code;
orat user-facing edges.
Try this
- Access
finalConfig.invalidwithoutor. - Rewrite
rec_demo.nixaslet domain = …; in { … }withoutrec. - Case 3: replace
//withmake { x = 10; }and confirmy == 11. (s.tls or {}).port or 443on a set withouttls.