Pills 5 Functions and Imports nameless lambdas currying argument sets — the scope that does not travel
An imported file does not inherit the importer's bindings. Data travels as arguments, not as ambient names.
<!-- hal:authoritative:yaml -->
An imported file does not inherit the importer's bindings. Data travels as arguments, not as ambient names.
§I — Frame
Asr session 03. Third live fire of the Nix language track. The page is Nix Pills Functions and Imports, Pill 5. Luca Bruno wrote the series. License CC BY-SA 4.0. Ground from the on-disk EPUB at 09-Tomes/Polyglot-Dev/Backend-Stack/Nix/nix-pills.epub, chapter OEBPS/05-functions-and-imports.html. The live URL is canonical if the EPUB drifts: https://nixos.org/guides/nix-pills/05-functions-and-imports.html.
Session 01 read nix.dev Nix language basics. Coin was the set that is not a JSON object. Session 02 was Pill 4 in nix repl: paths versus division, rec, with versus let, laziness. Coin was the slash that is not division. Both folders stay closed. This session is Pills 5. Not install. Not Pills 1 through 3. Not derivations. Those wait for Pills 6 and session 04. Dolstra 2006 stays on the shelf. NAR and store hashing are not in this lesson. Blandy is a Rust tome. Empty-by-scope for Nix. No flakes. No Python wrapping.
The Pill says: functions build reusable pieces in a large repository. Functions are anonymous. They take one parameter. More parameters arrive by returning another function. Argument sets name the inputs. Imports parse a .nix file as an expression. The imported file does not see the importer's let bindings.
Coin the name now: the scope that does not travel.
Done-criteria from the syllabus: you can write a: b:, { a, b ? x }@args, and import ./file.nix without leaking scope.
Launch nix repl when you have Nix. Type expressions. Exit with :q. Help with :?. The repl assignment syntax differs slightly from file syntax. The expressions below match the Pill. If Nix is not on the machine today, read the results here. Do not install Nix in this session.
§II — Nameless single-param lambdas
Functions are anonymous. One parameter. Parameter name, then :, then the body.
x: x*2
Result: «lambda». A value with no name until you store it.
# repl: double = x: x*2
double
Result: «lambda».
double 3
Result: 6.
Call form is space, not parentheses. Write double 3. Do not write double(3). Many other languages put the argument in parentheses. Nix puts a space. That is the call. Name, space, argument. Nothing else.
If you typed double(3) and expected a call, walk this section again. The parentheses form is not the Nix call. Space is the call.
The same rule applies when the argument is itself an expression. double (1+2) is 6. The parentheses group the argument. They do not mark the call. Without them, double 1+2 parses as (double 1) + 2 under Nix priorities, which is 2 + 2 equals 4. Group first when the argument is not a single atom. Then leave the space that means call.
Functions are values. You can put a lambda in a list. You can put a lambda in an attribute set. You can return a lambda from a lambda. Session 01 showed a lambda once. Pill 5 is the call rule and the composition rule. Hold the space.
§III — Multi-param via currying
One parameter only. Two parameters means a function that returns a function.
# repl: mul = a: (b: a*b)
mul
Result: «lambda».
mul 3
Result: «lambda». That returned function is b: 3*b.
(mul 3) 4
Result: 12.
Drop the parentheses. Nix parsing accepts the stacked form:
# repl: mul = a: b: a*b
mul 3 4
Result: 12.
mul (6+7) (8+9)
Result: 221. Complex arguments need parentheses because space separates the call from the argument. In other languages you might write mul(6+7, 8+9). Here each parenthesized sum is one argument.
Partial application follows from the single-parameter rule:
# repl: foo = mul 3
foo 4
Result: 12.
foo 5
Result: 15. mul 3 returned a function. You stored it. You reused it. That is the same hinge as a: b: a*b written without the inner parentheses.
Thus: multi-param is currying. Partial application is free. If you expected a single function that takes two arguments at once without returning a lambda, you were not reading Nix's one-parameter rule.
§IV — Argument sets
An argument set pattern-matches a set in the parameter. Two spellings of the same multiply:
# repl: mul = s: s.a*s.b
mul { a = 3; b = 4; }
Result: 12. One parameter. Access attributes with dots.
# repl: mul = { a, b }: a*b
mul { a = 3; b = 4; }
Result: 12. The pattern requires keys a and b. The body uses those names directly. Call form stays mul { ... } with a space. No mul({ ... }).
Exact attrs. Unexpected and missing both fail.
mul { a = 3; b = 4; c = 6; }
fails: error: anonymous function ... called with unexpected argument 'c'.
mul { a = 3; }
fails: error: anonymous function ... called without required argument 'b'.
Only a set with exactly the attributes the pattern names is accepted. Nothing more. Nothing less. Argument sets are not the same thing as the attribute sets from session 01 and 02. An attribute set is a value. An argument set is a pattern on the left of :. Do not confuse the two.
Named unordered arguments are the gain. You do not memorize call order. You name the keys. Partial application does not work with argument sets. You pass the whole set, not a fragment. Hold both facts.
If you write packages later, the pattern { stdenv, fetchurl, ... }: will look familiar. That is still an argument set. Today you only need the small form. Exact keys. Named keys. No silent extras without ....
Order of keys in the call set does not matter. { b = 4; a = 3; } satisfies { a, b }:. Position is not the contract. Names are the contract.
§V — Defaults, variadic, and the @-pattern
Defaults fill missing keys:
# repl: mul = { a, b ? 2 }: a*b
mul { a = 3; }
Result: 6.
mul { a = 3; b = 4; }
Result: 12.
Variadic ... allows extra keys, but the body still cannot name those extras by bare identifier:
# repl: mul = { a, b, ... }: a*b
mul { a = 3; b = 4; c = 2; }
Result: 12. Attribute c was accepted and then invisible to the body.
Give the whole parameter a name with @ before the set pattern:
# repl: mul = s@{ a, b, ... }: a*b*s.c
mul { a = 3; b = 4; c = 2; }
Result: 24. s is the whole set. Pattern names a and b are also in scope. Extra c is reached through s.c.
The syllabus form { a, b ? x }@args is the same hinge written with the name after the pattern. Both s@{ a, b, ... } and { a, b ? x }@args bind the whole argument set under a name. Prefer the spelling you can type without looking it up twice. The done-criteria want you able to write the form. They do not demand you invent a third spelling.
If you allowed ... and then wrote bare c in the body, walk this section again. Variadic accepts the extra. @ makes it addressable.
§VI — Imports and the scope that does not travel
import is built-in. It parses a .nix file as an expression. Compose by importing files that each hold one expression.
Suppose three files:
a.nix:
3
b.nix:
4
mul.nix:
a: b: a*b
# repl: a = import ./a.nix
# b = import ./b.nix
# mul = import ./mul.nix
mul a b
Result: 12. Each import is an expression value. mul is a function. Call it with space, twice.
The importer's scope does not enter the imported file.
test.nix:
x
let x = 5; in import ./test.nix
fails: error: undefined variable 'x' at test.nix. The let binds x for the in expression. import ./test.nix is that expression. The file itself still sees a bare x with no binding. The importer's let did not travel into the file.
That is the scope that does not travel.
Pass data with functions and argument sets. A fuller test.nix:
{ a, b ? 3, trueMsg ? "yes", falseMsg ? "no" }:
if a > b
then builtins.trace trueMsg true
else builtins.trace falseMsg false
import ./test.nix { a = 5; trueMsg = "ok"; }
Result (when evaluated): a trace: ok line, then true. The file returns a function. You call it with a set. Defaults fill b and falseMsg. builtins.trace takes a message and a value; it prints the message and returns the value. The message appears when the branch is demanded. Laziness from session 02 still applies. If the whole call is never forced, the trace stays quiet.
Thus: import parses. Scope does not leak across the file boundary. Arguments carry data. If you expected let x = 5; in import ./test.nix to make x visible inside test.nix, walk this section again. Name the scope that does not travel.
A file that returns a plain value needs no call after import. import ./a.nix is 3. A file that returns a function needs a call. import ./mul.nix is a lambda. Then import ./mul.nix 3 4 is 12, or assign then call. A file that returns a function of an argument set needs a set after the import: import ./test.nix { a = 5; trueMsg = "ok"; }. The import yields the function. The space and the set are the call. Same call rule as §II.
Do not put ambient let bindings in the importer and hope the file sees them. Put the bindings in the set you pass, or curry them as ordinary parameters. That is how data travels when the scope that does not travel blocks the ambient path.
§VII — Five proofs
The syllabus names three shapes. Stretch them into five reads. Run them in the repl when you have files on disk.
Proof 1. Space is the call.
double = x: x*2 then double 3 is 6. Not double(3). If parentheses were required for the call, walk §II again.
Proof 2. Currying and partial application.
mul = a: b: a*b then mul 3 4 is 12. foo = mul 3 then foo 5 is 15. If two parameters required a single non-curried form, walk §III again.
Proof 3. Exact argument sets.
{ a, b }: a*b accepts { a = 3; b = 4; }. Unexpected c errors. Missing b errors. If extras were silently ignored without ..., walk §IV again.
**Proof 4. Defaults, variadic, @.**
{ a, b ? 2 }: a*b with { a = 3; } is 6. { a, b, ... }: a*b accepts c but cannot name bare c. s@{ a, b, ... }: a*b*s.c reaches c. Write { a, b ? x }@args once without looking it up. If you could not, walk §V again.
Proof 5. Import isolation.
import ./a.nix yields the expression in the file. let x = 5; in import ./test.nix fails when test.nix is bare x. Pass a set into a function returned by the file. If importer scope leaked into the file, you were not in Nix. Name the scope that does not travel.
Closing
Pill 5 is functions and imports before the first derivation. You stored a nameless lambda and called it with a space. You stacked a: b: and reused a partial. You required exact argument sets, then opened defaults and ... and @. You imported files and watched importer scope refuse to enter them. You passed a set into a function the file returned, and watched builtins.trace fire only when that branch was demanded.
Name it when a bare name inside an imported file has no binding from the caller's let: the scope that does not travel. Pass data as arguments. Argument sets and curried parameters are the channels. Ambient importer scope is not.
Session 04 is Pills 6: first derivation. Do not start it in this folder. Do not write it today.
Examine well. Space-as-call is the first proof. Currying is the second. Exact sets are the third. Defaults and @ are the fourth. Import isolation is the fifth. The door is the let that never reached the other file.
Related
- Syllabus: Asr Nix syllabus, session 03
- Prior: Asr session 02 · the slash that is not division
- Prior: Asr session 01 · the set that is not a JSON object
- Grounding tome: Nix Pills EPUB (Pill 5)
- Tome hub: Nix language tomes (CC BY-SA 4.0 note)
- Live URL: https://nixos.org/guides/nix-pills/05-functions-and-imports.html
- Next fire: session 04, Pills 6 first derivation (unwritten)
🛡️ ⚖️ 📜 Leo.Syri — Praetor Consulate, Imperium Luminaura Filed 2026-09-02 · Asr · session 03 · Nix Pills 5 · the scope that does not travel