Functions and Argument Defaults

Updated

September 12, 2026

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 + 1

Two “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 --json

Output:

{"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 --json

Output:

{"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.nix

Output (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 --json

Output:

{"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.
  • @args when you must forward the whole set.
  • Unexpected-argument errors are a feature.

Try this

  1. Case 4: add path ? "" and append it when non-empty.
  2. Drop ... from Case 2 and pass team; read the error.
  3. nix eval --expr '({ x }: x) { x = 1; y = 2; }'.
  4. Write id = x: x; and nix eval --expr '(import ./id.nix) 41' after saving id.nix as x: x.