Nix Expression Language

2022-12-01

Nix Expression Language #

Ich erkunde die Nix Expression Language. Mein Startpunkt ist https://learnxinyminutes.com/docs/nix/. Sehr hilfreich ist auch die Dokumentation, welche über die Nix Repl verfügbar ist.

Auswertung #

Die auszuwertenden Ausdrücke schreibe ich in eine Datei scratch.nix. Die Auswertung erfolgt so:

$ nix-instantiate --eval scratch.nix

Für mehr Komfort kann man inotifywait benutzen um die Auswertung jedesmal anzustoßen wenn die Datei scratch.nix gespeichert wird:

$ while true
    inotifywait -q -e modify scratch.nix
    clear
    nix-instantiate --eval scratch.nix
    and echo "."
  end

Das ist eine Kommandozeile für die fish Shell. In sh usw. muss man das ein bisschen anders schreiben. Die fish Shell ist sehr komfortabel: man kann das einfach so mehrzeilig aufschreiben; die Shell weiß, wann die Eingabetaste die Zeile umbrechen soll und wann sie die Auswertung anstoßen soll.

Kurze Ausdrücke kann man auch direkt in der Nix Repl ausprobieren:

$ nix repl
Welcome to Nix 2.10.3. Type :? for help.

nix-repl> "Das ist eine Zeichenkette"
"Das ist eine Zeichenkette"

nix-repl> 7 * 11 * 13
1001

Update: Am Ende habe ich die Methode mit der Scratch-Datei kaum verwendet und mich stattdessen fast ausschließlich auf die Nix Repl verlassen. Die Nix Repl bietet integrierte Dokumentation und kann auch mit mehrzeiligen Eingaben gut umgehen.

Einfache Sprachbestandteile #

Im Schnelldurchlauf.

Kommentare #

# Einzeilige Kommentare (wie in Python).

/*
    Mehrzeilige Kommentare (wie in C++).

    Python-Einzeiler und C++-Mehrzeiler sind schon mal eine
    seltsame Kombination.  Wir haben noch nicht richtig begonnen
    und der Merkwürdigkeitsfaktor ist schon größer 1.
*/

Booleans #

nix-repl> true && false
false

nix-repl> true || false
true

Verzweigung #

nix-repl> if 3 < 4 then "a" else "b"
"a"

Zahlen #

Es scheint Integers und Floats zu geben. Man kann sie beim Rechnen mit einander kombinieren. Die arithmetischen Operatoren sind polymorph. Der Interpreter scheint selbständig nach gewissen Regeln zu enscheiden, wann das Ergebnis Integer und wann es Float ist. Daraus ergeben sich die üblichen Fallstricke:

nix-repl> 7.0 / 2
3.5

nix-repl> 7 / 2
3

Für den Einsatzzweck der Nix Expression Language ist das bestimmt kein Problem.

Strings #

nix-repl> "Das ist ein String"
"Das ist ein String"

Strings können mehrzeilig sein:

nix-repl> "asdf
...       asdf
...       asdf"
"asdf\nasdf\nasdf"

Die sogenannten Indented Strings sind ebenfalls mehrzeilig. Bei ihnen wird die Einrückung nicht mit ausgewertet. Das ist die kürzeste Folge von führenden Leerzeichen, in der alle nicht leeren Zeilen des Strings übereinstimmen. Ob und wie dabei Tabs berücksichtigt werden, habe ich nicht ausprobiert. Hier werden in jeder Zeile die vier führenden Leerzeichen entfernt:

nix-repl> ''    asdf
...           asdf
...           asdf''
"asdf\nasdf\nasdf"

Mit leeren Zeilen sind hier auch Zeilen gemeint, die nur aus Leerraum bestehen. Leerzeilen am Rand des Strings werden verworfen.

Verkettung von Strings mit +:

nix-repl> "ab" + "cd"
"abcd"

Auswertung in Strings:

nix-repl> "Home is ${builtins.getEnv "HOME"}."
"Home is /home/aramis."

Der Name builtins verweist auf eine Standardbibliothek (genauer: ein Set) von Funktionen und Prozeduren. Das builtins Set ist immer verfügbar wenn der Nix Interpreter startet. Die Prozedur builtins.getEnv liefert den Wert von Umgebungsvariablen.

nix-repl> builtins.getEnv "HOME"
"/home/aramis"

Pfade #

Für Pfade gibt es einen eigenen Basisdatentyp. Man notiert Pfade einfach so wie man sie auch sonst notieren würde:

nix-repl> /home/aramis
/home/aramis

Das hat aber Einschränkungen. Zum Beispiel sind abschließende Schrägstriche aus irgendwelchen Gründen nicht gestattet:

nix-repl> /home/aramis/
error: path has a trailing slash

nix-repl> /home/aramis/.
/home/aramis

Pfade ganz ohne Schrägstriche wie z.B. . werden auch nicht erkannt:

nix-repl> .
error: syntax error, unexpected '.'

nix-repl> ./
error: path has a trailing slash

nix-repl> ./.
/home/aramis

Relative Pfade werden in absolute Pfade aufgelöst relativ zu dem Verzeichnis, in welchem sich die Datei befindet, die ausgewertet wird:

nix-repl> ./.
/home/aramis

Pfade werden kanonisiert:

nix-repl> /home/aramis/.
/home/aramis

Daraus, dass der Parser auch relative Pfade als Pfade erkennt, ergibt sich, dass der Schrägstrich, wenn er als Divisionszeichen gelesen werden soll, von Leerraum umgeben sein muss

nix-repl> 0/0
/home/aramis/0/0

nix-repl> 0 / 0
error: division by zero

Imports #

Jedes Nix Skript enthält genau einen Top-Level-Ausdruck ohne freie Variable, der also vollständig ausgewertet werden kann. Wenn ein Skript importiert wird, entspricht der Wert des Import-Ausdrucks dem Wert des importierten Skripts:

$ echo "1 + 2" > foo.nix
$ nix repl
Welcome to Nix 2.10.3. Type :? for help.

nix-repl> import ./foo.nix
3

Das import Schlüsselwort kann auch Strings verarbeiten:

nix-repl> import "/home/aramis/foo.nix"
3

Aber das scheint nur zu funktionieren wenn der String einen absoluten Pfad enthält:

nix-repl> import "foo.nix"
error: string 'foo.nix' doesn't represent an absolute path

nix-repl> import "./foo.nix"
error: string './foo.nix' doesn't represent an absolute path

Let-In-Ausdrücke #

Dafür gibt es eine let ... ; in ... Syntax:

nix-repl> let x = "a" ; in x + x + x
"aaa"

Die Zuweisungen können sich auf einander beziehen, unabhängig von ihrer Reihenfolge:

nix-repl> let y = x + "b" ; x = "a" ; in y + "c"
"abc"

Innere Zuweisungen überschatten weiter außen liegende Zuweisungen:

nix-repl> let a = 1 ; in let a = 2 ; in a
2

Funktionen #

Das ist erstaunlich rudimentär gelöst. Nix kennt eigentlich nur einfache Lambdas:

nix-repl> (n : n + 1) 5
6

Lambdas mit mehr als einem Parameter ergeben sich aus der Syntax auf natürliche Weise:

nix-repl> (a : b : a + b) 5 6
11

Wenn man Namen für Funktionen vergeben möchte, kann man auf die let ... ; in ... Syntax zurückgreifen:

nix-repl> let plus = (a : b : a + b ) ; in plus 5 6
11

Listen #

Listen sind von eckigen Klammern begrenzt. Die Elemente sind durch Lerraum getrennt.

nix-repl>[ 1 2 3 ]
[ 1 2 3 ]

Es gibt viele Built-Ins für Listen:

nix-repl> builtins.length [ 1 2 3 ]
3

nix-repl> builtins.concatLists [ [ 1 2 3 ] [ 4 5 6 ] ]
[ 1 2 3 4 5 6 ]

nix-repl> builtins.head [ 1 2 3 ]
1

nix-repl> builtins.filter (n : n < 3) [ 1 2 3 ]
[ 1 2 ]

Sets (Mengen) #

In Nix Sets hat jedes Element einen String als Schlüssel also einen Namen. Demnach sind Nix Sets keine bloßen Mengen sondern eher Records bzw. Maps.

nix-repl> { a = 1 ; b = 2 ; }.a
1

nix-repl> let s = { a = 1 ; b = 2 ; } ; in s.a
1

Der ? Operator prüft die Existenz eines Schlüssels:

nix-repl> { a = 1 ; b = 2 ; } ? a
true

Der // Operator vereinigt zwei Sets:

nix-repl> { a = 1 ; } // { b = 2 ; }
{ a = 1; b = 2; }

Wenn dabei Schlüssel kollidieren, hat das rechte Set Vorrang:

nix-repl> { a = 1 ; } // { a = 2 ; }
{ a = 2; }

Mit dem rec Schlüsselwort können rekursive Sets deklariert werden. Darin können sich Werte auf andere Werte des selben Sets beziehen:

nix-repl> rec { a = 1 ; b = a ; }
{ a = 1; b = 1; }

nix-repl> rec { a = b ; b = a ; }
error: infinite recursion encountered

Für verschachtelte Sets gibt es eine Kurznotation:

nix-repl> { a.b = 2 ; a.c = 3 ; }
{ a = { ... }; }

Das entspricht:

nix-repl> { a = { b = 2 ; c = 3 ; } ; }
{ a = { ... }; }

Man kann die Kurznotation mit der ausführlicheren mischen:

nix-repl> { a = { b = 2 ; } ; a.c = 3 ; }
{ a = { ... }; }

Ich finde, das sieht widersprüchlich aus.

Das with Schlüsselwort #

Das with Schlüsselwort nimmt ein Set und einen Ausdruck. In dem Ausdruck gelten die Schlüssel-Wert-Paare des Sets als Bindungen:

nix-repl> with { a = 1 ; b = 2 ; } ; a + b
3

Das ist also wie die let ... ; in ... Syntax:

nix-repl> let a = 1 ; b = 2 ; in a + b
3

Der Unterschied ist, dass bei with die gesamte Menge von Bindungen in einem (Set-)Ausdruck zusammengefasst ist. Dadurch kann man sich die Bindungen sozusagen als ein ganzes Bündel aus dem Kontext holen. Man sieht das häufig in Nix-Expressions bspw. um die Built-Ins unqualifiziert verfügbar zu machen:

nix-repl builtins.length [ 1 2 3 ]
3

nix-repl> with builtins ; length [ 1 2 3 ]
3

Das ist sinnvoll wenn man z.B. viele Built-Ins in einem Ausdruck verwenden möchte und sie nicht jedesmal mit builtins. qualifizieren möchte.

Ohne with:

nix-repl> [ ( builtins.length [ 1 2 3 ] )
...         ( builtins.head [ 1 2 3 ] )
...         ( builtins.elem 2 [ 1 2 3 ] )
...       ]
[ 3 1 true ]

Mit with:

nix-repl> with builtins ;
...       [ ( length [ 1 2 3 ] )
...         ( head [ 1 2 3 ] )
...         ( elem 2 [ 1 2 3 ] )
...       ]
[ 3 1 true ]

Wenn man also qualifiziert importieren möchte, ganz grob ungefähr so wie man es in Haskell machen würde, könnte das so aussehen:

$ echo "{ plus = a : b : a + b ; }" > foo.nix
$ nix repl
Welcome to Nix 2.10.3. Type :? for help.

nix-repl> let foo = import ./foo.nix ; in with foo ; foo.plus 5 6
11

Das würde einem unqualifizierten Import entsprechen:

nix-repl> with import ./foo.nix ; plus 5 6
11

Anstatt wie oben den Import in einem Let-Ausdruck an den Namen foo zu binden, kann man diesen Namen in der importierten Datei festlegen. Dann spart man sich den Let-Ausdruck:

$ echo "{ foo = { plus = a : b : a + b ; } ; }" > foo.nix
$ nix repl
Welcome to Nix 2.10.3. Type :? for help.

nix-repl> with import ./foo.nix ; foo.plus 5 6
11

Die Variante hat den großen Nachteil, dass die Festlegung des Qualifiers foo in der importierenden Datei nicht explizit ist.

Mit with lässt sich jeweils nur ein Set von Bindungen angeben aber, ähnlich wie bie den Lambdas, ergibt sich auch hier aus der Syntax eine natürliche Erweiterung auf mehrere Sets:

nix-repl> with builtins ;
...       with { myList = [ 1 2 3 ] ; } ;
...       length myList
3

Set Patterns #

Funktionen unterstützen per se keine benannten Parameter. Lambdas können aber selbstverständlich auch auf Sets operieren:

nix-repl> (x : x.a + x.b) { a = 5 ; b = 6 ; }
11

Mit sogenannten Set Patterns kann man letztlich doch benannte Funktionsparameter aufschreiben:

nix-repl> ({a,b} : a + b) { a = 5 ; b = 6 ; }
11

Dabei ist {a,b} ein sogenanntes Set Pattern.

Achtung: Wenn das Argument ein Set ist, welches zusätzliche Werte enthält, die im Set Pattern nicht angegeben sind, ist das ein Fehler:

nix-repl> ({a,b} : a + b) { a = 5 ; b = 6 ; c = 7 ; }
error: anonymous function at (string):1:2 called with unexpected argument 'c'

Man kann den Fall aber im Set Pattern durch Auslassungspunkte ausdrücklich zulassen:

nix-repl> ({a,b,...} : a + b) { a = 5 ; b = 6 ; c = 7 ; }
11

Fehler #

Das throw Schlüsselwort bricht die Auswertung ab und gibt eine Fehlermeldung aus:

nix-repl> 1 + 2 + throw "drei"
error: drei

Anscheinend werden Fehler innerhalb von Listen wie normale Elemente behandelt. Die Auswertung wird nicht abgebrochen:

nix-repl> [ 1 (throw "zwei") 3 ]
[ 1 «error: error: zwei» 3 ]

Das gleiche Verhalten tritt innerhalb von Sets auf:

nix-repl> { eins = 1 ; zwei = throw "zwei" ; drei = 3 ; }
{ drei = 3; eins = 1; zwei = «error: error: zwei»; }

Neben throw gibt es auch noch das Schlüsselwort abort, welches ebenfalls die Auswertung abbricht und eine Fehlermeldung ausgibt:

nix-repl> 1 + 2 + abort "drei"
error: evaluation aborted with the following error message: 'drei'

Im Unterschied zu throw führt abort auch innerhalb von Listen und Sets zum Abbruch der Auswertung:

nix-repl> [ 1 (abort "zwei") 3 ]
error: evaluation aborted with the following error message: 'zwei'

nix-repl> { eins = 1 ; zwei = abort "zwei" ; drei = 3 ; }
error: evaluation aborted with the following error message: 'zwei'

Der Unterschied zwischen throw und abort ist nützlich. In nix-env -qa und anderen Shell-Anweisungen, die Listen oder Sets von Nix Derivationen verarbeiten, wird throw verwendet, sofern bei einem Fehler in einer Derivation trotzdem alle nachfolgenden Derivationen verarbeitet werden sollen. Natürlich gibt es auch Fälle, in denen ein Fehler zum Abbruch der ganzen Auswertung führen soll. In solchen Fällen verwendet man abort.

Mit builtins.tryEval können Fehler aufgefangen werden, die mit throw ausgelöst worden sind:

nix-repl> builtins.tryEval (1 + 2 + 3)
{ success = true; value = 6; }

nix-repl> builtins.tryEval (1 + 2 + throw "drei")
{ success = false; value = false; }

Mit abort ausgelöste Fehler kann builtins.tryEval nicht auffangen:

nix-repl> builtins.tryEval (1 + 2 + abort "drei")
error: evaluation aborted with the following error message: 'drei'

Das assert Schlüsselwort erwartet zwei Ausdrücke, getrennt durch ein Semikolon:

assert <erster Ausdruck> ; <zweiter Ausdruck>

Der erste Ausdruck muss boolsch sein. Der zweite Ausdruck kann beliebig sein. Wenn der erste Ausdruck zu true auswertet, wertet der gesamte assert Ausdruck zum zweiten Ausdruck aus:

nix-repl> assert true ; "1199Panigale"
"1199Panigale"

nix-repl> assert 3 < 4 ; "1199" + "Panigale"
"1199Panigale"

Wenn der erste Ausdruck zu false auswertet, löst der assert Ausdruck einen Fehler aus:

nix-repl> assert false ; "1199Panigale"
error: assertion 'false' failed

nix-repl> assert 4 < 3 ; "1199" + "Panigale"
error: assertion '(__lessThan 4 3)' failed

Fehler, die von assert ausgelöst worden sind, können mit builtins.tryEval aufgefangen werden:

nix-repl> builtins.tryEval (assert true ; "1199Panigale")
{ success = true; value = "1199Panigale"; }

nix-repl> builtins.tryEval (assert false ; "1199Panigale")
{ success = false; value = false; }

Impurity #

Die Nix Expression Language ist keine pure funktionale Sprache. Sie folgt dem Anspruch, möglichst pur zu arbeiten, um die Reproduzierbarkeit von Builds sicherzustellen. Es gibt aber ein paar Ausnahmen.

Ich will kein Haskell Snob sein, bin aber geneigt, anzumerken, dass eine Programmiersprache entweder purely functional ist oder nicht, also ganz oder gar nicht. Es gibt dazwischen keinen Kompromiss, bzw. der Kompromiss wäre immer impure. Schließlich ist eine mathematische Relation entweder eine Abbildung oder eben nicht. Andererseits kann man sehr wohl auch in einer Programmiersprache, welche diese Eigenschaft nicht hat, purely functional programmieren; es obliegt dann nur dem Menschen, sicherzustellen dass der Code ausschließlich statische Werte aus dem Kontext liest und klar unterscheidet zwischen Prozeduren, die Nebeneffekte hervorrufen, und solchen die das nicht tun.

Es folgen ein paar Ursachen dafür, dass die Nix Expression Language nicht pure ist. (Es gibt noch weitere.)

builtins.getEnv #

Die Prozedur builtins.getEnv liest Umgebungsvariable:

nix-repl> builtins.getEnv "HOME"
"/home/aramis"

Das ist unter allen Prozeduren, die hier aufgeführt sind, der eine, von dem ich mir vorstellen könnte, dass er in die tolle Idee vom Purely Functional Package Management eine große Lücke reißt.

builtins.trace #

Die Prozedur builtins.trace nimmt zwei Werte entgegen, sendet den ersten Wert zur Standardfehlerausgabe und wertet selbst zum zweiten Wert aus:

nix-repl> builtins.trace "err" "val"
trace: err
"val"

Auf die Standardfehlerausgabe zu schreiben, ist strenggenommen ein Nebeneffekt, aber auch Sprachen, die sich rühmen, purely functional zu sein, brauchen und haben eine trace Prozedur.

builtins.toFile #

Die Prozedur builtins.toFile schreibt Dateien in den Nix Store:

nix-repl> builtins.toFile "foo.txt" "hello!"
"/nix/store/ayh05aay2anx135prqp0cy34h891247x-foo.txt"

nix-repl> :q

$ cat /nix/store/ayh05aay2anx135prqp0cy34h891247x-foo.txt
hello!

In eine Datei zu schreiben ist natürlich ein Nebeneffekt. Zumindest wird hier sichergestellt, dass eine Datei, die einmal im Nix Store erzeugt worden ist, ihren Inhalt nicht mehr unbemerkt ändern kann, denn ein Hashwert ihres Inhalts wird dem Dateinamen als Präfix vorangestellt. Was Nix anschließend mit diesen Hashwerten macht – insbesondere ob Nix wirklich jede Datei vor jeder Verarbeitung erneut hasht und mit dem Dateinamen abgleicht – weiß ich nicht, aber das finde ich bestimmt noch heraus.

An der Stelle bin ich durch mit https://learnxinyminutes.com/docs/nix/. Aktuell ist mein Plan, mir als nächstes die Namen anzuschauen, die in der Nix Repl aufgelistet werden wenn man direkt nach dem Start die Tab-Taste drückt. Dann schaue ich mir den Inhalt des builtins Sets genauer an.

Top-Level Namen #

Es gibt ein paar Namen (für Funktionen, Prozeduren, …), die nach dem Start der Nix Repl unqualifiziert im Top-Level zur Verfügung stehen:

abort baseNameOf break builtins derivation derivationStrict dirOf
false fetchGit fetchMercurial fetchTarball fetchTree fromTOML
import isNull map null placeholder removeAttrs scopedImport
throw toString true

Daneben gibt es noch eine Menge von Namen, die mit einem Unterstrich beginnen, wie z.B. __typeOf. Das scheinen allesamt Aliase für die Namen im builtins Set zu sein. Diese Namen lasse ich hier aus, weil ich vorhabe, später auf den Inhalt von builtins einzugehen.

Die folgenden Namen wurden schon weiter oben erläutert:

abort builtins false import throw true

Jetzt schaue ich mir den Rest an.

map #

Damit kann man eine Funktion auf jedes Element in einer Liste anwenden:

nix-repl> map (n : 2 * n) [ 1 2 3 ]
[ 2 4 6 ]

Wer schon funktional programmiert hat, dürfte map kennen.

baseNameOf #

Liefert das letzte Segment in einem Pfad:

nix-repl> baseNameOf /home/aramis
"aramis"

nix-repl> baseNameOf ./.
"aramis"

Das funktioniert auch mit Strings:

nix-repl> baseNameOf "/home/aramis"
"aramis"

nix-repl> baseNameOf "/home/aramis/"
"aramis"

Pfade in einem String werden aber nicht kanonisiert:

nix-repl> baseNameOf "../../.."
".."

dirOf #

Liefert den Pfad ohne das letzte Segment:

nix-repl> dirOf /home/aramis
/home

nix-repl> dirOf "/home/aramis"
"/home"

nix-repl> dirOf "/home/aramis/"
"/home/aramis"

nix-repl> dirOf "../../.."
"../.."

null, isNull #

Offenbar gibt es einen null Wert. Ich weiß nicht, welche Rolle null in der Nix Expression Language spielt. Hoffentlich keine allzu große. In anderen Sprachen repräsentiert der null Wert häufig die Abwesenheit eines “echten” Wertes.

Der null Wert scheint seinen eigenen Datentyp zu haben. Ich greife an der Stelle vor auf builtins.typeOf. Damit kann man den Datentyp eines Ausdrucks finden:

nix-repl> builtins.typeOf 0
"int"

nix-repl> builtins.typeOf 0.0
"float"

nix-repl> builtins.typeOf false
"bool"

nix-repl> builtins.typeOf ""
"string"

nix-repl> builtins.typeOf []
"list"

nix-repl> builtins.typeOf {}
"set"

nix-repl> builtins.typeOf null
"null"

Die Funktion isNull ist markiert als deprecated. Man soll stattdessen ... == null verwenden:

nix-repl> isNull null
true

nix-repl> isNull 0
false

nix-repl> isNull ""
false

nix-repl> null == null
true

nix-repl> 0 == null
false

nix-repl> "" == null
false

toString #

Damit können Ausdrücke in eine Stringdarstellung überführt werden:

nix-repl> toString 123
"123"

nix-repl> toString "Das ist schon ein String"
"Das ist schon ein String"

Pfade werden kanonisiert:

nix-repl> toString ./.
"/home/aramis"

Für andere Datentypen liefert toString Ergebnisse, die ich so nicht erwartet hätte:

nix-repl> toString 0.0
"0.000000"

nix-repl> toString true
"1"

nix-repl> toString false
""

nix-repl> toString []
""

nix-repl> toString [ 1 2 3 ]
"1 2 3"

nix-repl> toString [ 1 2 3 [ 4 5 6 ] ]
"1 2 3 4 5 6"

nix-repl> toString null
""

Auf Sets kann toString nur operieren wenn darin ein spezieller Schlüssel __toString oder outPath vorhanden ist:

nix-repl> toString { a = 1 ; b = 2 ; c = 3 ; }
error: cannot coerce a set to a string

nix-repl> toString { a = 1 ; b = 2 ; c = 3 ; outPath = "asdf" ; }
"asdf"

nix-repl> toString
...         { a = 1 ; b = 2 ; c = 3 ; __toString = self : self.a }
"1"

break #

Die Prozedur break hat hier nichts mit While-Schleifen oder ähnlichem zu tun. Man kann damit Breakpoints im Code setzen, an denen der Interpreter im Debug Modus die Auswertung pausiert und in die Repl wechselt. Um den Interpreter im Debug Modus zu starten, muss das Flag --debugger mit übergeben werden.

Da wir hier nicht imperativ sondern deklarativ bzw. funktional programmieren, kann break nicht als Anweisung im Code platziert werden ohne selbst zu irgendetwas auszuwerten. Deswegen erwartet break ein Argument und wertet zu diesem Argument aus:

nix-repl> break 1
1

removeAttrs #

Die Funktion removeAttrs nimmt ein Set und eine Liste mit Schlüsseln entgegen. Die aufgeführten Schlüssel werden aus dem Set entfernt. Schlüssel, die in dem Set nicht vorkommen, werden ignoriert:

nix-repl> removeAttrs { x = 1 ; y = 2 ; z = 3 ; } [ "a" "x" "z" ]
{ y = 2; }

Ich glaube, diese Stelle markiert einen Übergang. Ich habe zunächst versucht, die Nix Expression Language ganz allgemein als Programmiersprache zu betrachten, ohne darauf Rücksicht zu nehmen, wofür sie tatsächlich eingesetzt werden soll. Ich denke, dass das ab hier immer weniger möglich sein wird.

fromTOML #

Ich bin mit TOML nicht besonders vertraut, aber ich glaube, es ist nicht viel mehr als eine Folge von zeilenweisen Schlüssel-Wert-Zuweisungen in der Form key=value. Die Funktion fromTOML scheint TOML Markup in ein Nix Set zu überführen:

nix-repl> fromTOML "a=1\nb=2"
{ a = 1; b = 2; }

scopedImport #

Das ist nirgendwo richtig dokumentiert und wird nicht einmal im Nix Manual erwähnt. Es gibt eine Issue auf Github dazu:

https://github.com/NixOS/nix/issues/1450

Ein paar Auszüge:

  • “scopedImport has nasty performance consequences since it disables the parser/evaluation cache”
  • “It allows doing some pretty nasty/nifty things like overriding every primop (including import)”

Ok, also die Finger davon lassen. Es hätte mich trotzdem ein bisschen interessiert, wie man das benutzt, aber nicht so sehr, dass ich an der Stelle weiterbuddeln möchte.

fetchGit, fetchMercurial, fetchTarball, fetchTree #

Mit den Fetch-Befehlen lassen sich Dateien herunterladen und in den Nix Store schreiben. fetchMercurial und fetchTree sind undokumentiert, also schaue ich mir zunächst die anderen beiden an.

fetchGit holt einen Pfad aus einem Git Repository und schreibt ihn in den Nix Store. In der Nix Repl erhält man mit :doc fetchGit reichlich Dokumentation dazu, wie das genau zu benutzen ist. Ich möchte das hier nicht alles wiedergeben.

fetchTarball lädt ein Tar-Archiv herunter und entpackt es in den Nix Sore. Das Tar-Archiv kann (oder muss?) zusätzlich mit gzip, bzip7 oder xv komprimiert sein. Die Dokumentation in der Repl dazu ist auch recht ausführlich.

fetchMercurial ist in der Repl nicht dokumentiert. Ich nehme an, es ist wie fetchGit aber für Mercurial Repositories.

fetchTree ist ebenfalls undokumentiert in der Repl. Das Nix Manual erwähnt, dass fetchTree die Funktionalität der anderen Fetch-Befehle in sich vereint und somit beliebige Quellen herunterladen kann. Möglicherweise ist das nur eine Fassade, die je nach Quelle das passende Backend wählt, ungefähr so wie aunpack das Entpacken von Archivdateien handhabt.

derivation, derivationStrict #

Beides hat in der Nix Repl keine Dokumentation. Was ist eine Derivation? Man kann das wörtlich mit Ableitung übersetzen. Im Kontext von Nix ist damit eine sogenannte Build Action gemeint. Ich nehme an, das ist ein Rezept für den Bau eines Softwarepaketes und seine “Installation” im Nix Store. Laut Handbuch ist derivation die wichtigste built-in Funktion: schließlich ist Nix genau dafür gedacht/gemacht, solche Derivationen zu beschreiben und auszuführen.

derivation verarbeitet ein Set, das genau beschreibt, was gebaut wird, und zwar mit den folgenden Schlüsseln (Attributen):

  • system
    • Nix Systemtyp, z.B. "i686-linux" oder "x86_64-darwin"
    • siehe nix -vv --version
  • name
    • der Name des Pakets
  • builder
    • das Programm, welches zum Bauen verwendet wird
    • kann eine Derivation oder eine lokale Datei sein (ein Script)
    • Die Attribute der Derivation werden als Umgebungsvariable übergeben:
      • Strings und Zahlen werden unverändert übergeben.
      • Pfade werden zunächst in den Nix Store kopiert und der Zielpfad landet in der Umgebungsvariable.
      • Derivationen werden gebaut und der Pfad des Zielartefakts landet in der Umgebungsvariable.
        • Listen werden leerzeichensepariert übergeben.
        • true wird als 1 übergeben.
        • false und null werden als "" übergeben.
  • args
    • optionale Liste von CLI-Argumenten für das bauende Programm
  • outputs
    • optionale Liste von Ausgabepfaden
    • Normalerweise gibt es nur einen Outputpfad out, aber man kann diesen Schlüssel verwenden, um verschiedene Ausgabepfade zu deklarieren, bspw. [ "lib" "headers" "doc" ], dann stehen dem bauenden Programm drei Pfade statt nur einem zur Verfügung, die separat garbage-collected werden können.

Damit habe ich eine erste, grobe Vorstlelung davon, wie Nix baut.

Für derivationStrict habe ich nirgendwo Dokumentation gefunden.

placeholder #

Nimmt einen Ausgabepfad entgegen ("out", "bin", "dev", …) und liefert einen Plazhalter, der beim Bauen durch den Ausgabepfad ersetzt wird. Es scheint wirklich eine Funktion zu sein:

nix-repl> placeholder "out"
"/1rz4g4znpzjwh1xymhjpm42vipw92pr73vdgl6xs1hycac8kf2n9"

nix-repl> placeholder "out"
"/1rz4g4znpzjwh1xymhjpm42vipw92pr73vdgl6xs1hycac8kf2n9"

nix-repl> placeholder "out"
"/1rz4g4znpzjwh1xymhjpm42vipw92pr73vdgl6xs1hycac8kf2n9"

Keine Ahnung, wofür das gut ist.

Built-ins #

Im letzten Abschnitt habe ich mir die Namen angeschaut, die nach dem Start der Nix Repl unqualifiziert im Top-Level verfügbar sind. Unterhalb von builtins gibt es eine ganze Reihe weiterer Namen. Die schaue ich mir jetzt an. Einige davon sind nicht neu, weil sie auch im Top-Level verfügbar sind. Die lasse ich unerwähnt aus.

builtins.add #

Die Funktion hinter dem + Operator:

nix-repl> builtins.add 3 4
7

builtins.addErrorContext #

Dafür finde ich keine Dokumentation.

builtins.all #

Prüft ob ein Prädikat auf alle Elemente einer Liste zutrifft:

nix-repl> builtins.all ( n : n > 3 ) [ 1 2 3 ]
false

nix-repl> builtins.all ( n : n > 3 ) [ 4 5 6 ]
true

builtins.any #

Prüft ob ein Prädikat auf mindestens ein Element einer Liste zutrifft:

nix-repl> builtins.any ( n : n < 3 ) [ 1 2 3 ]
true

nix-repl> builtins.any ( n : n < 3 ) [ 4 5 6 ]
false

builtins.appendContext #

Dafür finde ich keine Dokumentation.

builtins.attrNames #

Liefert eine sortierte Liste der Schlüssel in einem Set:

nix-repl> builtins.attrNames { a = 1 ; b = 2 ; c = 3 ; }
[ "a" "b" "c" ]

builtins.attrValues #

Wie Sartres Autodidakt, gehe ich die Built-ins in alphabetischer Reihder sonderliche Autodidakt in Bouville, gehe ich die Built-ins in alphabetischer Reihenfolge durch. Immerhin sind es bei mir nicht die Bücher einer ganzen Bibliothek, sondern nur ein paar Nix-Funktionen. Jetzt wäre builtins.deepSeq

nix-repl> builtins.attrValues { a = 3 ; b = 2 ; c = 1 ; }
[ 3 2 1 ]

builtins.bitAnd #

Bitweise Konjunktion zweier Integers:

nix-repl> builtins.bitAnd 123 456
72

Rechnen wir das spaßeshalber durch:

123 = 64  + 32  + 16  + 8   + 2   + 1
    = 2^6 + 2^5 + 2^4 + 2^3 + 2^1 + 2^0
    = 1111011

456 = 256 + 128 + 64  + 8
    = 2^8 + 2^7 + 2^6 + 2^3
    = 111001000

  001111011
+ 111001000
= 001001000 = 2^3 + 2^6 = 72

builtins.bitOr #

Bitweise Disjunktion zweier Integers:

nix-repl> builtins.bitOr 123 456
507

builtins.bitXor #

Bitweise Kontravalenz (“exklusive Disjunktion”) zweiter Integers:

nix-repl> builtins.bitXor 123 456
435

builtins.builtins #

Offenbar enthält das builtins Set eine Referenz auf sich selbst.

nix-repl> builtins.builtins == builtins
true

nix-repl> builtins.builtins.builtins.builtins == builtins
true

Schrullig, aber was soll’s.

builtins.catAttrs #

Das nimmt einen Schlüssel (String) und sammelt aus einer Liste von Sets die Werte für diesen Schlüssel ein:

nix-repl> builtins.catAttrs "a"
...         [ { a = 1 ; } { b = 2 ; } { a = 3 ; } ]
[ 1 3 ]

builtins.ceil #

Liefert für eine Zahl x die nächste Ganzzahl n sodass x <= n:

nix-repl> builtins.ceil 1.5
2

nix-repl> builtins.ceil 2
2

builtins.compareVersions #

Vergleicht zwei Strings anhand der typischen Ordnung von Versionsnummern. Das Ergebnis ist -1, 0 oder 1, je nachdem ob das erste Argument gegenüber dem zweiten kleiner, gleich oder größer ist.

nix-repl> builtins.compareVersions "0.0.0" "0.0.1"
-1

nix-repl> builtins.compareVersions "0.0.1" "0.0.1"
0

nix-repl> builtins.compareVersions "0.0.2" "0.0.1"
1

builtins.concatLists #

Konkateniert Listen:

nix-repl> builtins.concatLists [ [ 1 2 3 ] [ 4 5 6 ] ]
[ 1 2 3 4 5 6 ]

builtins.concatMap #

Das ist eine Verkettung von map und concatLists. Das heißt, für eine Funktion f und eine Liste ls sind die folgenden beiden Ausdrücke äquivalent:

builtins.concatLists (map f ls)

builtins.concatMap f ls

Beispielsweise:

nix-repl> with
...         { f  = map ( n : 2 * n ) ;
...           ls = [ [ 1 2 3 ] [ 4 5 6 ] ] ;
...         } ;
...         builtins.concatLists ( map f ls )
[ 2 4 6 8 10 12 ]

nix-repl> with
...         { f  = map ( n : 2 * n ) ;
...           ls = [ [ 1 2 3 ] [ 4 5 6 ] ] ;
...         } ;
...         builtins.concatMap f ls
[ 2 4 6 8 10 12 ]

builtins.concatStringsSep #

Konkateniert Strings mit einem Trennzeichen:

nix-repl> builtins.concatStringsSep "/" [ "usr" "local" "bin" ]
"usr/local/bin"

builtins.currentSystem #

Liefert einen Namen für das System, auf dem Nix gerade läuft:

nix-repl> builtins.currentSystem
"x86_64-linux"

builtins.currentTime #

Liefert die aktuelle Posix-Zeit:

nix-repl> builtins.currentTime
1661168166

builtins.seq #

Wie der sonderliche Autodidakt in Bouville, gehe ich die Built-ins in alphabetischer Reihenfolge durch. Immerhin sind es bei mir nicht die Bücher einer ganzen Bibliothek, sondern nur ein paar Nix-Funktionen. Jetzt wäre builtins.deepSeq an der Reihe, aber das wäre Quatsch ohne vorher builtins.seq zu betrachten. Deswegen ziehe ich das vor.

Nix wertet verzögert aus (so wie Haskell). Das bedeutet, dass Ausdrücke erst dann ausgewertet werden wenn sie tatsächlich gebraucht werden und auch nur so weit wie es tatsächlich nötig ist. Ich möchte das kurz an einem Beispiel demonstrieren. Das geht vielleicht am besten mit einer Funktion, die viel Rechenzeit frisst. Ad hoc fällt mir die Fibonacci-Funktion ein. Das hier wäre eine einfache Implementierung dafür in Haskell:

-- Haskell:

fib n =
    if n < 1 then 0
    else if n < 2 then 1
    else fib ( n - 1 ) + fib ( n - 2 )

Das ist so rechenaufwändig, dass ich mit meinem Rechner auf fib 32 schon ein paar Sekunden warten muss, also ein guter Kandidat. Wir können das auch als Lambda-Ausdruck schreiben. Dann wandert der Parameter n nach rechts hinter das = Zeichen:

-- Haskell:

fib = \ n ->
    if n < 1 then 0
    else if n < 2 then 1
    else fib ( n - 1 ) + fib ( n - 2 )

In Nix müssen Funktionen als Lambda-Ausdrücke notiert werden. Die konventionelle Notation mit dem Parameter auf der linken Seite wird nicht unterstützt. Mein erster Versuch, diese Funktion in Nix zu schreiben, sah so aus:

nix-repl> fib = n :
...         if n < 1 then 0
...         else if n < 2 then 1
...         else fib ( n - 1 ) + fib ( n - 2 )
error: undefined variable 'fib'

Nix unterstützt keine rekursiven Funktionen, jedenfalls nicht auf diese Weise. Damit das klappt, müssen wir die Funktion in ein rekursives Set stecken:

nix-repl> funs = rec
...         { fib = n :
...             if n < 1 then 0
...             else if n < 2 then 1
...             else fib ( n - 1 ) + fib ( n - 2 ) ;
...         }

nix-repl> funs.fib 32
2178309

Nix rechnet funs.fib 32 schneller aus als Haskell aber es dauert mit meinem Rechner immer noch mehr als eine Sekunde, bis der Interpreter die Berechnung gestemmt hat und das Ergebnis ausdruckt. Damit haben wir alles beisammen um verzögerte Auswertung zu demonstrieren. Dafür ergänzen wir das Set um zwei weitere Schlüssel-Wert-Paare:

nix-repl> funs = rec
...         { fib = n :
...             if n < 1 then 0
...             else if n < 2 then 1
...             else fib ( n - 1 ) + fib ( n - 2 ) ;
...
...           fib35 = fib 35 ;
...           x = 1 ;
...         }

nix-repl>

Den Wert für den Schlüssel fib35 auszurechnen, sollte ein paar Sekunden dauern, aber die Nix Repl nimmt das Set ohne Zeitverzögerung entgegen. Das spricht dafür, dass der Wert für fib35 nicht sofort berechnet wird. Wir können uns auch den Wert für den Schlüssel x ohne Zeitverzögurung ausgeben lassen:

nix-repl> funs.x
1

Erst wenn wir uns den Wert für fib35 ausgeben lassen, gibt es eine deutliche Verzögerung von mehreren Sekunden, die darauf hinweist, dass der Wert jetzt tatsächlich berechnet wird:

nix-repl> funs.fib35
9227465

Wenn wir uns im Anschluss noch einmal das ganze Set ausgeben lassen, geschieht das wieder ohne Verzögerung:

nix-repl> funs
{ fib = «lambda @ (string):1:14»; fib35 = 9227465; x = 1; }

Der Wert für den Schlüssel fib35 wurde schon berechnet und wird hier einfach wiederverwendet anstatt ihn ein zweites Mal zu berechnen. Deswegen erfolgt die Ausgabe ohne zeitliche Verzögerung. Wenn wir vorher nicht funs.fib35 ausgewertet hätten, dann hätten wir an dieser Stelle ein paar Sekunden auf die Auswertung warten müssen.

Das ist verzögerte Auswertung: die Ausdrücke und Teilausdrücke werden nicht sofort ausgewertet sondern erst dann wenn ihr Wert tatsächlich benötigt wird, bspw. um eine Ausgabe zu erzeuen.

Im Allgemeinen ist das eine gute Sache, aber manchmal möchte man, dass die Auswertung nicht verzögert sondern sofort stattfindet. Das ist vor allem dann wichtig, wenn zwei Ausdrücke irgendwelche externen Effeke haben und diese Effekte in einer bestimmen Reihenfolge auftreten sollen. Dann muss man irgendwie sicherstellen, dass die Ausdrücke in der richtigen Reihenfolge ausgewertet werden. In Programmiersprachen, die standardmäßig strikt (i.e. unverzögert) auswerten, hat man dieses Problem nicht. Da ist die Auswertungsreihenfolge dadurch vorgegeben, in welcher Reihenfolge man Ausdrücke notiert. In Programmiersprachen, die standardmäßig verzögert auswerten, benötigt man dafür besondere Hilfsmittel, die eine strikte Auswertung sicherstellen.

Hier kommt builtins.seq ins Spiel. builtins.seq nimmt zwei Ausdrücke entgegen, wertet den ersten Ausdruck aus, verwirft das Resultat und wertet dann den zweiten Ausdruck aus:

nix-repl> builtins.seq 1 2
2

So wird sichergestellt, dass der erste Ausdruck vor dem zweiten ausgewertet wird. Ich kann gerade kein leicht demonstrierbares Beispiel aus dem Ärmel schütteln, bei dem das eine Rolle spielen würde, aber wenn bspw. der erste Ausdruck eine Datei schreibt und der zweite diese Datei liest, stellt builtins.seq sicher, dass das Schreiben tatsächlich vor dem Lesen erfolgt. Ohne builtins.seq wäre diese Reihenfolge wegen der verzögerten Auswertung nicht sichergestellt.

builtins.deepSeq #

Die Auswertung von builtins.seq e1 e2 ist zwar strikt im Ausdruck e1, aber mit einem Haken: der Ausdruck e1 wird nur oberflächlich strikt ausgewertet. Ich erkläre kurz, was das bedeutet. Es gibt einfache Ausdrücke, die direkt ausgewertet werden können ohne dafür weiter vereinfacht werden zu müssen. Das sind bspw. einfache Zahlenausdrücke 123 oder Zeichenketten wie "asdf". Daneben gibt es aber auch komplexe Ausdrücke, die bei der Auswertung zunächst auf einen einfachen Ausdruck reduziert werden müssen. Das ist der Fall wenn ein Ausdruck Funktionen (bzw. Operationen) enthält, die bei der Auswertung angewendet werden müssen. Diese Reduktion auf einen einfachen Ausdruck erfolgt schrittweise. Hier ist ein Ausdruck, der in mehreren Schritten ausgewertet werden muss:

nix-repl> let
...         f1 = n : 1 + n ;
...         f2 = n : 2 + n ;
...         f3 = n : 3 + n ;
...       in
...         f3 ( f2 ( f1 0 ) )

Hier ist eine mögliche Auswertung für diesen Ausdruck:

=> f3            ( f2            ( f1            0 ) )
-----------------------------------^^^^^^^^^^^^^----------
=> f3            ( f2            ( ( n : 1 + n ) 0 ) )
-----------------------------------^^^^^^^^^^^^^^^--------
=> f3            ( f2            ( 1 + 0           ) )
---------------------------------^^^^^^^^^^^^^^^^^^^------
=> f3            ( f2            1                   )
-------------------^^^^^^^^^^^^^--------------------------
=> f3            ( ( n : 2 + n ) 1                   )
-------------------^^^^^^^^^^^^^^^------------------------
=> f3            ( 2 + 1                             )
-----------------^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^----
=> f3            3
---^^^^^^^^^^^^^------------------------------------------
=> ( n : 3 + n ) 3
---^^^^^^^^^^^^^^^----------------------------------------
=> 3 + 3
---^^^^^--------------------------------------------------
=> 6

Ob die Auswertung tatsächlich genau in dieser Reihenfolge stattfindet, weiß ich nicht. Es gibt da verschiedene Möglichkeiten. Relevant ist für uns nur, dass die Auswertung schrittweise erfolgt.

Wenn das nun der Teilausausdruck e1 im Ausdruck builtins.seq e1 e2 wäre, würde builtins.seq nicht garantieren, dass das ganze vor der Auswertung von e2 vollständig auf den Wert 6 reduziert wird. builtins.seq würde lediglich gewährleisten, dass e1 auf der ersten Ebene strikt ausgewertet wird, also vielleicht bis f3 ( f2 ( 1 + 0 ) ). Damit wäre die Funktion f1 schon vollständig abgefrühstückt, aber wenn sich in f2 oder f3 noch irgendwelche externen Nebeneffekte verbergen würden, wäre durch builtins.seq nicht sichergestellt, dass diese Effekte vor der Auswertung von e2 eintreten. Genau das war gemeint mit der Feststellung, builtins.seq würde e1 nur oberflächlich strikt auswerten.

Ich möchte hier anmerken, dass ich mich mit dieser Erläuterung recht weit aus dem Fenster lehne. Ich weiß nicht, wie der Nix Interpreter tatsächlich auswertet und ob meine Charakterisierung einer nur oberflächlich strikten Auswertung den Nagel auf den Kopf trifft. Ich hoffe, dass es hier kein fundamentales Missverständnis meinerseits gibt, das diese Erläuterung zu Stuss macht. In jedem Fall ist das gegenwärtig mein mentales Modell dieser Sache.

Wenn e1 eine Prozedur wie fetchGit ist, die direkt einen externen Nebeneffekt erzeugt, genügt die oberflächlich strikte Auswertung durch builtins.seq, aber wenn ein komplexer Ausdruck vollständig, also in voller Tiefe, strikt ausgewertet werden soll, muss dafür builtins.deepSeq verwendet werden.

Meine Motivation, dafür ein gutes Beispiel zu finden, ist bei Null, denn ich habe noch reichlich Built-ins vor mir. Vielleicht ergänze ich später eins.

builtins.div #

Die Funktion hinter dem Divisionsoperator /, den wir schon weiter oben behandelt haben:

nix-repl> builtins.div 7.0 2
3.5

nix-repl> builtins.div 7 2
3

builtins.elem #

Prüft, ob ein Wert als Element in einer Liste enthalten ist:

nix-repl> builtins.elem 3 [ 1 2 3 ]
true

nix-repl> builtins.elem 4 [ 1 2 3 ]
false

builtins.elemAt #

Liefert das n-te Element einer Liste:

nix-repl> builtins.elemAt [ "a" "b" "c" ] 0
"a"

nix-repl> builtins.elemAt [ "a" "b" "c" ] 1
"b"

nix-repl> builtins.elemAt [ "a" "b" "c" ] 2
"c"

nix-repl> builtins.elemAt [ "a" "b" "c" ] 3
error: list index 3 is out of bounds

Das hier ist aufschlussreich:

nix-repl> builtins.elemAt [ "a" "b" "c" ] -1
error: value is the partially applied built-in function 'elemAt'
       while an integer was expected

Die Fehlermeldung legt nahe, dass das Minuszeichen hier nicht als Vorzeichen sondern als Subtraktionsoperator interpretiert wird. Um das zu ändern, muss man Klammern setzen:

nix-repl> builtins.elemAt [ "a" "b" "c" ] (-1)
error: list index -1 is out of bounds

Immer noch ein Fehler, aber ein besserer.

builtins.fetchurl #

Lädt etwas herunter, legt es im Nix Store ab und gibt den Pfad aus:

nix-repl> builtins.fetchurl https://arxiv.org/pdf/2208.10524
[4.2 MiB DL]"/nix/store/97ggi3ryxkvdljycw05nq82bgs6kdxcx-2208.10524"

Wenn man schon im Vorfeld den SHA-256 Hash der Datei kennt, kann man den Hash mit angeben und damit kryptografisch verifizieren, dass es die richtige Datei ist:

nix-repl> builtins.fetchurl
...         { url = https://arxiv.org/pdf/2208.10524 ;
...           sha256 = "cbd791862a937ac2f823b81a771e63bdb5313d204e0011965c833ee3889fdc73" ;
...         }
"/nix/store/97ggi3ryxkvdljycw05nq82bgs6kdxcx-2208.10524"

Wenn der Hash nicht passt, löst das einen Fehler aus:

nix-repl> builtins.fetchurl
...         { url = https://arxiv.org/pdf/2208.10524 ;
...           sha256 = "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" ;
...         }
error: hash mismatch in file downloaded from 'https://arxiv.org/pdf/2208.10524':
         specified: sha256:1zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz
                  got:       sha256:0wywky4f6gl3bjb1202f40yk3ddxccg7f6mq4gwc4ylk5a393myb
                  [4.2 MiB DL]

Der Hash in der Fehlermeldung unterscheidet sich von dem Hash in der Anweisung:

sha256 = "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" ;
...
specified: sha256:1zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz

Ich nehme an, dass Nix einfach nur eine andere Darstellung verwendet um die Hashes kompakter zu machen. Man sieht das auch daran, dass der Hash im Nix Store Pfad ein anderer ist. Es wäre gut, zu wissen, welcher Hash und welche Darstellung das genau ist, aber darum kümmere ich mich später.

Ich bin mir gar nich sicher, ob Nix die Datei jetzt noch einmal heruntergeladen hat oder einfach die bereits heruntergeladene Datei wiederverwendet hat. Also würde ich gern die Datei aus dem Store löschen und dann noch einmal herunterladen. Wie geht das? So:

$ nix-store --delete
    /nix/store/97ggi3ryxkvdljycw05nq82bgs6kdxcx-2208.10524

Aber Achtung: Bevor man die Datei aus dem Store löschen kann, muss man die Nix Repl terminieren. Solange die Repl noch läuft, verweigert Nix das Löschen, weil die Datei noch in Verwendung ist.

Ich lade die Datei also noch einmal mit einem falschen Hash herunter:

nix-repl> builtins.fetchurl
...         { url = https://arxiv.org/pdf/2208.10524 ;
...           sha256 = "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" ;
...         }
error: hash mismatch in file downloaded from 'https://arxiv.org/pdf/2208.10524':
         specified: sha256:1zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz
                  got:       sha256:0wywky4f6gl3bjb1202f40yk3ddxccg7f6mq4gwc4ylk5a393myb
                  [4.2 MiB DL]
                  n

Die finale Ausgabe stimmt überein, aber die Ausführung hat deutlich länger gedauert, weil Nix die Datei wirklich ein weiteres Mal heruntergeladen hat.

Mit builtins.fetchurl haben wir also die Möglichkeit, beliebige Dateien aus dem Netz herunterzuladen und die Echtheit dieser Dateien durch Angabe eines SHA-256 Hashes abzusichern. Das bedeutet, dass wir Dateien, deren Hash wir bereits kennen, aus beliebigen Quellen herunterladen können, ohne uns Sorgen darüber machen zu müssen, dass vielleicht jemand diese Dateien ohne unser Wissen verändert haben könnte. Das ist großartig! Nix ist ein System für die Verwaltung von Softwarepaketen, aber auch jenseits davon fallen mir für so etwas viele Einsatzmöglichkeiten ein.

builtins.filter #

Bereinigt eine Liste um alle Elemente, denen ein bestimmtes Prädikat fehlt:

nix-repl> isEven = n : n / 2 == n / 2.0

nix-repl> builtins.filter isEven
...         [ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 ]
[ 2 4 6 8 10 12 14 16 ]

builtins.filterSource #

Damit kann man Quellen in den Nix Store übertragen und dabei gewisse Dateien herausfiltern. Näher will ich das hier nicht betrachten. Die Dokumentation enthält eine Warnung, die man vor dem Gebrauch lesen und verstehen sollte.

builtins.findFile #

Weder in der Nix Repl noch im Handbuch dokumentiert.

builtins.floor #

Liefert für eine Zahl x die nächste Ganzzahl n sodass x >= n:

nix-repl> builtins.floor 1.5
1

nix-repl> builtins.floor 1
1

builtins.foldl' #

Damit kann man eine Liste von Werten sozusagen “zusammenfalten” auf einen einzigen Wert, indem man sukzessive von links nach rechts eine Funktion darauf anwendet, die jeweils zwei Werte mit einander kombiniert. Man muss einen Startwert mit übergeben. Ausgehend von diesem Startwert werden sukzessive die Elemente der Liste eingesammelt und mit dem bisherigen Zwischenergebnis kombiniert.

Hier wird eine Liste von Zahlen via builtins.add mit dem Startwert 0 zusammengefaltet:

nix-repl> builtins.foldl' builtins.add 0 [ 1 2 3 4 5 6 ]
21

Das entspricht einer Verallgemeinerung der zweiwertigen Addition auf beliebig viele Werte. Mit builtins.foldl' lassen sich viele zweiwertige Funktionen auf natürliche Weise verallgemeinern auf beliebig viele Werte. Das setzt natürlich voraus, dass es zu den Werten, auf denen man operiert einen neutralen Wert gibt, also eine Entsprechung zur Null bei den Zahlen. Mathematisch ist so eine Struktur artikuliert im algebraischen Begriff eines Monoiden. Damit muss man sich aber nicht unbedingt befassen: in der Praxis kann man auch einfach ein paar Werte mit builtins.foldl' zusammenstauchen, ohne sich vorher Gedanken darüber zu machen, ob man wirklich eine Null zur Verfügung hat.

Das zweite l in foldl steht für left, weil von rechts nach links gefaltet wird. Nix stellt nur diese eine Funktion als Built-in zur Verfügung, aber im Allgemeinen gibt es auch Faltungsfunktionen die von links nach rechts falten. Die heißen dann typischerweise foldr. Bei der Addition macht das keinen Unterschied, aber bei Funktionen, die nicht kommutativ sind ,bei denen also das Ergebnis von der Reihenfolge der beiden Argumente abhängt, hängt auch das Ergebnis einer Faltung davon ab, in welche Richtung gefaltet wird.

Das abschließende Hochkomma im Namen builtins.foldl' soll anzeigen, dass diese Funktion strikt ausgewertet wird. Ich bin weiter oben darauf eingegangen, dass Nix standardmäßig verzögert auswertet und dass man in manchen Fällen lieber eine strikte Auswertung haben möchte. Das hier ist ein solcher Fall. Das Hochkomma ist nur eine Namenskonvention, die warscheinlich daher rührt, dass man sich hier recht stark an Haskell orientiert. In Haskell wertet die foldl Funktion aus der Standardbibliothek verzögert aus. Daneben gibt es auch eine mit Hochkomma markierte foldl' Funktion, die strikt auswertet. In der Praxis will man eigentlich immer eine strikte Faltung nach links, also wäre es vielleicht besser gewesen, die strikte Variante foldl (ohne Hochkomma) zu nennen. Schade, dass Nix sich an dieser ungünstigen Konvention orientiert, zumal ohnehin nur diese eine Faltungsfunktion als Built-in mitgeliefert wird.

builtins.fromJSON #

Parst und überführt JSON-Werte in Nix-Werte:

nix-repl> builtins.fromJSON "{ \"a\" : 1 , \"b\" : 2 , \"c\" : 3 }"
{ a = 1; b = 2; c = 3; }

nix-repl> builtins.fromJSON "[ 1, 2, 3 ]"
[ 1 2 3 ]

nix-repl> builtins.fromJSON "123.456"
123.456

nix-repl> builtins.fromJSON "\"asdf\""
"asdf"

builtins.functionArgs #

Nimmt eine Funktion entgegen und liefert ein Set mit den Namen ihrer Parameter als Schlüssel. Die Werte geben für jeden Parameter Auskunft darüber, ob es zu ihm einen Standardwert gibt:

nix-repl> builtins.functionArgs ( { x , y ? 123 } : x + y )
{ x = false; y = true; }

Das Fragezeichen ist neue Syntax für mich. Ziemlich weit am Anfang dieses Dokumentes hatte ich Set Patterns erwähnt, die man verwenden kann, um benannte Funktionsparameter aufzuschreiben:

nix-repl> ({a,b} : a + b) { a = 5 ; b = 6 ; }
11

Offenbar kann man mit dem Fragezeichen Standardargumente festhalten:

nix-repl> ({a?5,b?6} : a + b) {}
11

nix-repl> ({a?5,b?6} : a + b) { a = 11 ; }
17

nix-repl> ({a?5,b?6} : a + b) { b = 12 ; }
17

nix-repl> ({a?5,b?6} : a + b) { a = 11 ; b = 12 ; }
23

Die sinnvolle Verwendung von builtins.functionArgs setzt natürlich voraus, dass eine Funktion mit benannten Parametern via Set Patterns aufgeschrieben worden ist. Für reine Lambda-Funktionen liefert builtins.functionArgs immer ein leeres Set:

nix-repl> builtins.functionArgs ( a : b : a + b )
{ }

builtins.genList #

builtins.genlist f n erzeugt eine Liste der Länge n, wobei die Funktion f die jeweiligen Elemente aus den jeweiligen Indizes berechnet:

nix-repl> builtins.genList ( x : -x ) 4
[ 0 -1 -2 -3 ]

nix-repl> builtins.genList ( x : 1.0/(x+1) ) 4
[ 1 0.5 0.333333 0.25 ]

builtins.genericClosure #

Das ist eine eigenartig spezifische und zugleich eigenartig allgemeine Funktion! Sie erinnert mich ein bisschen an das Märchen vom süßen Brei. Ich würde wirklich gern wissen, wofür sie gedacht ist. Ich erkenne darin jedenfalls keine so klare und universell brauchbare Abstraktion wie map oder filter oder foldl.

builtins.genericClosure nimmt ein Set mit den Schlüsseln startSet und operator entgegen. startSet ist eine Liste mit Sets. operator ist eine Funktion, die auf einem Set operiert und daraus eine Liste von weiteren Sets erzeugt. builtins.genericClosure verwendet ausgehend von der startSet Liste die operator Funktion um rekursiv aus den bestehenden Sets weitere Sets zu berechnen. Alle hier erwähnten Sets (abgesehen von dem äußeren) müssen einen Schlüssel key enthalten. Alle Sets, mit einem key Wert, der schon einmal aufgetaucht ist, werden verworfen. Sobald keine Sets mit neuen key Werten hinzukommen, endet die Rekursion.

Das ist ziemlich kompliziert. Hier sind ein paar Beispiele. Zunächst der einfachste Fall:

nix-repl> builtins.genericClosure
...         { startSet = [] ;
...           operator = s : [] ;
...         }
[ ]

Wir füllen startSet mit ein paar Sets:

nix-repl> builtins.genericClosure
...         { startSet =
...             [ { key = 5 ; }
...               { key = 6 ; }
...               { key = 7 ; }
...             ] ;
...           operator = s : [] ;
...         }
[ { ... } { ... } { ... } ]

Die Auslassungpunkte kommen daher, dass die Nix Repl beim Auswerten und beim Ausgeben der Ergebnisse maximal eine Ebene in die Tiefe geht. Bisher bin ich darauf nicht eingegangen, aber das ist schon einmal vorgekommen, nämlich im Abschnitt Sets (Mengen) wo ich die Kurznotation für verschachtelte Sets einführe. Man erhält die Auslassungspunkte schon wenn man eine Liste in eine Liste steckt:

nix-repl> [[]]
[ [ ... ] ]

Die innere Liste ist leer, aber der Interpreter schaut gar nicht erst hinein sondern gibt sie gleich mit den Auslassungspunkten aus.

Man kann die Nix Repl veranlassen, einen Ausdruck in voller Tiefe auszuwerten und das Ergebnis in voller Tiefe auszugeben. Dafür muss man dem auszuwertenden Ausdruck die Repl-Anweisung :p voranstellen. Damit können wir uns wieder builtins.genericClosure zuwenden:

nix-repl> :p builtins.genericClosure
...             { startSet =
...                 [ { key = 5 ; }
...                   { key = 6 ; }
...                   { key = 7 ; }
...                 ] ;
...               operator = s : [] ;
...             }
[ { key = 5; } { key = 6; } { key = 7; } ]

Dass Sets, deren key schon einmal vorgekommen ist, verworfen werden, gilt schon für die Sets in der startSet Liste:

nix-repl> :p builtins.genericClosure
...             { startSet =
...                 [ { key = 5 ; }
...                   { key = 6 ; }
...                   { key = 7 ; }
...                   { key = 5 ; }
...                 ] ;
...               operator = s : [] ;
...             }
[ { key = 5; } { key = 6; } { key = 7; } ]

Um die Wirkung der operator Funktion zu demonstrieren, inkrementieren wir einfach die key Werte. Damit built.genericClosure trotzdem terminiert, müssen wir das irgendwo deckeln. Ich schlage vor, wir belassen es bei key < 10:

nix-repl> :p builtins.genericClosure
...             { startSet =
...                 [ { key = 5 ; }
...                   { key = 6 ; }
...                   { key = 7 ; }
...                   { key = 5 ; }
...                 ] ;
...               operator = s :
...                 [ { key =
...                         if s.key < 9
...                         then s.key + 1
...                         else s.key ;
...                   }
...                 ] ;
...             }
[ { key = 5; } { key = 6; } { key = 7; } { key = 8; } { key = 9; } ]

Das heißt, wir inkrementieren den key Wert so lange er kleiner als 9 ist. Das Set mit key = 9 wird noch erzeugt, aber darüber kommt nichts mehr. Die Ausgabe ist so wie ich es erwartet habe. Ich glaube, ich habe damit vollständig erfasst, was builtins.genericClosure macht. Einen guten Anwendungsfall dafür habe ich nicht, aber zumindest den vagen Eindruck, dass man damit etwas nützliches machen kann, vielleicht irgend etwas in Richtung transitiver Hüllen.

builtins.getAttr #

Liefert aus einem Set den Wert zu einem Schlüssel:

nix-repl> builtins.getAttr "foo" { foo = 123 ; }
123

Wenn es den Schlüssel nicht gibt, löst das einen Fehler aus:

nix-repl> builtins.getAttr "bar" { foo = 123 ; }
error: attribute 'bar' missing for call to 'getAttr'

Das selbe kann man natürlich schon mit dem . Operator machen:

nix-repl> s = { foo = 123 ; }

nix-repl> s.foo
123

Aber es gibt einen Unterschied: builtins.getAttr nimmt den Schlüssel als String. Dadurch kann man den Schlüssel dynamisch konstruieren:

nix-repl> s = { foo = 123 ; }

nix-repl> builtins.getAttr ( "f" + "o" + "o" ) s
123

Update: der . Operator akzeptiert auch Strings:

nix-repl> { foo = 123 ; }."foo"
123

Aber dynamisch konstruieren kann man den Schlüssel trotzdem nicht:

nix-repl> { foo = 123 ; }.( "f" + "o" + "o" )
error: syntax error, unexpected '(', ...

nix-repl> { foo = 123 ; }.( "foo" )
error: syntax error, unexpected '(', ...

builtins.getContext #

Weder in der Nix Repl noch im Handbuch dokumentiert.

builtins.getEnv #

Liefert den Wert einer Umgebungsvariable als String:

nix-repl> builtins.getEnv "HOME"
"/home/aramis"

Für fehlende Umgebungsvariable wird der leere String wird der leere String gegeben:

nix-repl> builtins.getEnv "sa0iboojav9ood5C"
""

builtins.getFlake #

Lädt eine Nix Flake herunter und gibt ihre Attribute zusammen mit ein paar Metadaten aus.

Flakes sind ein alternativer Mechanismus um Abhängigkeiten zu beschreiben und Software in den Nix Store herunterzuladen. Nix Channels folgen in der Handhabung dem typischen Paketverwaltungsmodell von Linux Distros: Es gibt Kanäle, in denen versionierte Softwarepakete verfügbar sind, die man bei Bedarf herunterladen kann. Um seine Software auf dem neusten Stand zu halten, ruft man zunächst aus seinen Kanälen Informationen zu den aktuellen Versionen ab und installiert dann Updates für die Software, die nicht mehr auf dem neusten Stand ist.

So sieht das für die Debian/Ubuntu Paketverwaltung aus:

$ apt update            # Aktualisiere Kanäle
$ apt upgrade           # Aktualisiere Softwarepakete

So sieht das für Nix aus:

$ nix-channel --update  # Aktualisiere Kanäle
$ nix-env --upgrade     # Aktualisiere Softwarepakete

Mit Debian/Ubuntu verwendet man apt für beide Schritte. Mit Nix verwendet man im ersten Schritt nix-channel und im zweiten Schritt nix-env, aber an den Flags sieht man schon, mit wes Geistes Kind man es hier zu tun hat.

Im Unterschied dazu, orientieren sich Flakes an Paketverwaltungen wie Cargo (Rust) oder NPM (JS). Es gibt keine Kanäle. Man deklariert die Abhängigkeiten einer Software als Git Repositories in einer flake.nix Datei. Während der Installation der Abhängigkeiten wird eine flake.lock Datei erzeugt, welche die Hashes der verwendeten Git Commits protokolliert und dadurch die Abhängigkeiten sozusagen einfriert.

Flakes sind noch als experimentell markiert und müssen in einer Konfigurationsdatei aktiviert werden bevor sie verwendet werden können:

$ echo experimental-features = nix-command flakes  
    >> ~/.config/nix/nix.conf

Ich habe mir, über das hier aufgeschriebene hinaus, Flakes noch nicht genauer angeschaut.

builtins.groupBy #

builtings.groupBy f ls gruppiert die Liste ls anhand der Funktion f:

nix-repl> isEven = n : n / 2 == n / 2.0

nix-repl> :p builtins.groupBy
...             ( n : toString ( isEven n ) ) [ 1 2 3 4 5 6 7 8 9 ]
{ "" = [ 1 3 5 7 9 ]; "1" = [ 2 4 6 8 ]; }

Dabei muss f stets einen String zurückgeben. Sonst funktioniert das nicht:

nix-repl> :p builtins.groupBy isEven [ 1 2 3 4 5 6 7 8 9 ]
error: value is a Boolean while a string was expected

builtins.hasAttr #

Prüft ob ein Schlüssel in einem Set enthalten ist:

nix-repl> builtins.hasAttr "foo" {}
false

nix-repl> builtins.hasAttr "foo" { foo = 123 ; }
true

Offenbar gibt es auch einen ? Operator, der dasselbe macht:

nix-repl> {} ? "foo"
false

nix-repl> { foo = 123 ; } ? "foo"
true

Der ? Operator kann den zu prüfenden Schlüssel nicht nur als String sondern auch als einfachen Bezeichner verarbeiten:

nix-repl> { foo = 123 ; } ? foo
true

nix-repl> {} ? foo
false

Der ? Operator hat die selbe Beschränkung wie der . Operator, nämlich dass der Schlüssel nicht dynamisch erzeugt werden kann:

nix-repl> { foo = 123 ; } ? ( "f" + "o" + "o" )
error: syntax error, unexpected '(', ...

nix-repl> { foo = 123 ; } ? ( "foo" )
error: syntax error, unexpected '(', ...

builtins.hasContext #

Weder in der Nix Repl noch im Handbuch dokumentiert.

builtins.hashFile #

Erzeugt einen Hash Wert aus einer Datei. Erwartet werden zwei Argumente. Das erste Argument ist der Hash-Algorithmus. Möglich sind "md5", "sha1", "sha256" und "sha512". Das zweite Argument ist der Pfad zur Datei. Der Pfad kann als Nix Pfad oder als String übergeben werden.

nix-repl> builtins.hashFile "md5"
...         /nix/store/97ggi3ryxkvdljycw05nq82bgs6kdxcx-2208.10524
"ffab34ab46902e10183dc2a065e50ebd"

nix-repl> builtins.hashFile "md5"
...         "/nix/store/97ggi3ryxkvdljycw05nq82bgs6kdxcx-2208.10524"
"ffab34ab46902e10183dc2a065e50ebd"

nix-repl> builtins.hashFile "sha1"
...         /nix/store/97ggi3ryxkvdljycw05nq82bgs6kdxcx-2208.10524
"95936f2f3d784c7a90623acfbb9d093384f5a6a2"

nix-repl> builtins.hashFile "sha1"
...         "/nix/store/97ggi3ryxkvdljycw05nq82bgs6kdxcx-2208.10524"
"95936f2f3d784c7a90623acfbb9d093384f5a6a2"

Die Funktion verarbeitet beliebige Pfade, auch außerhalb des Nix Store. Leider weiß ich jetzt immer noch nicht, was es mit dem Hash 97ggi3ryxkvdljycw05nq82bgs6kdxcx im Store Path auf sich hat.

builtins.hashString #

Erzeugt einen Hash Wert aus einem String. Erwartet werden zwei Argumente. Das erste Argument ist der Hash-Algorithmus. Möglich sind "md5", "sha1", "sha256" und "sha512". Das zweite ist der zu verarbeitende String:

nix-repl> builtins.hashString "md5" "foobar"
"3858f62230ac3c915f300c664312c63f"

builtins.head #

Liefert das erste Element einer Liste:

nix-repl> builtins.head [ "a" "b" "c" ]
"a"

Wenn man sich das ganze im mathematichen Sinne als eine Abbildung von der Menge aller Listen auf die Menge aller möglichen Listenelemente vorzustellen versucht, stellt man fest, dass es in der Sprechweise der Schulmathematik eine mögliche Definitionslücke gibt: nämlich bei der leeren Liste:

nix-repl> builtins.head []
error: list index 0 is out of bounds

Die Fehlermeldung lässt vermuten, dass builtins.head ls intern in builtins.elemAt ls 0 übersetzt wird. Jedenfalls löst die Funktion für die leere Liste einen Fehler aus. Es ist strenggenommen nur eine partielle Funktion. Das ist schade, wobei ich hier noch mehr Verständnis dafür habe als bei der head Funktion aus der Haskell Standardbibliothek, die das gleiche Problem hat.

builtins.intersectAttrs #

Erwartet zwei Sets und liefert ein Set mit den Schlüssel-Wert-Paaren aus dem zweiten Set, deren Schlüssel auch im ersten Set vorkommen:

nix-repl> builtins.intersectAttrs
...         { a = 1 ; b = 2 ; } { b = 3 ; c = 4 ; }
{ b = 3; }

builtins.isAttrs #

Prüft, ob es sich bei einem Wert um ein Set handelt:

nix-repl> builtins.isAttrs {}
true

nix-repl> builtins.isAttrs 123
false

builtins.isBool #

Prüft, ob es sich bei einem Wert um einen boolschen Wert handelt:

nix-repl> builtins.isBool false
true

nix-repl> builtins.isBool 123
false

builtins.isFloat #

Prüft, ob es sich bei einem Wert um eine Fließkommazahl handelt:

nix-repl> builtins.isFloat 123.456
true

nix-repl> builtins.isFloat 123
false

builtins.isFunction #

Prüft, ob es sich bei einem Wert um eine Funktion handelt:

nix-repl> builtins.isFunction ( x : x )
true

nix-repl> builtins.isFunction 123
false

builtins.isInt #

Prüft, ob es sich bei einem Wert um eine Ganzzahl handelt:

nix-repl> builtins.isInt 123
true

nix-repl> builtins.isInt "123"
false

builtins.isList #

Prüft, ob es sich bei einem Wert um eine Liste handelt:

nix-repl> builtins.isList []
true

nix-repl> builtins.isList 123
false

builtins.isPath #

Prüft, ob es sich bei einem Wert um einen Nix Pfad handelt:

nix-repl> builtins.isPath ./.
true

nix-repl> builtins.isPath 123
false

builtins.isString #

Prüft, ob es sich bei einem Wert um eine Zeichenkette (String) handelt:

nix-repl> builtins.isString ""
true

nix-repl> builtins.isString 123
false

builtins.langVersion #

Undokumentiert. Wahrscheinlich ist Nix (die Sprache) irgendwie versioniert:

nix-repl> builtins.langVersion
6

builtins.length #

Liefert die Länge einer Liste:

nix-repl> builtins.length [ "a" "b" "c" ]
3

builtins.lessThan #

Die Funktion hinter dem < Operator:

nix-repl> builtins.lessThan 3 4
true

nix-repl> builtins.lessThan 4 3
false

builtins.listToAttrs #

Verarbeitet eine Liste von Sets mit Schlüsseln name und value zu einem Set mit entsprechenden Schlüssel-Wert-Paaren:

nix-repl> builtins.listToAttrs
...         [ { name = "foo" ; value = 123 ; }
...           { name = "bar" ; value = 456 ; }
...         ]
{ bar = 456; foo = 123; }

builtins.mapAttrs #

Eine Map-Funktion für Sets, die auf den Werten operiert und dabei die Schlüssel berücksichtigen kann:

nix-repl> builtins.mapAttrs
...         ( k : v : k + ":" + builtins.toString v )
...         { a = 1 ; b = 2 ; c = 3 ; }
{ a = "a:1"; b = "b:2"; c = "c:3"; }

builtins.match #

Erwartet einen regulären Ausdruck und einen String. Der reguläre Ausdruck kann RegEx-Gruppen enthalten. Als Ergebnis liefert builtins.match eine Liste der Übereinstimmungen für diese RegEx-Gruppen. Ohne RegEx-Gruppen ist das Ergebnis natürlich die leere Liste (bei Übereinstimmung). Wenn der String nicht auf den regulären Ausdruck passt, ist das Ergebnis null:

nix-repl> builtins.match "foo" "bar"
null

nix-repl> builtins.match "foo" "foo"
[ ]

nix-repl> builtins.match "a(b)c(d)e" "abcde"
[ "b" "d" ]

RegEx-Gruppen sind Teile eines regulären Ausdrucks, die durch Klammern hervorgehoben sind. Man verwendet sie, um nicht nur zu prüfen ob ein String auf einen regulären Ausdruck passt, sondern auch Teile aus dem String zu extrahieren. Beispielsweise könnte man einen regulären Ausdruck für postalische Adressen konstruieren, der die Straße, die Hausnummer, die Postleitzahl und die Stadt extrahiert.

builtins.mul #

Die Funktion hinter dem * Operator:

nix-repl> builtins.mul 3 5
15

builtins.nixPath #

Weder in der Nix Repl noch im Handbuch dokumentiert.

nix-repl> :p builtins.nixPath
[ { path = "/home/aramis/.nix-defexpr/channels"; prefix = ""; } ]

builtins.nixVersion #

Liefert die Nix Versionsnummer:

nix-repl> builtins.nixVersion
"2.10.3"

Die Nix Versionsnummer wird auch ausgegeben wenn man die Repl startet:

$ nix repl
Welcome to Nix 2.10.3. Type :? for help.

nix-repl>

builtins.parseDrvName #

Zerlegt einen Paketbezeichner der Form <Name>-<Version> in den Namen und die Version des Pakets auf. Das Ergebnis wird als Set mit den Schlüsseln name und version gegeben:

nix-repl> builtins.parseDrvName "nix-0.12pre12876"
{ name = "nix"; version = "0.12pre12876"; }

Die beiden Teile müssen durch einen Bindestrich getrennt sein. Die Version muss mit Ziffern beginnen. Sonst ist mindestens einer der beiden Werte der leere String:

nix-repl> builtins.parseDrvName ""
{ name = ""; version = ""; }

nix-repl> builtins.parseDrvName "foo123"
{ name = "foo123"; version = ""; }

nix-repl> builtins.parseDrvName "foo-bar"
{ name = "foo-bar"; version = ""; }

nix-repl> builtins.parseDrvName "foo-1bar"
{ name = "foo"; version = "1bar"; }

builtins.partition #

Trennt die Spreu vom Weizen:

nix-repl> :p builtins.partition ( n : n > 3 ) [ 1 2 3 4 5 6 ]
{ right = [ 4 5 6 ]; wrong = [ 1 2 3 ]; }

builtins.path #

Fügt Daten zum Nix Store hinzu. Die funktion nimmt ein Set als Argument entgegen. Nur der Schlüssel path ist Pflicht. Hier sind alle Schlüssel:

  • path: der Pfad zu den Daten
  • name: der Pfadname im Nix Store
  • filter: die Funktion; filtert unerwünschte Unterpfade heraus
  • recursive:
    • false: fügt den Pfad mit einem flachen Hash zum Store hinzu
    • true: fügt den Pfad mit einem Hash der NAR Serialisierung zum Store hinzu
  • sha256: der zu erwartende Hash für die Daten in path

NAR steht für Nix Archive. Das ist ein Serialisierungsformat für Dateisystemobjekte (i.e. Dateien, Verzeichnisse, Symlinks). NAR ist vergleichbar mit Archivformaten wie TAR und ZIP. Allerdings kann es bei diesen Formaten für ein und dasselbe Objekt mehrere gültige Serialisierungen geben, zum Beispiel weil die Reihenfolge der Serialisierung von Verzeichnisinhalten nicht definiert ist oder weil eine variable Menge an Füllbytes zwischen Segmenten der Serialisierung zulässig ist oder weil auch Zeitstempel mit in die Serialisierung aufgenommen werden. Die Beziehung zwischen einem serialisierten Objekt und den Bytes seiner Serialisierung hat somit keinen Abbildungscharakter. Das macht diese Archivformate unbrauchbar wenn der Hash der Serialisierung als Hash für das serialisierte Objekt geeignet sein soll. NAR ist speziell dafür entwickelt worden, Dateisystemobjekte für das Hashing zu serialisieren und lässt keinen Implementierungsspielraum, der die Serialisierung eines Dateisystemobjektes in verschiedene Bytefolgen gestatten würde. (Quelle: https://edolstra.github.io/pubs/phd-thesis.pdf)

Fügt Daten zum Nix Store hinzu. Die funktion nimmt ein Set als Argument entgegen. Nur der Schlüssel path ist Pflicht. Hier sind alle Schlüssel:

  • path: der Pfad zu den Daten
  • name: der Pfadname im Nix Store
  • filter: die Funktion; filtert unerwünschte Unterpfade heraus
  • recursive:
    • false: fügt den Pfad mit einem flachen Hash zum Store hinzu
    • true: fügt den Pfad mit einem Hash der NAR Serialisierung zum Store hinzu
  • sha256: der zu erwartende Hash für die Daten in path

NAR steht für Nix Archive. Das ist ein Serialisierungsformat für Dateisystemobjekte (i.e. Dateien, Verzeichnisse, Symlinks). NAR ist vergleichbar mit Archivformaten wie TAR und ZIP. Allerdings kann es bei diesen Formaten für ein und dasselbe Objekt mehrere gültige Serialisierungen geben, zum Beispiel weil die Reihenfolge der Serialisierung von Verzeichnisinhalten nicht definiert ist oder weil eine variable Menge an Füllbytes zwischen Segmenten der Serialisierung zulässig ist oder weil auch Zeitstempel mit in die Serialisierung aufgenommen werden. Die Beziehung zwischen einem serialisierten Objekt und den Bytes seiner Serialisierung hat somit keinen Abbildungscharakter. Das macht diese Archivformate unbrauchbar wenn der Hash der Serialisierung als Hash für das serialisierte Objekt geeignet sein soll. NAR ist speziell dafür entwickelt worden, Dateisystemobjekte für das Hashing zu serialisieren und lässt keinen Implementierungsspielraum, der die Serialisierung eines Dateisystemobjektes in verschiedene Bytefolgen gestatten würde. (Quelle: https://edolstra.github.io/pubs/phd-thesis.pdf)

builtins.pathExists #

Prüft, ob ein Pfad im lokalen Dateisystem existiert:

nix-repl> builtins.pathExists /home/aramis
true

nix-repl> builtins.pathExists /home/foo
false

Kann auch Strings verarbeiten:

nix-repl> builtins.pathExists "/"
true

Oben hatte ich ein paar Prozeduren aufgelistet, die Nix impure machen. Diese hier (und die nachfolgenden) hätte man auch mit auflisten können.

Prüft, ob ein Pfad im lokalen Dateisystem existiert:

nix-repl> builtins.pathExists /home/aramis
true

nix-repl> builtins.pathExists /home/foo
false

Kann auch Strings verarbeiten:

nix-repl> builtins.pathExists "/"
true

Oben hatte ich ein paar Prozeduren aufgelistet, die Nix impure machen. Diese hier (und die nachfolgenden) hätte man auch mit auflisten können.

builtins.readDir #

Liefert für einen Verzeichnispfad ein Set mit den Verzeichnisinhalten als Schlüssel und den jeweiligen Dateitypen als Werte:

nix-repl> builtins.readDir /home
{ aramis = "directory"; }

Die möglichen Werte für den Dateityp sind "regular", "directory", "symlink" und "unknown".

Liefert für einen Verzeichnispfad ein Set mit den Verzeichnisinhalten als Schlüssel und den jeweiligen Dateitypen als Werte:

nix-repl> builtins.readDir /home
{ aramis = "directory"; }

Die möglichen Werte für den Dateityp sind "regular", "directory", "symlink" und "unknown".

builtins.readFile #

Liefert den Inhalt einer Datei als String:

$ echo hallo > greeting.txt

$ nix repl
Welcome to Nix 2.10.3. Type :? for help.

nix-repl> builtins.readFile ./greeting.txt
"hallo\n"

Liefert den Inhalt einer Datei als String:

$ echo hallo > greeting.txt

$ nix repl
Welcome to Nix 2.10.3. Type :? for help.

nix-repl> builtins.readFile ./greeting.txt
"hallo\n"

builtins.replaceStrings #

Ersetzt alle Vorkommen eines Teilstrings:

nix-repl> builtins.replaceStrings [ "o" ] [ "x" ] "foobar"
"fxxbar"

nix-repl> builtins.replaceStrings [ "o" "a" ] [ "x" "y" ] "foobar"
"fxxbyr"

Ersetzt alle Vorkommen eines Teilstrings:

nix-repl> builtins.replaceStrings [ "o" ] [ "x" ] "foobar"
"fxxbar"

nix-repl> builtins.replaceStrings [ "o" "a" ] [ "x" "y" ] "foobar"
"fxxbyr"

builtins.sort #

Sortiert eine Liste anhand einer Vergleichsfunktion:

nix-repl> builtins.sort
...         ( a : b : a < b )
...         [ 3 1 4 1 5 9 2 6 5 3 5 8 9 7 9 3 ]
[ 1 1 2 3 3 3 4 5 5 5 6 7 8 9 9 9 ]

nix-repl> builtins.sort
...         ( a : b : a > b )
...         [ 3 1 4 1 5 9 2 6 5 3 5 8 9 7 9 3 ]
[ 9 9 9 8 7 6 5 5 5 4 3 3 3 2 1 1 ]

Sortiert eine Liste anhand einer Vergleichsfunktion:

nix-repl> builtins.sort ( a : b : a < b ) [ 3 1 4 1 5 9 2 6 5 3 5 8 9 7 9 3 ]
[ 1 1 2 3 3 3 4 5 5 5 6 7 8 9 9 9 ]

nix-repl> builtins.sort ( a : b : a > b ) [ 3 1 4 1 5 9 2 6 5 3 5 8 9 7 9 3 ]
[ 9 9 9 8 7 6 5 5 5 4 3 3 3 2 1 1 ]

builtins.split #

Zerteilt einen String anhand eines regulären Ausdrucks in eine Liste. Alles, was auf den regulären Ausdruck passt, wird als Trennzeichen behandelt. Der reguläre Ausdruck kann RegEx-Gruppen enthalten. An jeder Trennstelle wird eine Liste mit den Übereinstimmungen für die RegEx-Gruppen an dieser Stelle in die Ergebnisliste aufgenommen. Ohne RegEx-Gruppen werden an den Trennstellen entsprechend leere Listen eingefügt:

nix-repl> :p builtins.split "a" "bacadaeafagahai"
[ "b" [ ] "c" [ ] "d" [ ] "e" [ ] "f" [ ] "g" [ ] "h" [ ] "i" ]

nix-repl> builtins.split "a" "bcde"
[ "bcde" ]

Siehe builtins.match für mehr zu RegEx-Gruppen.

Zerteilt einen String anhand eines regulären Ausdrucks in eine Liste. Alles, was auf den regulären Ausdruck passt, wird als Trennzeichen behandelt. Der reguläre Ausdruck kann RegEx-Gruppen enthalten. An jeder Trennstelle wird eine Liste mit den Übereinstimmungen für die RegEx-Gruppen an dieser Stelle in die Ergebnisliste aufgenommen. Ohne RegEx-Gruppen werden an den Trennstellen entsprechend leere Listen eingefügt:

nix-repl> :p builtins.split "a" "bacadaeafagahai"
[ "b" [ ] "c" [ ] "d" [ ] "e" [ ] "f" [ ] "g" [ ] "h" [ ] "i" ]

nix-repl> builtins.split "a" "bcde"
[ "bcde" ]

Siehe builtins.match für mehr zu RegEx-Gruppen.

builtins.splitVersion #

Teilt einen String, der eine (Software) Version darstellt, in seine Bestandteile auf. Ich bin mir nicht 100%ig sicher, aber ich denke, die Trennstellen sind Punkte ., Bindestriche - und Übergänge zwischen Ziffern und Buchstaben:

nix-repl> builtins.splitVersion "a.b-c123e"
[ "a" "b" "c" "123" "e" ]

Teilt einen String, der eine (Software) Version darstellt, in seine Bestandteile auf. Ich bin mir nicht 100%ig sicher, aber ich denke, die Trennstellen sind Punkte ., Bindestriche - und Übergänge zwischen Ziffern und Buchstaben:

nix-repl> builtins.splitVersion "a.b-c123e"
[ "a" "b" "c" "123" "e" ]

builtins.storeDir #

Undokumentiert. Liefert den Pfad zum Nix Store als String:

nix-repl> builtins.storeDir
"/nix/store"

Undokumentiert. Liefert den Pfad zum Nix Store als String:

nix-repl> builtins.storeDir
"/nix/store"

builtins.storePath #

Alles, was ein Programm benötigt, um gebaut zu werden, soll sich im Nix Store befinden. Entsprechend werden die Abhängigkeiten, die in Ableitungen (Derivations) deklariert sind, zunächst zum Nix Store hinzugefügt. Das geschieht normalerweise auch dann, wenn sich etwas schon im Nix Store befindet. Mit builtins.storePath kann man das vermeiden.

Alles, was ein Programm benötigt, um gebaut zu werden, soll sich im Nix Store befinden. Entsprechend werden die Abhängigkeiten, die in Ableitungen (Derivations) deklariert sind, zunächst zum Nix Store hinzugefügt. Das geschieht normalerweise auch dann, wenn sich etwas schon im Nix Store befindet. Mit builtins.storePath kann man das vermeiden.

builtins.stringLength #

Liefert die Länge eines Strings:

nix-repl> builtins.stringLength "asdf"
4

Liefert die Länge eines Strings:

nix-repl> builtins.stringLength "asdf"
4

builtins.sub #

Die Funktion hinter dem - Operator:

nix-repl> builtins.sub 7 5
2

Die Funktion hinter dem - Operator:

nix-repl> builtins.sub 7 5
2

builtins.substring #

Selbsterklärend:

nix-repl> builtins.substring 0 3 "nixos"
"nix"

Selbsterklärend:

nix-repl> builtins.substring 0 3 "nixos"
"nix"

builtins.tail #

Liefert eine Liste ohne das erste Element:

nix-repl> builtins.tail [ 1 2 3 ]
[ 2 3 ]

nix-repl> builtins.tail []
error: 'tail' called on an empty list

Liefert eine Liste ohne das erste Element:

nix-repl> builtins.tail [ 1 2 3 ]
[ 2 3 ]

nix-repl> builtins.tail []
error: 'tail' called on an empty list

builtins.toFile #

Schreibt einen String in eine Datei im Nix Store und gibt den Pfad dieser Datei zurück:

nix-repl> builtins.toFile "greeting" "hallo"
"/nix/store/8fday1j2s9rpc28yirwbjp59a1wr3rx3-greeting"

Die Datei kann dann beispielsweise als Input für Derivationen verwendet werden. Damit lassen sich beispielsweise Build Skripte inline unterbringen.

builtins.toJSON #

Übersetzt einen Nix Ausdruck in sein Json Äquivalent, aber mit ein paar effektvollen Besonderheiten. Strings, Integers, Floats, Bools, null und Listen werden einfach in ihr Json Äquivalent übersetzt. Nix Sets werden zu Json Objekten. Davon ausgenommen sind Derivationen: die werden in den entsprechenden Ausgabepfad übersetzt (als Json String). Nix Paths werden in den Nix Store kopiert und zu ihrem Zielpfad evaluiert (als Json String).

nix-repl> builtins.toJSON /home/aramis/todo.txt
"\"/nix/store/dmj65s7zfyzzypymaihy8yd1mrdz27pm-todo.txt\""

nix-repl> builtins.toJSON 123
"123"

nix-repl> builtins.toJSON "123"
"\"123\""

nix-repl> builtins.toJSON []
"[]"

nix-repl> builtins.toJSON {}
"{}"

Ich frage mich, wie hier unterschieden wird zwischen Derivationen und anderen Sets.

builtins.toPath #

DEPRECATED.

Man soll stattdessen für absolute Pfade /. + "/path" und für relative Pfade ./. + "/path" verwenden.

builtins.toXML #

Übersetzt einen Nix Ausdruck in eine XML Darstellung:

nix-repl> builtins.toXML 123
"""<?xml version='1.0' encoding='utf-8'?>
<expr>
  <int value=\"123\" />
</expr>
"""

nix-repl> builtins.toXML "123"
"""<?xml version='1.0' encoding='utf-8'?>
<expr>
  <string value=\"123\" />
</expr>
"""

nix-repl> builtins.toXML []
"""<?xml version='1.0' encoding='utf-8'?>
<expr>
  <list>
  </list>
</expr>
"""

nix-repl> builtins.toXML {}
"""<?xml version='1.0' encoding='utf-8'?>
<expr>
  <attrs>
  </attrs>
</expr>
"""

Das ist dafür da, mit einem Build Script auf eine strukturiertere Weise Daten auszutauschen als es allein mit Umgebungsvariablen möglich ist.

Irgendwie verursacht XML in mir so etwas wie PTSD.

builtins.traceVerbose #

Wenn die Flag --trace-verbose aktiv ist, entspricht ein Aufruf von builtins.trace einem Aufruf von builtins.trace:

$ nix repl --trace-verbose
Welcome to Nix 2.10.3. Type :? for help.

nix-repl> builtins.traceVerbose [ 1 2 3 ] "foo"
trace: [ 1 2 3 ]
"foo"

Ohne die Flag wertet builtins.traceVerbose e1 e2 einfach nur zu e2 aus:

$ nix repl
Welcome to Nix 2.10.3. Type :? for help.

nix-repl> builtins.traceVerbose [ 1 2 3 ] "foo"
"foo"

builtins.tryEval #

Darauf gehe ich im Abschnitt Fehler ein.

builtins.typeOf #

Liefert einen String, der den Datentyp eines Ausdrucks bezeichnet. Die Datentypen sind "int", "bool", "string", "path", "null", "set", "list", "lambda" und "float":

nix-repl> builtins.typeOf 0
"int"

nix-repl> builtins.typeOf false
"bool"

nix-repl> builtins.typeOf ""
"string"

nix-repl> builtins.typeOf /.
"path"

nix-repl> builtins.typeOf null
"null"

nix-repl> builtins.typeOf {}
"set"

nix-repl> builtins.typeOf []
"list"

nix-repl> builtins.typeOf ( x : x )
"lambda"

nix-repl> builtins.typeOf 0.0
"float"

builtins.unsafeDiscardOutputDependency #

Undokumentiert.

builtins.unsafeDiscardStringContext #

Undokumentiert.

builtins.unsafeGetAttrPos #

Undokumentiert.

builtins.zipAttrsWith #

Das ist wieder so eine Funktion, die etwas ausführlicher beschrieben werden muss.

Sie nimmt eine zweiwertige Funktion f und eine Liste von Sets entgegen. Aus den Sets werden zunächst die Werte für die jeweiligen Schlüssel in Listen gesammelt. Aus den Schlüsseln und den zugehörigen Listen wird ein Set erstellt. Auf dieses Set wird builtins.mapAttrs f angewendet.

Schauen wir uns das zunächst mit einer neutralen Funktion f an:

nix-repl> :p builtins.zipAttrsWith
...             ( k : v : v )
...             [ { x = 1 ; y = 2 ; }
...               { x = 3 ; y = 4 ; z = 5 ; }
...             ]
{ x = [ 1 3 ]; y = [ 2 4 ]; z = [ 5 ]; }

Ich denke, hier sieht man ganz gut, was vor sich geht: alle Schlüssel werden eingesammelt und für jeden Schlüssel werden die zugehörigen Werte in Listen akkumuliert. Diese Listen sind naturgemäß nichtleer (sonst gäbe es keinen zugehörigen Schlüssel).

Wir können dann mit f auf diesen Listen operieren. Beispielsweise können wir zählen, wie oft jeder Schlüssel vorkommt:

nix-repl> :p builtins.zipAttrsWith
...             ( k : v : builtins.length v )
...             [ { x = 1 ; y = 2 ; }
...               { x = 3 ; y = 4 ; z = 5 ; }
...             ]
{ x = 2; y = 2; z = 1; }

Oder wir finden den jeweils größten Wert für jeden Schlüssel. Dafür bauen wir uns zunächst eine max Funktion für nichtnegative Zahlen:

nix-repl> max = ls :
...         builtins.foldl'
...             ( a : b : if a > b then a else b )
...             0
...             ls

nix-repl> max [ 1 2 3 4 5 4 3 2 1 ]
5

Damit können wir die größten Schlüssel finden:

nix-repl> :p builtins.zipAttrsWith
...             ( k : v : max v )
...             [ { x = 1 ; y = 2 ; }
...               { x = 3 ; y = 4 ; z = 5 ; }
...             ]
{ x = 3; y = 4; z = 5; }

Damit ist meine erste Erkundung der Nix Expression Language abgeschlossen. Vielleicht gibt es hier und da noch einen unbeleuchteten Aspekt, aber im großen und ganzen habe ich einen guten Überblick und ein gutes Gefühl für die Sprache. Syntaktisch erinnert sie mich weniger an Haskell aber vielleicht ein bisschen an OCaml und an Coqs Gallina Sprache. Semantisch sind wir eher im Erlang/Ruby/JavaScript-Land: dynamische Typisierung, polymorphe Listen usw.

Damit kann ich Nix (die Sprache) zunächst abhaken. Als nächstes arbeite ich G. Gonzalez’ Vortrag Nix: under the hood durch, um den Nix Store und die Nix CLI-Befehle besser kennenzulernen.