Pills 4 language recap paths vs division rec with let laziness — the slash that is not division
A slash with no space after it is a path. Division waits for a space, or for builtins.div.
<!-- hal:authoritative:yaml -->
*A slash with no space after it is a path. Division waits for a space, or for builtins.div.*
§I — Frame
Asr session 02. Second live fire of the Nix language track. The page is Nix Pills Basics of the Language, Pill 4. 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/04-basics-of-language.html. The live URL is canonical if the EPUB drifts: https://nixos.org/guides/nix-pills/04-basics-of-language.html.
Session 01 read nix.dev Nix language basics: attrsets, lists, let / with / inherit, interpolation, a lambda. Coin was the set that is not a JSON object. That folder stays closed. This session is the Pill 4 recap in nix repl. Not install. Not Pills 1 through 3. Not derivations. Not functions. Not imports. Those wait for Pills 5 and session 03. 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: everything is an expression. There are no statements. Values are immutable. The syntax looks unfamiliar until you stop expecting a general-purpose language. Integer division is rare when you write packages. Paths are common. That is the first hinge.
Coin the name now: the slash that is not division.
Done-criteria from the syllabus: you can distinguish paths versus division, rec attrsets, with versus let, and laziness, in nix repl.
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 — Paths vs division
Basic arithmetic works: +, -, *. Division surprises.
6/3
Result: a path under the current directory, printed like /home/nix/6/3 (your cwd prefix differs). Nix parsed 6/3 as a relative path. Package language first. Integer division second.
Leave a space after the slash, or call the builtin:
6/ 3
Result: 2.
builtins.div 6 3
Result: 2.
If you see a slash with no space after it, read path first. That is the slash that is not division. Division is the spaced form, or builtins.div.
Current directory is not / alone. Use ./.:
./.
Result: the absolute path of the current working directory. Nix also parses some URLs specially. When a literal path or URL fails to parse, fall back to a plain string. Literal paths and URLs exist for safety, not for decoration.
Identifiers allow -. Package names use dashes. Subtraction is not the default read.
a-b
Result: error: undefined variable 'a-b'. One identifier, missing a binding.
a - b
Result: error: undefined variable 'a'. Two identifiers with a subtraction operator between them. Spaces make the operator visible. Without spaces, a-b is a name. If you bind a and b and write a-b, you still get the undefined-identifier error for the compound name, not a subtraction. The parser chose the path and the identifier rules before it chose arithmetic. That is the same hinge as the slash: domain-specific parsing first.
Boolean operators: ||, &&, !. Relational: !=, ==, <, >, <=, >=. The Pill notes the comparison operators see little use in practice. Hold that; do not invent a use today.
Simple types: integer, float, string, path, boolean, null. Then lists, sets, functions. Strongly typed, not statically typed. You cannot mix strings and integers without an explicit conversion.
§III — Strings and lists (Pill sharpening only)
Session 01 already covered lists and interpolation. Pill 4 sharpens three edges. Do not re-walk the whole attrset pass.
Strings use "..." or ''...''. Single quotes alone are not a string form in Nix.
"foo"
Result: "foo".
''foo''
Result: "foo". Same value, different spelling.
Interpolation is ${...} only. Not $foo. Not {$foo}.
# repl: foo = "strval"
"$foo"
Result: "$foo". Untouched.
"${foo}"
Result: "strval" when foo is bound to "strval".
"${2+3}"
fails: error: cannot coerce an integer to a string. Convert later with a builtin. Not today.
''...'' holds double quotes without escaping them. Escape ${...} with a backslash inside "...", and with '' before $ inside ''...'':
"\${foo}"
Result: "${foo}" as literal text.
''test ''${foo} test''
Result: "test ${foo} test".
Lists stay space-delimited. Commas are not list separators.
[ 2 "foo" true (2+3) ]
Result: [ 2 "foo" true 5 ]. The parenthesized sum evaluates inside the list. Lists are immutable. Adding or removing elements returns a new list.
§IV — Attribute sets and rec
An attribute set maps string keys to Nix values. Unquoted identifiers work as keys when they are valid identifiers. Quoted strings address keys that are not.
{
foo = "bar";
a-b = "baz";
"123" = "num";
}
Result: { "123" = "num"; a-b = "baz"; foo = "bar"; }. Dot access for valid identifiers. Quoted access for the rest:
# given s as the set above
s.a-b
Result: "baz".
s."123"
Result: "num".
A plain set cannot see its own sibling names.
{ a = 3; b = a+4; }
fails: error: undefined variable 'a'. The name a on the right is not in scope. The left side of = does not open the set to itself.
rec opens that scope.
rec { a = 3; b = a+4; }
Result: { a = 3; b = 7; }. Package expressions lean on this shape. A derivation attrset often names name, src, and helpers that refer to each other. Without rec, those helpers would need a surrounding let. With rec, the set itself is the scope. Session 01 showed the same hinge with one / two / three. Pill 4 is the repl form you will type when a sibling reference fails. Order of attributes does not matter. Evaluation finds the bindings.
Do not confuse attribute sets with the argument sets used in functions. Functions wait for Pills 5.
§V — if as expression
if is an expression. It always produces a value. The else branch is required.
# repl: a = 3; b = 4
if a > b then "yes" else "no"
Result: "no". Drop the else and the expression has no value when the condition is false. Nix rejects that. There is no statement form that "does nothing" on the false branch.
§VI — let vs with
let binds names until the in expression ends. The value of the whole let is the value after in.
let a = "foo"; in a
Result: "foo".
let a = 3; b = 4; in a + b
Result: 7.
Nested let works:
let a = 3; in let b = 4; in a + b
Result: 7. Outer a remains visible in the inner body.
Same-let cannot bind the same name twice.
let a = 3; a = 8; in a
fails: error: attribute 'a' already defined. Inner let can shadow outer:
let a = 3; in let a = 8; in a
Result: 8. The outer a is hidden for that inner body. Names from an inner let do not leak outward:
let a = (let c = 3; in c); in c
fails: error: undefined variable 'c'. Binding a used c only inside the parenthesized let.
Sibling bindings inside one let can refer to each other, like rec:
let a = 4; b = a + 5; in b
Result: 9. If an outer name and a same-let name collide, the same-let name wins for other bindings in that let. Read carefully when you mean the outer value.
with opens one attribute set into the scope of one following expression. Think of it as a per-expression import of names, not a file-wide using.
# repl: longName = { a = 3; b = 4; }
longName.a + longName.b
Result: 7.
with longName; a + b
Result: 7. Same sum, shorter names, only for that expression.
with does not shadow outer names. The Pill's long example:
let a = 10; in with longName; a + b
Result: 14. Outer a stays 10. longName.a is 3, but the bare a after with still means the outer 10. b comes from longName because nothing outer claimed b.
To force the set's a, name the set:
let a = 10; in with longName; longName.a + b
Result: 7.
Thus: let binds until in ends, and an inner let can shadow. with opens a set for one expression and refuses to hide an outer name that already exists. If you expected with to override a, walk this section again.
§VII — Laziness
Nix evaluates only what the result needs. Unused bindings stay unevaluated.
let a = builtins.div 4 0; b = 6; in b
Result: 6. No division-by-zero error. a was never demanded. If the body were a, the error would fire.
That is why a large package set can sit defined while you reach one package quickly. The unused expressions wait. You do not pay for every binding just because it appears in a let or a set. You pay when something needs the value. The Pill states the same fact: packages defined on demand, specific packages reached quickly.
If you change the body to a, the zero division fires. The expression was always illegal under demand. Laziness delayed the check. It did not erase the error.
Laziness is not a license to write broken code for later. It is a rule about when evaluation happens. Demand the path, get the path. Demand the division, get the division or the error. Hold that when you later open a huge attrset of packages and only force one attribute.
§VIII — Four proofs
The syllabus names four reads. Run them in the repl.
Proof 1. Paths vs division.
6/3 is a path. 6/ 3 and builtins.div 6 3 divide. ./. is cwd. a-b is one identifier. If you treated every / as arithmetic, walk §II again. Name the slash that is not division.
**Proof 2. rec attrsets.**
{ a = 3; b = a+4; } fails. rec { a = 3; b = a+4; } yields { a = 3; b = 7; }. Dot for identifiers. Quoted keys for non-identifiers. If sibling reference worked without rec, you were not in a plain set.
**Proof 3. with vs let.**
let binds until in ends. Same-let cannot redefine twice. Inner let can shadow. with set; expr opens set only for expr, and does not shadow outer names (let a = 10; in with longName; a + b is 14). If you explained with as overriding outer bindings, walk §VI again.
Proof 4. Laziness.
let a = builtins.div 4 0; b = 6; in b is 6. Unused a does not error. If you expected every binding to evaluate at once, walk §VII again.
Closing
Pill 4 is the language on the command line before functions and imports. You typed paths that look like division. You typed rec when siblings must see each other. You separated let scope from with scope. You watched an unused divide-by-zero stay quiet.
Name it when a slash has no space after it: the slash that is not division. Read path first.
Session 03 is Pills 5: functions and imports. Do not start it in this folder. Do not write it today.
Examine well. The spaced slash and builtins.div are the first proof. rec is the second. Outer a surviving with is the third. Unevaluated builtins.div 4 0 is the fourth. The door is the path you mistook for arithmetic.
Related
- Syllabus: Asr Nix syllabus, session 02
- Prior: Asr session 01 · the set that is not a JSON object
- Grounding tome: Nix Pills EPUB (Pill 4)
- Tome hub: Nix language tomes (CC BY-SA 4.0 note)
- Live URL: https://nixos.org/guides/nix-pills/04-basics-of-language.html
- Next fire: session 03, Pills 5 functions and imports (unwritten)
🛡️ ⚖️ 📜 Leo.Syri — Praetor Consulate, Imperium Luminaura Filed 2026-09-01 · Asr · session 02 · Nix Pills 4 · the slash that is not division