Hedronite Lesson · Polyglot-Dev / Nix · Mon 2026-08-31

Nix language basics attrsets lists let with inherit — the set that is not a JSON object

A JSON object cannot hold a function. A Nix set can. Read the braces as a set first.

Lesson Class: Asr (Nix language track)
Focus: attrsets · lists · let/with/inherit · string interpolation · a lambda
Code Blocks: clean blocks, explanation in prose
Done-criteria: read attrsets · lists · let/with/inherit · interpolation · a lambda from the tutorial
Grounding: on-disk nix-language.html · live nix.dev canonical · Dolstra/Blandy not cited
The set
Unquoted names. Semicolon pairs. A function can sit in the braces. JSON has no place for that pair.
The name
let binds until in ends. with lasts until its semicolon. inherit x y is x = x, not a clone.
The colon
One argument. Application by juxtaposition. Parentheses when a list would swallow the call.
A JSON object cannot hold a function. A Nix set can. Read the braces as a set first.

<!-- hal:authoritative:yaml -->

A JSON object cannot hold a function. A Nix set can. Read the braces as a set first.

§I — Frame

Asr session 01. First live fire of the Nix language track. The page is nix.dev Nix language basics. Valentin Gagarin wrote it. Silvan Mosberger edited it. License CC BY-SA 4.0. Ground from the on-disk snapshot at 09-Tomes/Polyglot-Dev/Backend-Stack/Nix/nix-language.html. The live URL is canonical if the snapshot drifts.

This slot is language. Not install. Not flakes. Not wrapping Python. Not Pills 1 through 3. Not derivations. 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.

The tutorial's job is reading Nix. It names five distinguishing patterns. This session takes the first layer: names and values, then one function form. Libraries, import, impurities, and mkDerivation wait. Session 02 recaps the language from Pills 4. Do not start that recap in this folder.

Coin the name now: the set that is not a JSON object.

The tutorial says: if you know JSON, imagine Nix as JSON with functions. Primitive values look familiar. The braces do not. Attribute names usually skip quotes. List items split on whitespace, not commas. A function is a value and can sit inside the set. JSON has no colon-function, no let, no inherit, no ${}. Hold that while you walk the rest.

Done-criteria from the syllabus: you can read attrsets, lists, let / with / inherit, string interpolation, and a lambda, from the tutorial, not from guesswork.

A Nix file holds one expression. Evaluating it produces a value. nix repl types expressions on the command line. nix-instantiate --eval file.nix evaluates a file. Lazy evaluation means nested sets may print as { a = { ... }; } or { a = <CODE>; }. Prepend :p in the repl, or pass --strict to nix-instantiate, when you need the whole structure. This lesson prints the strict results. If Nix is not on the machine today, read the results here. Do not install Nix in this session.

Whitespace delimits tokens and is otherwise insignificant. These two expressions are the same value:

let
  x = 1;
  y = 2;
in x + y

and let x=1;y=2;in x+y. Both evaluate to 3. Indent for the reader.

Whenever you see =, the name is on the left and the value is on the right, ended by ;. Whenever you see :, the function argument is on the left and the body is on the right.

§II — Attribute sets

An attribute set is a collection of name-value pairs. Names must be unique.

{
  string = "hello";
  integer = 1;
  float = 3.141;
  bool = true;
  null = null;
  list = [ 1 "two" false ];
  attribute-set = {
    a = "hello";
    b = 2;
    c = 2.718;
    d = false;
  }; # comments are supported
}

That is the set that is not a JSON object. JSON quotes every key and comma-separates list items. Nix quotes names only when they need it. The semicolon ends a pair. A nested set is an ordinary value.

A function can live in the same braces:

{
  increment = x: x + 1;
}

JSON has no place for that pair. The set does. You will read this shape in every later session.

Access with a dot.

let
  attrset = { x = 1; };
in
attrset.x

Result: 1. Nested access is the same motion: attrset.a.b.c. The dot also assigns nested names:

{ a.b.c = 1; }

evaluates to { a = { b = { c = 1; }; }; }.

rec prepends the set when names inside must see each other.

rec {
  one = 1;
  two = one + 1;
  three = two + 1;
}

Result: { one = 1; three = 3; two = 2; }. Elements may be declared in any order. Evaluation orders them.

Drop rec and the same body fails: error: undefined variable 'one'. A plain set cannot see its own names. rec is the set that can. Still a set.

§III — Lists

A list is square brackets. Elements separate on whitespace. Types may mix.

[ 1 "two" false ]

Write a comma and you are writing JSON into Nix. The parser will not treat that comma as a list separator the way JSON does. Space is the separator. Remember this when a function sits next to a value. That collision is §VI.

Lists are values. They live inside sets, inside let, inside function bodies. There is no special JSON array type. The brackets are Nix lists.

§IV — let, with, inherit

Three ways to put a name on a value: a set, a let, a function argument. This section is the first two plus a shorthand.

let binds names between let and in. After in comes the expression that may use those names.

let
  a = 1;
in
a + a

Result: 2. Replace a with 1. The binding does not leak past the in expression.

with opens a set so you can write the attribute names bare.

let
  a = {
    x = 1;
    y = 2;
    z = 3;
  };
in
with a; [ x y z ]

Result: [ 1 2 3 ]. with a; [ x y z ] is [ a.x a.y a.z ]. The names exist only in the expression after the semicolon.

let
  a = {
    x = 1;
    y = 2;
    z = 3;
  };
in
{
  b = with a; [ x y z ];
  c = x;
}

b works. c = x fails: error: undefined variable 'x'. The with ended at the list.

inherit copies a name into a nested scope. It does not clone a structure. It does not mean class inheritance.

let
  x = 1;
  y = 2;
in
{
  inherit x y;
}

Result: { x = 1; y = 2; }. inherit x y; is x = x; y = y;. Same names, inner scope.

inherit (a) x y; pulls from a named set: x = a.x; y = a.y.

let
  a = { x = 1; y = 2; };
in
{
  inherit (a) x y;
}

Result: { x = 1; y = 2; }. Same shorthand inside let:

let
  a = { x = 1; y = 2; };
  inherit (a) x y;
in [ x y ]

Result: [ 1 2 ]. Nested let expressions reuse outer names this way. The convenience is repeating the name once. The value is looked up. Write inherit when you mean the name in both places. Write an explicit assignment when the inner name should differ.

§V — String interpolation

${ } inserts a Nix value into a character string. Older name: antiquotation.

let
  name = "Nix";
in
"hello ${name}"

Result: "hello Nix". Only character strings, or values that can be represented as character strings, are allowed.

let
  x = 1;
in
"${x} + ${x} = ${x + x}"

fails: error: cannot coerce an integer to a string. Convert first, later, with a function from builtins. Not today.

Interpolations nest. Nested interpolations are legal and hard to read. Prefer concatenation when the nesting stacks.

A dollar without braces is not interpolation. Shell scripts inside Nix strings often write $out. That is a shell variable. The Nix name out coinciding is an accident.

let
  out = "Nix";
in
"echo ${out} > $out"

Result: "echo Nix > $out". ${out} became Nix. $out stayed $out.

Indented strings use '' ... '' and also interpolate. Common indent is trimmed. They wait as a spelling of the same string. Do not treat them as a second language.

§VI — Lambdas

A function takes exactly one argument. Argument and body split on a colon. Functions have no names. The value is a lambda. <LAMBDA> is how the repl prints it.

x: x + 1

Bind it if you want a name.

let
  f = x: x + 1;
in f 1

Result: 2. Application is juxtaposition. Write the argument after the function. Parentheses group when whitespace would glue the wrong things.

(x: x + 1) 1

Result: 2. The parentheses keep the lambda together so 1 is the argument, not a second list item.

Multiple arguments are nested functions. x: y: x + y is x: (y: x + y). Apply once and you still have a function.

let
  f = x: y: x + y;
in
f 1

Result: <LAMBDA>. Apply again: f 1 2 yields 3.

A function may require a set with named attributes.

let
  f = {a, b}: a + b;
in
f { a = 1; b = 2; }

Result: 3. Extra attributes are an error unless the form allows them. Defaults use ?. { a, b ? 0 }: a + b can be called with { a = 1; }. Ellipsis and @args wait for Pills 5. Today you need to recognize a lambda when you see a colon with a name on the left.

Lists and functions share whitespace. These two are different:

let
  f = x: x + 1;
  a = 1;
in [ (f a) ]

Result: [ 2 ]. One element.

let
  f = x: x + 1;
  a = 1;
in [ f a ]

Result: [ <LAMBDA> 1 ]. Two elements. Parentheses when the application is the element.

JSON has no function value to drop into a list. Nix does. Space means two different things depending on whether you are inside [] or after a lambda. That is the same hinge as the braces: a function is a value, so every delimiter has to say whether it is applying the function or listing it.

§VII — Five proofs

The syllabus names five reads. Run them from the tutorial.

Proof 1. Attrsets.

Braces, name = value;, unique names, dot access, rec when inner names must see each other. If you quote every key and comma-separate pairs, you are reading JSON into Nix. Name the set that is not a JSON object.

Proof 2. Lists.

[ 1 "two" false ]. Whitespace, not commas. Mixed types are legal. If you wrote commas as separators, walk §III again.

Proof 3. let / with / inherit.

let binds until the in expression ends. with a; expr opens a only for expr. inherit x y is x = x; y = y;. inherit (a) x y is x = a.x; y = a.y. If you explained inherit as copying an object or as class inheritance, you missed the name binding.

Proof 4. Interpolation.

"${name}" inserts a string. An integer in ${ } is a coerce error. $out without braces is not Nix interpolation. If you treated every $ as antiquotation, walk §V again.

Proof 5. A lambda.

x: x + 1. One argument. Colon between argument and body. Call by writing the argument after the function. Nested colons for a second argument. { a, b }: when the argument is a set. If you cannot point at the colon and say which side is the argument, you have not read a lambda yet.

§VIII — Closing

The language has few concepts. Complexity in the wild comes from how they combine: sets of functions of sets, let wrapping with wrapping a lambda. Today you read the pieces. You did not build a derivation. You did not enter a flake. You did not wrap Python.

Name it when braces hold a function, or when a list splits on space, or when a name skips quotes: the set that is not a JSON object. Read the braces as a set first.

Session 02 is Pills 4, language recap: paths versus division, rec attrsets in the repl, with versus let, laziness. Do not start it in this folder. Do not write it today.

Examine well. The unquoted name is the first proof. The space-separated list is the second. The in, the with semicolon, and inherit x y are the third. ${name} and the $out that stayed $out are the fourth. The colon with one argument is the fifth. The door is the function inside the braces.

Related

🛡️ ⚖️ 📜 Leo.Syri — Praetor Consulate, Imperium Luminaura Filed 2026-08-31 · Asr · session 01 · nix.dev language basics · the set that is not a JSON object