key-value maps. nix uses them for everything: derivation descriptions, function arguments, package collections, configuration modules. nixpkgs itself evaluates to one.
{ name = "curl" ; version = "8.18.0" ; }
# { name = "curl"; version = "8.18.0"; }
{ name = "curl" ; version = "8.18.0" ; } . name
# "curl"
every binding ends with a semicolon. miss one and the parser blames the wrong line.
{ meta = { description = "a url transfer tool" ; }; } . meta . description
# "a url transfer tool"
shorthand for nested keys:
{ meta . description = "a url transfer tool" ; meta . license = "mit" ; }
# { meta = { ... }; }
meta.description = "..." is sugar for meta = { description = "..."; }. same value.
// merges two attribute sets. same key on both sides: right wins.
{ a = 1 ; b = 2 ; } // { b = 3 ; c = 4 ; }
# { a = 1; b = 3; c = 4; }
left
pname = "hello"
version = "1.0"
debug = true
// does not recurse into nested sets:
{ meta = { a = 1 ; b = 2 ; }; } // { meta = { b = 3 ; }; }
# { meta = { b = 3; }; }
the entire meta value got replaced. meta.a is gone. toggle between shallow and deep to see the difference:
// (shallow) lib.recursiveUpdate (deep)
result
meta.description = "a url transfer tool" gone
meta.homepage = "https://curl.se" // replaced the entire meta attribute. description and license are gone.
this bites people constantly in nixos configurations and package overrides. lib.recursiveUpdate or the module system handle the deep case.
{ a = 1 ; } ? a # true
{ a = 1 ; } ? b # false
{ a = { b = 1 ; }; } ? a . b # true
or for default access:
{ a = 1 ; } . b or 0 # 0
{ a = 1 ; } . a or 0 # 1
local bindings:
let
name = "curl" ;
version = "8.18.0" ;
in
" ${ name } - ${ version } "
# "curl-8.18.0"
bindings can reference each other:
let
x = 1 ;
y = x + 1 ;
in
y
# 2
let is an expression. it evaluates to whatever follows in. no mutation, no reassignment.
attribute sets cannot normally reference their own keys:
{ x = 1 ; y = x + 1 ; }
# error: undefined variable 'x'
rec enables it:
rec { x = 1 ; y = x + 1 ; }
# { x = 1; y = 2; }
use it sparingly. rec makes evaluation order less obvious and opens the door to infinite recursion. let...in is almost always clearer:
let x = 1 ; in { x = x ; y = x + 1 ; }
brings all attributes of a set into scope:
with { a = 1 ; b = 2 ; }; a + b
# 3
the common use:
buildInputs = with pkgs ; [ openssl zlib curl ] ;
# instead of:
buildInputs = [ pkgs . openssl pkgs . zlib pkgs . curl ] ;
with does not shadow existing bindings:
let a = 99 ; in with { a = 1 ; }; a
# 99
the let binding wins. with is convenience, not an override mechanism.
copies a binding from the surrounding scope:
let name = "curl" ; in { inherit name ; version = "8.18.0" ; }
# { name = "curl"; version = "8.18.0"; }
inherit name; is sugar for name = name;. inherit from a specific set:
let pkg = { name = "curl" ; version = "8.18.0" ; }; in { inherit ( pkg ) name version ; }
# { name = "curl"; version = "8.18.0"; }
inherit (pkg) name version; is name = pkg.name; version = pkg.version;.
derivations are attribute sets. mkDerivation takes one, adds defaults, returns one. overriding a package means merging with //. the nixpkgs override system (overrideAttrs, override) is built on that.
// as shallow merge. let as the primary binding form. with as a convenience scope opener. those three get you through most real nix code.
functions are the other half: single-argument lambdas, currying, and the pattern matching syntax that shows up in every nix file.