Nix Pills, Chapter 4 (Kommentar)

Im vierten Kapitel der Nix Pills, The Basics of the Language, geht es um die Nix Expression Language.

Im vierten Kapitel der Nix Pills, The Basics of the Language, geht es um die Nix Expression Language. Darin geht es nicht primär darum, Features der Sprache für die Zwecke von Nix zu motivieren. Stattdessen wird (anders als der Titel vermuten lässt) auf einzelne Details einiger Typen und Ausdrüce der Sprache eingegangen. Beiläufig werden einige Merkmale genannt, die die funktionale Sprache von anderen Programmiersprachen unterscheidet.

Das Kapitel erzählt keine “Geschichte”. Ich werde hier deshalb nur versuchen, die gegebenen Informationen auf übersichtliche Weise zu strukturieren.

Zwecke der Sprache

The Nix language is used to write expressions that produce derivations.

Das ist eine bemerkenswerte Redeweise. Was sind Ausdrücke, die Derivations produzieren? Ist damit gemeint, dass die Ausdrücke etwas ableiten? Oder ist lediglich gemeint, dass sie Derivations (als Konstrukt der Sprache) ausdrücken?

Wir erfahren auch, dass Derivations etwas sind, das gebaut (build) werden kann. Dazu wird das nix-build-Tool genutzt: “The nix-build tool is used to build derivations from an expression.”

Im vorausgegangenen Kapiteln wurde gesagt, dass “Derivation” Nix-Jargon für Pakete ist. Das folgende Zitat scheint deshalb zu sagen, dass die Nix-Sprache dahingehend optimiert wurde, darin Derivations (Paketdefinitionen) auszudrücken:

(…) (T)he (…) syntax is great for describing packages, so learning the language itself will pay off when writing package expressions.

Leider erfahren wir sehr wenig darüber, was Nix so toll für diese Zwecke macht. Soweit ich sehe wird dieser Punkt nur im Abschnitt über Laziness angesprochen:

Nix evaluates expressions only when needed. This is a great feature when working with packages. (…) (W)e can have all the packages defined on demand, yet have access to specific packages very quickly.

Nix ist nicht dazu geschaffen, algorithmisch beliebige Probleme zu lösen. Mit anderen Worten, es ist keine general-purpose language, sondern dient einem einzig den Zwecken des Nix-Deployment-Systems. Daraus erklärt sich folgendes Zitat:

The syntax of Nix is (…) mostly about writing utility functions to make things convenient.

Die Nix-Sprache ist darauf zugeschnitten, Pakte zu beschreiben. Doch Nix ist nicht bloß ein Paketmanager; es ist ein Deployment-System. Wahrscheinlich gilt, was für Pakete gesagt wird, auf ähnliche Weise auch für Entwicklungsumgebungen und vielleicht sogar Systemkonfigurationen?

Merkmale der Sprache

Nix REPL

Mit dem Befehl Tool nix repl kann eine interaktive Umgebung betreten werden, in der Ausdrücke zur Auswertung eingegeben werden können. Die Resultate werden unmittelbar ausgegeben. Dadurch können wir mit der Sprache rumspielen und testen, ob komplexere Ausdrücke die Werte haben, die wir erwarten würden.

Die Umgebung weist eine Besonderheit auf. Variablen können durch <Name> = <Wert> definiert werden. Für gewöhnlich können Variablen nur im Rahmen von let ... ; in-Ausdrücke definiert und verwendet werden.

Typen

Nix has integer, floating point, string, path, boolean and null simple types. Then there are also lists, (attribute) sets and functions.

Oben wurde gesagt, dass der Hauptzweck der Sprache darin besteht, Pakete zu beschreiben. Dieser domainspezifische Zweck informiert die Syntax der Sprache. Dafür wird ein Beispiel gegeben: 6/3 wird als Pfad relativ zum Arbeitsverzeichnis interpretiert. Andere Sprachen würden den Ausdruck so verstehen, dass damit Integerdivision denotiert wird. Das ist in Nix nur dann der Fall, wenn Leerzeichen um den Operator eingefügt werden. Diese Designentscheidung erklärt sich dadurch, dass Pfade für den Zweck der Sprache weitaus wichtiger sind als arithmetische Rechnungen.

Zur allgemeinen Syntax von Pfaden wird gesagt: “(…) (E)xpressions will be parsed as paths as long as there’s a slash not followed by a space. Therefore to specify the current directory, use ./.. In addition, Nix also parses urls specially.”

Es stellt sich die Frage, was Pfade von Strings unterscheidet. Dazu wird lediglich gesagt: “Literal urls and paths are convenient for additional safety.” Die Resultate der Beispiele legen nahe, dass sie stets als absolute Pfade aufgelöst werden. Von anderen Artikeln meine ich mich zu erinnern, dass es in Nix so etwas wie eine Pfad-Normalform gibt.

Operatoren

Es werden eine Reihe von Operatoren aufgelistet. Arithmetische Operatoren, boolesche Operatoren, Vergleichsoperatoren, was man erwartet würde.

Verkettungsoperatoren für Strings, Listen und Attributmengen werden nicht angeführt. Von andern Quellen weiß ich aber, dass es sie gibt. + für Strings und Pfade, ++ für Listen und // für die Vereinigung von Attributmengen. Wenn ein Attributname in beiden aber mit verschiedenen Werten vorkommt, hat der Wert der Menge auf der rechten Seite Vorrang.

Ähnlich wie das obige Beispiel mit / verhält es sich bei -. Es wird nur dann als Subtraktion interpretiert, wenn Leerzeichen darum eingefügt werden. Ansonsten wird es als Teil eines Bezeichners (identifier) geparst. Kebab Case wird vor allem zugelassen, weil viele Paketnamen Bindestriche enthalten.

Strings

Strings werden durch doppelte Anführungszeichen (") oder durch zwei einfache Anführungszeichen ('') angezeigt. String-Literale mit einfachen Anführungszeichen gibt es in Nix nicht.

Zu zwei einfachen Anführungszeichen wird lediglich gesagt, dass dadurch Escapes von doppelten Anführungszeichen (als Zeichen der Kette) vermieden werden können. Ihr eigentlicher Zweck, damit mehrzeilige Strings zu denotieren, wird nicht angesprochen.

Der Abschnitt geht vor allem um Interpolation. Mit ${<Ausdruck>} kann der Wert eines Ausdrucks an die entsprechende Stelle in einen String eingefügt werden. Es wird darauf hingewiesen, dass der Wert des Ausdrucks kein Integer sein darf. Das heißt Zahlen werden nicht automatisch zu Ketten von Ziffern umgewandelt.

Es wird nicht gesagt, ob nur String-Ausdrücke durch Interpolation in ein String-Literal eingefügt werden kann. Pfade vermutlich auch.

In den seltenen Fällen, wo wir ${...} literal verwenden wollen, kann die gesamte Konstruktion durch einen Escape-String escapt werden. Es ist zwischen doppelten Anführungszeichen und zwei einfachen Anführungszeichen zu unterscheiden:

nix-repl> "\${foo}"
"${foo}"
nix-repl> ''test ''${foo} test''
"test ${foo} test"

Listen

Über Listen erfahren wir zwei Dinge. Syntaktisch werden sie dadurch konstruiert, dass Ausdrücke von eckigen Klammern umschlossen und voneinander durch Leerzeichen (nicht Kommas) getrennt werden. Dadurch entsteht eine (geordnete) Liste mit den Werten der Ausdrücke.

Es wird auch betont, dass Listen unveränderlich sind. Das ist genau genommen nicht weiter überraschend; alle Daten sind in Nix unverändlich. Im Falle von Listen ist die Idee davon, Elemente hinzuzufügen oder zu entfernen, aber vielleicht so natürlich, dass man sie nicht weiter in Frage stellt.

Wir erfahren, dass es in Nix Operationen gibt, mit denen wir ausgehend von bestehenden Listen neue Listen erzeugen können. Dazu gehören auch Operationen, die ähnlich wie das Hinzufügen oder Entfernen von Elementen verwendet werden können.

Attributmengen

Bei Attributmengen werden Schlüsseln (keys) jeweils einem Wert zugewiesen. Es wird ausdrücklich gesagt, dass Schlüssel notwendig Strings sein müssen. Doch in vielen Fällen ist es nicht notwendig, Attributnamen durch Anführungszeichen anzuzeigen.

(…) (D)o not confuse attribute sets with argument sets used in functions.

Das ist interessant. Ich habe die Konstruktionen mit den geschweiften Klammern bei Lambdas bisher tatsächlich als eine Art Attributmenge aufgefasst. Natürlich fehlen die Werte für die Attribute. Vermutlich findet beim Funktionsaufruf eine Art Pattern-Matching statt.

Statt Argumentmenge sollte es bei Funktionsdefinitionen vielleicht besser Parametermengen heißen. Beim Aufruf wird der Funktion sehr wohl eine Attributmenge als Argument übergeben, und die Attribute werden den gleichnamigen Parametern in der Parametermenge zugeordnet. Die Parameter erhalten dadurch die Attributwerte als ihre Werte.

Wir erfahren, dass durch .-Notation auf die Werte von Attributen in Mengen zugegriffen werden kann. Vorm Punkt können Literale und Variablen stehen.

Wir erfahren auch, dass wir wir innerhalb einer Attributmenge für gewöhnlich nicht auf Attribute derselben Menge verweisen können. Das ist aber möglich, wenn der Menge rec vorangestellt wird.

if-Ausdrücke

Wie oben gesagt, gibt es in Nix keine Anweisungen. Das bedeutet, dass sogar Abfragen Ausdrücke sind. Syntaktisch haben sie die Form if ... then ... (kein Semikolon).

Der else-Zweig ist obligatorisch. Der Wert des Gesamtausdrucks ist der Wert des Teilausdrucks auf dem Zweig, der gewählt wird. else garantiert, dass auf jeden Fall ein Zweig gewählt wird; und damit, dass der Ausdruck insgesamt einen Wert hat.

Es wird nicht darauf eingegangen, in welchen Kontexten wir in Nix für gewöhnlich Gebrauch von Abfragen machen. Es liegt nahe, dass sie für eine modulare Herangehensweise sehr nützlich sind.

let-Ausdrücke

Variablen können in Nix nicht global definiert werden. Das ist wohl auch eine Konsequenz daraus, dass es keine Anweisungen in der Sprache gibt. Stattdessen haben wir einen Ausdruck der folgenden Form:

let 
  <Variablendefinition 1>; 
  ...; 
  <Variablendefinition n>;
in 
  <Ausdruck>

Dadurch werden eine oder mehr Variablen lokal für den Ausdruck gültig gemacht. Der Wert des Gesamtausdrucks ist der Wert des Ausdrucks im in-Teilausdruck.

let-Ausdrücke können ineinander verschachtelt werden.

nix-repl> let a = 3; in let b = 4; in a + b
7

In Nix sind nicht nur Werte, sondern auch Variablen unveränderlich (immutable). Das heißt einer bereits initialisierten Variable kann kein neuer Wert zugeschrieben werden.

nix-repl> let a = 3; a = 8; in a
error: attribute `a' at (string):1:12 already defined at (string):1:5

Shadowing hingegen ist möglich. Das heißt es kann in einem inneren Skopus eine Variable mit dem gleichen Namen definiert werden; wenn der innere Gültigkeitsbereich verlassen wird, ist über den Namen wieder die andere außen noch gültige Variable erreichbar.

nix-repl> let a = 3; in let a = 8; in a
8

Wenn ich das Beispiel richtig lese, dann findet sich im in-Teil des ersten let-Ausdrucks gar keine Verwendung von a. Tatsächlich wird gar keine Variable verwendet: es ist, als hätten wir let a = 3; in 8 geschrieben.

Die im let-Teil definierten Variablen stehen für andere Definitionen zu Verfügung.

nix-repl> let a = 4; b = a + 5; in b
9

Hier wurde die zuvor definierte a-Variable verwendet, um b zu definieren.

Der Abschnitt endet mit einer Warnung:

(…) (B)eware when you want to refer to a variable from the outer scope, but it’s also defined in the current let expression.

Es wird nicht darauf eingegangen, wie man in diesem Fall auf den Wert der äußeren Variable zugreift. Vielleicht gibt es dafür keine Möglichkeit?

with-Ausdrücke

Oben wurde gesagt, dass wir mit .-Notation auf den Wert von Attributen in Attributmengen zugreifen. Wenn die Menge einer Variablen zugewiesen wurde, steht vorm . der Name der Variablen. Die Attributnamen sind nur in dem Namensraum (namespace) verfügbar, den wir auf diese Weise definiert haben.

Durch with <Attributmenge-Variable>; <Ausdruck> wird es möglich, die Attribute im Ausdruck zu verwenden als wären sie als Variablen definiert worden.

nix-repl> longName = { a = 3; b = 4; }
nix-repl> longName.a + longName.b
7
nix-repl> with longName; a + b
7

with verhält sich nicht perfekt, als wäre eine Variable mit dem Attributnamen als Bezeichner definiert worden. Zwei Dinge gilt es zu beachten.

Of course, only valid identifiers from the keys of the set will be included.

Beliebige Strings können als Attributnamen verwendet werden, doch nicht jeder Name ist ein gültiger Variablenbezeichner. Das gilt beispielsweise für "123", ein oben verwendeter Schlüssel.

If a symbol exists in the outer scope and would also be introduced by the with, it will not be shadowed.

Dadurch erklärt sich die folgende Ausgabe:

nix-repl> longName = { a = 3; b = 4; }
nix-repl> let a = 10; in with longName; a + b
14

Das neu in den Skopus eingeführte a (aus der Attributmenge) überschattet nicht das im let-Teil definierte a. Ein b hingegeben gab es noch nicht im Gültigkeitsbereich, der Name zeigt also auf das b der Menge. Wollen wir auf das a der Menge zugreifen, muss weiterhin der Namensraum angezeigt werden.

nix-repl> longName = { a = 3; b = 4; }
nix-repl> let a = 10; in with longName; longName.a + b
7

Offene Fragen

Fußnoten

  1. Es wird darauf hingewiesen, dass sich funktionale Sprachen allgemein dadurch auszeichnen, dass sie nur Ausdrücke und keine Anweisungen nutzen.