Why this matters
Every flake.nix output is a function. Every file in pkgs/by-name/ is a function. callPackage, overlays, modules: functions. Nix functions take exactly one argument, and the attrset-pattern form is what makes { lib, stdenv, ... }: at the top of a package file readable instead of magic.
Concept
One argument, always
x: body. The colon separates parameter from body. Call by juxtaposition, no parentheses:
let f = x: x + 1; in f 12A function printed on its own is opaque: x: x + 1 evaluates to «lambda @ «string»:1:1».
Currying
"Two arguments" is a function returning a function:
let add = a: b: a + b; inc = add 1; in inc 4142add 1 is a partial application. builtins.typeOf (add 1) is "lambda". This is why map (x: x * 2) [ 1 2 3 ] reads as map applied to a function, then to a list.
Attrset patterns
The form you will read most. The argument must be an attrset with exactly these keys:
let f = { a, b }: a + b; in f { a = 1; b = 2; }3Extra keys are an error, and the message names the function and the key:
error: function 'f' called with unexpected argument 'c'Missing keys too:
error: function 'f' called without required argument 'a'Defaults with ?
let f = { a, b ? 10 }: a + b; in f { a = 1; }11Defaults can reference other arguments: { a, b ? a * 2 }: b called with { a = 5; } gives 10.
Allow extras with ...
let f = { a, b, ... }: a + b; in f { a = 1; b = 2; c = 3; }3Without ... this is the "unexpected argument" error. Package files use ... rarely; module functions use it always, because the module system passes many arguments you do not name.
Capture the whole set with @
let f = { a, ... }@args: builtins.attrNames args; in f { a = 1; z = 26; }[ "a" "z" ]args@{ ... } is the same thing with the name first. One subtlety: args is the set as passed, defaults are not included:
let f = { a, b ? 10, ... }@args: args; in f { a = 1; }{ a = 1; }args.b would be attribute 'b' missing. Use the bare b to get the default.
A file can be a function
A .nix file holds one expression. If that expression is a function, import returns the function and you call it:
{ name, greeting ? "Hello" }:
"${greeting}, ${name}!"Save as greet.nix, then:
nix eval --impure --expr 'import ./greet.nix { name = "Dave"; }'"Hello, Dave!"--impure because --expr runs in pure mode and may not read arbitrary paths. Cleaner: nix eval -f greet.nix --apply 'f: f { name = "Dave"; }', same output.
Nix can tell you a function's declared parameters:
nix eval -f greet.nix --apply 'f: builtins.functionArgs f'{ greeting = true; name = false; }true means "has a default". Tooling uses this.
callPackage: automatic argument filling
A package file in nixpkgs looks like this:
{ lib, nodejs, pnpm }:
{
node = nodejs.version;
pnpm = pnpm.version;
label = lib.toUpper "sveltekit";
}Nobody passes lib, nodejs and pnpm by hand. pkgs.callPackage ./mypkg.nix { } reads the pattern with functionArgs, looks each name up in pkgs, and calls the function. The second argument overrides:
nix eval --impure --expr 'let pkgs = import (builtins.getFlake "nixpkgs") {}; in (pkgs.callPackage ./mypkg.nix { nodejs = pkgs.nodejs_22; }).node'"22.23.3"Without the override .node is "24.21.0". This is the entire mechanism behind pkgs/by-name: the function declares what it needs, callPackage supplies it. See Builtins and lib for functionArgs and friends.
Try it
nix eval --expr 'let f = { a, b ? 10, ... }@args: { sum = a + b; passed = builtins.attrNames args; }; in f { a = 1; z = 0; }'{ passed = [ "a" "z" ]; sum = 11; }nix eval --expr 'let f = { a }: a; in f { a = 1; b = 2; }'error: function 'f' called with unexpected argument 'b'Exercise
Write a function mkShellPkgs that takes { node ? "nodejs_24", extra ? [ ] } and returns the list [ node "pnpm" "biome" ] ++ extra. Call it twice: once with no arguments, once with node = "nodejs_22" and extra = [ "turso-cli" ].
Solution
let
mkShellPkgs = { node ? "nodejs_24", extra ? [ ] }: [ node "pnpm" "biome" ] ++ extra;
in
[
(mkShellPkgs { })
(mkShellPkgs { node = "nodejs_22"; extra = [ "turso-cli" ]; })
][ [ "nodejs_24" "pnpm" "biome" ] [ "nodejs_22" "pnpm" "biome" "turso-cli" ] ]mkShellPkgs { } still needs the empty set: a pattern function requires an attrset argument, even if every key has a default.
Trap
f a b where you meant f (a b). Function application is left-associative and binds tighter than everything, so toString 1 + 2 parses as (toString 1) + 2 and fails with "cannot coerce". Any argument that is itself an expression needs parentheses: toString (1 + 2), map (x: x + 1) list, import ./f.nix { }.