Day 4 — Language: functions
Day 4 — Language: functions
Stage I · ~4h (theory-heavy)
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 day exists
In Nix, packages, shells, overlays, and modules are functions returning values. If you only know x: x + 1, you cannot read nixpkgs. Today is the real functional core.
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) # 8Theory 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).
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—Day 5), but bad defaults can still surprise you.
Theory 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 on Day 8; 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.
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. Today: 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.
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" ]
]Lab 1 — Workspace
mkdir -p ~/lab/90daysofx/02-nixos/day04
cd ~/lab/90daysofx/02-nixos/day04Create 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 today’s theory.
Common gotchas
| 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 |
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 call shapes
Commit
cd ~/lab/90daysofx/02-nixos/day04
git init 2>/dev/null || true
git add .
git commit -m "day04: language functions patterns defaults ellipsis"Tomorrow
Day 5 — Laziness, imports & lib. Multi-file expressions, when work actually runs, and a first taste of lib pipelines.