Die Programmiersprache Ruby

Blog|

Forum|

Wiki  


Alle Zeiten sind UTC + 1 Stunde [ Sommerzeit ]

Ein neues Thema erstellen Auf das Thema antworten  [ 3 Beiträge ] 
Autor Nachricht
BeitragVerfasst: 13 Jan 2012, 17:23 
Offline
Interpreter
Benutzeravatar

Registriert: 15 Mär 2005, 19:26
Beiträge: 6069
Wohnort: Karlsruhe
Moin, moin!

Meine Hoffnung ist, dass sich hier noch immer ein paar Leute herumtreiben, die sich mit Haskell beschäftigt haben. Im Haskell-Forum ist doch eher sehr wenig los, deshalb schreibe ich das hier nochmal.

Ausführungsreihenfolge bei Monaden

Ich bin noch recht neu in der praktischen Anwendung von Haskell und schlage mich gerade mit einem konzeptionellen Problem herum. Am Beispiel beschrieben, ist...main = do
putStrLn "Vorname: "
vorname <- getLine
putStrLn "Nachname: "
nachname <- getLine
putStrLn (vorname ++ " " ++ nachname)...identisch mit...main = machs

machs =
(>>=) (putStrLn "Vorname: ")
(\ _ -> (>>=) getLine
(\ vorname -> (>>=) (putStrLn "Nachname: ")
(\ _ -> (>>=) getLine
(\ nachname -> putStrLn (vorname ++ " " ++ nachname))
)
)
)...wobei ich nur jede Art von syntaktischem Zucker entfernt habe.

Mir geht es um folgendes Verständnisproblem: (>==) ist eine Funktion, die zwei Parameter hat, eine Aktion und eine Funktion. Die erste Aktion putStrLn "Vorname: " liefert ja keinen Wert an die durch (>==) aufgerufene Funktion, was wegen \_ -> auch der Haskell-Compiler "sieht". Er könnte also wegen der Haskell-Lazyness durchaus zuerst die Funktion ausführen, bevor die Ausgabe erfolgt, da deren "Wert" für die Ausführung der Funktion nicht nötig ist.

Ich sehe nicht, wieso allein durch diesen Monaden-Operator (>==) im Fall \ _ -> eine Ausführungsreihenfolge erzwungen wird - oder liegt das daran, dass bei Monaden Haskell wegen der Nebenwirkungen die Lazyness aufgibt?

Vielleicht wird aus dem bisherigen Text mein Anliegen noch nicht so ganz klar, deshab noch eine kleine Ergänzung. Die Definition von "machs" hat gekürzt folgende Form:

(>>=) (putstrLn "Vorname: ") (\ _ -> ...)

Da der zweite Parameter der Funktion (>>=), wegen des Dummy-Parameters im Lambda-Ausdruck, für seine Auswertung die vorherige Auswertung des ersten Parameters nicht benötigt, sehe ich nicht, wieso rein formal eine Ausführungsreihenfolge erzwungen wird. Es wäre etwas anderes, wenn dort ein echter Parameter stehen würde.

Woher weiss denn nun ein Lazyness berücksichtigender Haskell-Compiler, dass der erste Parameter zuerst ausgewertet werden muss, ohne dass er über die normalen Regeln der Auswertung hinausgehende Spezialinformationen braucht. Die Definition m a -> (a -> m b) -> m b für (>>=), respective die "abgekürzte" für >>, die ja m a -> m b -> m b lautet, entspricht strukturell doch einer Funktionsdefinition

bla a b = b

bei der der Parameter a ja nicht ausgewertet werden muss und wegen der Lazyness auch nicht ausgewertet wird.

Nun ist ja eine Monade nichts Besonderes, sollte doch also seitens des Haskell-Compilers nicht anders behandelt werden, ausser es gäbe ein Bruch im puren System, der aber doch durch die Einführung der monadischen IO genau vermieden werden sollte.

Woher "weiss" der Haskell-Compiler also, dass er in (>>=) (putstrLn "Vorname: ") (\ _ -> ...) den ersten Parameter anfassen muss, während er das beim einem Aufruf der oben definierten Funktion in Form von bla 41 42 nicht braucht?

_________________
WoNáDo.set_state!(:active)


Nach oben
 Profil  
 
BeitragVerfasst: 13 Jan 2012, 23:49 
Offline
Nuby

Registriert: 13 Jan 2012, 21:43
Beiträge: 2
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:
  1. 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.
  2. 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.
  3. 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 :)


Nach oben
 Profil  
 
BeitragVerfasst: 14 Jan 2012, 00:21 
Offline
Interpreter
Benutzeravatar

Registriert: 15 Mär 2005, 19:26
Beiträge: 6069
Wohnort: Karlsruhe
Vielen Dank für die Erklärung. Ich muss das noch mal in Ruhe vertieft durchgehen, aber im Grossen und Ganzen ist es mir jetzt schon klarer. Bin auch deshalb etwas beruhigt, weil ich eben nicht erkennen konnte, wie allein die Definitionen, die zu Monaden gehören, eine Sequenzialisierung erzwingen können. Ich habe da schon langsam arg an mir gezweifelt.

_________________
WoNáDo.set_state!(:active)


Nach oben
 Profil  
 
Beiträge der letzten Zeit anzeigen:  Sortiere nach  
Ein neues Thema erstellen Auf das Thema antworten  [ 3 Beiträge ] 

Alle Zeiten sind UTC + 1 Stunde [ Sommerzeit ]


Wer ist online?

Mitglieder in diesem Forum: 0 Mitglieder und 1 Gast


Du darfst keine neuen Themen in diesem Forum erstellen.
Du darfst keine Antworten zu Themen in diesem Forum erstellen.
Du darfst deine Beiträge in diesem Forum nicht ändern.
Du darfst deine Beiträge in diesem Forum nicht löschen.
Du darfst keine Dateianhänge in diesem Forum erstellen.

Suche nach:
cron