Nix language: functions
Nix language: functions
Goal: Write and call Nix functions fluently—especially attribute-set pattern arguments, defaults, @-binding, and ellipsis—so flake and module snippets stop looking like magic.
Why this chapter matters
In Nix, packages, shells, overlays, and modules are functions returning values. If you only know x: x + 1, you cannot read nixpkgs. This chapter is the real functional core.
Ops motivation: every flake outputs = { self, nixpkgs, ... }: … and every module { config, lib, pkgs, ... }: … is the same skill. Fluency here turns “paste and pray” into “read and adapt.”
Theory 1 — Everything callable is a lambda
let
inc = x: x + 1;
add = x: y: x + y; # curried
in
add 2 3 # 5Application is juxtaposition
f x # call f with x
f x y # (f x) y if curried
f { a = 1; } # call with one attrset argParenthesize when needed: (f x) + 1, map (x: x + 1) list.
Functions are values
let
ops = {
inc = x: x + 1;
double = x: x * 2;
};
in
ops.double (ops.inc 3) # 8Identity and composition (lightweight)
let
id = x: x;
compose = f: g: x: f (g x);
in
compose (x: x + 1) (x: x * 2) 3 # 7Theory 2 — Attrset pattern arguments (the Nix idiom)
Almost every public Nix API looks like:
{ a, b }: a + bCall site:
({ a, b }: a + b) { a = 1; b = 2; }Destructuring is the point
{ hostName, user }: {
networking.hostName = hostName;
users.users.${user}.isNormalUser = true;
}You name the fields you need. Extra fields are an error unless you allow them (ellipsis—below).
Why not positional for public APIs?
| Positional | Attr pattern |
|---|---|
| Order fragile | Names document call sites |
| Hard to default | ? defaults natural |
| Hard to extend | Ellipsis / optional fields |
Theory 3 — Default arguments with ?
{ force ? false, retries ? 3 }: {
inherit force retries;
}f { } # force=false, retries=3
f { force = true; } # force=true, retries=3
f { retries = 10; } # force=false, retries=10Order of attributes in the pattern does not matter at the call site; names bind by key.
Defaults can be expressions
{ pkgs, system ? builtins.currentSystem }:
# system falls back if omitted
{ inherit pkgs system; }Be careful: defaults that force heavy evaluation run when the binding is used (laziness—the next chapter), but bad defaults can still surprise you.
Explicit null vs missing key
# if you pass force = null, the default is NOT used
# defaults apply only when the attribute is absentTheory 4 — Ellipsis ... (allow unknown attributes)
{ a, b, ... }: a + bCall with extras:
({ a, b, ... }: a + b) { a = 1; b = 2; c = 99; } # 3 — c ignoredWithout ...:
({ a, b }: a + b) { a = 1; b = 2; c = 99; }
# error: unsupported argument 'c'When to use ellipsis
| Situation | Ellipsis? |
|---|---|
| Strict public API | Often no—catch typos |
| Pass-through / forward kwargs | Yes |
| Module-style “rest of config” | Sometimes |
House rule: use ... when forwarding; avoid it when you want misspelled option names to fail loudly.
Theory 5 — @ binding (name the whole set)
args@{ a, b, ... }: {
inherit a b;
allKeys = builtins.attrNames args;
}Or:
{ a, b, ... }@args: … # same idea, different sugar positionWhy @ exists
- Forward the full attrset to another function
- Inspect extras
- Merge overrides
let
f = { a, b ? 0, ... }@args:
g (args // { b = b + 1; });
in
fClassic flake-shaped pattern:
outputs = inputs@{ self, nixpkgs, ... }:
let
system = "x86_64-linux";
pkgs = import nixpkgs { inherit system; };
in {
packages.${system}.default = pkgs.hello;
};You will write this shape in the flakes chapter; understand the inputs@{ … } part now.
Theory 6 — Composition and higher-order helpers
map / filter (builtins)
map (x: x * 2) [ 1 2 3 ]
# [ 2 4 6 ]
builtins.filter (x: x > 1) [ 0 1 2 3 ]
# [ 2 3 ]Function returning function
let
multiply = n: x: n * x;
triple = multiply 3;
in
triple 4 # 12enableIf-style helper (preview of module style)
let
enableIf = cond: value: if cond then value else { };
in
enableIf true { services.foo.enable = true; }Real modules use lib.mkIf—same idea, better merge semantics (host modules chapters).
Theory 7 — Fixed-point awareness (not mastery)
You will see:
lib.fixpoint (self: {
a = 1;
b = self.a + 1;
})Idea: define an attrset in terms of its final form. Overlays and package sets use this heavily. In this chapter: recognize it; do not invent fixed points for homework.
Informal:
self → { a = 1; b = self.a + 1; } → { a = 1; b = 2; }
Theory 8 — Call patterns you will see in the wild
1. Package “callPackage” shape
{ lib, stdenv, fetchurl }:
stdenv.mkDerivation { … }Someone injects dependencies by calling the function with an attrset of packages.
2. Overlay shape
final: prev: {
myPkg = prev.hello.overrideAttrs (old: { … });
}Two arguments, both attrsets of packages (final = after overlays, prev = before this overlay).
3. Module shape (preview)
{ config, lib, pkgs, ... }: {
options = { … };
config = { … };
}Ellipsis is standard—modules receive many arguments.
4. override / overrideAttrs as functions
Packages often expose functions that take functions:
pkgs.hello.overrideAttrs (old: {
pname = old.pname;
})You are passing a function that receives old attrs—same language skill.
Theory 9 — Error messages as teachers
| Error fragment | Likely cause |
|---|---|
called without required argument 'x' |
Missing key in attr call |
unsupported argument 'y' |
Extra key; need ... or remove |
cannot coerce a function to … |
Forgot to call f / f args |
value is a function while a set was expected |
Passed lambda where attrset needed |
| Infinite recursion | Self application without base |
Worked examples bank
Example A — Defaults + ellipsis
let
mkService = { name, port ? 8080, enable ? true, ... }: {
inherit name port enable;
};
in
mkService { name = "api"; host = "ignored-extra"; }Example B — @ forward
let
core = { a, b }: a + b;
wrap = args@{ a, b ? 0, ... }:
core { inherit a; b = b + 1; };
in
wrap { a = 10; b = 1; extra = true; }
# → 12Example C — User skeleton helper
let
mkUser = {
name,
groups ? [ "wheel" ],
keys ? [ ],
}: {
users.users.${name} = {
isNormalUser = true;
extraGroups = groups;
openssh.authorizedKeys.keys = keys;
};
};
in
mkUser {
name = "alice";
keys = [ "ssh-ed25519 AAAA… alice@lab" ];
}Example D — Package list merge helper
let
mergePkgs = lists: builtins.concatLists lists;
in
mergePkgs [
[ "git" "vim" ]
[ "curl" ]
[ "htop" ]
]Example E — Strict API for typos
let
# no ellipsis — typo "hostNme" fails loudly
mkHost = { hostName, user }: {
inherit hostName user;
};
in
mkHost { hostName = "lab"; user = "alice"; }Lab 1 — Workspace
mkdir -p ~/lab/nixos-book/concepts/functions
cd ~/lab/nixos-book/concepts/functionsCreate helpers.nix:
rec {
inc = x: x + 1;
add = { a, b ? 0 }: a + b;
mkHost = { hostName, user, sshPort ? 22, ... }: {
networking = {
inherit hostName;
firewall.allowedTCPPorts = [ sshPort ];
};
users.users.${user}.isNormalUser = true;
};
enableIf = cond: attrs: if cond then attrs else { };
mergeLists = builtins.concatLists;
}Evaluate pieces:
nix eval --file ./helpers.nix --apply 'h: h.add { a = 2; b = 3; }'
nix eval --file ./helpers.nix --apply 'h: h.mkHost { hostName = "lab"; user = "alice"; }'Lab 2 — Strict vs ellipsis
Create strict.nix and loose.nix:
# strict.nix
{ a, b }: a + b# loose.nix
{ a, b, ... }: a + bnix eval --file ./strict.nix --apply 'f: f { a = 1; b = 2; c = 3; }' # should fail
nix eval --file ./loose.nix --apply 'f: f { a = 1; b = 2; c = 3; }' # 3Journal: when would failure be better?
Lab 3 — @ args inventory
# inspect.nix
args@{ a, b ? 1, ... }: {
sum = a + b;
keys = builtins.attrNames args;
raw = args;
}nix eval --file ./inspect.nix --apply 'f: f { a = 5; c = "extra"; }'Confirm keys includes c and default b behavior matches your mental model.
Lab 4 — Compose three helpers into a “mini host”
In mini-host.nix, import/use helpers to produce one attrset that includes:
- Hostname + admin user (via
mkHost)
- Conditionally enabled
services.demowithenableIf
- Merged package string lists
nix eval --file ./mini-host.nixLab 5 — Read real shapes (optional but high value)
If network/cache available:
nix repl:lf nixpkgs
# skim types only — do not deep dive packaging yet
builtins.typeOf pkgs.helloOpen any small module online or local nixpkgs checkout later—note the header:
{ config, lib, pkgs, ... }:Map each binder to this chapter’s theory.
Exercises
- Write curried
addand attr-patternadd; call both.
- Implement
mkUserwith defaults for shell and groups.
- Show that passing
retries = nulldoes not use the?default.
- Build a strict validator function that rejects unknown keys (no
...).
- Use
@to log (via returned attrset) all keys passed to a function.
- Write
map/filterpipeline over a list of host attrsets.
- Sketch an overlay
final: prev: …that renames nothing—just returns{}.
- Translate a Python
def f(a, b=1, **kw)mental model into Nix patterns.
- Break and fix parentheses around lambdas in a list:
[ (x: x) ].
- Read
outputs = { self, nixpkgs, ... }:and rewrite with@inputs.
- Implement
enableIfand contrast with rawiffor empty attrsets.
- Document three error messages you triggered and their causes.
- Commit the functions lab directory.
- Preview: write the module header line from memory.
Common pitfalls
| Symptom | Theory |
|---|---|
called without required argument |
Missing pattern field |
unsupported argument '…' |
Need ... or remove extra |
| Defaults not applied | You passed null explicitly—defaults only for missing keys |
f x y vs f {…} confusion |
Know arity/shape of f |
| Forgetting parentheses around lambdas in lists | [ (x: x) ] not [ x: x ] |
| Trying defaults with positional args | Defaults are for attr patterns |
| Infinite recursion | Self-call without base case |
| Ellipsis hides typos forever | Prefer strict public APIs |
Checkpoint
- Write a curried and an attr-pattern function
- Use
?defaults correctly
- Explain ellipsis trade-off (strict vs loose)
- Use
@to inspect or forward args
- Build
mkHost/enableIfstyle helpers
- Recognize module/overlay/flake call shapes
Further depth (this book)
| Topic | Chapter |
|---|---|
| Lazy evaluation of args | Laziness, imports, and lib |
Flake outputs function |
Flakes and project devShells |
| Module functions in production | Host: Modules as a consumer, Writing custom modules |
callPackage packaging |
Part Packaging |