lazy evaluation
why nix only evaluates what it needs and what that means in practice.
“i'll get to it eventually”
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.
x = 1 + 2 does not compute 3 immediately. nix stores a : a pointer to the expression plus the variables in scope. the first time something demands x, nix evaluates the thunk and caches the result. subsequent accesses reuse the cache.
each expression is computed at most once.
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.
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.
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.
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.
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.
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.
laziness is not opt-in. every let binding, every attribute, every function body is a thunk until demanded.
lib.mkIf in nixos modules works because unevaluated branches are never forcedoverrideAttrs is cheap because original attributes are thunks that get replaced, not recomputednix-env -qa is slow because listing all packages forces every name and version attributederivations is where the language meets the store. a derivation is an attribute set that nix knows how to build. store paths, references, functions, and lazy values come together there.