So wird die Cont-Monade definiert:
newtype Cont r a = Cont { runCont :: (a -> r) -> r }
instance Monad (Cont r) where
return a = Cont ($ a)
m >>= k = Cont $ \c -> runCont m $ \a -> runCont (k a) c
Können Sie erklären, wie und warum dies funktioniert? Was macht es?
haskell
monads
continuations
monb
quelle
quelle
Antworten:
Das Erste, was man an der Fortsetzungsmonade erkennen muss, ist, dass sie im Grunde genommen überhaupt nichts tut . Das ist wahr!
Die Grundidee einer Fortsetzung im Allgemeinen ist, dass sie den Rest einer Berechnung darstellt . Angenommen, wir haben einen Ausdruck wie diesen :
foo (bar x y) z
. Extrahieren Sie nun nur den in Klammernbar x y
gesetzten Teil. Dies ist Teil des Gesamtausdrucks, aber nicht nur eine Funktion, die wir anwenden können. Stattdessen müssen sie etwas , das wir eine Funktion anwenden zu . Wir können also in diesem Fall vom "Rest der Berechnung" sprechen\a -> foo a z
, auf den wir uns beziehen können,bar x y
um die vollständige Form zu rekonstruieren.Nun kommt es vor, dass dieses Konzept des "Restes der Berechnung" nützlich ist, aber es ist umständlich, damit zu arbeiten, da es etwas außerhalb des von uns in Betracht gezogenen Unterausdrucks ist. Damit die Dinge besser funktionieren, können wir die Dinge auf den Kopf stellen: Extrahieren Sie den Unterausdruck, an dem wir interessiert sind, und wickeln Sie ihn in eine Funktion ein, die ein Argument enthält, das den Rest der Berechnung darstellt :
\k -> k (bar x y)
.Diese modifizierte Version bietet uns viel Flexibilität - sie extrahiert nicht nur einen Unterausdruck aus ihrem Kontext, sondern ermöglicht es uns auch , diesen äußeren Kontext innerhalb des Unterausdrucks selbst zu manipulieren . Wir können es uns als eine Art suspendierte Berechnung vorstellen , die uns explizite Kontrolle darüber gibt, was als nächstes passiert. Wie können wir das verallgemeinern? Nun, der Unterausdruck ist so gut wie unverändert. Ersetzen wir ihn also einfach durch einen Parameter für die Inside-Out-Funktion und geben uns
\x k -> k x
- mit anderen Worten, nichts weiter als eine umgekehrte Funktionsanwendung . Wir könnten genauso gut schreibenflip ($)
oder ein bisschen exotische Fremdsprache hinzufügen und es als Operator definieren|>
.Nun wäre es einfach, wenn auch mühsam und schrecklich verschleiert, jedes Stück eines Ausdrucks in diese Form zu übersetzen. Zum Glück gibt es einen besseren Weg. Wenn wir als Haskell-Programmierer denken, dass das Erstellen einer Berechnung in einem Hintergrundkontext das nächste ist, was wir denken , ist dies eine Monade? Und in diesem Fall lautet die Antwort ja , ja, das ist es.
Um daraus eine Monade zu machen, beginnen wir mit zwei Grundbausteinen:
m
steht ein Wert vom Typm a
für den Zugriff auf einen Wert vom Typa
im Kontext der Monade.Was bedeutet es, in
a
diesem Zusammenhang Zugang zu etwas Typischem zu haben ? Es bedeutet nur , dass, für einen Wertx :: a
haben wir angewandtflip ($)
aufx
, uns eine Funktion geben , die eine Funktion übernimmt , die ein Argument vom Typ nimmta
, und wendet diese Funktionx
. Angenommen, wir haben eine angehaltene Berechnung, die einen Wert vom Typ enthältBool
. Welchen Typ gibt uns das?> :t flip ($) True flip ($) True :: (Bool -> b) -> b
Für suspendierte Berechnungen
m a
funktioniert der Typ also mit(a -> b) -> b
... was vielleicht ein Höhepunkt ist, da wir die Signatur für bereits kanntenCont
, aber mich vorerst humorisieren .Interessant ist hier, dass eine Art "Umkehrung" auch für den Typ der Monade gilt:
Cont b a
stellt eine Funktion dar, die eine Funktion übernimmta -> b
und auswertetb
. Da eine Fortsetzung "die Zukunft" einer Berechnung darstellt,a
repräsentiert der Typ in der Signatur in gewissem Sinne "die Vergangenheit".Also, ersetzt
(a -> b) -> b
mitCont b a
, was ist der monadischen Typ für unsere Grundbaustein der Reverse - Funktion Anwendung?a -> (a -> b) -> b
übersetzt zua -> Cont b a
... der gleichen Typensignatur wiereturn
und tatsächlich ist es genau das, was es ist.Von hier an fällt alles ziemlich direkt aus den Typen heraus: Es gibt im Grunde keinen vernünftigen Weg, um
>>=
außer der tatsächlichen Implementierung zu implementieren. Aber was ist es eigentlich zu tun ?An dieser Stelle kommen wir zurück zu dem, was ich sagte zu Beginn: die Fortsetzung Monade ist nicht wirklich tut so gut wie nichts. Etwas vom Typ
Cont r a
ist trivial äquivalent zu etwas vom gerechten Typa
, indem einfachid
als Argument für die angehaltene Berechnung angegeben wird. Dies könnte dazu führen, dass man sich fragt, ob, wennCont r a
es sich um eine Monade handelt, die Bekehrung aber so trivial ist, nichta
allein auch eine Monade sein sollte. Das funktioniert natürlich nicht so wie es ist, da es keinen Typkonstruktor gibt, der alsMonad
Instanz definiert werden kann, aber sagen wir, wir fügen einen trivialen Wrapper hinzu, wie zdata Id a = Id a
. Dies ist in der Tat eine Monade, nämlich die Identitätsmonade.Was macht
>>=
die Identitätsmonade? Die Typensignatur istId a -> (a -> Id b) -> Id b
äquivalent zua -> (a -> b) -> b
, was wiederum nur eine einfache Funktionsanwendung ist. Nachdem wir festgestellt haben, dass diesCont r a
trivial äquivalent zu istId a
, können wir daraus schließen, dass es sich auch in diesem Fall(>>=)
nur um eine Funktionsanwendung handelt .Natürlich
Cont r a
ist es eine verrückte umgekehrte Welt, in der jeder Ziegenbart hat. Was also tatsächlich passiert, ist, die Dinge auf verwirrende Weise durcheinander zu bringen, um zwei suspendierte Berechnungen zu einer neuen suspendierten Berechnung zusammenzufassen, aber im Wesentlichen gibt es eigentlich nichts Ungewöhnliches auf! Funktionen auf Argumente anwenden, ho hum, ein weiterer Tag im Leben eines funktionierenden Programmierers.quelle
flip ($) a
als($ a)
.Hier ist Fibonacci:
fib 0 = 0 fib 1 = 1 fib n = fib (n-1) + fib (n-2)
Stellen Sie sich vor, Sie haben eine Maschine ohne Aufrufstapel - sie ermöglicht nur die Schwanzrekursion. Wie
fib
auf diesem Computer ausführen ? Sie können die Funktion leicht umschreiben, um in linearer statt in exponentieller Zeit zu arbeiten, aber das erfordert ein wenig Einsicht und ist nicht mechanisch.Das Hindernis, um den Schwanz rekursiv zu machen, ist die dritte Zeile, in der es zwei rekursive Aufrufe gibt. Wir können nur einen einzigen Anruf tätigen, der auch das Ergebnis liefern muss. Hier kommen Fortsetzungen ins Spiel.
Wir werden einen
fib (n-1)
zusätzlichen Parameter nehmen, der eine Funktion ist, die angibt, was nach der Berechnung des Ergebnisses zu tun ist, und ihn aufrufenx
. Es wirdfib (n-2)
natürlich dazu beitragen. Also: berechnenfib n
Sie berechnenfib (n-1)
danach, wenn Sie das Ergebnis nennenx
, berechnen Siefib (n-2)
, danach, wenn Sie das Ergebnis nenneny
, Sie zurückkommenx+y
.Mit anderen Worten muss man sagen:
Wie wird die folgende Berechnung durchgeführt: "
fib' n c
= Berechnenfib n
undc
auf das Ergebnis anwenden "?Die Antwort lautet, dass Sie Folgendes tun: "Berechnen
fib (n-1)
undd
auf das Ergebnis anwenden ", wobeid x
"Berechnenfib (n-2)
unde
auf das Ergebnis anwenden "e y
bedeutet , wobei " bedeutet"c (x+y)
. In Code:fib' 0 c = c 0 fib' 1 c = c 1 fib' n c = fib' (n-1) d where d x = fib' (n-2) e where e y = c (x+y)
Gleichermaßen können wir Lambdas verwenden:
fib' 0 = \c -> c 0 fib' 1 = \c -> c 1 fib' n = \c -> fib' (n-1) $ \x -> fib' (n-2) $ \y -> c (x+y)
Um tatsächliche Fibonacci zu erhalten, verwenden Sie Identität :
fib' n id
. Sie können sich vorstellen, dass die Zeilefib (n-1) $ ...
ihr Ergebnisx
an die nächste weitergibt.Die letzten drei Zeilen riechen
do
tatsächlich nach einem Blockfib' 0 = return 0 fib' 1 = return 1 fib' n = do x <- fib' (n-1) y <- fib' (n-2) return (x+y)
ist bis auf neue Typen per Definition der Monade gleich
Cont
. Unterschiede beachten. Es gibt\c ->
am Anfang, stattx <- ...
gibt... $ \x ->
undc
stattreturn
.Versuchen Sie,
factorial n = n * factorial (n-1)
mit CPS in einem rekursiven Schwanzstil zu schreiben .Wie funktioniert das
>>=
?m >>= k
ist äquivalent zudo a <- m t <- k a return t
Wenn Sie die Übersetzung im gleichen Stil wie in
fib'
zurücksetzen, erhalten SieVereinfachung
\t -> c t
zuc
m >>= k = \c -> m $ \a -> k a c
Hinzufügen neuer Typen, die Sie erhalten
m >>= k = Cont $ \c -> runCont m $ \a -> runCont (k a) c
Das ist oben auf dieser Seite. Es ist komplex, aber wenn Sie wissen, wie man zwischen
do
Notation und direkter Verwendung übersetzt, müssen Sie die genaue Definition von nicht kennen>>=
! Die Fortsetzungsmonade ist viel klarer, wenn Sie sich Do-Blöcke ansehen.Monaden und Fortsetzungen
Wenn Sie sich diese Verwendung von Listenmonade ansehen ...
do x <- [10, 20] y <- [3,5] return (x+y) [10,20] >>= \x -> [3,5] >>= \y -> return (x+y) ([10,20] >>=) $ \x -> ([3,5] >>=) $ \y -> return (x+y)
das sieht nach Fortsetzung aus! In der Tat,
(>>=)
wenn Sie ein Argument anwenden, hat Typ,(a -> m b) -> m b
der istCont (m b) a
. Siehe sigfpes Mutter aller Monaden zur Erklärung. Ich würde das als eine gute Fortsetzung Monad Tutorial betrachten, obwohl es wahrscheinlich nicht so gemeint war.Da Fortsetzungen und Monaden in beiden Richtungen so stark miteinander verbunden sind, denke ich, dass das, was für Monaden gilt, für Fortsetzungen gilt: Nur harte Arbeit wird sie lehren und keine Burrito-Metapher oder Analogie lesen.
quelle
where e y = c (x+y)
BEARBEITEN: Artikel auf den unten stehenden Link migriert.
Ich habe ein Tutorial geschrieben, das sich direkt mit diesem Thema befasst und das Sie hoffentlich nützlich finden werden. (Es hat sicherlich dazu beigetragen, mein Verständnis zu festigen!) Es ist etwas zu lang, um bequem in ein Stapelüberlauf-Thema zu passen, also habe ich es in das Haskell-Wiki migriert.
Bitte sehen Sie: MonadCont unter der Haube
quelle
Ich denke, der einfachste Weg, die
Cont
Monade in den Griff zu bekommen, besteht darin, zu verstehen, wie man ihren Konstruktor benutzt. Ich gehe vorerst von der folgenden Definition aus, obwohl die Realitäten destransformers
Pakets etwas anders sind:newtype Cont r a = Cont { runCont :: (a -> r) -> r }
Das gibt:
Cont :: ((a -> r) -> r) -> Cont r a
Cont r a
Um einen Wert vom Typ Typ zu erstellen , müssen wir eine Funktion zuweisen fürCont
:value = Cont $ \k -> ...
Jetzt hat es
k
selbst Typa -> r
, und der Körper des Lambda muss Typ habenr
. Eine naheliegende Sache wäre,k
auf einen Wert vom Typ anzuwendena
und einen Wert vom Typ zu erhaltenr
. Wir können das tun, ja, aber das ist wirklich nur eines von vielen Dingen, die wir tun können. Denken Sie daran,value
dass es nicht polymorph sein mussr
, sondern vom TypCont String Integer
oder etwas anderem Konkretes sein kann. Damit:k
auf mehrere Werte des Typs anwendena
und die Ergebnisse irgendwie kombinieren.k
auf einen Wert vom Typ anwendena
, das Ergebnis beobachten und uns dann entscheiden,k
auf etwas anderes basierend darauf anzuwenden .k
und selbst einenr
Typwert produzieren.Aber was bedeutet das alles? Was ist am
k
Ende? Nun, in einem Do-Block könnten wir so etwas haben:flip runCont id $ do v <- thing1 thing2 v x <- Cont $ \k -> ... thing3 x thing4
Hier ist der lustige Teil: Wir können in unseren Gedanken und etwas informell den do-Block beim Auftreten des
Cont
Konstruktors in zwei Teile teilen und den Rest der gesamten Berechnung danach als einen Wert an sich betrachten. Aber Momentx
mal , was es ist, hängt davon ab, was es ist. Es ist also wirklich eine Funktion von einem Wertx
vom Typa
bis zu einem Ergebniswert:restOfTheComputation x = do thing3 x thing4
In der Tat, dies
restOfTheComputation
ist grob gesagt , wask
oben ist , endet. Mit anderen Worten, Sie rufenk
mit einem Wert auf, der das Ergebnisx
IhrerCont
Berechnung wird, der Rest der Berechnung wird ausgeführt, und dannr
erzeugt sich das erzeugte Ergebnis als Ergebnis des Aufrufs an in Ihr Lambda zurückk
. Damit:k
mehrmals aufgerufen haben, wird der Rest der Berechnung mehrmals ausgeführt, und die Ergebnisse können beliebig kombiniert werden.k
, wird der Rest der gesamten Berechnung übersprungen, und der beiliegenderunCont
Aufruf gibt Ihnen nur den Wert des Typs zurück, denr
Sie synthetisiert haben. Das heißt, es sei denn , ein anderer Teil der Berechnung ruft man von ihrk
, und Flickschusterei mit dem Ergebnis ...Wenn Sie zu diesem Zeitpunkt noch bei mir sind, sollte es leicht zu erkennen sein, dass dies ziemlich mächtig sein könnte. Lassen Sie uns einige Standardtypklassen implementieren, um den Punkt ein wenig zu verdeutlichen.
instance Functor (Cont r) where fmap f (Cont c) = Cont $ \k -> ...
Wir erhalten einen
Cont
Wert mit dem Bindungsergebnisx
vom Typa
und eine Funktionf :: a -> b
, und wir möchten einenCont
Wert mit dem Bindungsergebnisf x
vom Typ erstellenb
. Um das Bindungsergebnis festzulegen, rufen Sie einfachk
...fmap f (Cont c) = Cont $ \k -> k (f ...
Warten Sie, woher kommen wir
x
? Nun, es wird involviert seinc
, was wir noch nicht benutzt haben. Denkenc
Sie daran, wie es funktioniert: Es erhält eine Funktion und ruft diese Funktion mit ihrem Bindungsergebnis auf. Wir wollen unsere Funktion mitf
auf dieses Bindungsergebnis angewendet aufrufen . Damit:fmap f (Cont c) = Cont $ \k -> c (\x -> k (f x))
Tada! Als nächstes
Applicative
:instance Applicative (Cont r) where pure x = Cont $ \k -> ...
Das ist einfach. Wir möchten, dass das Bindungsergebnis das ist, das
x
wir erhalten.pure x = Cont $ \k -> k x
Nun
<*>
:Cont cf <*> Cont cx = Cont $ \k -> ...
Dies ist etwas kniffliger, verwendet jedoch im Wesentlichen dieselben Ideen wie in fmap: Holen Sie sich zuerst die Funktion von Anfang an
Cont
, indem Sie ein Lambda erstellen , damit es Folgendes aufruft :Cont cf <*> Cont cx = Cont $ \k -> cf (\fn -> ...
Holen Sie sich dann den Wert
x
aus der Sekunde und erstellen Siefn x
das Bindungsergebnis:Cont cf <*> Cont cx = Cont $ \k -> cf (\fn -> cx (\x -> k (fn x)))
Monad
ist ähnlich, obwohl erfordertrunCont
oder ein Fall oder lassen Sie den Newtype entpacken.Diese Antwort ist schon ziemlich lang, also werde ich nicht darauf eingehen
ContT
(kurz: es ist genau das gleiche wieCont
! Der einzige Unterschied besteht in der Art des Typkonstruktors, die Implementierungen von allem sind identisch) odercallCC
(ein nützlicher Kombinator, der bietet eine bequeme Möglichkeit zum Ignorierenk
und Implementieren eines frühen Austritts aus einem Unterblock.Versuchen Sie für eine einfache und plausible Anwendung den Blog-Beitrag von Edward Z. Yang, in dem die Bezeichnung break implementiert ist , und fahren Sie mit den Schleifen fort .
quelle
Der Versuch, die anderen Antworten zu ergänzen:
Verschachtelte Lambdas sind für die Lesbarkeit schrecklich. Dies ist genau der Grund, warum ... in ... und ... wo ... existieren, um verschachtelte Lambdas mithilfe von Zwischenvariablen zu entfernen. Mit diesen kann die Bindungsimplementierung in Folgendes umgestaltet werden:
newtype Cont r a = Cont { runCont :: (a -> r) -> r } instance Monad (Cont r) where return a = Cont ($ a) m >>= k = k a where a = runCont m id
Was hoffentlich klarer macht, was passiert. Der Wert für die Rückgabeimplementierungsfelder ist faul. Wenn Sie die runCont-ID verwenden, wird die ID auf den Boxwert angewendet, der den ursprünglichen Wert zurückgibt.
Für jede Monade, bei der ein Boxwert einfach entpackt werden kann, gibt es im Allgemeinen eine triviale Implementierung von bind, bei der der Wert einfach entpackt und eine monadische Funktion darauf angewendet wird.
Um die verschleierte Implementierung in der ursprünglichen Frage zu erhalten, ersetzen Sie zuerst ka durch Cont $ runCont (ka), das wiederum durch Cont $ \ c-> runCont (ka) c ersetzt werden kann
Jetzt können wir das Wo in einen Unterausdruck verschieben, so dass wir übrig bleiben
Cont $ \c-> ( runCont (k a) c where a = runCont m id )
Der Ausdruck in Klammern kann in \ a -> runCont (ka) c $ runCont m id desugariert werden.
Zum Abschluss verwenden wir die Eigenschaft von runCont, f (runCont mg) = runCont m (fg), und kehren zum ursprünglichen verschleierten Ausdruck zurück.
quelle