Ograniczona kontynuacja
W językach programowania rozdzielona kontynuacja , kontynuacja możliwa do złożenia lub kontynuacja częściowa jest „wycinkiem” ramki kontynuacji , która została zreifikowana w funkcję . W przeciwieństwie do zwykłych kontynuacji, kontynuacje z ogranicznikami zwracają wartość, dzięki czemu można je ponownie wykorzystać i skomponować . Ograniczniki kontrolne, podstawa kontynuacji z ogranicznikami, zostały wprowadzone przez Matthiasa Felleisena w 1988 r., Chociaż wczesne aluzje do kontynuacji możliwych do złożenia i z ogranicznikami można znaleźć w rozprawie Carolyn Talcott ze Stanford z 1984 r., Felleisen i in. , rozprawa Felleisena z 1987 r. i algorytmy funkcjonalnego cofania się.
Historia
w 1988 r. Wraz z operatorem o nazwie , raz pierwszy wprowadzonym w raporcie technicznym w 1987 r., Wraz z . Operator został zaprojektowany jako uogólnienie operatorów sterujących, które zostały opisane w literaturze, takich jak call/cc
ze Scheme , operator J ISWIM , operator escape
Johna C. Reynoldsa i inne. Następnie społeczność badaczy języków programowania wymyśliła wiele konkurujących ze sobą operatorów sterowania rozdzielanego, takich jak prompt
and control
, shift
and reset
, cupto
, fcontrol
i inne.
Przykłady
W literaturze naukowej zaproponowano różne operatory dla ograniczonych kontynuacji.
Jedna niezależna propozycja jest oparta na stylu przekazywania kontynuacji (CPS) — tj. nie na klatkach kontynuacji — i oferuje dwa operatory sterowania: shift
i reset
. Operator reset
ustawia limit kontynuacji, podczas gdy operator shift
przechwytuje lub reifikuje bieżącą kontynuację aż do najgłębszego otaczającego resetu
. Rozważmy na przykład następujący fragment w Scheme :
( * 2 ( reset ( + 1 ( przesunięcie k ( k 5 )))))
Reset ogranicza kontynuację przechwytywaną przez
shift
(nazwaną przez k
w tym przykładzie). Gdy ten fragment zostanie wykonany, użycie shift
spowoduje powiązanie k
z kontynuacją (+ 1 []),
gdzie []
reprezentuje część obliczeń, która ma zostać wypełniona wartością. Ta kontynuacja bezpośrednio odpowiada kodowi, który otacza przesunięcie w
górę do resetu
. Ponieważ treść przesunięcia (tj. (k 5)
) natychmiast wywołuje kontynuację, ten kod jest równoważny następującemu:
( * 2 ( + 1 5 ))
Ogólnie rzecz biorąc, operatorzy ci mogą zakodować bardziej interesujące zachowanie, na przykład zwracając przechwyconą kontynuację k
jako wartość lub wielokrotnie wywołując k .
Operator przesunięcia
przekazuje przechwyconą kontynuację k
do kodu w swoim ciele, który może ją wywołać, wygenerować jako wynik lub całkowicie ją zignorować. Jakikolwiek wynik przesunięcie
, jest on przekazywany do najbardziej wewnętrznego resetu
, odrzucając kontynuację pomiędzy resetem
a przesunięciem
. Jeśli jednak kontynuacja zostanie wywołana, to skutecznie ponownie instaluje kontynuację po powrocie do resetu
. Gdy całe obliczenie w ramach resetowania
zostanie zakończone, wynik jest zwracany przez kontynuację rozdzielaną. Na przykład w tym schematu :
( reset ( * 2 ( shift k KOD )))
ilekroć CODE
wywołuje (k N)
, (* 2 N)
jest obliczane i zwracane.
Jest to równoważne następującemu:
( niech (( k ( lambda ( x ) ( * 2 x )))) KOD )
Ponadto, gdy całe obliczenie w ramach shift
zostanie zakończone, kontynuacja jest odrzucana, a wykonywanie jest uruchamiane ponownie poza reset
. Dlatego,
( reset ( * 2 ( przesunięcie k ( k ( k 4 )))))
wywołuje (k 4)
najpierw (co zwraca 8), a następnie (k 8)
(co zwraca 16). W tym momencie shift
zostało zakończone, a reszta wyrażenia resetowania
jest odrzucana. Dlatego ostateczny wynik to 16.
Wszystko, co dzieje się poza wyrażeniem resetowania
, jest ukryte, tj. nie ma na nie wpływu przekazanie sterowania. Na przykład to zwraca 17:
( + 1 ( reset ( * 2 ( przesunięcie k ( k ( k 4 ))))))
Ograniczone kontynuacje zostały po raz pierwszy opisane niezależnie przez Felleisena i in. i Johnsona. Od tego czasu były używane w wielu dziedzinach, szczególnie w definiowaniu nowych operatorów sterujących ; zobacz Queinnec w celu uzyskania ankiety.
Spójrzmy na bardziej skomplikowany przykład. Niech null
będzie pustą listą:
( zresetować ( rozpocząć ( shift k ( cons 1 ( k ( void )))) ;; (1) null ))
Kontekst przechwycony przez shift
to (begin [*] null)
, gdzie [*]
to dziura, do której zostanie wstrzyknięty parametr k .
Pierwsze wywołanie k
wewnątrz przesunięcia
ocenia ten kontekst z (void)
= #<void>
zastępując dziurę, więc wartość (k (void))
to (begin #<void> null)
= null
. Ciało przesunięcia
, a mianowicie (cons 1 null)
= (1)
, staje się ogólną wartością wyrażenia resetującego
jako wynik końcowy.
Aby ten przykład był bardziej skomplikowany, dodaj linię:
( reset ( start ( shift k ( cons 1 ( k ( void )))) ( shift k ( cons 2 ( k ( void )))) null ))
Jeśli zakomentujemy pierwszą zmianę
, to już znamy wynik, to jest (2)
; więc równie dobrze możemy przepisać wyrażenie w ten sposób:
( reset ( start ( shift k ( cons 1 ( k ( void )))) ( lista 2 )))
Jest to dość znajome i można je przepisać jako (cons 1 (list 2))
, czyli (list 1 2)
.
Możemy zdefiniować yield
za pomocą tej sztuczki:
(definiuj (wydajność x) (przesuń k (przeciw x (k (pusta)))))
i użyj go na listach budynków:
( zresetuj ( początek ( wydajność 1 ) ( wydajność 2 ) ( wydajność 3 ) null )) ;; (lista 1 2 3)
Jeśli zamienimy cons
na stream-cons
, możemy zbudować leniwe strumienie:
( zdefiniuj ( wydajność strumienia x ) ( shift k ( wydajność strumienia x ( k ( void ))))) ( zdefiniuj leniwy przykład ( reset ( początek ( wydajność strumienia 1 ) ( wydajność strumienia 2 ) ( wydajność strumienia 3 ) strumień-null )))
Możemy to uogólnić i za jednym zamachem przekonwertować listy na strumień:
( definiuj ( lista->strumień xs ) ( resetuj ( rozpocznij ( dla-każdego strumienia-wydajność xs ) strumień-null )))
W bardziej skomplikowanym przykładzie poniżej kontynuacja może być bezpiecznie zawinięta w ciało lambda i używana jako taka:
( zdefiniuj ( for-each->stream-maker for-each ) ( lambda ( kolekcja ) ( reset ( begin ( for-each ( lambda ( element ) ( shift k ( element stream-cons ( k 'ignorowane )))) kolekcja ) strumień-null ))))
Część między resetem
a przesunięciem
obejmuje funkcje kontrolne, takie jak lambda
i for-each
; nie można tego przeformułować za pomocą lambda [ dlaczego? ] .
Ograniczone kontynuacje są również przydatne w językoznawstwie : zobacz Kontynuacje w językoznawstwie, aby uzyskać szczegółowe informacje.
Linki zewnętrzne
- Samouczek kontynuacji, które można komponować na SchemeWiki
- Ograniczone kontynuacje w systemach operacyjnych autorstwa Olega Kiselyova i Chung-chieh Shan
- Natywne rozdzielane kontynuacje w (kodzie bajtowym i kodzie natywnym) OCaml
- Shift / reset для самых маленьких (po rosyjsku)
- Kilka fajnych artykułów na temat kontynuacji z ogranicznikami i pierwszorzędnych makr