1 Punkte von GN⁺ 2025-01-08 | 1 Kommentare | Auf WhatsApp teilen
  • Zigs Compile-Time- (comptime-) Metaprogrammierung liest sich wie Runtime-Code, kann aber wiederholten Code, etwa das Aufsummieren von Struct-Feldern, in Ausführungscode verwandeln, der so effizient ist wie eine von Hand geschriebene C-Funktion
  • Statt einer separaten Generic-Syntax nutzt Zig comptime T: type und anytype; das ist ein zentraler Einsatzbereich von Zig comptime für Generic Programming
  • Runtime, comptime und Build-System teilen sich in Zig dieselbe Sprache, sodass sich sogar eine Funktion wie Fizz Buzz während der Kompilierung ausführen und in ein vorab berechnetes Array verwandeln lässt
  • Der Compiler wertet mögliche Teile zuerst aus und belässt nur die Teile, die Runtime-Werte benötigen, als Ausgabe-Bytecode, der später zu Maschinencode wird
  • Zig comptime kann ähnliche Probleme lösen wie textbasierte Codegenerierung, ersetzt aber im Gegensatz zu C-Makros keine Keywords beliebig und reduziert so den Aufwand beim Lesen und Debuggen von Code

Wie man Zig comptime lesen sollte

  • Zigs comptime ist eine Metaprogrammierungsfunktion, aber beim Lesen des endgültigen Verhaltens muss man die Unterscheidung zwischen Compile Time und Runtime nicht stark im Kopf behalten
  • Der dritte Punkt aus zig zen, “Favor reading code over writing code”, ist dabei ein wichtiger Maßstab
    • Makro- oder codegenerierungsbasierte Metaprogrammierung kann zwei Ebenen erzeugen: den ursprünglichen Code und den expandierten Code
    • Solche Indirektionsschichten erschweren das Lesen, Debuggen und Ändern des Verhaltens
  • Runtime-Code, der die Summe eines Arrays [3]i64 = .{1,2,3} berechnet, wird direkt mit comptime-Code verglichen, der die Felder a, b, c des Structs MyStruct aufsummiert
    • Mit inline for (comptime std.meta.fieldNames(MyStruct)) wird über die Feldnamen iteriert
    • Mit @field(my_struct, field_name) wird über den zur Compile Time bekannten Feldnamen auf ein Feld zugegriffen
  • Das Beispiel zum Aufsummieren von Struct-Feldern sieht wie Runtime-Reflection aus, im ausführbaren Programm bleibt jedoch effizienter Code übrig, als hätte man die Summenfunktion für diesen Struct-Typ von Hand geschrieben
  • Der Transformationsprozess im Beispiel ist keine exakte Nachbildung der Zig-Implementierung, sondern eher eine konzeptionelle Erklärung
  • Das Zig-Core-Team arbeitet an einem Debugger, der Code mit gemischtem comptime und Runtime wie im Beispiel schrittweise ausführen kann

Generics werden über comptime behandelt

  • Zig hat keine separate Generic-Funktion
  • Um einen Typ generisch zu machen, schreibt man eine Funktion, die einen Typ entgegennimmt und einen Typ zurückgibt
    • Beispiel: pub fn GenericMyStruct(comptime T: type) type
    • Die Felder a, b, c des zurückgegebenen struct verwenden alle den Typ T
  • Generische Funktionen lassen sich auf dieselbe Weise schreiben
    • Beispiel: fn quadratic(comptime T: type, a: T, b: T, c: T, x: T) T
    • Typ-Argumente können wie in quadratic(f32, ...) und quadratic(i64, ...) explizit angegeben werden
  • Wenn der Argumenttyp für andere Teile der Funktionssignatur nicht wichtig ist, kann er mit dem speziellen Typ anytype inferiert werden
  • Diese Sicht erklärt comptime nicht vollständig, ist aber ein Ausgangspunkt, um häufige Generic-Aufgaben in Zig zu verstehen

Derselbe Zig-Code wird zur Compile Time ausgeführt

  • Zig verwendet für Runtime, comptime und Build-System dieselbe Sprache
  • Das Fizz-Buzz-Beispiel schreibt die Funktion fizzBuzz(writer) zunächst als normale Runtime-Ausgabefunktion
    • Sie iteriert von 1 bis 100
    • Für Vielfache von 3 und 5 gibt sie fizzbuzz aus, für Vielfache von 3 fizz, für Vielfache von 5 buzz und sonst die Zahl
  • Führt man dieselbe Funktion fizzBuzz in einem comptime-Block aus, lässt sich die gesamte Ausgabe vorab berechnen
    • Mit std.io.countingWriter(std.io.null_writer) wird zunächst die benötigte Bytezahl berechnet
    • Aus der berechneten Länge wird ein Array buffer: [cw.bytes_written]u8 erzeugt
    • Durch erneute Ausgabe in std.io.fixedBufferStream(&buffer) entsteht der Wert full_fizzbuzz
  • Misst man nur den Kernabschnitt, läuft die vorab berechnete Version etwa 9-mal schneller
    • Da das Beispiel selbst klein ist, wird die gesamte Laufzeit stärker von anderen Faktoren beeinflusst
  • Zwischen comptime und Runtime gibt es Unterschiede bei zugänglichen Werten und Funktionen
    • Werte wie comptime_int, comptime_float und type sind nur in comptime zugänglich
    • Manche Funktionen akzeptieren nur comptime-Argumente und sind damit praktisch comptime-only
    • Systemaufrufe oder Funktionen, die sie verwenden, sind nur zur Runtime zugänglich
  • Code, der solche exklusiven Funktionen nicht nutzt, verhält sich in comptime und Runtime gleich

comptime als partielle Auswertung verstehen

  • comptime lässt sich als partielle Auswertung (partial evaluation) während des Kompilierens betrachten
  • Partielle Auswertung bedeutet: Wenn einer Funktion nur ein Teil ihrer Argumente gegeben ist, werden Ausdrücke, die ausschließlich bereits bekannte Werte verwenden, zuerst ersetzt und berechnet; übrig bleibt eine neue Funktion, die nur noch die unbekannten Argumente entgegennimmt
  • Das Beispiel zur Array-Summe zeigt den Auswertungsprozess Schritt für Schritt
    • Die for-Schleife wird in einzelne Anweisungen pro Iteration zerlegt
    • array[0], array[1], array[2] werden jeweils durch 1, 2, 3 ersetzt
    • sum wird von 0 → 1 → 3 → 6 aktualisiert
    • Am Ende wird bis zu einer Form wie std.debug.print(..., .{6}) vereinfacht
  • Das Beispiel zum Aufsummieren von Struct-Feldern lässt sich genauso lesen
    • Da inline for zur comptime ausgeführt wird, wird es zu einzelnen Anweisungen für die Felder "a", "b", "c" entfaltet
    • @field(my_struct, "a") wird zu my_struct.a
    • Die finale Funktion enthält direkt sum += my_struct.a, sum += my_struct.b, sum += my_struct.c
  • Dieser Ansatz drückt die Absicht direkt im Code aus und reduziert zugleich die Notwendigkeit, per Kommentar daran zu erinnern, die Summenfunktion bei Feldänderungen manuell zu aktualisieren

Compile-Time-Auswertung und Emission von Runtime-Code

  • Zig comptime lässt sich auch als Kombination aus Compile-Time-Auswertung und Emission von Runtime-Code betrachten
  • Der Eingabecode wird vom Compiler ausgeführt
    • Anweisungen, die zur Compile Time bekannt sind, werden sofort ausgewertet
    • Anweisungen, die Runtime-Werte benötigen, werden dem Ausgabecode hinzugefügt
  • Diese Sichtweise kommt der tatsächlichen Arbeitsweise des Zig-Compilers am nächsten
    • Zig parst zunächst die Syntax
    • Der Code wird in Bytecode für eine virtuelle Maschine umgewandelt
    • Die VM wertet die möglichen Teile aus und emittiert für Teile, die Runtime-Verarbeitung benötigen, neuen Bytecode
    • Dieser neue Bytecode wird später in Maschinencode umgewandelt
  • Bei Bedingungen wie if mit Runtime-Eingaben werden beide Pfade emittiert
  • Toter Code wird keiner semantischen Analyse unterzogen
    • Selbst wenn man eine falsche Funktion schreibt, kann es sein, dass kein Kompilierfehler entsteht, bis sie tatsächlich verwendet wird
    • Diese Eigenschaft macht das Kompilieren effizienter und ermöglicht natürliche bedingte Kompilierung ohne #ifdef
  • Auch Typnamen in Zig sind Ausdrücke, die in comptime zu type-Werten ausgewertet werden
    • Selbst scheinbar einfache Typnamen, etwa in Funktionsargumenttypen, sind Ergebnisse einer comptime-Auswertung
    • Diese Eigenschaft ermöglicht die zuvor gezeigten Generic-Beispiele
    • Bei Bedarf lassen sich Typen über komplexere Ausdrücke berechnen
  • Zigs statische Analyse ist komplexer als in vielen anderen statisch typisierten Sprachen
    • Um alle Typen zu ermitteln, muss ein beträchtlicher Teil des Compilers ausgeführt werden
    • Bis Werkzeuge aufgeholt haben, funktionieren Editorfunktionen wie Code Completion nicht immer zuverlässig

Verhältnis zu textbasierter Codegenerierung

  • Zig comptime hat zwar eine andere Form als textbasierte Codegenerierung, kann aber ähnliche Probleme lösen
  • Die Codegenerierungsfunktion writeSumFn gibt mit writer.print den Quellcode der Funktion sumFields aus
    • Der im Generator ausgeführte Code entspricht dem Zig-comptime-Teil
    • Der vom Generator ausgegebene Code entspricht dem Runtime-Code
  • Die comptime-Version in Zig drückt dieselbe Logik direkter aus
    • Mit std.meta.fieldNames(MyStruct) werden die Feldnamen ermittelt
    • Mit inline for und @field wird der Code für den Feldzugriff aufgebaut
  • Codegenerierung, die Typinformationen als Eingabe nutzt, wird in Zig einfacher
    • Bei textbasierter Generierung muss separat gelöst werden, woher Informationen wie Typnamen und Feldnamen stammen
    • Zig kann den Typ selbst und die Feldinformationen direkt in comptime verarbeiten
  • Wenn Eingaben wie externe Spezifikationen verwendet werden, kann man den Dateiinhalt mit @embedFile einbinden und ihn dann wie gewohnt parsen
  • Im Beispiel für generische Structs behandelt comptime Referenzen auf Typnamen direkter als textbasierte Codegenerierung
    • Textgenerierung muss Namen wie MyStruct_{s} als Strings zusammenfügen und konsistent halten
    • comptime verwendet direkt Funktionen wie GenericMyStruct(T)
  • Es gibt auch Ausnahmen
    • Man kann Typen erzeugen, deren Feldnamen in comptime festgelegt werden
    • In diesem Fall muss eine eingebaute Funktion mit einer Spezifikation aufgerufen werden, die die Liste der Felddefinitionen enthält
    • Für solche Typen lassen sich keine Deklarationen wie Methoden definieren
    • Das schränkt nicht die Ausdruckskraft des Codes selbst ein, wohl aber die Form der API, die man anderem Code bereitstellen kann
  • Die meisten vernünftigen Aufgaben, die mit textbasierten Makros wie in C möglich sind, sind auch mit comptime möglich, aber die Form des Codes kann ganz anders aussehen
  • Zig bietet keine Funktion, die wie textbasierte Makros Keywords durch andere Namen ersetzt

Weiterführende Lektüre und Beispiele aus der Standardbibliothek

  • Zig selbst ist keine Sprache, die nur auf comptime angewiesen ist; mehr dazu gibt es auf der offiziellen Website
  • Die Sprachreferenz behandelt die konkreten Funktionen von comptime
  • In der Zig-Standardbibliothek gibt es Beispiele für den Einsatz von comptime
    • Die von std.debug.print verwendete Formatfunktion ist eine leistungsfähige generische Funktion
      • Viele Sprachen parsen Formatstrings zur Runtime und können für frühzeitige Fehlererkennung separate Validatoren ergänzen
      • Zig parst den Formatstring in comptime, erzeugt effizienten Ausgabecode und führt alle Prüfungen ebenfalls zur Compile Time aus
    • ArrayList ist ein relativ einfacher, aber voll funktionsfähiger generischer Container
    • Zigs main-Funktion kann mehrere Rückgabetypen haben; das ist keine Compiler-Magie, sondern wird über normalen comptime-Code behandelt

1 Kommentare

 
GN⁺ 2025-01-08
Meinungen auf Hacker News
  • Ich würde mir wünschen, dass die bereits bekannten Probleme der Compile-Time-Programmierung tiefer behandelt werden, statt sie unkritisch zu loben
    Staged Programming ist nicht neu, und auch in Lisp-Familien macht man seit Jahrzehnten Ähnliches; dabei sind jedoch viele Design-Trade-offs und Probleme sichtbar geworden. Generics dieser Art brechen zum Beispiel die Parametrizität (parametricity), sodass es schwieriger wird, allein aus der Typsignatur einer Funktion Schlüsse zu ziehen. Unklar ist auch, wie Zig rekursive generische Typen behandelt und in welcher Reihenfolge Typprüfung und Compile-Time-Berechnung miteinander interagieren. Der Artikel sagt, Compile-Time-Code könne nicht nur Werte, sondern auch Code erzeugen, behandelt aber Hygiene nicht. Eine gute Diskussion dazu ist https://typesanitizer.com/blog/zig-generics.html

    • Ich mag Zig ziemlich und verfolge es seit Jahren, habe es auch gelegentlich genutzt, aber comptime glänzt besonders bei Generics, der Compile-Time-Initialisierung komplexer Datenstrukturen und zielabhängiger Codegenerierung
      In anderen Situationen löst comptime in Zig-Code bei mir allerdings manchmal ein „Ach …“ aus. Wie Lisp-Makros lässt es sich leicht verwenden, um Probleme zu umgehen, die es eigentlich nicht gäbe oder die verschwinden würden, wenn man die Codestruktur besser anlegt. Das Beispiel aus dem Originalartikel, bei dem über die Felder einer Struct iteriert und Werte addiert werden, ist typisch dafür, wie Leute comptime tatsächlich einsetzen; in vielen Fällen ist es aber besser, eine wirklich iterierbare Datenstruktur wie std.enums.EnumArray zu verwenden
    • Ich frage mich, ob mit dem Bruch der Parametrizität gemeint ist, dass praktische Programmierer das Verhalten einer Funktion schlechter verstehen können, oder ob es um Schlussfolgerungen in einem typtheoretischen System wie dem typisierten Lambda-Kalkül geht
      Theoretisch sind die Bedenken klar, aber wie wichtig das in einer praktischen Sprache ist, ist eine andere Frage. Auch C++-Templates brechen Parametrizität durch Template-Spezialisierung, was in der Praxis meist kein großes Problem ist und vielmehr Optimierungen ermöglicht. Eine Implementierung wie std::vector<bool>, bei der jedes Element tatsächlich als 1 Bit gespeichert wird, ist zum Beispiel nur mit solcher Flexibilität möglich
    • Als Autor: Ich habe diesen Artikel geschrieben, um nach einem Treffen zu ordnen, was ich dort nicht gut erklären konnte; deshalb liegt der Fokus eher auf Erklärung als auf Kritik
      Ich sehe Zigs comptime als direkten Trade-off zwischen Ableitbarkeit und Ausdrucksstärke im Vergleich zu statischer definierten Funktionssignaturen. Es betrifft nur Code, der solche Schlüsse über Typen zieht, und ist etwas, das man je nach Bedarf wählen kann. Selbst wenn die Typsignatur nicht ausreichte, reichten meist ein paar Dutzend Zeilen Quellcode, um die Verwendung zu verstehen, und ich war nie lange verwirrt. Beim Beispiel rekursiver generischer Typen führt die Verwendung des gerade deklarierten Namens zu „dependency loop detected“, es gibt aber Workarounds; auch das Generic-Beispiel im Artikel verweist auf sich selbst. Die Typprüfung erfolgt während comptime: Ein erstes @compileLog("Hi") wird zum Beispiel ausgeführt, aber wenn danach bei const a: u32 = "42"; ein Typfehler auftritt, kommt man nicht bis zum zweiten Log. Zwischen comptime und Laufzeit kann es bei der Typprüfung subtile Probleme geben, doch die zeigen sich nur in ziemlich schwierigen Codefällen und sind leicht zu beheben; das Kernteam weiß davon, und ich erwarte, dass sie vor 1.0 behandelt werden. Was mit Hygiene genau gemeint ist, müsste näher erklärt werden
    • Dass man etwas allein aus einer Funktionsdeklaration ableiten kann, ist unabhängig von Compile-Time-Ausführung
      Wenn eine Funktion einen klaren logischen Zweck hat und Name sowie Parameternamen gut gewählt sind, kann man sie auch aus der Deklaration heraus verstehen. Auch Parameterlabels, Typnamen und andere vom Programmierer festlegbare Teile lassen sich als Teil des Namens auffassen. Dem verlinkten Artikel stimme ich in der Schlussfolgerung des Titels zu, aber er wirkt eher wie ein Debattierclub-Text zum Punktesammeln als wie eine substanzielle Argumentation. Die bessere Gegenüberstellung ist Flexibilität versus Komplexität. Ein festes Generic-System ist, wenn es gut entworfen ist, einfacher als ein programmierbares, aber weniger flexibel. Zigs Vorteil liegt darin, dass die Metaprogrammiersprache fast dieselbe ist wie die normale Sprache und auch diese normale Sprache selbst schlicht ist, wodurch die Kosten zusätzlicher Komplexität sinken. Allerdings entsteht Komplexität dadurch, dass Compile-Time-Code und Runtime-Code vermischt werden und es wenige Hinweise gibt, sodass Programmierer schwerer unterscheiden können, was wann ausgeführt wird. Vielleicht könnten Language Server oder Editor-Plugins einen „comptime-Shader“ anbieten, der comptime-Code mit einer anderen Hintergrundfarbe markiert
    • Dass dieses Thema auf einer 15-Minuten-Demo basiert und mit wenig Verständnis der tatsächlichen Trade-offs diskutiert wird, ist bedauerlich. Der verlinkte Artikel von Varun Gandhi ist ausgezeichnet
      Aus Rust-Erfahrung scheint mir, dass vieles, was Leute mit Const Generics erreichen wollen, mit einer Funktion wie comptime einfacher wäre. Arithmetik über Const Generics zu erlauben und dabei Parametrizität zu erhalten, ist schwer zu implementieren; wenn man eigentlich nur so etwas wie „ein Hash-Funktions-Trait mit Ausgabelänge N“ will, kann es für diesen Zweck vertretbar sein, Parametrizität aufzugeben und aus N in einer früheren Codegenerierungsphase ein Trait zu machen. Rust-Makros sind für diese Art Nutzung allerdings zu flexibel und umständlich. Trotzdem beginnt der Leidensweg in dem Moment, in dem man parametrische Polymorphie durch naive Codegenerierungsfunktionen ersetzt
  • Gab es in D schon vor 17 Jahren. D-Features wandern stetig in andere Sprachen ab
    Zig markiert mit dem Schlüsselwort comptime Blöcke, die während der Kompilierung ausgeführt werden, während in D nicht ein Schlüsselwort entscheidet, sondern ob es sich um einen konstanten Ausdruck handelt. Zum Beispiel wird int s = sum(3, 4); zur Laufzeit ausgeführt und enum e = sum(3, 4); zur Compile-Zeit. Wenn man nicht-konstante Globals, Ein-/Ausgabe und Aufrufe von Systemfunktionen wie malloc() vermeidet, können viele Funktionen unverändert zur Compile-Zeit ausgeführt werden. Dank Ds automatischer Speicherverwaltung ist auch Speicherallokation möglich.

    • Eine meiner Lieblingsanwendungen ist die Erzeugung statischer Tabellen
      Früher musste man ein separates Programm schreiben, um statische Tabellen zu erstellen, aber durch Funktionsausführung zur Compile-Zeit ist das nicht mehr nötig. Die Initialisierungswerte des Arrays tytab kommen aus einem Lambda, das ein Array berechnet und zurückgibt. Das vollständige Beispiel findet sich unter https://github.com/dlang/dmd/blob/master/compiler/src/dmd/ba.... Ein weiterer häufiger Einsatz von CTFE ist das Erstellen von DSLs.
    • Ich möchte die Gelegenheit nicht verpassen, mich für die Entwicklung von D zu bedanken. Allerdings frage ich mich, wo die Grenzen der Ausführung zur Compile-Zeit liegen
      Wie stellt der D-Compiler die Korrektheit sicher, wenn die Maschine, auf der der Compiler läuft, eine andere ist als die Maschine, auf der das Programm ausgeführt wird? Ich frage mich zum Beispiel, woher der Compiler weiß, dass int s = sum(100000, 1000000) auf allen x86-Maschinen denselben Wert hat. Zwischen CPU-Generationen kann es subtile Unterschiede geben; wie wird also garantiert, dass eine tatsächlich auf dem Host durchgeführte Berechnung auf der Zielmaschine dasselbe Ergebnis liefert? Oder wird angenommen, dass Host und Ziel hinreichend ähnlich sind, wenn die Architektur gleich ist?
    • Ds ImportC kann ebenfalls CTFE mit C-Code durchführen
      Man kann int sum(int a, int b) { return a + b; } nehmen und es etwa als _Static_assert(sum(3, 4) == 7, "look ma, check at compile time!"); verwenden. Ich weiß nicht, warum der C-Standard das nicht ergänzt, und es funktioniert gut.
    • Fairerweise kann auch Zig dieselbe Funktion sowohl in einem Laufzeitkontext als auch in einem comptime-Kontext aufrufen
      square(2) ist ein Laufzeitaufruf, comptime square(3) ein Compile-Zeit-Aufruf. Ein comptime-Aufruf erzeugt einen Compilerfehler, wenn etwas nicht Compile-Zeit-kompatibel ist; das halte ich für eine wichtige Funktion. Denn es ist ein Warnsignal, wenn Code, von dem man erwartet, dass er zur Compile-Zeit ausgeführt wird, durch veränderte Eingabeargumente versehentlich in eine Laufzeitauswertung rutscht.
  • Zig sieht interessant aus, aber ich wünschte, es hätte Operator Overloading
    Die meisten Argumente gegen Operator Overloading überzeugen mich nicht wirklich. Das Argument, man wisse nicht, was intern tatsächlich passiert, reicht nicht aus, denn man könnte auch eine Funktion namens add so schreiben, dass sie multipliziert. C++-iostreams oder boost::spirit werden oft als Missbrauchsbeispiele genannt, aber in anderen Sprachen mit Operator Overloading sieht man so etwas kaum; es wirkt eher wie ein C++-spezifisches Problem.

    • Eine Lösung im Stil von OCaml würde gut zu Zigs Einsatzbereich passen
      In OCaml kann man Operatoren neu definieren, aber nur im Kontext eines anderen Moduls. Wenn man zum Beispiel im Modul Vec3 + neu definiert hat, kann man es als Vec3.(a + b + c + d) oder let open Vec3 in a + b + c + d verwenden. Beim Lesen des Codes fragt man sich nicht: „Woher kommt dieses +?“, und es ist viel besser als a.add(b).add(c).add(d). Allerdings ist Zig bereits dabei, sich zu verfestigen, daher scheint es schwierig, noch eine große Änderung einzubauen, die dieses Problem löst.
    • Man kann kaum wissen, wie viel Magie hinter Pythons und PHPs __-Funktionen abläuft
      Zigs Ansatz ist erfrischend, und die Fähigkeit, Code nachvollziehen zu können, ist wichtiger als die paar Sekunden, die ein paar zusätzliche Zeichen beim Tippen kosten.
    • Viele Gründe, Operator Overloading nicht zu mögen, hängen meiner Ansicht nach mit unerwarteter Laufzeit-Performance zusammen
      Die ideale Lösung wäre, dass die Sprache benutzerdefinierte Operatoren bereitstellt, bei denen klar erkennbar ist, dass sie überladen sind. Bei einer Präfix-/Suffixform wie let c = a |+| b würde jemand, der den Code liest, erkennen, dass die Operation |+| tatsächlich ein Funktionsaufruf ist. Das kann zwar weiterhin missbraucht werden, würde aber eine der wichtigsten Sorgen verringern.
    • In C++ habe ich Operator Overloading nur für Vektor-/Matrixmathematik verwendet, und in diesem Fall war es wirklich nützlich
      Wenn die Sprache Syntax für Vektormathematik direkt bereitstellt, wie Shading-Sprachen es tun, wäre das wohl in Ordnung. Zig hat immerhin einen @Vector()-Typ, der ein wenig Clangs Vector Extension ähnelt, aber leider nicht die Extended Vector Extension ist. Siehe https://ziglang.org/documentation/master/#Vector und https://clang.llvm.org/docs/LanguageExtensions.html#vectors-....
    • Vielleicht sollten grundlegende lineare Algebraoperatoren für Zahlenarrays einfach in die Sprache eingebaut werden, statt über Overloading zu laufen
      Ich weiß nicht genau, ob es dafür bereits einen Vorschlag gibt.
  • fieldNames sieht Nims fieldPairs sehr ähnlich, und das ist eine enorm praktische Konstruktion
    Es macht effiziente Serialisierung sehr einfach, und vor Kurzem habe ich mit fieldPairs in etwa 20 Zeilen eine Compile-Zeit-Prüfung auf Thread-Sicherheit für Typen implementiert. Solche Funktionen sollten meiner Ansicht nach Standardfunktionen von Programmiersprachen sein. Einer der Punkte, die ich in Rust am meisten vermisse, liegt genau hier; beim letzten Versuch war man auf untypisierte Makros beschränkt. Das ist viel zu eingeschränkt. Man kann nur hoffen, dass serde für die Structs einer Crate implementiert ist, und man kann auch keine Structs mit denselben Feldern programmatisch selbst erzeugen.

    • Es gab einmal Diskussionen über Compile-Zeit-Reflection, und vermutlich hätte das solche Funktionen einschließen können, aber es scheint zusammen mit dem dazugehörigen Drama verschwunden zu sein
      Das ist ziemlich schade. Dinge wie serde wären mit Compile-Zeit-Reflection deutlich einfacher zu implementieren gewesen.
    • Mit Compile-Zeit-Reflection kann man Frameworks wie ORMs oder Web-Frameworks bauen
      Der einzige Kompromiss ist, dass solche Libraries in Form von Quellcode eingebunden werden müssen.
  • Seit ich einmal eine halbwegs vollständige C-Parser-Bibliothek geschrieben habe, bin ich mir nicht mehr sicher, ob die Sprache selbst Metaprogrammierung unbedingt braucht.
    Wenn man Strukturen, Serialisierung, Properties, Instrumentierung usw. generieren will, kann man einfach ein normales C-Programm schreiben, das Quelldateien verarbeitet und Quelldateien ausgibt, und es im Build-Skript vorher ausführen. Ich frage mich, wie solche Metaprogramme debuggt und getestet werden. Meines ist ein normales C-Programm, daher verwende ich einfach denselben Debugger und dieselben Tools wie für anderen Code.

    • Genau so etwas wollen die Leute eben nicht machen.
    • C# – genauer gesagt Roslyn/dotnet – bietet das ziemlich gut an.
      Da der Compiler selbst in derselben Sprache geschrieben ist, kann man ein Plugin einhängen, das lesenden Zugriff auf den AST hat und C#-Quellcode ausgibt. Fürs Debugging muss man etwas mehr Arbeit investieren, um ein gutes Test-Framework zu bauen, aber danach kann man den Compiler mit Plugin innerhalb eines Standard-Unit-Test-Frameworks ausführen und ihn im interaktiven Debugger betrachten.
    • Das entspricht dem Ansatz, den Ryan Fleury und andere vertreten, und ist völlig brauchbar.
      „Beliebige Compile-Time-Ausführung in C: cl /nologo /Zi metaprogram.c && metaprogram.exe, cl /nologo /Zi program.c. Compile-Time-Code läuft mit nativer Geschwindigkeit, ist debuggbar, vollständig prozedural und beliebig. Der Compiler muss den Code nicht für einen ausführen.“ https://x.com/ryanjfleury/status/1875824288487571873
    • Zu Zig weiß ich nicht viel, aber die Stärke von Lisp liegt in S-Ausdrücken, also darin, den AST zu manipulieren.
      Um das in C zu tun, müsste man für das C-Programm, das Quelldateien verarbeitet, einen vollständigen C-Parser schreiben.
    • Ich habe früher in Python zusammen mit numba JIT etwas Ähnliches gemacht.
      Ich habe Python-Code geschrieben, der Python-Code generiert, und diesen dann wieder kompilieren lassen. Das war fragil und ein furchtbares Durcheinander, und dass ich solche Dinge tun musste, war einer der Hauptgründe, warum ich Python verlassen habe. Es ist ähnlich wie die Frage, ob man nicht einfach alle Funktionsargumente als Strings übergeben könne. Manche verwenden stringbasierten Code, aber das sollte selten sein, und eine Sprache sollte Mittel bieten, das zu vermeiden.
  • Ein interessantes Muster ist, dass man zur Compile Time Strukturen erzeugen kann.
    Ich habe experimentiert, indem ich in PyTorch eine JSON-Datei erstellt, sie in Zig mit @embedFile gelesen und daraus Strukturen mit einer bestimmten run-Methode erzeugt habe, um ein neuronales Netz zu implementieren. Theoretisch ermöglicht das dem Compiler, das neuronale Netz direkt zu optimieren. Einen großen Vorteil konnte ich noch nicht nachweisen, aber es bedeutet auch, dass das gesamte Netzwerk auf dem Stack liegt und es keine dynamische Allokation gibt. Ob das gut ist, weiß ich nicht sicher.

    • So etwas habe ich mit einem in Python geschriebenen Codegenerator statt mit comptime gemacht.
      Ich bin nicht sicher, ob comptime in Zig besonders schnell wäre, und ich möchte auch nicht jedes Mal einen JSON-Parser laufen lassen, der Strukturen erzeugt.
    • Ich frage mich, welche Auswirkungen das auf die Kompilierzeit hat.
  • Interessant ist es schon, aber bei solchen Fällen stimme ich eher Kommentaren der Art „D hat das zuerst gemacht“ zu.

    • Stimmt, und zwar hygienisch umgesetzt und nicht als Präprozessor-Ansatz.
  • Wenn dich Zigs comptime beeindruckt hat, lohnt sich unbedingt auch ein Blick auf Nim, das Compile-Time-Code-Evaluation und ein vollständiges AST-Makrosystem hat.

    • Nim ist eine interessante Sprache, aber für „ernsthafte“ Arbeit würde ich sie wohl nicht in Betracht ziehen.
      Es hat dieselben Ökosystemprobleme wie andere Nischensprachen, dazu kommen Faktoren wie ein polarisierender Maintainer, Kernbeitragende, die offenbar nicht lange dabeibleiben, und die hauptsächliche Finanzierung durch eine Kryptofirma. Vor zehn Jahren hätten mich solche Dinge nicht gestört, heute schon.
    • Zig hat den Vorteil, keine Exceptions zu haben.
      Nim scheint sich ebenfalls von Exceptions wegbewegen zu wollen, aber Exceptions färben Funktionen ein und zwingen einen, sich mit ihnen zu beschäftigen, selbst wenn man keine Funktionen verwendet, die Exceptions werfen[1]. Das Leben ist zu kurz für unsichtbaren Control Flow. [1]: https://github.com/status-im/nim-stew
  • Insgesamt ist Zig eine ziemlich gute Sprache und tut, was nötig ist.
    Es ist sehr wichtig, nicht vom eigentlichen Ziel abzukommen, und deshalb mag ich es nicht besonders, wenn manche Sprachen überall eingesetzt werden, nur weil sie eben verwendet werden.

  • Ich wünschte, es gäbe etwas, das Zigs Metaprogrammierungsfähigkeiten mit Rusts riesigem Ökosystem, Community und Sicherheit kombiniert.
    Wenn es nur um das Sprachdesign geht, gefällt mir Zig viel besser als Rust, aber als unbeholfener Hobbyprogrammierer traue ich mir zumindest im Moment nicht zu, mit Zig etwas wirklich Nützliches oder Zuverlässiges zu schreiben.

    • Aus der Perspektive von jemandem, der mit Rust vertraut ist, habe ich Zig kurz ausprobiert und schnell wieder aufgegeben.
      Der Compiler hat mir nicht dabei geholfen, solche Probleme zur Compile Time zu finden. Mir ist klar, dass Zig dieses Versprechen nicht gibt, aber für mich ist das ein entscheidender Nachteil, sodass Zig wohl nicht die richtige Sprache für mich ist. Das Konzept von comptime gefällt mir dagegen besser als Rust-Makros.
    • Ich hoffe, die Rust-Community hält sich von Zig fern. Als Witz gemeint, aber größtenteils ernst.