Functions and Argument Defaults
Functions and Argument Defaults
A Nix function takes one argument. The boring default for desk config is: { name, port ? 8080, ... }: — named attrs, defaults with ?, ... only when extras are allowed.
Mental model
x: x + 1Two “parameters” are currying: x: y: x + y, called as add 1 2. Configuration code should not look like that.
Named args:
{ host, port ? 80 }: "${host}:${toString port}"| Piece | Meaning |
|---|---|
{ host, port } |
Both required |
port ? 80 |
Optional |
... |
Ignore extra attrs |
args@{ host, ... } |
Bind the whole set as args |
Without ..., an unexpected key is an eval error. That is often what you want.
Worked examples
Case 1: Curry vs attrset
Save as functions_demo.nix:
# functions_demo.nix
let
add = x: y: x + y;
formatServer = { host, port ? 80 }: "${host}:${toString port}";
in
{
sum = add 10 25;
defaultPort = formatServer { host = "api.desk"; };
customPort = formatServer { host = "api.desk"; port = 443; };
}nix eval --file functions_demo.nix --jsonOutput:
{"customPort":"api.desk:443","defaultPort":"api.desk:80","sum":35}
Call formatServer with a missing host — error. That is better than "null:80".
Case 2: @ captures extras
Save as capture.nix:
# capture.nix
let
makeProfile = args@{ user, role ? "member", ... }: {
username = user;
userRole = role;
rawInput = args;
};
in
makeProfile { user = "alice"; team = "desk-ops"; }nix eval --file capture.nix --jsonOutput:
{"rawInput":{"team":"desk-ops","user":"alice"},"userRole":"member","username":"alice"}
team survived in rawInput because of ....
Case 3: Unexpected argument without ...
Save as strict_fn.nix:
# strict_fn.nix
let
f = { host }: host;
in
f { host = "desk"; extra = 1; }nix eval --file strict_fn.nixOutput (shape):
error: function 'anonymous lambda' called with unexpected argument 'extra'
Package functions ({ stdenv, fetchurl }:) stay strict so typos fail. Module-shaped { config, pkgs, ... }: uses ... because the module system passes extra keys.
Case 4: secure ? false
Save as url.nix:
# url.nix
let
formatServer = { host, port ? 80, secure ? false }:
let
scheme = if secure then "https" else "http";
p = if secure && port == 80 then 443 else port;
in
"${scheme}://${host}:${toString p}";
in
{
a = formatServer { host = "api.desk"; };
b = formatServer { host = "api.desk"; secure = true; };
}nix eval --file url.nix --jsonOutput:
{"a":"http://api.desk:80","b":"https://api.desk:443"}
Case 5: Apply with --apply
nix eval --file functions_demo.nix --apply 'x: x.customPort'Output:
"api.desk:443"
Functions are values. You can pass them around; you cannot mutate a default after the fact — you call again with a different set.
The trap
The trap is ... on every function so “it never errors.” Typos (prt = 443) silently drop. Use ... at module boundaries. Keep package headers strict.
The other trap is currying five levels (a: b: c: d: e:) for config. Named attrsets.
{ pkgs, ... }@args: keeps extras and names. { pkgs }: rejects unknown keys. { pkgs, lib ? pkgs.lib, ... }: is the usual package/module header. @args is for args.config forwarding, not for ignoring typos — still list the keys you read.
The boring rule
- Config functions take
{ … }. ?for defaults. Required keys stay required....only when extras are part of the contract.@argswhen you must forward the whole set.- Unexpected-argument errors are a feature.
Try this
- Case 4: add
path ? ""and append it when non-empty. - Drop
...from Case 2 and passteam; read the error. nix eval --expr '({ x }: x) { x = 1; y = 2; }'.- Write
id = x: x;andnix eval --expr '(import ./id.nix) 41'after savingid.nixasx: x.