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
# 42x 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.curlonly 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 # 1accessing 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 integerthe 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 encounterednix detects simple cycles. indirect ones are harder:
let a = { x = b.x; }; b = { x = a.x; }; in a.x
# error: infinite recursion encounteredrec sets are the usual culprit.
forcing evaluation
builtins.seq forces its first argument, returns the second:
builtins.seq (throw "checked") "hello"
# error: checkedseq 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