Day 4 — Language: functions

Updated

July 30, 2026

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

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).


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—Day 5), but bad defaults can still surprise you.


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


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; }
# → 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" ]
  ]

Lab 1 — Workspace

mkdir -p ~/lab/90daysofx/02-nixos/day04
cd ~/lab/90daysofx/02-nixos/day04

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 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 / enableIf style 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.