call-with-current - continuation-концепция сохранения состояния

после прочтения Опытный Интриган я почувствовал, что понял call/cc правильно. Но, увидев некоторые WOW трюки с call/cc я обнаружил, что ошибался.

(define cc 0)
(define (f)
  (call/cc (lambda (k)
             (set! cc k)
             3)))

(+ 1 2 4 (f)) ; eval's to 10
(cc 13) ; eval's to 20

это полностью соответствует моему пониманию. Я думаю, когда я достигаю call/cc вызов я просто сохраняю состояние программы. и вызов функции-это функция. Если эта функция (k) вызывается откуда-то, чем я просто заменяю весь (call/cc ...) материал с параметром, заданным он. вышеуказанная программа, похоже, тоже работала таким образом


а,

(define (itr lst)
  (define (state k)
    (for-each (lambda (item)
                (call/cc (lambda (h)
                           (set! state h)
                           (k item))))
              lst)
    (k 'done))

  (define (generator)
    (call/cc (lambda (k) (state k))))
  generator)

(define (next)
  (itr (range 2)))

вызов next 3 раза производит 0, 1 и 'done. Это означает, когда state используется функция k дано generator это не восстановило состояние программы. я только что показал вам, что пытался понять это.


так как call/cc на самом деле работают?

2 ответов


С продолжением прохождения стиля (без call/cc)

возможно, будет легче понять этот пример, если вы реализуете версию, которая использует явный стиль передачи продолжения, а не call/cc первый. В этом случае начнем с продолжения прохождения версии map:

(define (kmap fn list k)
  (if (null? list)
      (k list)
      (fn (car list)
          (lambda (head)
            (kmap fn
                  (cdr list)
                  (lambda (tail)
                    (k (cons head tail))))))))
(define (identity x) x)

(kmap (lambda (x k) (k (+ 1 x))) '(1 2 3 4) identity)
;=> (2 3 4 5)

если вы не знакомы со стилем прохождения продолжения, это может быть немного, чтобы обернуть голову, но это не слишком сложно. Запомните это kmap и fn каждый принимает дополнительный параметр в конце, который должен быть вызван с "результатом". Поэтому, когда мы призываем fn С (car list), мы также передаем ему процедуру (lambda (head) ...) это отвечает за заботу об остальной части отображения для нас. Остальная часть отображения определяется в терминах kmap снова. Каждый вызов kmap принимает окончательное продолжение, которое ожидает получить список, полученный в результате сопоставления fn список.

теперь, так как мы можем видеть, как отображение может быть реализовано со стилем передачи продолжения, с его помощью можно написать процедуру генерации итератора. Процедура iterator берет список и возвращает процедуру, которую мы можем вызвать, чтобы получить каждый элемент list.

(define (iterator list)
  (define (next k)
    (kmap (lambda (item more-next)
            (set! next more-next)
            (k item))
          list
          (lambda (results)
            (k 'done))))
  (lambda ()
    (next identity)))
> (define get (iterator '(1 2)))
> (get)
1
> (get)
2
> (get)
done
> (get)
done
> (define get (iterator '(a b c)))
> (get)
a
> (get)
b
> (get)
c
> (get)
done
> (get)
done

хитрость здесь в том, что мы определяем локальную процедуру next. Он зовет kmap С процедурой, которая redfines next когда каждый элемент list обрабатывается как процедура, которая будет обработать оставшуюся часть list. После переопределения next, он называет k с элементом. Окончательное продолжение перешло к kmap фактически игнорирует результаты, переданные ему, и просто вызывает k символ done. Что мы возвращение с iterator не является значением next, но процедура, которая называет next с продолжением identity. Косвенность здесь означает, что мы всегда называем последний стоимостью next С identity. Проходя identity поскольку продолжение означает, что мы просто получаем элемент списка обратно.

С call/cc

теперь, когда мы видим, как мы можем это сделать без call/cc, мы лучше оснащены, чтобы видеть, как мы можем использовать call/cc сделать это. Вспомните определение из вопроса:

(define (itr lst)
  (define (state k)
    (for-each (lambda (item)
                (call/cc (lambda (h)
                           (set! state h)
                           (k item))))
              lst)
    (k 'done))

  (define (generator)                   
    (call/cc (lambda (k) (state k))))   

  generator)                            

возвращение генератора

во-первых, обратите внимание, что

  (define (generator)
    (call/cc (lambda (k) (state k))))

  generator

можно упростить до

(lambda () (call/cc (lambda (k) (state k))))

и это как раз то, что мы сделали в нашей реализации. Когда вы вызываете это из REPL, все это k хочет сделать, это получить значение и вернуть его (и распечатать его). В нашей версии мы приближаемся к этому, просто возвращая его без изменений. То есть, мы используем identity, и мы использовали имя next вместо state. Так что

(lambda () (call/cc (lambda (k) (state k))))

как

(lambda () (next identity))

The state (или next) процедура

остальное

  (define (state k)
    (for-each (lambda (item)
                (call/cc (lambda (h)
                           (set! state h)
                           (k item))))
              lst)
    (k 'done))

очень похоже на то, что мы сделали, слишком. Вместо использования kmap и fn это принимает два аргумента (элемент и продолжение), мы используем for-each который принимает процедуру одного аргумента (элемента), и внутри этой процедуры мы используем call/cc чтобы захватить продолжение. Так что

(for-each
  (lambda (item)
    (call/cc (lambda (h)
               ...

как

(kmap (lambda (item h)
        ...

for-each не нужен окончательный аргумент продолжения, поэтому мы не удается передать результат-игнорирование (lambda () (k 'done)). Вместо этого, мы просто называем (k 'done) после the for-each звонок. То есть,

(for-each fn list)
(k 'done)

как

(kmap fn
      list
      (lambda (result)
        (k 'done)))

сохранение состояния программы

в обеих этих реализациях вы в некотором смысле "сохраняете состояние программы". Важное состояние, которое вы сохраняете, - это то, которое будет продолжать итерацию по списку.


ваше подозрение, что что-то не так правильно. Код полностью сломан, и это очевидно из того, что генератор не может захватить новое продолжение для основной программы всякий раз, когда он вызывается для вызова элемента. Или, вернее, он нащупывает и отбрасывает это продолжение. В результате при попытке получить второй элемент вызывается неправильное Продолжение,что приводит к бесконечному циклу.

во-первых, давайте исправим что-то из формулировки свой вопрос. Зову next не дает элементов; вызов next дает функцию генератора. Путь next предполагается использовать примере так:

(let ((g (next)))
  (list (g) (g) (g)))  ;; should return (0 1 done)

но на самом деле это не может работать. Давайте рассмотрим его:

(define (itr lst)
  (define (state k)
    (for-each (lambda (item)
                (call/cc (lambda (h)
                           (set! state h)
                           (k item))))
              lst)
    (k 'done))

  (define (generator)
    (call/cc (lambda (k) (state k))))
  generator)

(define (next)
  (itr (range 2)))

давайте проследим, что происходит.

настройки:, когда (next) называется выражение (iter (range 2)) возвращает generator, закрытие, захваченное в среде, где itr, lst и state переменные связаны.

первая итерация: первый вызов генератора, возвращенный вызывает generator. Теперь generator захватывает собственное продолжение, которое появляется как k на lambda, и передает его в state. Итак,state и generatorпродолжение привязано к k. Он входит в первую итерацию и сохраняет свое собственное состояние, заменяя себя новым продолжением: (set! state h). В этот момент предыдущая привязка state до define-функция d перезаписывается;state теперь является функцией продолжения для возобновления for-each. Следующий шаг-дать item обратно в k продолжение, которое возвращает нас к generator, который возвращает товар. Отлично, так вот как первый элемент появляется из первого вызова (next).

второй вариант: с этого момента все идет не так. Второй вызов генератора, который был возвращен next снова захватывает продолжение снова и вызывает state который теперь является продолжением генерирующей совместной подпрограммы. Генератор передает свое продолжение в state. Но!--11--> больше не является функцией, которая была define - d by itr! и так вновь захваченное продолжение в generator не соединяется с k параметр, который находится в лексической области for-each., когда (k item) вызывается, чтобы дать второй пункт, это k еще относится к оригиналу k привязка, которая содержит первоначально захваченное продолжение в первом вызове generator. это аналогично обратному goto и приводит к не прекращающемуся поведению.

вот как мы можем это исправить:

(define (itr lst)
  (define yield '()) ;; forward definition (could use let for this).

  (define (state)    ;; k parameter is gone
    (for-each (lambda (item)
                (call/cc (lambda (h)
                           (set! state h)
                           (yield item))))  ;; call yield, not k
              lst)
    (yield 'done))  ;; yield, not k.

  (define (generator)
    (call/cc (lambda (self) 
               (set! yield self) ;; save new escape on each call
               (state))))
  generator)

;; test
(let ((g (itr (range 2))) ;; let's eliminate the "next" wrapper
  (display (list (g) (g) (g))))

выход (0 1 done).