Какая простейшая модель оценки объясняет call / cc? - PullRequest
1 голос
/ 01 ноября 2019

TL; DR: Что делает call / cc, (полу) формально говоря?

Длинная версия: я смутно знаком с продолжениями и call / cc, но у меня нетсильное формальное понимание. Я хотел бы один.

В видео-лекциях SICP мы представлены с моделью замещения и метациклическим интерпретатором. В курсе языка программирования Шрирам Кришнамурти нам показан стиль «среда и магазин». В курсе по компилятору, который я взял в универе, я вычислял выражения Java, манипулируя стеком.

Какая простейшая модель оценки, в которой можно выразить call / cc, и как в ней выражается call / cc?

Ответы [ 2 ]

2 голосов
/ 03 ноября 2019

TL; DR call/cc позволяет подключиться к внутреннему коду Schemes, чтобы вы могли использовать продолжения без написания кода в стиле передачи продолжения. Лучшая модель оценки - модель замещения, учитывая, что вы не используете set и просматриваете ее из кода CPS

Представьте себе эту маленькую программу.

(define (sum-list lst)
  (cond ((null? lst) 0)
        ((number? (car lst)) (+ (car lst) (sum-list (cdr lsr))))
        (else (sum-list (cdr lsr)))))

(display (sum-list '(1 2 3 4))) ; displays 10

Представьте, что вы хотите, чтобы результат был 1337, если вы достигнете условия else. Как бы вы пошли на это, не переписывая все это? Ты не можешьОднако в большинстве реализаций Scheme преобразование CPS в ваш код выполняется там, где его изменение тривиально:

(define (sum-list& lst k)
  (null?& lst
          (lambda (nlst?)
            (if& nlst?
                 (lambda ()
                   (k 0))
                 (lambda ()
                   (car& lst
                         (lambda (car-lst)
                           (number?& car-lst
                                     (lambda (ncl?)
                                       (if& ncl?
                                            (lambda ()
                                              (cdr& lst
                                                    (lambda (clst)
                                                      (sum-list& clst
                                                                 (lambda (sclst)
                                                                   (+& car-lst sclst k))))))
                                            (lambda ()
                                              (cdr& lst
                                                    (lambda (clst)
                                                      (sum-list& clst k))))))))))))))
(sum-list& '(1 2 3 4)
           (lambda (sum)
             (display& sum halt)))

cond - это макрос, поэтому вы видите только вызовы if&. Я знаю, о чем ты думаешь. Почему бы не сказать программистам делать это в первую очередь так? Просто шучу!

Если вы измените второе продолжение во втором if& на (lambda () (k 1337)), все готово. Каким бы красивым ни был CPS, читать и писать ужасно, но так как компилятор делает это в любом случае, не может быть способа написать свой код первым способом и все же добраться до потока управления кодом? Лучшее из двух миров доступно с call/cc. call/cc это в CPS:

(define (call/cc& f& continuation)
  (define (exit& value actual-continuation)
    (continuation value))
  (f& exit& continuation))

Это ничего не значит вообще. Он передает exit&, который игнорирует реальное продолжение программы при вызове и вызывает значение с исходным продолжением вызова call/cc&. Со списком '(1 2 3 #f) у вас будет 3 вложенных продолжения, ожидающих добавления с результатом, но все это нужно отменить. Если вы выбираете значение продолжения до этого расчета, оно автоматически отменяется. И это все. Когда вы пишете свой код с ним, вам не нужно делать полный CPS, только то продолжение, которое вы хотели, показалось call/cc следующим образом:

(define (sum-list lst)
  (call/cc
   (lambda (k)
     (define (helper lst)
       (cond ((null? lst) 0)
             ((number? (car lst))
              (+ (car lst) (helper (cdr lst))))
             (else (k 1337))))
     (helper lst))))

Это преобразуется в CPS:

(define (sum-list& lst k)
  (call/cc&
   (lambda (k& real-k)
     (define (helper& lst k)
       (null?& lst
               (lambda (nlst?)
                 (if& nlst?
                      (lambda ()
                        (k 0))
                      (lambda ()
                        (car& lst
                              (lambda (car-lst)
                                (number?& car-lst
                                          (lambda (ncl?)
                                            (if& ncl?
                                                 (lambda ()
                                                   (cdr& lst
                                                         (lambda (clst)
                                                           (helper& clst
                                                                    (lambda (sclst)
                                                                      (+& car-lst sclst k))))))
                                                 (lambda ()
                                                   (k& 1337 real-k))))))))))))
     (helper& lst real-k))
     k))


(sum-list& '(1 2 3 4)
           (lambda (sum)
             (display& sum halt)))

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

(define (sum-list lst)
  (define (helper lst sum)
    (cond ((null? lst) sum)
          ((number? (car lst)) (helper (cdr lst) (+ sum (car lst))))
          (else 1337)))
  (helper lst 0))

Это хвостовая рекурсия, и вместо создания продолжений он накапливается. Для полноты вот его версия CPS:

(define (helper& lst sum k)
  (null?& lst
          (lambda (nlst)
            (if& nlst
                 (lambda () (k sum))
                 (lambda ()
                   (car& lst
                       (lambda (cl)
                         (number?& cl
                                 (lambda (ncl?)
                                   (if& ncl?
                                        (lambda ()
                                          (cdr& lst
                                                (lambda (cdrl)
                                                  (+& sum
                                                      cl
                                                      (lambda (scl)
                                                        (helper& cdrl
                                                                 scl
                                                                 k))))))
                                        (lambda () (k 1337))))))))))))


(define (sum-list& lst k)
  (helper& lst 0 k))
0 голосов
/ 09 ноября 2019

Я нашел эту превосходную презентацию (на немецком языке), которая ответила на мой вопрос: https://www.youtube.com/watch?v=iuaM9-PX1ls

Чтобы оценить лямбда-исчисление с помощью call / CC, вы передаете контекст оценки , состоящий изсреды (как обычно) и стека вызовов . Вызов call/cc создает специальный функциональный объект продолжения, который хранит контекст оценки. Результат применения специального объекта к выражению expr является результатом интерпретации expr в контексте оценки, захваченном в объекте продолжения.

TL; DR: вы можете реализовать call/cc с помощью среды-and-call-stack-pass интерпретатор.

Если вы хотите также обернуть изменяемое хранилище, объекты продолжения не должны захватывать его. Скорее, при вызове продолжения вы передаете хранилище в качестве аргумента интерпретации в восстановленном контексте оценки. (Хранилище может быть линейного типа.)

Вот одна из таких реализаций в Haskell. Вот пример выражения, которое вы можете захотеть оценить: interpret 0 (Application (Lambda (1, (CallCC (Lambda (2, (Application (Variable 2) (Lambda (3, (Variable 4))))))))) (Lambda (4, (Variable 5)))).

(числа - это просто старые имена, а не (например) индексы де Брюина. Если вы хотите использовать символы или строки, измените type Name = Integerна type Name = Char.)

Обратите внимание, особенно interp применяется к CallCC и InvokeContinuation, а также continue применяется к ContinuationArgument.

import qualified Data.Map as Map

type Name = Integer
type NameAndBody = (Name, LambdaCallCC)
data LambdaCallCC
  = Lambda NameAndBody
  | Variable Name
  | InvokeContinuation EvaluationContext LambdaCallCC
  | CallCC LambdaCallCC
  | Application LambdaCallCC LambdaCallCC
  deriving Show

type Environment = Map.Map Name NameAndBody

type EvaluationContext = (CallStack, Environment)
type CallStack = [Frame]

data Frame
  = FunctionPosition LambdaCallCC
  | ArgumentPosition NameAndBody
  | ContinuationArgument EvaluationContext
  deriving Show

type Fail = (Name, EvaluationContext)
interpret :: Name -> LambdaCallCC -> Either Fail NameAndBody
interpret thunkVarName expression = interp [] Map.empty expression
  where interp stk env (Lambda nameAndBody)
          = continue stk env nameAndBody
        interp stk env (Variable name)
          = case Map.lookup name env of
              Nothing -> Left (name, (stk, env))
              Just e -> continue stk env e
        interp stk env (Application e1 e2)
          = interp (FunctionPosition e2 : stk) env e1
        interp stk env (CallCC expr)
          = interp stk env (Application expr
                              (Lambda (thunkVarName,
                                        (InvokeContinuation
                                          (stk, env)
                                          (Variable thunkVarName)))))
        interp stk env (InvokeContinuation capturedContext expr)
          = interp [ContinuationArgument capturedContext] env expr

        continue [] env value
          = Right value
        continue ((FunctionPosition expr) : frames) env value
          = interp ((ArgumentPosition value) : frames) env expr
        continue ((ArgumentPosition (name, body)) : frames) env value
          = interp frames (Map.insert name value env) body
        continue ((ContinuationArgument (stk, env)) : nil) _ value
          = interp stk env (Lambda value)
...