kontynuacja programu dla manekinów

/ Align = "center" bgcolor = "# e0ffe0 " / cesarz chin / / align = center / Myślę, że problem wynika z tego, że nie rozumiem, do czego są . Wszystkie przykłady, które znalazłem w książkach lub internecie są bardzo trywialne. Zastanawiają mnie, po co ktoś miałby w ogóle chcieć kontynuacji?

Oto typowy niepraktyczny przykład z TSPL , który moim zdaniem jest dość rozpoznawalną książką na ten temat. W języku angielskim opisują kontynuację jako "co robić" z wynikiem obliczeń. OK, to jestw pewnym sensie zrozumiałe.

Następnie, drugi przykład podany:

(call/cc
  (lambda (k)
    (* 5 (k 4)))) => 4 

Jaki to ma sens?? k nie jest nawet zdefiniowana! Jak można ocenić ten kod, skoro (k 4) nie można nawet obliczyć? Nie wspominając już o tym, jak call/cc wiedzieć, aby wyrwać argument 4 do wewnętrznego wyrażenia most i zwrócić go? Co się stanie z (* 5 .. ?? Jeśli to wyrażenie jest odrzucane, to po co w ogóle je zapisywać?

Następnie, "mniej" trywialny przykład stwierdził jest jak używać call/cc aby zapewnić nielokalne wyjście z rekurencji. To brzmi jak dyrektywa kontroli przepływu, czyli jak break/return w języku imperatywnym, a nie obliczenie.

A jaki jest cel tych ruchów? Jeśli ktoś potrzebuje wyniku obliczeń, dlaczego po prostu nie zapisać go i przypomnieć później, w razie potrzeby.

Author: Paulo Tomé, 2013-05-13

4 answers

Zapomnij o call/cc na chwilę. Każde wyrażenie/polecenie w dowolnym języku programowania ma kontynuację - czyli to, co robisz z wynikiem. W C, na przykład,

x = (1 + (2 * 3)); 
printf ("Done");

Kontynuacją zadania matematycznego jest printf(...); kontynuacją (2 * 3) jest ' add 1; assign to x; printf(...)'. Koncepcyjnie kontynuacja jest tam, Czy masz do niej dostęp, czy nie. Zastanów się przez chwilę, jakich informacji potrzebujesz do kontynuacji-informacja jest 1) sterta stan pamiÄ ™ ci (w ogĂłle), 2) stos, 3) wszelkie rejestry i 4) Licznik programu.

Więc kontynuacje istnieją, ale zazwyczaj są tylko ukryte i nie mogą być dostępne.

W Scheme i kilku innych językach, masz dostęp do kontynuacji. Zasadniczo, za twoimi plecami, kompilator + runtime gromadzi wszystkie informacje potrzebne do kontynuacji, przechowuje je (zazwyczaj w stercie) i daje Ci do niej dostęp. Uchwyt, który otrzymujesz to funkcja " k " - jeśli wywołasz ta funkcja będzie kontynuowana dokładnie po punkcie call/cc. Co ważne, możesz wywołać tę funkcję wiele razy i zawsze będziesz kontynuował po punkcie call/cc.

Spójrzmy na kilka przykładów:

> (+ 2 (call/cc (lambda (cont) 3)))
5

W powyższym, wynik call/cc jest wynikiem lambda, który jest 3. Kontynuacja nie została odwołana.

Teraz wywołajmy kontynuację:

> (+ 2 (call/cc (lambda (cont) (cont 10) 3)))
12

Wywołując kontynuację pomijamy cokolwiek po wywołaniu i kontynuujemy dokładnie w punkcie call/cc. Z (cont 10) kontynuacja zwraca 10, która jest dodawana do 2 za 12.

Teraz zachowajmy kontynuację.

> (define add-2 #f)
> (+ 2 (call/cc (lambda (cont) (set! add-2 cont) 3)))
5
> (add-2 10)
12
> (add-2 100)
102

Zapisując kontynuację możemy użyć jej tak, jak nam się podoba, aby "wrócić do" dowolnego obliczenia, które nastąpiły po punkcie call/cc.

Często kontynuacje są używane do wyjścia pozalokalnego. Pomyśl o funkcji, która zwróci listę, chyba że jest jakiś problem, w którym momencie '() zostanie zwrócona.

(define (hairy-list-function list)
  (call/cc
    (lambda (cont)
       ;; process the list ...

       (when (a-problem-arises? ...)
         (cont '()))

       ;; continue processing the list ...

       value-to-return)))
 19
Author: GoZoner,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-05-13 22:55:18

Oto tekst z notatek z mojej klasy: http://tmp.barzilay.org/cont.txt . jest on oparty na wielu źródłach i jest znacznie rozszerzony. Ma motywacje, podstawowe wyjaśnienia, bardziej zaawansowane wyjaśnienia, jak to się robi, i wiele przykładów, które przechodzą od prostych do zaawansowanych, a nawet kilka szybkich dyskusji o ograniczonych kontynuacjach.

(próbowałem pobawić się umieszczeniem tutaj całego tekstu, ale jak się spodziewałem, 120K tekstu nie jest czymś, co sprawia tyle radości.

 7
Author: Eli Barzilay,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-05-13 19:27:36

TL; DR : kontynuacje są po prostu przechwytywane Goto, z wartościami, mniej więcej.

Przykład, o który pytasz,

(call/cc
  (lambda (k)
    ;;;;;;;;;;;;;;;;
    (* 5 (k 4))                     ;; body of code
    ;;;;;;;;;;;;;;;;
    )) => 4 

Można w przybliżeniu przetłumaczyć na np. Common Lisp, jako

(prog (k retval)
    (setq k (lambda (x)             ;; capture the current continuation:
                    (setq retval x) ;;   set! the return value
                    (go EXIT)))     ;;   and jump to exit point

    (setq retval                    ;; get the value of the last expression,
      (progn                        ;;   as usual, in the
         ;;;;;;;;;;;;;;;;
         (* 5 (funcall k 4))        ;; body of code
         ;;;;;;;;;;;;;;;;
         ))
  EXIT                              ;; the goto label
    (return retval))

To tylko ilustracja; w Common Lispie nie możemy wrócić do Prog tagbody po wyjściu z niego za pierwszym razem. Ale w schemacie, z prawdziwymi kontynuacjami, możemy. Jeśli ustawimy jakąś zmienną globalną wewnątrz ciała funkcji wywołanej przez call/cc, powiedzmy (setq qq k), w Scheme możemy ją wywołać w dowolnym momencie, z dowolnego miejsca, ponownie wchodząc w ten sam kontekst(np. (qq 42)).

Chodzi o to, że ciało formy call/cc może zawierać if lub condwyrażenie. Może wywoływać kontynuację tylko w niektórych przypadkach, a w innych zwracać normalnie, oceniając wszystkie wyrażenia w kodzie i zwracając ostatnią wartość, jak zwykle. Może tam występować głęboka rekurencja. Wywołując przechwyconą kontynuację uzyskuje się natychmiastowe wyjście.

Więc my Zobacz też k jest zdefiniowana. Jest ona zdefiniowana przez wywołanie call/cc. Kiedy (call/cc g) jest wywołana, wywołuje swój argument z bieżącą kontynuacją: (g the-current-continuation). the current-continuation jest "procedurą ucieczki" wskazującą punkt zwrotny formularza call/cc. Wywołanie to oznacza podanie wartości tak, jakby została zwrócona przez samą formę call/cc.

Tak więc powyższe wyniki w

((lambda(k) (* 5 (k 4))) the-current-continuation) ==>

(* 5 (the-current-continuation 4)) ==>

; to call the-current-continuation means to return the value from
; the call/cc form, so, jump to the return point, and return the value:

4
 3
Author: Will Ness,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-12-12 01:13:09

Nie będę próbował wyjaśniać wszystkich miejsc, w których kontynuacje mogą być przydatne, ale mam nadzieję, że będę mógł podać krótkie przykłady głównego miejsca, w którym znalazłem kontynuacje przydatne w moim własnym doświadczeniu. Zamiast mówić o Scheme ' s call/cc, skupiłbym uwagę na kontynuowaniu stylu przechodzenia . W niektórych językach programowania zmienne mogą być dynamicznie skalowane, a w językach bez dynamicznie skalowanych-ze zmiennymi globalnymi (zakładając, że nie ma zagadnienia kodu wielowątkowego itp.). Na przykład, załóżmy, że istnieje lista aktualnie aktywnych strumieni logowania, *logging-streams* i które chcemy wywołać function w dynamicznym środowisku, w którym {[9] } jest rozszerzona o logging-stream-x. W Common Lispie możemy zrobić

(let ((*logging-streams* (cons logging-stream-x *logging-streams*)))
  (function))

Jeśli nie mamy dynamicznie skalowanych zmiennych, jak w Scheme, nadal możemy wykonać

(let ((old-streams *logging-streams*))
  (set! *logging-streams* (cons logging-stream-x *logging-streams*)
  (let ((result (function)))
    (set! *logging-streams* old-streams)
    result))

Teraz Załóżmy, że rzeczywiście otrzymaliśmy drzewo cons, którego liście nie - nil są strumieniami, z których wszystkie powinny być w *logging-streams* Kiedy function jest wywołana. Mamy dwie opcje:

  1. możemy spłaszczyć drzewo, zebrać wszystkie strumienie logowania, rozszerzyć *logging-streams*, a następnie wywołać function.
  2. możemy, używając stylu continuation passing, przemierzać drzewo, stopniowo rozszerzając *logging-streams*, W końcu wywołując function, gdy nie ma już tree do trawersowania.

Opcja 2 wygląda jak

(defparameter *logging-streams* '())

(defun extend-streams (stream-tree continuation)
  (cond
    ;; a null leaf
    ((null stream-tree)
     (funcall continuation))
    ;; a non-null leaf
    ((atom stream-tree)
     (let ((*logging-streams* (cons stream-tree *logging-streams*)))
       (funcall continuation)))
    ;; a cons cell
    (t
     (extend-streams (car stream-tree)
                     #'(lambda ()
                         (extend-streams (cdr stream-tree)
                                         continuation))))))

Z tą definicją mamy

CL-USER> (extend-streams
          '((a b) (c (d e)))
          #'(lambda ()
              (print *logging-streams*)))
=> (E D C B A) 
Czy było w tym coś pożytecznego? W tym przypadku, prawdopodobnie nie. Niektóre drobne korzyści mogą być takie, że extend-streams jest rekurencyjna, więc nie mamy zbyt dużego użycia stosu, chociaż pośrednie zamknięcia nadrabiają to w przestrzeni sterty. Mamy fakt, że ewentualna kontynuacja jest wykonywana w zakresie dynamicznym dowolnego elementu pośredniego, który extend-streams ustawiono. W tym przypadku nie jest to aż tak ważne, ale w innych przypadkach może być.

Jest w stanie wyodrębnić część przepływu sterowania i mieć bardzo przydatne mogą być wyjścia pozalokalne, czy też Może to być przydatne na przykład w wyszukiwaniu wstecznym. Oto kontynuacja przechodząca styl propositional calculus solver dla formuł, w których formuła jest symbolem( literał propositional), lub Lista postaci (not formula), (and left right), lub (or left right).

(defun fail ()
  '(() () fail))

(defun satisfy (formula 
                &optional 
                (positives '())
                (negatives '())
                (succeed #'(lambda (ps ns retry) `(,ps ,ns ,retry)))
                (retry 'fail))
  ;; succeed is a function of three arguments: a list of positive literals,
  ;; a list of negative literals.  retry is a function of zero
  ;; arguments, and is used to `try again` from the last place that a
  ;; choice was made.
  (if (symbolp formula)
      (if (member formula negatives) 
          (funcall retry)
          (funcall succeed (adjoin formula positives) negatives retry))
      (destructuring-bind (op left &optional right) formula
        (case op
          ((not)
           (satisfy left negatives positives 
                    #'(lambda (negatives positives retry)
                        (funcall succeed positives negatives retry))
                    retry))
          ((and) 
           (satisfy left positives negatives
                    #'(lambda (positives negatives retry)
                        (satisfy right positives negatives succeed retry))
                    retry))
          ((or)
           (satisfy left positives negatives
                    succeed
                    #'(lambda ()
                        (satisfy right positives negatives
                                 succeed retry))))))))

Jeśli zostanie znalezione zadowalające przypisanie, to succeed jest wywoływane z trzema argumentami: listą dodatnich liter, listą ujemnych literały i funkcja, która może ponowić wyszukiwanie (tj. próbować znaleźć inne rozwiązanie). Na przykład:

CL-USER> (satisfy '(and p (not p)))
(NIL NIL FAIL)
CL-USER> (satisfy '(or p q))
((P) NIL #<CLOSURE (LAMBDA #) {1002B99469}>)
CL-USER> (satisfy '(and (or p q) (and (not p) r)))
((R Q) (P) FAIL)

Drugi przypadek jest interesujący, ponieważ trzeci wynik nie jest FAIL, ale jakąś funkcją, która spróbuje znaleźć inne rozwiązanie. W tym przypadku możemy zobaczyć, że (or p q) jest spełnialny, czyniąc albo p albo q prawdą: {[37]]}

CL-USER> (destructuring-bind (ps ns retry) (satisfy '(or p q))
           (declare (ignore ps ns))
           (funcall retry))
((Q) NIL FAIL)

Byłoby to bardzo trudne do zrobienia, gdybyśmy nie używali stylu przejścia kontynuacyjnego, w którym możemy zapisać alternatywny przepływ i wrócić do niego później. Używając tego, możemy zrobić kilka sprytnych rzeczy, takich jak zebranie wszystkich satysfakcjonujących zadań: {]}

(defun satisfy-all (formula &aux (assignments '()) retry)
  (setf retry #'(lambda () 
                  (satisfy formula '() '()
                           #'(lambda (ps ns new-retry)
                               (push (list ps ns) assignments)
                               (setf retry new-retry))
                           'fail)))
  (loop while (not (eq retry 'fail))
     do (funcall retry)
     finally (return assignments)))

CL-USER> (satisfy-all '(or p (or (and q (not r)) (or r s))))
(((S) NIL)   ; make S true
 ((R) NIL)   ; make R true
 ((Q) (R))   ; make Q true and R false
 ((P) NIL))  ; make P true

Możemy zmienić loop trochę i uzyskać tylko N zadania, aż do niektórych n , lub wariacje na ten temat. Często kontynuacja stylu przechodzenia nie jest potrzebna lub może sprawić, że kod będzie trudny do utrzymania i zrozumienia, ale w przypadkach, w których jest użyteczny, może sprawić, że niektóre inne rzeczy będą bardzo trudne spokojnie.

 2
Author: Joshua Taylor,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-05-13 21:47:38