Begriff
Continuation Passing Style (CPS)
Warum wichtig?
Dieser Begriff ist ein Knoten im SengakujiWorks-Wissensnetz. Nutze Level 0 für die erste Einordnung, Level 1 für Praxis, Level 2 für technische Struktur und Level 3 für Grenzen, Fallstricke und Expertenkontext.
Normaler Code:
x = add(1, 2);
print(x);
Die Funktion kehrt zurück (return).
In CPS kehrt eine Funktion niemals zurück.
Stattdessen übergibst du ihr den "Rest des Programms" als Extra-Argument (die Continuation).
add(1, 2, (x) => print(x));
"Addiere 1 und 2. Und wenn du fertig bist, ruf diese Funktion mit dem Ergebnis auf."
Das ist das Prinzip von Node.js Callbacks.
Aber man kann damit krasse Dinge bauen:
Eigene Control-Flows (Exceptions, Coroutinen, Backtracking) ohne Sprach-Support.
Merksatz: Ein Programmierstil, bei dem der Kontrollfluss explizit durch Übergabe von Fortsetzungsfunktionen (Continuations) gesteuert wird, anstatt durch den impliziten Call-Stack und Return-Anweisungen.
JavaScript Promise und async/await sind Syntax-Zucker für CPS.
Compiler (wie für Haskell oder Scheme) wandeln deinen Code oft intern in CPS um.
Warum? Weil CPS die Reihenfolge der Ausführung explizit macht.
Optimierungen werden einfacher.
1. Call/CC (Call with Current Continuation)
Ein Operator in Scheme/Lisp.
Er friert den "jetzigen Moment" (Stack) als Funktion k ein.
Du kannst k speichern.
Später rufst du k(wert) auf -> Und das Programm springt sofort zurück an diese Stelle, als wäre nichts passiert.
Damit baut man "Time-Travel Debugger" oder kooperatives Multitasking.
2. Stackless
In reinem CPS braucht man keinen Call-Stack (im CPU-Sinne).
Jeder Aufruf ist ein "Tail Call" (Sprung).
Ein CPS-Programm ist im Grunde eine unendliche GOTO-Schleife.
1. CPS-Transformation (Compilation)
In modernen Compilern (z. B. für SML/NJ oder Scheme) ist die CPS-Transformation ein zentraler Optimierungsschritt.
Dabei wird jede Zwischenrechnung benannt und in eine Kette von Funktionen verwandelt.
Aus (a + b) * c wird add(a, b, (sum) => mult(sum, c, (res) => halt(res))).
Der Vorteil: Der Compiler muss sich nicht mehr um die Reihenfolge der Auswertung (Evaluation Order) kümmern, da sie nun explizit im Code steht. Zudem werden Variablen-Lebensdauern offensichtlich, was die Register-Allocation massiv vereinfacht.
2. Defunctionalization
CPS erzeugt extrem viele kleine Funktionen (Closures), was in der Produktion (Heap-Allocation) teuer sein kann.
Ein Trick, um das zu lösen, ist die Defunctionalization.
Man ersetzt die Continuations durch Daten-Strukturen (Enums/Objects). Anstatt eine Funktion zu übergeben "Drucke den Wert", übergibt man ein Symbol PRINT_TASK. Ein zentraler Dispatcher (ein Loop) arbeitet diese Symbole dann ab. Das verwandelt das abstrakte CPS-Modell zurück in ein hocheffizientes, iteratives Programm, das ohne ständige Speicherallokation auskommt.
3. First-Class Continuations & Web-Server
Ein berühmter Einsatz von CPS war in Web-Frameworks wie Seaside (Smalltalk) oder Links. Web-Anwendungen sind zustandslos. Ein User füllt Seite 1 aus, klickt auf "Next", dann Seite 2. Mit CPS kann man den gesamten Zustand des Servers nach Seite 1 einfrieren (als Continuation speichern) und dem User als verschlüsselten String (oder Session-ID) mitschicken. Klickt der User auf "Next", wird die Continuation "reanimiert". Der Code auf dem Server sieht dann so aus:
let name = show_form("name?"); // Hier pausiert der Server!
let age = show_form("age?"); // Hier geht es weiter, wenn der User antwortet.
Das erlaubt es, Web-Workflows wie einfache Desktop-Skripte zu schreiben, ohne sich um manuelle State-Management-Datenbanken zu kümmern.
Quick-Check
Lesbar?
Hölle ("Callback Hell"). Menschen denken linear, nicht verschachtelt. CPS schreibt man nie freiwillig, außer man baut Compiler.Exceptions?
In CPS trivial. Man übergibt einfach zwei Continuations:success_kunderror_k.div(a, b, success, error). Wenn b=0 -> ruferror. Sonstsuccess.Erfindung?
1970er (Steele & Sussman). Scheme wurde erfunden, um CPS zu verstehen.