Values and Immutability
Values and Immutability
Once a Nix value exists, nothing mutates it. The boring default is: // and ++ allocate new values; names in a let are bound once; the original attrset is still what you passed in.
Mental model
Python dicts surprise you:
config = {"port": 8080}
mutate(config) # original changedNix:
- Attrsets, lists, and strings do not have setters.
a // bis a new set.ais unchanged.- Memoisation is safe because nothing behind a pointer can change.
{ a = 1; } ──┐
├──► { a = 1; b = 2; } (new)
{ b = 2; } ──┘
first set still { a = 1; }
Worked examples
Case 1: Merge does not rewrite
Save as immutable_merge.nix:
# immutable_merge.nix
let
baseDesk = {
location = "building-a";
tier = "standard";
};
upgradedDesk = baseDesk // {
tier = "executive";
desksCount = 12;
};
in
{
original = baseDesk;
upgraded = upgradedDesk;
}nix eval --file immutable_merge.nix --jsonOutput:
{"original":{"location":"building-a","tier":"standard"},"upgraded":{"desksCount":12,"location":"building-a","tier":"executive"}}
baseDesk.tier is still "standard". Fifty functions can receive baseDesk.
Case 2: Lists concatenate to a new list
Save as immutable_list.nix:
# immutable_list.nix
let
activeTables = [ 1 2 3 ];
extendedTables = activeTables ++ [ 4 5 ];
in
{
initial = activeTables;
result = extendedTables;
}nix eval --file immutable_list.nix --jsonOutput:
{"initial":[1,2,3],"result":[1,2,3,4,5]}
There is no .push.
Case 3: Same name twice is an error
Save as double_bind.nix:
# double_bind.nix
let
x = 1;
x = 2;
in
xnix eval --file double_bind.nixOutput (shape):
error: attribute 'x' already defined at double_bind.nix:3
Use a second name (x0, xNext) or an inner let that shadows.
Case 4: Inner let shadows, outer unchanged
Save as shadow.nix:
# shadow.nix
let
x = 1;
in
{
outer = x;
inner = let x = 2; in x;
}nix eval --file shadow.nix --jsonOutput:
{"inner":2,"outer":1}
Case 5: Deep merge is not //
# shallow.nix
let
a = { desk = { port = 80; host = "a"; }; };
b = { desk = { port = 443; }; };
in
a // bnix eval --file shallow.nix --jsonOutput:
{"desk":{"port":443}}
host is gone. // replaces the desk attr wholesale. Nested merge is lib.recursiveUpdate (imports chapter) or a function that rebuilds the nest. Do not expect // to patch deep keys.
The trap
The trap is let x = 1; x = 2;. The trap after that is assuming // is deep. Both look like assignment in other languages. They are not.
The boring rule
- Values do not change. New names, new values.
//shallow.++new list.- One binding per name per
let. - Shadow in a nested
let, do not reassign. - For nested config, write a function or
lib.recursiveUpdate, not a pile of//.
Try this
nix eval --expr 'let a = { x = 1; }; b = a // { x = 2; }; in a.x'— expect1.- Case 5: add
liblater; for now rebuild{ desk = a.desk // b.desk; }by hand and keephost. - Duplicate a
letname; save the error text. nix eval --expr '[ 1 ] ++ [ 1 ]'— duplicates are allowed; lists are not sets.