7 Punkte von GN⁺ 2025-08-22 | 1 Kommentare | Auf WhatsApp teilen
  • Zig basiert wie Rust auf einer geschweiften Klammern basierten Syntax, verbessert sie jedoch durch einfachere Sprachsemantik und elegantere Syntaxentscheidungen
  • Integer-Literale beginnen unabhängig vom Typ als comptime_int und werden bei der Zuweisung explizit konvertiert, während String-Literale eine kompakte Raw-String-Notation auf Basis von \\ verwenden
  • Record-Literale in der Form .x = 1 machen Feldzuweisungen leichter durchsuchbar, und alle Typen werden konsistent in Präfixnotation dargestellt
  • and und or werden als Schlüsselwörter für den Kontrollfluss verwendet, und bei if- und **loop-**Konstrukten können geschweifte Klammern optional entfallen, wobei der Formatter die Sicherheit gewährleistet
  • Ohne Namespaces wird alles als Ausdruck behandelt, wodurch die Syntax für Typen, Werte und Muster vereinheitlicht wird und Generics, Record-Literale sowie Built-ins (@import, @as usw.) kompakt genutzt werden können

Überblick

  • Zig hat eine Rust ähnliche äußere Form, setzt aber auf eine einfachere Sprachstruktur
  • Beim Syntaxdesign liegt der Fokus auf grep-Freundlichkeit, syntaktischer Konsistenz und der Reduzierung unnötigen visuellen Rauschens

Integer-Literale

const an_integer = 92;  
assert(@TypeOf(an_integer) == comptime_int);  
  
const x: i32 = 92;  
const y = @as(i32, 92);  
  • Alle Integer-Literale haben den Typ comptime_int
  • Bei der Zuweisung an Variablen wird der Typ explizit angegeben oder per @as konvertiert
  • Die Form var x = 92; funktioniert nicht; ein expliziter Typ ist erforderlich

String-Literale

const raw =  
    \\Roses are red  
    \\  Violets are blue,  
    \\Sugar is sweet  
    \\  And so are you.  
    \\  
;  
  • Jede Zeile ist ein eigenes Token, daher gibt es keine Einrückungsprobleme
  • \\ selbst muss nicht escaped werden

Record-Literale

const p: Point = .{  
    .x = 1,  
    .y = 2,  
};  
  • Das Format .x = 1 ist vorteilhaft, um Lesen und Schreiben zu unterscheiden
  • Die Schreibweise .{} grenzt sich von Blöcken ab und wird automatisch in den Ergebnistyp umgewandelt

Typnotation

u32        // Integer  
[3]u32     // Array der Länge 3  
?[3]u32    // nullbares Array  
*const ?[3]u32 // konstanter Zeiger  
  • Alle Typen verwenden Präfixnotation
  • Die Dereferenzierung verwendet Suffixnotation (ptr.*)

Bezeichner

const @"a name with space" = 42;  
  • Verhindert Konflikte mit Schlüsselwörtern oder erlaubt spezielle Namen

Funktionsdeklaration

pub fn main() void {}  
fn add(x: i32, y: i32) i32 {  
    return x + y;  
}  
  • Das Schlüsselwort fn und der Funktionsname stehen zusammen, was die Suche erleichtert
  • Für die Angabe des Rückgabetyps wird kein -> verwendet

Variablendeklaration

const mid = lo + @divFloor(hi - lo, 2);  
var count: u32 = 0;  
  • Verwendet const und var
  • Die Typangabe folgt der Reihenfolge Name: Typ

Kontrollfluss: and/or

while (count > 0 and ascii.isWhitespace(buffer[count - 1])) {  
    count -= 1;  
}  
  • and und or sind Schlüsselwörter für den Kontrollfluss
  • Für Bit-Operationen werden & und | verwendet

if-Anweisung

.direction = if (prng.boolean()) .ascending else .descending;  
  • Klammern sind Pflicht, geschweifte Klammern optional
  • zig fmt garantiert ein sicheres Formatting

Schleifen

for (0..10) |i| {  
    print("{d}\n", .{i});  
} else @panic("loop safety counter exceeded");  
  • Sowohl for als auch while unterstützen einen else-Zweig
  • Iterator und Elementname sind intuitiv angeordnet

Namespaces und Namensauflösung

const std = @import("std");  
const ArrayList = std.ArrayList;  
  • Shadowing von Variablen ist verboten
  • Es gibt weder Namespaces noch Glob-Imports

Alles ist ein Ausdruck

const E = enum { a, b };  
const e: if (true) E else void = .a;  
  • Die Syntax für Typen, Werte und Muster wird vereinheitlicht
  • Ein bedingter Ausdruck kann an einer Typ-Position stehen

Generics

fn ArrayListType(comptime T: type) type {  
    return struct {  
        fn init() void {}  
    };  
}  
  
var xs: ArrayListType(u32) = .init();  
  • Generics werden mit Funktionsaufrufsyntax (Type(T)) ausgedrückt
  • Typargumente sind immer explizit

Built-ins

const foo = @import("./foo.zig");  
const num = @as(i32, 92);  
  • Mit dem Präfix @ werden vom Compiler bereitgestellte Funktionen aufgerufen
  • @import zeigt den Dateipfad eindeutig an
  • Argumente müssen zwingend String-Literale sein

Fazit

  • Zigs Syntax ist ein Beispiel dafür, wie eine Sammlung kleiner Entscheidungen eine gut lesbare Sprache entstehen lässt
  • Wenn die Anzahl der Features sinkt, sinkt auch der Bedarf an Syntax, und die Wahrscheinlichkeit syntaktischer Konflikte nimmt ebenfalls ab
  • Gute Ideen aus bestehenden Sprachen werden übernommen, aber wenn nötig, werden mutig neue Syntaxformen eingeführt

1 Kommentare

 
GN⁺ 2025-08-22
Hacker-News-Kommentare
  • Dieser Beitrag behandelt die vielen Trade-offs beim Grammatikdesign sehr tiefgehend, und ich fand besonders beeindruckend, wie minimalistisch und konsistent Zigs Syntax ist und wie unerbittlich sie auf Lesbarkeit fokussiert. Das ist keine abstrakte Schönheit, sondern ein „Brutalismus“, bei dem es im industriellen Einsatz keine Überraschungen gibt, und genau das gefällt mir. So ein ausgewogenes Syntaxdesign ist wirklich selten, und ich finde, Zig hat das sehr gut hinbekommen

    • Schade, dass der Artikel das Error Handling nicht erwähnt. Zigs try/catch-Ansatz ist hervorragend und unter vielen Sprachen meine liebste Art der Fehlerbehandlung. Es wäre noch besser gewesen, wenn dieser Teil ebenfalls vorgestellt worden wäre

    • Nicht „oberflächlich schöne Lesbarkeit“, sondern die konsistente Schönheit, die sich durch Abstraktion ergibt, ist der wahre Reiz von Zig. Wie bei der Analogie von S-Expressions und M-Expressions ist ein guter Ansatz für den allgemeinen Fall langfristig oft besser als spezielle Designs für viele Ausnahmen. Wenn man wie in C++ immer neue Sonderfälle hinzufügt, wächst am Ende nur die Last, sich alle Regeln merken zu müssen. Wenn man im Sprachdesign nach Einfachheit und Konsistenz strebt, kann man sonst leicht in einen „Turing tarpit“ geraten, in dem die Komplexität letztlich auf die Nutzer abgewälzt wird; wichtig ist daher ein Ansatz, bei dem Sonderfälle sich natürlich aus allgemeinen Regeln ergeben. Ein Beispiel dafür sieht man auch im XKCD-Comic New Pet

    • Mich würde interessieren, ob du ein Beispiel teilen könntest, das dir besonders in Erinnerung geblieben ist

  • Dass Zig wie Rust die Schreibweise Name: Typ für Typannotationen verwendet, gefällt mir ehrlich gesagt weniger; ich mag die traditionelle Reihenfolge mit dem Typ zuerst lieber. Wenn ich eine Variablendeklaration noch einmal nachschlage, interessiert mich vor allem der Typ dieser Variable, und wenn ich den nicht schnell finde, ist das unpraktisch. Besonders in Rust gibt es mit let mut viele unnötig wiederholte Elemente, was es eher umständlich macht, und auch C oder C++ mit dem Typ zuerst finde ich gut. Im Idealfall sollte Typinferenz in der Praxis nur dort minimal eingesetzt werden, wo sie wirklich nötig ist

    • Das Schlüsselwort let ist zum Teil nötig, weil es klar macht, dass es sich tatsächlich um eine Deklaration handelt. Sonst kann man auf die mehrdeutigen Parsing-Probleme stoßen, die C++ hat

    • Ich selbst prüfe ebenfalls immer zuerst den Variablentyp und bevorzuge daher die Variante mit dem Typ vorne. Aus Sicht des Parsers ist es praktischer, zuerst den Namen zu verarbeiten, und ich verstehe, dass TypeScript diese Struktur wegen der Kompatibilität mit JavaScript gewählt hat. Letztlich ist für mich aber eine einfach nutzbare Standardbibliothek am wichtigsten. Wie bei Beispielen, in denen Typsysteme überstrapaziert werden, ist es oft wichtiger, die Absicht klar zu vermitteln, statt jeden Zustand zwingend als Typ auszudrücken

    • Wenn ich im Code nach oben scrolle, um den Typ einer Variable nachzusehen, wird es mit Typen an erster Stelle gerade schwerer, die gesuchte Deklaration zu finden. Der Typname steht ganz am Anfang und hat variable Länge, daher muss der Blick ständig horizontal hin- und herwandern, was ich als ineffizient empfinde

    • In den meisten Fällen zeigt der Editor den Typ ohnehin sofort an, wenn man mit der Maus darüberfährt, daher ist die Position des Typs im Code vielleicht gar nicht so wichtig. Rust ist vor allem deshalb so ausführlich, weil man bei der Implementierung Parsing-Mehrdeutigkeiten vermeiden will. Wenn der Typ wie in C oder C++ zuerst kommt, ist es schwieriger, per grep Variablen zu finden, die mit einem bestimmten Namen deklariert wurden, und der Stil mit dem Rückgabetyp vorne wurde zwar wegen Templates eingeführt, kann aber je nach Fall das Lesen und Auffinden von Code erleichtern

    • Ich persönlich bevorzuge eher die Typannotationsweise im Pascal-Stil. Auch bei Typinferenz braucht man dann kein indirektes Hilfsmittel wie auto, und aus Parsing-Sicht ist es weniger mehrdeutig. Bei MyClass x ist nicht sofort klar, ob MyClass ein Typ oder ein Variablenname ist, und dieser Stil reduziert solche Mehrdeutigkeiten

  • Bei Zigs Syntax für raw/multiline strings wirkt die Schreibweise, bei der man mehrfach \\ verwenden muss, viel zu verwirrend und extrem

    • Wer schon einmal Multiline-Strings in Python, C++, Rust usw. formatiert hat, versteht diese Unannehmlichkeit. Weil Einrückungen Teil des String-Inhalts werden, ist das immer ein Thema, und Fälle wie YAML mit einem Modus zum Entfernen von Einrückungen machen es eher noch verwirrender. Zigs Ansatz ist, was Einrückung angeht, sehr eindeutig

    • Anfangs fand ich diese Syntax extrem unbequem, aber wenn man Zig eine Weile benutzt, gewöhnt man sich daran und erkennt sogar die Vorteile. Zig wirkt beim ersten Kontakt an manchen Stellen abschreckend neuartig, aber in der Praxis versteht man dann, warum es so gemacht ist

    • Eigentlich ist nicht die Syntax verrückt, sondern das Problem ist verrückt: nämlich dieses komplizierte Problem, sicher einen Multiline-String innerhalb eines anderen Multiline-Strings unterzubringen. In Zig braucht man dafür weder zusätzliche Escapes noch muss man sich über Einrückung Gedanken machen, und das gefällt mir

    • trimIndent in Kotlin, Textblöcke in Go oder Java und besonders Go mit seinen Raw Strings in Backticks fühlen sich für mich deutlich runder an. In Zig weiche ich wegen der \\ eher auf @embedFile aus

    • Optisch gefallen mir die \\ zwar nicht, aber ich denke, es ist eine saubere Lösung für das Problem von Multiline-Literalen und Einrückung. Mir fällt spontan keine Sprache ein, die dieses Problem ohne Hilfsfunktion löst

  • Zigs Syntax wirkt auf mich unruhig. Konstrukte wie @TypeOf, die mit @ beginnen, oder die Initialisierungssyntax wie .{.x} fühlen sich merkwürdig an. Vielleicht liegt es daran, dass ich im Umgang mit Zig noch nicht geübt bin, aber insgesamt habe ich den Eindruck, dass sich der Code schwer lesen lässt

    • Ich bevorzuge die Syntax von Odin, weil sie deutlich minimalistischer und sauberer ausgearbeitet ist. Zig wirkt etwas unruhig

    • . dient in Zig als Platzhalter für einen inferierten Typ. Zum Beispiel kann man ein Objekt so initialisieren

      const p = Point{ .x = 123, .y = 234 };
      

      Oder wenn man die Typinferenz explizit machen möchte:

      const p: Point = .{ .x = 123, .y = 234 };
      

      Auch bei Funktionsargumenten kann man den Typ weglassen, was es kürzer macht. In Rust muss man in solchen Situationen den Typ explizit hinschreiben

      takePoint(Point{ x: 123, y: 234 });
      

      Auch bei der Initialisierung verschachtelter Strukturen ist Zigs Inferenzansatz viel nützlicher. In Rust, wo man überall explizit Typen angeben muss, kann der Code schnell unruhig werden. Trotzdem fände ich es praktischer, die führende Punktnotation wegzulassen, aber vermutlich wird sie zur Vereinfachung der Parser-Implementierung beibehalten. Die Schreibweisen x: 123 bzw. .x = 123 sind jeweils aus JS bzw. C99 entlehnt. Ich persönlich benutze beides oft und finde es daher nicht unnatürlich

  • Ich bevorzuge deutlich die raw string literals aus C# 11. Die Einrückung der ersten Zeile dient als Referenz, an der die übrigen Zeilen automatisch ausgerichtet werden. Außerdem kann man geschweifte Klammern auch als Zeichen verwenden. Wenn $ mehrfach vorkommt, werden geschweifte Klammern vollständig als Wert behandelt

    string json = $"""
       {title}
    
         Welcome to {sitename}.
    
       """;
    string json = $$"""
       {{title}}
    
         Welcome to {{sitename}}, which uses the {sitename} syntax.
    
       """;
    
    • (Als Autor des C#-Features für raw string literals) Tatsächlich ist die Einrückung der letzten Zeile mit """ die Referenz, und auch die erste Zeile kann eingerückt werden. Es freut mich, dass dir dieses Feature gefällt, und ich halte es selbst für eine gute Funktion
  • Zigs Syntax ist zwar gut, aber im Vergleich zu Go, das auch ohne Semikolons oder : ausreichend sauber auskommt, würde ich sie nicht bis „lovely“ loben. Wenn man vergleichen will, ist es gegenüber Rust zwar deutlich verbessert, aber auch Go ist bereits sehr gut

    • Im Gegenteil: Eine übermäßig minimalistische Syntax wie in Go kann beim Lesen sogar schwerer zu interpretieren sein. Man verbringt mehr Zeit mit Lesen als mit Schreiben von Code, und übertriebene Kürze führt eher zu Fehlern und erschwert das Debugging. CoffeeScript oder J mit ihrer starken Verdichtung sind typische Beispiele dafür

    • Ich glaube nicht, dass Syntax automatisch besser wird, nur weil man Syntaxelemente entfernt. Wäre das so, würden alle wie in Lisp schreiben, und Texte würden in der Art von scriptio continua verfasst, also ohne Leerzeichen wie in alten Schriften. Siehe scriptio continua auf Wikipedia

  • Insgesamt bin ich mit Zig zufrieden, aber die folgenden Punkte finde ich schade

    • Es ist umständlich, einen Rückgabewert für einen Block festzulegen. Schön wäre es, wenn wie in Rust automatisch der letzte Ausdruck als Rückgabewert erkannt würde, aber in Zig muss man dafür Labels usw. verwenden, was mühsam ist
    • Chaining bei optionalen Typen (z. B. a?.b?.c) ist nicht möglich. Mit Unterstützung für monadische Typen wäre allgemeineres Chaining möglich, aber da fehlt noch etwas
    • Es gibt keine Unterstützung für Lambda-Funktionen. Schon jetzt kann man an Stellen wie Schleifen oder catch-Blöcken Funktionsblöcke verwenden; mit Lambdas wäre das noch flexibler
  • Was die Verwendung von void als Typbezeichnung betrifft: In der Typentheorie ist void eigentlich nicht die Rolle von unit, sondern bezeichnet einen unbewohnbaren Typ ohne Werte. Traditionell sind () oder unit Typen mit genau einem Mitglied. void wäre dann eher der Rückgabetyp von Funktionen wie abort

    • In C und C++ wird void schon seit langem ganz brauchbar verwendet, und viele Systemprogrammierer sind damit vertraut. Solche terminologischen Debatten aus der Typentheorie sind für die praktische Nutzung aus meiner Sicht bedeutungslos. Da viele Leute mit C/C++-Hintergrund zu Zig kommen, ist void völlig in Ordnung

    • abort hat eher einen Typ für einen „unerreichbaren“ Zustand wie Rusts !-Typ. void ist eher mit unit oder () vergleichbar und ein Typ, bei dem kein Wert vorhanden ist. Ein interessanter Trick ist, dass man in TypeScript void in generischen Constraints verwenden kann, um den entsprechenden Parameter optional zu machen

    • Der Typ void hat eine sehr lange Tradition und reicht bis ALGOL 68 zurück. Dort ist der Typ VOID als Typ mit genau einem Mitglied (EMPTY) definiert

  • Es überrascht mich, dass „Zig keine Lambdas hat“. In C++ verwende ich Lambdas fast überall; wie definiert man dann etwa einen Comparator für das Sortieren von Arrays?

    • Dass man dafür normalerweise separat eine Funktion deklarieren muss, finde ich in Zig eher unpraktisch

    • Man kann anonyme Structs und darin enthaltene Funktionen inline referenzieren. Die Capture-Funktionalität, die man bei Lambdas häufig nutzt, gibt es in Zig zwar nicht, aber man kann sie ersetzen, indem man einen Kontextparameter übergibt, meist in Form eines Structs

    • Im Grunde genauso wie in C: Man deklariert eine separate Vergleichsfunktion und übergibt dann deren Zeiger an die Sortierfunktion

  • Man sagt zwar „Syntax ist nicht wichtig“, aber in Wirklichkeit läuft es oft auf „Syntax ist nicht wichtig, also verwenden wir einfach die Art, die ich bevorzuge“ hinaus. Ich selbst bin mit C-artiger Syntax wie in Rust/Zig/Go vertraut, und Stile wie in Haskell oder OCaml, wo Funktionsaufrufe über Leerraum unterschieden werden, sind mir noch fremd; ich denke, das behindert die Verbreitung. Wie der Erfolg von Rust zeigt, könnten andere Sprachen davon lernen, wie dort der „Spinat“ der funktionalen Programmierung gut in den „Brownie“ einer Systemprogrammiersprache eingearbeitet wurde

    • Ich stimme der Aussage nicht zu, dass Syntax unwichtig sei. Syntax ist letztlich die Hauptschnittstelle, über die Nutzer mit einer Sprache interagieren. Jedes Mal, wenn ich eine Sprache lese, treten mir die Syntaxelemente unbewusst stärker entgegen

    • Wenn du eine funktionale Sprache mit C-artiger Syntax willst, empfehle ich Gleam: gleam.run Der Code sieht auch sehr schön aus

      fn spawn_greeter(i: Int) {
       process.spawn(fn() {
        let n = int.to_string(i)
        io.println("Hello from "  n)
       })
      }
      

      Reason ist ebenfalls einen Blick wert. Es basiert auf OCaml, hat aber eine C-artige Syntax: reasonml.github.io