lazy evaluation
why nix only evaluates what it needs and what that means in practice.
“i'll get to it eventually”
nix is lazy. nothing evaluates until something demands the value.
let x = throw "boom"; in 42
# 42
x is bound to throw "boom". nothing asks for x. the throw never runs.
thunks
x = 1 + 2 does not compute 3 immediately. nix stores a x, nix evaluates the thunk and caches the result. subsequent accesses reuse the cache.
each expression is computed at most once.
nixpkgs
nixpkgs defines over 100,000 packages.
pkgs = import <nixpkgs> {};
nix does not evaluate 100,000 package definitions. it creates an attribute set of thunks. each package is a suspended computation.
pkgs.curl
only now does nix force the curl thunk. that forces curl's dependencies, which force theirs. the tens of thousands of packages you did not ask for are never touched.
without laziness, import <nixpkgs> {} would take minutes and gigabytes. with it, near-instant.
stepping through
step through this expression. watch which bindings get forced:
debug calls builtins.trace. unused calls throw. neither runs. nix only evaluates what the result demands.
lazy attribute sets
each attribute is a separate thunk:
s = { a = 1; b = throw "boom"; }
s.a # 1
accessing s.a does not force s.b. 100,000 attributes in nixpkgs, and you only pay for the ones you touch.
when it bites
error locations
let f = x: { a = x + 1; }; in (f "hello").a
# error: cannot coerce a string to an integer
the error fires when .a is forced, not where f was called. definition site and evaluation site are different. stack traces can be confusing.
infinite recursion
let x = x; in x
# error: infinite recursion encountered
nix detects simple cycles. indirect ones are harder:
let a = { x = b.x; }; b = { x = a.x; }; in a.x
# error: infinite recursion encountered
rec sets are the usual culprit.
forcing evaluation
builtins.seq forces its first argument, returns the second:
builtins.seq (throw "checked") "hello"
# error: checked
seq is shallow. for attribute sets it checks that the set exists but does not force individual attributes. builtins.deepSeq forces everything:
builtins.deepSeq { a = 1; b = throw "deep"; } "ok"
# error: deep
builtins.seq { a = 1; b = throw "deep"; } "ok"
# "ok"
most nix code never touches either.
consequences
laziness is not opt-in. every let binding, every attribute, every function body is a thunk until demanded.
- nixpkgs is fast to import because nothing evaluates upfront
lib.mkIfin nixos modules works because unevaluated branches are never forcedoverrideAttrsis cheap because original attributes are thunks that get replaced, not recomputednix-env -qais slow because listing all packages forces everynameandversionattribute