Warum Zig comptime so hoch gelobt wird
(scottredig.com)- 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: typeundanytype; 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 Feldera,b,cdes StructsMyStructaufsummiert- 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
- Mit
- 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,cdes zurückgegebenenstructverwenden alle den TypT
- Beispiel:
- 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, ...)undquadratic(i64, ...)explizit angegeben werden
- Beispiel:
- Wenn der Argumenttyp für andere Teile der Funktionssignatur nicht wichtig ist, kann er mit dem speziellen Typ
anytypeinferiert 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
1bis100 - Für Vielfache von
3und5gibt siefizzbuzzaus, für Vielfache von3fizz, für Vielfache von5buzzund sonst die Zahl
- Sie iteriert von
- Führt man dieselbe Funktion
fizzBuzzin einemcomptime-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]u8erzeugt - Durch erneute Ausgabe in
std.io.fixedBufferStream(&buffer)entsteht der Wertfull_fizzbuzz
- Mit
- 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_floatundtypesind 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
- Werte wie
- 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 durch1,2,3ersetztsumwird von0 → 1 → 3 → 6aktualisiert- Am Ende wird bis zu einer Form wie
std.debug.print(..., .{6})vereinfacht
- Die
- Das Beispiel zum Aufsummieren von Struct-Feldern lässt sich genauso lesen
- Da
inline forzur comptime ausgeführt wird, wird es zu einzelnen Anweisungen für die Felder"a","b","c"entfaltet @field(my_struct, "a")wird zumy_struct.a- Die finale Funktion enthält direkt
sum += my_struct.a,sum += my_struct.b,sum += my_struct.c
- Da
- 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
ifmit 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
writeSumFngibt mitwriter.printden Quellcode der FunktionsumFieldsaus- 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 forund@fieldwird der Code für den Feldzugriff aufgebaut
- Mit
- 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
@embedFileeinbinden 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)
- Textgenerierung muss Namen wie
- 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.printverwendete 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
ArrayListist 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
- Die von
1 Kommentare
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
In anderen Situationen löst
comptimein 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 Leutecomptimetatsächlich einsetzen; in vielen Fällen ist es aber besser, eine wirklich iterierbare Datenstruktur wiestd.enums.EnumArrayzu verwendenTheoretisch 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öglichIch sehe Zigs
comptimeals 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ährendcomptime: Ein erstes@compileLog("Hi")wird zum Beispiel ausgeführt, aber wenn danach beiconst a: u32 = "42";ein Typfehler auftritt, kommt man nicht bis zum zweiten Log. Zwischencomptimeund 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 werdenWenn 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 markiertAus Rust-Erfahrung scheint mir, dass vieles, was Leute mit Const Generics erreichen wollen, mit einer Funktion wie
comptimeeinfacher 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 ersetztGab es in D schon vor 17 Jahren. D-Features wandern stetig in andere Sprachen ab
Zig markiert mit dem Schlüsselwort
comptimeBlö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 wirdint s = sum(3, 4);zur Laufzeit ausgeführt undenum e = sum(3, 4);zur Compile-Zeit. Wenn man nicht-konstante Globals, Ein-/Ausgabe und Aufrufe von Systemfunktionen wiemalloc()vermeidet, können viele Funktionen unverändert zur Compile-Zeit ausgeführt werden. Dank Ds automatischer Speicherverwaltung ist auch Speicherallokation möglich.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
tytabkommen 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.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?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.comptime-Kontext aufrufensquare(2)ist ein Laufzeitaufruf,comptime square(3)ein Compile-Zeit-Aufruf. Eincomptime-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
addso schreiben, dass sie multipliziert. C++-iostreamsoderboost::spiritwerden oft als Missbrauchsbeispiele genannt, aber in anderen Sprachen mit Operator Overloading sieht man so etwas kaum; es wirkt eher wie ein C++-spezifisches Problem.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 alsVec3.(a + b + c + d)oderlet open Vec3 in a + b + c + dverwenden. Beim Lesen des Codes fragt man sich nicht: „Woher kommt dieses+?“, und es ist viel besser alsa.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.__-Funktionen abläuftZigs 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.
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 |+| bwü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.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-....Ich weiß nicht genau, ob es dafür bereits einen Vorschlag gibt.
fieldNamessieht NimsfieldPairssehr ähnlich, und das ist eine enorm praktische KonstruktionEs macht effiziente Serialisierung sehr einfach, und vor Kurzem habe ich mit
fieldPairsin 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, dassserdefür die Structs einer Crate implementiert ist, und man kann auch keine Structs mit denselben Feldern programmatisch selbst erzeugen.Das ist ziemlich schade. Dinge wie
serdewären mit Compile-Zeit-Reflection deutlich einfacher zu implementieren gewesen.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.
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.
„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/1875824288487571873Um das in C zu tun, müsste man für das C-Programm, das Quelldateien verarbeitet, einen vollständigen C-Parser schreiben.
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
@embedFilegelesen und daraus Strukturen mit einer bestimmtenrun-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.comptimegemacht.Ich bin nicht sicher, ob
comptimein Zig besonders schnell wäre, und ich möchte auch nicht jedes Mal einen JSON-Parser laufen lassen, der Strukturen erzeugt.Interessant ist es schon, aber bei solchen Fällen stimme ich eher Kommentaren der Art „D hat das zuerst gemacht“ zu.
Wenn dich Zigs
comptimebeeindruckt hat, lohnt sich unbedingt auch ein Blick auf Nim, das Compile-Time-Code-Evaluation und ein vollständiges AST-Makrosystem hat.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.
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.
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
comptimegefällt mir dagegen besser als Rust-Makros.