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
- 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.