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
- Nix Systemtyp, z.B.
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 als1
übergeben.false
undnull
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 Datenname
: der Pfadname im Nix Storefilter
: die Funktion; filtert unerwünschte Unterpfade herausrecursive
:false
: fügt den Pfad mit einem flachen Hash zum Store hinzutrue
: fügt den Pfad mit einem Hash der NAR Serialisierung zum Store hinzu
sha256
: der zu erwartende Hash für die Daten inpath
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 Datenname
: der Pfadname im Nix Storefilter
: die Funktion; filtert unerwünschte Unterpfade herausrecursive
:false
: fügt den Pfad mit einem flachen Hash zum Store hinzutrue
: fügt den Pfad mit einem Hash der NAR Serialisierung zum Store hinzu
sha256
: der zu erwartende Hash für die Daten inpath
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.