Koroutinen in C (2000)
(chiark.greenend.org.uk)- Wenn Produzenten- und Konsumentencode gegenseitig Daten austauschen, wird die ursprünglich sichtbare Algorithmusstruktur leicht in Zustandsübergängen begraben, sobald man eine Seite in die Form eines Aufgerufenen umschreibt
- Das Koroutinen-Modell nach Knuth beschreibt zwei Routinen, die ihre Ausführungsposition speichern und die Kontrolle hin- und hergeben; in der stackbasierten Aufrufstruktur von C ist das jedoch nicht ohne Weiteres auf portable Weise direkt umsetzbar
- Der zentrale Trick dieses Texts ist, mit der C-Syntax, die
caseauch in Unterblöcken vonswitcherlaubt, und dem Makro__LINE__eine implizite Zustandsmaschine zu bauen, die nach einemreturnan der folgenden Stelle wieder einsteigt - Mit den Makros
crBegin,crReturnundcrFinishlässt sich die ursprüngliche Schleifenstruktur von Dekompressor und Parser beibehalten; lokale Variablen, die erhalten bleiben müssen, müssen jedochstaticsein, undcrReturndarf weder in einem explizitenswitchnoch zweimal in derselben Zeile stehen - In realem Code ist wegen Einschränkungen bei Reentranz und Multithreading eine verbesserte Variante nötig, die eine Kontextstruktur übergibt;
coroutine.hstellt sowohl einfachescr-Makros als auch reentranteccr-Makros bereit
Strukturprobleme beim Verbinden von Produzenten und Konsumenten
- In großen Programmen erzeugt ein Teil des Codes oft Daten, die ein anderer Teil konsumiert; dabei macht gerade die Frage, welche Seite Aufrufer und welche Aufgerufener ist, den Entwurf schwierig
- Das Beispiel besteht aus zwei kleinen Routinen
- Ein Code zur Lauflängendekompression liest Eingaben mit
getchar()und gibt Zeichen einzeln mitemit()aus - Ein Parser liest Zeichen mit
getchar()und behandelt zusammenhängende alphabetische Bereiche alsWORD, andere Zeichen alsPUNCT
- Ein Code zur Lauflängendekompression liest Eingaben mit
- Für sich genommen wirken beide Routinen natürlich, doch damit die
emit()-Ausgabe des Dekompressors direkt in diegetchar()-Eingabe des Parsers fließen kann, braucht es eine Struktur, die beide verbindet - Man kann das auch mit einer Pipe zwischen zwei Prozessen oder zwei Threads lösen
emit()des Dekompressors schreibt in die Pipe,getchar()des Parsers liest am anderen Ende- Diese Methode ist einfach und robust, aber schwergewichtig und wenig portabel; oft möchte man für eine einfache Aufgabe keine Threads aufteilen
Lesbarkeitsverlust durch Umformulierung von Funktionen
- Die traditionelle Lösung besteht darin, ein Ende des Kommunikationskanals in die Form einer aufrufbaren Funktion umzuschreiben
- Wenn man den Dekompressor in eine Funktion umwandelt, die bei jedem Aufruf ein Zeichen zurückgibt, kann der bestehende Parser statt
getchar()einfachdecompressor()aufrufen - Umgekehrt kann man den Parser in eine Funktion umschreiben, die bei jedem empfangenen Zeichen aufgerufen wird; der bestehende Dekompressor ruft dann statt
emit()einfachparser()auf - Man muss nicht beide Seiten ändern; eine einzige Umschreibung genügt, um sie zu verbinden, aber der umgeschriebene Code ist viel schwerer zu lesen als das Original
- Im ursprünglichen Dekompressor und Parser zeigt sich der Algorithmusfluss ganz natürlich in den Schleifen
- Die umgeschriebene Form hängt von
static-Zustandsvariablen undswitch-Zustandsübergängen ab, sodass sich Kompressionsformat oder Parsergrammatik nur schwer aus dem Code herauslesen lassen
- Das Ziel ist, beide Seiten zu verbinden, ohne eine davon in eine explizite Zustandsmaschine umzuschreiben
Koroutinen nach Knuth und die Grenzen von C
- Donald Knuths Koroutinenansatz verwirft die Unterscheidung zwischen Aufrufer und Aufgerufenem und behandelt beide Prozesse als gleichrangige kooperierende Einheiten
- Das Aufrufprinzip dieses Modells unterscheidet sich von gewöhnlichen Funktionsaufrufen
- Die aktuelle Ausführungsposition wird nicht auf dem Stack, sondern an einer separaten Stelle gespeichert
- Dann springt man an die Ausführungsposition, die die andere Routine zuletzt gespeichert hat
- Wenn der Dekompressor ein Zeichen ausgibt, speichert er seinen eigenen Programmzähler und springt an die gespeicherte Position des Parsers
- Wenn der Parser das nächste Zeichen braucht, speichert er seinen eigenen Programmzähler und springt an die gespeicherte Position des Dekompressors
- Die Kontrolle pendelt so oft wie nötig zwischen beiden Routinen hin und her
- Theoretisch ist das elegant, praktisch aber nur in Assembler möglich
- Hochsprachen wie C beruhen auf einer stackbasierten Struktur; bei der Kontrollübergabe zwischen Funktionen muss daher eine Seite Aufrufer und die andere Aufgerufener sein
- In portablem C-Code ist der reine Koroutinenansatz kaum praktischer als die Unix-Pipe-Lösung
„Return and continue“ in C nachahmen
- Das benötigte Verhalten in C ist, dass eine aufgerufene Funktion nach einem
returnbeim nächsten Aufruf direkt an der folgenden Stelle weiterläuft: return and continue - Ideal wäre zum Beispiel eine Funktion der Form
for (i = 0; i < 10; i++) return i;, die bei zehn Aufrufen nacheinander 0 bis 9 zurückgibt - Die erste Implementierung verwendet eine Zustandsvariable und
goto- Am Anfang der Funktion und hinter jedem
returnsteht ein Label - Eine zwischen Aufrufen erhaltene Variable
statezeigt auf das Label, bei dem fortgesetzt werden soll - Zu Beginn der Funktion springt
switch(state)an das passende Label - Direkt vor dem
returnwird instategespeichert, zu welchem Label der nächste Aufruf zurückkehren soll
- Am Anfang der Funktion und hinter jedem
- Das funktioniert, aber die manuelle Pflege der Labels ist wartungsintensiv
- Jedes neue
returnbraucht ein neues Label und einen zusätzlichen Eintrag im anfänglichenswitch - Wird ein
returnentfernt, muss auch das passende Label entfernt werden - Man muss die Konsistenz zwischen Funktionsrumpf und
switch-Liste ständig selbst sicherstellen
- Jedes neue
Eine versteckte Zustandsmaschine mit Duff’s device
- Das bekannte Duff’s device in C nutzt die Syntaxeigenschaft, dass zu einem
switchgehörendecase-Anweisungen auch in dessen Unterblöcken stehen dürfen - Überträgt man diese Eigenschaft auf den Koroutinen-Trick, wählt
switchnicht mehr nur eingotoaus, sondern funktioniert selbst wie ein Wiedereinstiegssprung - Die Grundform sieht so aus
- Ein
static int statespeichert den nächsten Wiedereinstiegspunkt - Zu Beginn betritt die Funktion
switch(state) { case 0: ... } - Unmittelbar vor dem
returnwird der nächstecase-Wert instategespeichert - Direkt nach dem
returnsteht dann das passendecase-Label
- Ein
- Verpackt man das in Makros, entsteht eine koroutinenähnliche Schnittstelle
crBegin: verbirgtstatic int state=0; switch(state) { case 0:crReturn: speichertstate, gibt einen Wert zurück und platziert an derselben Stelle dascase-LabelcrFinish: schließt den offenen Block
crReturnist indo ... while(0)eingeschlossen, sodass es auch zwischenifundelseohne zusätzliche Klammern syntaktisch korrekt bleibt- Anfangs muss man die Zustandsnummer noch explizit angeben, etwa mit
crReturn(1, i); mit dem ANSI-C-Makro__LINE__kann später aber die aktuelle Quellzeilennummer als Zustandswert dienen - Nach dieser Verbesserung genügt
crReturn(x), dafür gilt zusätzlich die Regel, dass nicht zweicrReturnin derselben Zeile stehen dürfen
Regeln und Beispiele für die Makros
- Die Makro-basierten Koroutinen setzen einige Regeln voraus
- Der Funktionsrumpf wird von
crBeginundcrFinishumschlossen - Lokale Variablen, die über ein
crReturnhinaus erhalten bleiben müssen, werden alsstaticdeklariert crReturndarf niemals innerhalb eines explizitenswitchstehen- In Implementierungen auf Basis von
__LINE__dürfen nicht zweicrReturnin derselben Zeile stehen
- Der Funktionsrumpf wird von
- Im Beispiel des Dekompressors bleibt die ursprüngliche Schleifenstruktur erhalten; beim Ausgeben eines Zeichens wird nur
emit(c)durchcrReturn(c)ersetzt - Im Parser-Beispiel kehrt die Funktion mit
crReturn()zum Aufrufer zurück, wenn sie ein neues Zeichen braucht, und läuft beim nächsten Aufruf mit dem neuen Zeichen im Parametercweiter - Im Parser gibt es eine kleine strukturelle Änderung
- Da beim Eintritt in die Funktion das erste Zeichen bereits in
csteht, wandert dascrReturn, das dem ursprünglichengetchar()am Schleifenanfang entspricht, ans Ende der Schleife - Alternativ könnte man festlegen, dass der Parser einen Initialisierungsaufruf benötigt
- Da beim Eintritt in die Funktion das erste Zeichen bereits in
- Es ist nicht nötig, beide Routinen in Koroutinenmakros umzuwandeln; man kann auch nur eine Seite ändern und die andere als Aufrufer belassen
- Insgesamt wird so ANSI C zusammen mit dem Präprozessor und einer wenig genutzten
switch-Syntax genutzt, um Daten zwischen Produzent und Konsument weiterzugeben, ohne den Code in eine explizite Zustandsmaschine umschreiben zu müssen
Konflikt zwischen Coding-Standards und algorithmischer Klarheit
- Diese Technik verletzt gängige Coding-Standards in erheblichem Maß
- Es erscheinen unpassende geschweifte Klammern innerhalb von Makros
- Es werden
case-Anweisungen in Unterblöcken verwendet crReturnverstecktswitch,returnundcasein einem einzigen Makro
- Makros, die syntaktische Struktur verbergen, können nach Coding-Standards als Beeinträchtigung der Klarheit gelten
- Allerdings bestehen auch in explizite Zustandsmaschinen umgeschriebene Funktionen aus kleinen
case STATE-Blöcken und Zustandsübergängen und unterscheiden sich optisch kaum von Funktionen mit aufgereihtengoto-Labelblöcken - Je länger eine Funktion wird, desto stärker zerstört die Umschreibung in eine Zustandsmaschine die ursprüngliche algorithmische Struktur
- Die Technik ist ein Kompromiss: Sie verbirgt einen Teil der Syntax, um die Algorithmusstruktur deutlicher sichtbar zu machen
Reentrante Variante und bereitgestellter Code
- Die einfache Spielzeug-Implementierung hängt von
static-Variablen ab und ist daher nicht reentrant und auch für Multithreading ungeeignet - In echten Anwendungen muss sich dieselbe Funktion in mehreren Kontexten aufrufen lassen, wobei jeder Kontext hinter seinem jeweils letzten
returnweiterlaufen können muss - Die verbesserte Methode besteht darin, einen Zeiger auf eine Kontextstruktur als zusätzlichen Parameter zu übergeben
- Sowohl lokale Zustände als auch die Koroutinen-Zustandsvariable werden zu Mitgliedern dieser Struktur
- Variablen wie Schleifenzähler müssen dann statt als
ietwa alsctx->iangesprochen werden - Der Code wird etwas unschöner, beseitigt aber das Reentranzproblem, ohne die Gesamtstruktur der Routine zu zerstören
- C++-Benutzer können Koroutinen als Klassenmitglieder anlegen und den lokalen Variablen entsprechende Zustände in der Klasse unterbringen, was mit Scopes natürlicher umgeht
- Das bereitgestellte
coroutine.himplementiert diesen Koroutinen-Trick als vordefinierte Makrosammlung- Makros mit dem Präfix
scrsind die einfache Variante mitstatic-Variablen - Makros mit dem Präfix
ccrsind die fortgeschrittene reentrante Variante - Die ausführliche Dokumentation steht in den Kommentaren der Header-Datei
- Makros mit dem Präfix
- Visual C++ 6 kommt in der Standard-Debug-Einstellung „Program Database for Edit and Continue“ mit dem Makro
__LINE__auf seltsame Weise nicht zurecht und mag diesen Trick daher nicht- Um Programme mit Koroutinen unter VC++ 6 zu kompilieren, muss Edit and Continue deaktiviert werden
- Dazu wählt man in den Projekteinstellungen auf dem Reiter „C/C++“, in der Kategorie „General“, bei „Debug info“ eine andere Option als „Program Database for Edit and Continue“
- Die Header-Datei wird unter der MIT-Lizenz bereitgestellt
Verwandte Hinweise und praktischer Einsatz
- Donald Knuths The Art of Computer Programming, Volume 1, Section 1.4.2 behandelt Koroutinen in ihrer reinen Form
- In Tom Duffs Diskussion über Duff’s device gibt es Hinweise darauf, dass er unabhängig auf einen ähnlichen Koroutinen-Trick gekommen sein könnte; ein Update vom 2005-03-07 bestätigt, dass Tom Duff dies in einem Blogkommentar bestätigte
- Der SSH-Protokollcode von PuTTY verwendet diesen Koroutinen-Trick tatsächlich in der Praxis
- Das PuTTY-Beispiel zeigt ein ungewöhnlich starkes Maß an C-Hacking in ernsthaftem Produktionscode
1 Kommentare
Meinungen auf Hacker News
Beim Versuch, in einem C-Projekt die API-Komplexität zu reduzieren, bin ich mehrfach wieder auf diese Seite gestoßen, und ich finde die Erklärung zum Kontrollfluss hervorragend.
Sie hat mir auch geholfen, klarer über das Speichern von Zustand innerhalb und außerhalb des Stacks sowie über die Unterschiede in der Lesbarkeit je nach Ansatz nachzudenken.
Mein aktuelles Fazit ist, dass besser die Nutzer einer Bibliothek entscheiden sollten, ob sie C-Coroutinen verwenden wollen. Mongoose (https://github.com/cesanta/mongoose behandelt Asynchronität zum Beispiel über Event-Callbacks; solche Bibliotheken lassen sich viel angenehmer mit den Thread-/Task-Primitiven des jeweiligen Systems umhüllen, statt sie auf mythische plattformübergreifende C-Coroutinen oder, noch schlimmer, auf
std::threadportieren zu wollen.[1] https://www.chiark.greenend.org.uk/~sgtatham/putty/
[2] https://www.chiark.greenend.org.uk/~sgtatham/puzzles/
Coroutinen sind ein wirklich schönes Konzept, und es macht besonders Spaß, sich die C++-Coroutine-Videos von der CppCon anzusehen, die vor allem von Microsoft-Leuten präsentiert wurden. Auch der Aufhänger „negative-cost abstraction“ ist ziemlich gut.
Vor ein paar Jahren haben Freunde bei Meta angefangen, C++-Coroutinen zu verwenden, und später erzählten sie mir, dass das letztlich ein großer Fehler gewesen sei. Sie mussten sich mit Bugs in Compiler-Implementierungen herumschlagen, was ziemlich unschön nachzuverfolgen gewesen sein dürfte. Bei Google warte ich darauf, dass die sehr guten Leute, die das sauber in google3/ integrieren, uns sagen, dass man es jetzt verwenden kann.
Dieser Beitrag erklärt über Duff's Device [1] eine makrobasierte, strukturierte
goto-Technik als Implementierungsstrategie für C-Coroutinen. Der Kern ist, dass man innerhalb einesswitch-Blocks fast überallcase-Anweisungen platzieren kann; man umschließt die gesamte Funktion mit einemswitch, speichert die letzte Rücksprungposition der Coroutine in einerstatic-Variablen und markiert jedescoReturnmit einemcase-Label.Auch Sustriks Beitrag zu C-Coroutinen könnte interessant sein [2].
[1] https://en.wikipedia.org/wiki/Duff%27s_device
[2] https://250bpm.com/blog:48/index.html
co_yield,co_returnundco_awaitüberall im C++-Code verstreut ist.Der Vorteil gegenüber der internen Vorgehensweise bei Google ist, dass beim Lesen des Codes die Asynchronität der einzelnen Teile klar sichtbar ist. Manche Programmierer bei Google kennen das Threading-Modell nur bis auf Ebene einzelner Branches und bauen später dadurch schwerwiegende Bugs ein.
Der Nachteil ist einfacher: Viel Code ist „möglicherweise asynchron“, und mit der Zeit wird alles asynchron, nur weil Programmierer in diesem Modus schreiben. Ob man einen Spinlock oder einen yieldenden Mutex verwendet, sollte von der Größe des kritischen Abschnitts und der jeweiligen Threading-Situation abhängen; achtet man jedoch auf Lesbarkeit und Konsistenz, kippt leicht das ganze Projekt in die eine oder andere Richtung.
Ich würde gern mehr über Threading-Sprachimplementierungen wissen, die keinen der beiden Ansätze als Default setzen und anhand von Profilen früherer Ausführungen die nächste Ausführung weiter optimieren, ohne Codeänderungen oder Bugs zu verursachen.
lc-addrlabels.hsteckt.Ich habe außerdem GCCs lokale Labels genutzt, um die Verwendung von
__LINE__vollständig zu vermeiden; dadurch konnten auch mehrerecoReturnin einer Zeile stehen.switch-Blocks fast überallcase-Anweisungen verwenden kann, stimmt wahrscheinlich; die Funktion selbst ist aber mit ziemlicher Sicherheit beabsichtigt.Wie auch am Ende des Artikels steht, erkannte Duff zwar ebenfalls, dass man darauf Coroutinen aufbauen kann, hielt diese Idee aber für „widerlich“.
Wenn man Cs
switchwie eine wenig ausdrucksstarke Form von Pattern Matching betrachtet, wirkt „fallthrough“ leicht wie ein Bug, ist es aber nicht. Es gehört zur selben Familie wie das berechnetegotoin Fortran und ist bequemer, weil die Werte nicht fortlaufend sein müssen und man nicht alle Labels ganz oben auflisten muss. Wenn ich es so aufschreibe, wirkt es vielleicht sogar eher wie ein berechnetesCOMEFROM.Die Aussage „Keine weit verbreitete High-Level-Sprache unterstützt Coroutinen“ mag im Jahr 2000 gestimmt haben, heute unterstützen sie jedoch viele Sprachen, darunter C++20, Lua, Python und Ruby.
yieldvermutlich schon damals oder nicht allzu lange danach gegeben.Der am Ende des Artikels vorgeschlagene Verbesserungsansatz – „einen Zeiger auf eine Kontextstruktur als zusätzliches Funktionsargument hinzufügen und allen lokalen Zustand sowie die Zustandsvariablen der Coroutine als Elemente dieser Struktur deklarieren“ – wirkt wie die Implementierung einer Closure. Es scheint darum zu gehen, die aufgerufene Seite zu einem Lambda zu machen und externe Variablen/Kontext/Zustand zu nutzen, um zu entscheiden, was getan oder welcher Wert verwendet werden soll. Ich frage mich, ob ich das richtig verstanden habe.
Die
switch-Methode ist zwar nicht extrem selten, aber üblicherweise verwendet man einen Zustandszeiger, der an die Initialisierungsfunktion und die Coroutine-Funktion übergeben wird.In Embedded-Projekten habe ich diese Methode oft verwendet: Eine Coroutine kümmerte sich etwa um Beschleunigen/Abbremsen eines Motors, eine andere teilte nur mit, in welche Richtung es gehen sollte. Ich habe sie auch schon in einer Netzwerkbibliothek[1] eingesetzt. Auch in der Standardbibliothek gibt es Coroutine-Funktionen wie
strtok()[2].Um das handhabbar zu machen, muss man nicht gleich die Makro-Hölle einführen, aber das Lesen des
switch/case-Kontrollflusses hat mir noch nie Spaß gemacht.[1]: https://github.com/REONTeam/libmobile/blob/master/relay.c#L3...
[2]: https://manpages.debian.org/bookworm/manpages-dev/strtok.3.e...
Vom selben Autor gibt es auch Simon Tatham's Portable Puzzle Collection.
https://www.chiark.greenend.org.uk/~sgtatham/puzzles/
Wenn das wie schwarze Magie in C wirkt, lohnt sich vom selben Autor auch der Text darüber, wie man mit Makros beliebige Kontrollstrukturen baut: https://www.chiark.greenend.org.uk/~sgtatham/mp/
Coroutinen sind interessant, aber in echtem Code sollte man auch den Einsatz echter Threads erwägen. Moderne Prozessoren haben viele Kerne, Coroutinen nutzen aber häufig nur einen einzelnen Kern.
Das ist auch ein reales Problem. Bis vor Kurzem hat qemu, das viele Coroutinen verwendet, einen erheblichen Teil des Block-Device-I/O über einen einzelnen Thread geleitet, was Performance-Probleme verursachte. Kevin Wolf und andere haben das über Jahre hinweg behoben, sodass modernes qemu nun mehrere Threads für I/O verwendet; diese Arbeit soll in RHEL 9.4 einfließen.
Coroutinen sind eine Methode zur Strukturierung von Single-Thread-Ausführung und für sich genommen nützlich. Das Producer-Consumer-Pattern aus dem Text ist ein gutes Beispiel; einen Stream an einen Parser anzubinden ist kein paralleler Algorithmus, daher helfen Threads beim Schreiben dieses Codes nicht.
Wenn man ein Single-Thread-Paradigma für Aufgaben verwendet, die parallel ausführbar sind, ist das natürlich ineffizient. Coroutinen sind aber nicht „Parallelität für Arme“, sondern eine eigenständig sinnvolle Kontrollstruktur. Sie lassen sich auch produktiv mit Threads kombinieren, etwa wenn in einem Webserver eine Event Loop zusammen mit einem Dispatcher Coroutinen zwischen mehreren blockierenden Ereignissen hindurchfädelt. Wenn die Runtime zur Parallelisierung pro Kern einen Thread startet, reduziert sich die Koordination zwischen Threads darauf, die Tiefe der jeweiligen Work Queues zu prüfen und Requests an die weniger ausgelastete Seite weiterzureichen.
Die Idee von Coroutinen ist, dass man sie verwendet, wenn es eine lokale Aufgabe und synchrone Daten gibt und es einfacher ist, dies in einer invertierten Form auszudrücken: Eine Funktion loopt über etwas und „pusht“ Ergebnisse an einen abstrakten Consumer an anderer Stelle, statt im funktionalen Paradigma, in dem der Aufrufer die innere Schleife „pullt“.
Danach verschiebt man Coroutinen kaum zwischen Schedulern, und auch Daten zwischen Coroutinen verschiedener Scheduler werden kaum geteilt.
Coroutinen ermöglichen durch kooperatives Scheduling einen angenehmen Stil nebenläufiger Programmierung ganz ohne Locks. Üblicherweise wird die Scheduling-Latenz größer, aber weil der Overhead durch atomare Operationen/Locks wegfällt und Timer die Ausführung nicht ständig für präemptives Scheduling unterbrechen, kann der Durchsatz ziemlich hoch werden.
Die C++-Version dieses Ansatzes: https://www.codeproject.com/Tips/29524/Generators-in-C
Ich verwende das vorsichtshalber auch in meinem Sciter; es funktioniert ziemlich gut und ist praktisch.
Eine modulare und sichere Möglichkeit, dies zu erreichen, wären vermutlich Effekt-Handler. Sie ähneln
yieldin Python, können aber Werte zurückgeben und sind nicht auf Funktionsaufrufe beschränkt, sondern wie Exceptions gescoped. Wenn man damit nicht vertraut ist, liefert dieser Artikel eine gute Motivation.Jede Funktion, die im Direct Style geschrieben ist, kann einen „Effekt“ ausführen, wenn die Kontrolle an eine andere Stelle gehen soll. Hier wären
c=getchar()undemit(c)solche Fälle.Dann geht die Kontrolle an den Effekt-Handler; in diesem Fall entscheidet vermutlich der Aufrufer der beiden Funktionen, was als Nächstes passiert. Gibt der Dekompressor ein Zeichen aus, wird dieses an den Parser-Code übergeben und dieser fortgesetzt; er läuft weiter, bis der Parser mehr benötigt, und dann wird wieder der Dekompressor fortgesetzt.
Effekte können effizient implementiert werden, insbesondere wenn man Continuations darauf beschränkt, nur einmal aufgerufen werden zu können. OCaml ist ein Beispiel dafür. Das erlaubt Direct-Style-Code zusammen mit Typ- und Speichersicherheit und ist auch in nebenläufigen Umgebungen sehr nützlich.
Ein Beispiel gibt es hier: https://effekt-lang.org/docs/casestudies/lexer
Dem Teil „Dieser Trick verstößt natürlich gegen alle Coding-Standards … ich würde behaupten, dass die Coding-Standards falsch sind“ stimme ich überhaupt nicht zu.
Es ist nicht falsch, dass Coding-Standards diesen Code ablehnen; dieser Code ist lediglich ein netter Trick. Software Engineering im großen Maßstab bedeutet, Überraschungen zu eliminieren und Code zu schreiben, den auch jemand mit Schlafmangel lesen kann, der um 3 Uhr morgens zum Debuggen gerufen wird. Man kann nicht erwarten, dass Programmierer immer vier Grundregeln im Kopf behalten.
Die Behauptung, man habe wichtige Elemente wie
switch,returnundcasein „obfuskierenden“ Makros versteckt und damit zwar die syntaktische Struktur verschleiert, aber die algorithmische Struktur offengelegt, überzeugt mich ebenfalls nicht. Ein gutes Programm sollte sowohl in seiner syntaktischen Struktur als auch in seiner algorithmischen Struktur klar sein, und dieser Ansatz bleibt dahinter zurück. Ich denke, Rusts Art, inasync-Funktionen implizite Zustandsmaschinen zu erzeugen, sollte hier das Vorbild sein.In der C-Welt gilt schon der ternäre Operator als zu aufregend, und C99 wird als neumodischer Kram behandelt. In der C++-Welt ist der einzige Grund, von Template-Metaprogrammierung abzuraten, dass man im verwendeten Standard dasselbe mit
constexprerledigen kann.