Es ist leider ein verbreitetes Missverständnis, dass der Monad irgendwie eine Ausführungsreihenfolge erzwingt. Die Wahrheit ist subtiler: Der Monad ist ein Abstraktions-Mechanismus, der sich zufälligerweise gut eignet, um den *eigentlichen* Mechanismus zu kapseln. Denn der ist definitiv ein Implementierungs-Detail - es gibt tatsächlich mehrere verschiedene Möglichkeiten! Ich versuche mal zwei zu erklären - einmal den "pur funktionalen" Weg, und dann den praktischen Weg, wie ihn GHC implementiert.
Die erste Möglichkeit wäre, sich das Programm als eine große Funktion vorzustellen, die einen Stream von "Responses" zu einem Stream von "Requests" macht, und auf diese Weise mit der Runtime kommuniziert. Jeder Seiteneffekt wäre hier ein Element in der "Request"-Liste. Durch die Listen-Konstruktion wäre das Ganze auf natürliche Weise geordnet - es kann nichts übersprungen oder ignoriert werden. Dank Lazy Evaluation ist es auch kein Problem, einen Request aus dem Ergebnis zu extrahieren, um erst dann die entsprechende Antwort in den Parameter einzufügen. Auf der anderen Seite wäre das allerdings ziemlich ineffizient - naiv implementiert hätten wir zwei Heap-Allokationen pro Seiteneffekt!
Statt dessen tut GHC, was jeder gute Compiler tut (*hust*): Es implementiert einen geschickt verpackten Hack. Die Idee ist, dass wir einen speziellen "State# RealWorld" Typ haben - Werte stehen für einen Zustand der "realen Welt" außerhalb von Haskell's Zugriff. Hier ist die ungeschönte Original-Definition von IO aus den Tiefen von
GHC.Types: newtype IO a = IO (State# RealWorld -> (# State# RealWorld, a #))
Jede IO-Aktion nimmt also den Zustand der Welt - und ergibt eine neue Welt, plus eventuell ein paar im Programm verwendbare Daten. Die Annahme ist, dass wir diesen Typ immer so verwenden, dass eine Aktion die "reale Welt" der nächsten übergibt. Die Monad-Implementierung stellt das sicher:instance Monad IO where
return x = IO (\s -> (# s, x #) )
(IO m) >>= k = IO (\s -> let (# s2, a #) = m s
IO m2 = k a
in m2 s2)
An diesem Punkt hat der Monad seine Schuldigkeit getan. Alle weitere Magie steckt darin, wie wir primitive IO-Aktionen defineren, und den Eigenschaften des "State# RealWorld" Typs:
- Der "State#"-Typ ist undurchsichtig. Frühe Optimierungs-Stufen wissen nichts über seinen Aufbau, entsprechend können sie ihn nur unter maximal pessimistischen Annahmen durch reichen.
- Jede primitive IO-Aktion mündet in einer primitiven (oder FFI) Funktion, die einen solchen Wert nimmt und wieder zurück gibt. Damit muss GHC immer pessimistisch annehmen, dass eine andere "reale Welt" zurück gegeben wird, als rein gegeben wurde.
- Jeder Thread startet mit genau einem "State# RealWorld" Wert.
All das zusammen führt dazu, dass es in einem Haskell-Programm zu jedem Zeitpunkt der Auswertung nur maximal so viele ausführbare IO-Aktionen wie Threads geben kann. Entsprechend kann GHC gar nicht anders, als alles in der richtigen Reihenfolge auszuwerten. Späte Optimierungsstufen nutzen das aus, indem sie sobald die Ausführungsreihenfolge fest steht dem "State# RealWorld" Typ eine Größe von 0 zu weisen und ihn raus optimieren.
Schlussendlich ist es ein klares "Das willst du nicht wissen". Der Verdienst des Monads ist nicht, das Problem zu lösen, sondern dass du die Lösung nicht kennen musst, um Programme zu schreiben
