Nix language: functions

Updated

July 30, 2026

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              # 5

Application is juxtaposition

f x        # call f with x
f x y      # (f x) y if curried
f { a = 1; }  # call with one attrset arg

Parenthesize 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)  # 8

Identity and composition (lightweight)

let
  id = x: x;
  compose = f: g: x: f (g x);
in
  compose (x: x + 1) (x: x * 2) 3  # 7

Theory 2 — Attrset pattern arguments (the Nix idiom)

Almost every public Nix API looks like:

{ a, b }: a + b

Call 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=10

Order 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 absent

Theory 4 — Ellipsis ... (allow unknown attributes)

{ a, b, ... }: a + b

Call with extras:

({ a, b, ... }: a + b) { a = 1; b = 2; c = 99; }  # 3 — c ignored

Without ...:

({ 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 position

Why @ exists

  1. Forward the full attrset to another function
  2. Inspect extras
  3. Merge overrides
let
  f = { a, b ? 0, ... }@args:
    g (args // { b = b + 1; });
in
  f

Classic 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  # 12

enableIf-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; }
# → 12

Example 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/functions

Create 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 + b
nix 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; }'   # 3

Journal: 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:

  1. Hostname + admin user (via mkHost)
  2. Conditionally enabled services.demo with enableIf
  3. Merged package string lists
nix eval --file ./mini-host.nix

Lab 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.hello

Open any small module online or local nixpkgs checkout later—note the header:

{ config, lib, pkgs, ... }:

Map each binder to this chapter’s theory.


Exercises

  1. Write curried add and attr-pattern add; call both.
  2. Implement mkUser with defaults for shell and groups.
  3. Show that passing retries = null does not use the ? default.
  4. Build a strict validator function that rejects unknown keys (no ...).
  5. Use @ to log (via returned attrset) all keys passed to a function.
  6. Write map/filter pipeline over a list of host attrsets.
  7. Sketch an overlay final: prev: … that renames nothing—just returns {}.
  8. Translate a Python def f(a, b=1, **kw) mental model into Nix patterns.
  9. Break and fix parentheses around lambdas in a list: [ (x: x) ].
  10. Read outputs = { self, nixpkgs, ... }: and rewrite with @inputs.
  11. Implement enableIf and contrast with raw if for empty attrsets.
  12. Document three error messages you triggered and their causes.
  13. Commit the functions lab directory.
  14. 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 / enableIf style 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