Während ich versuchte, etwas Intuition für den ContT-Monade-Transformer aufzubauen, fand ich mich (vielleicht nicht überraschend) verwirrt. Das Problem liegt in der shiftT-Operation, die nichts Nützliches zu tun scheint.
Zuerst ein einfaches Beispiel, wie man es benutzen könnte
%Vor% famr a
könnte ein etwas komplexerer Ausdruck sein, solange er m r
zurückgibt. Nun, ein Versuch, meine Intuition zu erklären, dass shiftT ist, fügt nichts hinzu:
Es stellte sich heraus, dass wir ContT direkt erstellen könnten.
Fragestunde: Gibt es eine Situation, in der shift / shiftT etwas über cont / ContT hinzufügt? Oder werden sie nur verwendet, um den Code lesbarer zu machen?
Nach dem Suche Github von gurkenglas 's Rat, den ich diese sehr schöne Erklärung von shiftT
und resetT
mit Anwendungsbeispielen, Motivation und Semantik!
Diese Funktionen sind sehr einfach. Ihre Definition in transformers
Bibliothek ist einfach:
Aber Philosophie und Bedeutung liegen weit hinter einem intuitiven Verständnis. Daher empfehle ich Ihnen, die Erklärung über den obigen Link zu lesen. Manchmal passiert es, dass Dinge, die einfach zu definieren sind, etwas Komplexes tun können.
Angepasste Dokumentation aus der Erklärung in oben verlinkten Pugs:
shiftT
shiftT
ist wiecallCC
, außer dass Sie die Fortsetzung aktivieren vonshiftT
zur Verfügung gestellt, wird es bis zum Ende des nächsten einschließendenresetT
laufen, Springe dann zurück zu dem Punkt, an dem du die Fortsetzung aktiviert hast. Beachten Sie, dass die Kontrolle schließlich zu dem Punkt nach dem zurückkehrt Subkontinuation ist aktiviert, Sie können es mehrmals in der aktivieren gleicher Block. Dies ist anders als in den Fortsetzungen voncallCC
, die den aktuellen Wert verwerfen Ausführungspfad bei Aktivierung.Siehe
resetT
für ein Beispiel, wie diese Begrenzungsunterteilungen tatsächlich aussehen arbeiten.
resetT
Erstellen Sie einen Gültigkeitsbereich, für den
%Vor%shiftT
die letzten Unterteilungen garantiert Beenden Sie das Ende von. Betrachten Sie dieses Beispiel:Dies wird:
- aus
Führe
alfa
- aus
Führe
bravo
- aus
Führe
charlie
- aus
Binden Sie
x
an 1 und führen Sie daherzulu 1
- zurück
Fällt vom Ende von
resetT
und springt direkt nachesc 1
- aus
Führe
delta
- aus
Binden Sie
x
an 2 und führen Sie daherzulu 2
- zurück
Fällt vom Ende von
resetT
und springt direkt nachesc 2
Escape von
resetT
, was dazu führt, dass es 0 ergibtIm Gegensatz zu den Fortsetzungen von
callCC
werden diese Unterkontinuationen schließlich zurück zum Punkt, nachdem sie aktiviert wurden, nachdem sie vom Ende des nächsteresetT
.
Sie haben Recht, dass Fortsetzungen begrenzt undefinierten oder unzureichend abgegrenzten Fortsetzungen mit ausgedrückt werden kann. Daher können die Definitionen von shiftT
und resetT
immer nur mit ContT
beschrieben werden. Aber:
Im Wesentlichen ermöglichen Fortsetzungen, ein Programm umzudrehen: Der durch reset
abgegrenzte Block wird innerhalb des inneren Teils des Programms gequetscht, wenn shift
die übergebene Funktion aufruft. (Im Falle von undefinierten Fortsetzungen ist der gesamte Ausführungskontext drinnen gedrängt, was sie so komisch macht.)
Lassen Sie uns ein paar Beispiele machen:
%Vor% Wenn wir reset
ohne shift
haben, ist es nur eine reine Berechnung, nichts Besonderes. Die obige Funktion gibt einfach 0
zurück.
Nun können wir beide verwenden:
%Vor% Das wird interessanter.
Der Code zwischen shift
und reset
wird tatsächlich in die Aufrufe von esc
gedrückt, in diesem einfachen Beispiel ist es nur return $ 1 + r
. Wenn wir esc
aufrufen, wird die gesamte Berechnung durchgeführt und ihr Ergebnis wird das Ergebnis des Aufrufs esc
. Wir machen das zweimal, also rufen wir im Wesentlichen alles zwischen shift
und reset
zweimal auf. Und das Ergebnis der gesamten Berechnung ist result $ x * y
, das Ergebnis des Aufrufs shift
.
So in einem Sinn, der shift
Block wird der äußere Teil der Berechnung und der Block zwischen reset
und shift
wird der innere Teil der Berechnung.
So weit, so gut. Aber es wird noch entmutigender, wenn wir shift
zweimal aufrufen, wie in diesem Codebeispiel:
Und hier ist, was es produziert (versteckt für diejenigen, die versuchen wollen, es als Übung herauszufinden):
[(1,"a"),(1,"b"),(1,"c"),(2,"a"),(2,"b"),(2,"c"),(3,"a"),(3,"b"),(3,"c")]
Nun passiert das Programm zweimal :
x <- shift ...
an den Aufruf yieldx
gebunden, einschließlich des nächsten shift
. Und das Ergebnis der Berechnung ist das Ergebnis des Blocks x <- shift ...
. y <- shift ...
in yieldx
aufgerufen wird, ist der Rest der Berechnung wiederum an den Aufruf yieldy
gebunden. Und das Ergebnis dieser inneren Berechnung ist das Ergebnis des Blocks y <- shift ...
. Also lassen wir in x <- shift
den Rest der Berechnung für jedes der drei Argumente laufen, und während jedes von ihnen machen wir eine ähnliche Sache für jedes der anderen drei Argumente. Das Ergebnis ist dann das kartesische Produkt der beiden Listen, da wir im Wesentlichen zwei verschachtelte Schleifen durchgeführt haben.
Das gleiche gilt für shiftT
und resetT
, nur mit zusätzlichen Nebenwirkungen. Wenn wir zum Beispiel debuggen wollen, was tatsächlich passiert, können wir den obigen Code in den IO
monad- und print-Debugging-Anweisungen ausführen:
Tags und Links haskell continuations delimited-continuations