продолжение в common lisp макросами относительно реализации в OnLisp

В On Lisp, стр. 267, Пол Грэм предлагает реализацию макросов передачи продолжения:

(setq *cont* #'identity)

(defmacro =lambda (parms &body body)
  `#'(lambda (*cont* ,@parms) ,@body))

(defmacro =defun (name parms &body body)
  (let ((f (intern (concatenate 'string
                "=" (symbol-name name)))))
    `(progn
       (defmacro ,name ,parms
     `(,',f *cont* ,,@parms))
       (defun ,f (*cont* ,@parms) ,@body))))

(defmacro =bind (parms expr &body body)
  `(let ((*cont* #'(lambda ,parms ,@body))) ,expr))

(defmacro =values (&rest retvals)
  `(funcall *cont* ,@retvals))

Следующий код для обхода дерева t2 для каждого листа дерева t1 использует эту реализацию, и мне интересно, что происходит, когда вызывается restart, особенно после того, как лист t1 изменился с A (первый элемент) на B (первый элемент). второй элемент). Когда вызывается restart, он просто извлекает лямбда-функцию из *saved*, и эта лямбда-функция снова вызывает dft-node с (cdr tree). Но этот вызов выполняется за пределами самого внешнего =bind, а =bind отвечает за привязку *cont*. Как тогда связывание *cont*, введенное внешним =bind, все еще находится в области видимости?

(setq *saved* nil)

(=defun dft-node (tree)
    (cond ((null tree) (restart))
          ((atom tree) (=values tree))
          (t (push #'(lambda () (dft-node (cdr tree))) *saved*)
             (dft-node (car tree)))))

(=defun restart ()
    (if *saved*
        (funcall (pop *saved*))
      (=values 'done)))

(setq t1 '(a (b (d h)) (c e (f i) g))
      t2 '(1 (2 (3 6 7) 4 5)))

(=bind (node1) (dft-node t1)
  (if (eq node1 'done)
      'done
    (=bind (node2) (dft-node t2)
      (list node1 node2))))

Последняя форма расширяется до

(let ((*cont* (lambda (node1)
                (if (eq node1 'done)
                    'done
                    (let ((*cont* (lambda (node2)
                                    (list node1 node2))))
                      (dft-node t2))
  (dft-node t1))))))

Это производит (a 1). Согласно Грэму, последующие вызовы restart должны давать (a 2) и так далее, вплоть до (a 5), а затем последующие вызовы должны давать (b 1), (b 2) и так далее, пока, наконец, (g 5):

> (let ((node1 (dft-node t1)))
    (if (eq? node1 ’done)
        ’done
        (list node1 (dft-node t2))))
(A 1)
> (restart)
(A 2)
…
> (restart)
(B 1)

После (a 1) привязка *cont*, установленная let, больше не должна действовать. Как последующие вызовы restart этих значений? Применяется ли область let к отдельному вызову restart? Что здесь происходит?


person user1461328    schedule 13.07.2014    source источник
comment
Ваша интерпретация того, что это не имеет большого смысла, абсолютно верна. Позже Грэм отмечает, что это не работает, если *cont* объявлен как специальная переменная (т. е. с динамической областью видимости); *cont* должен иметь лексическую область действия. Однако в Common Lisp нет глобальных переменных с лексической областью видимости, и его предположение, что (setq *cont* …) создает одну, просто неверно. Я обновил свой ответ, чтобы отразить это, и теперь я лучше понимаю ваш вопрос.   -  person Joshua Taylor    schedule 14.07.2014
comment
Однако в Common Lisp нет глобальных переменных с лексической областью видимости Эта часть верна. В Common Lisp нет глобальных лексических переменных. и его предположение о том, что (setq *cont* …) делает единицу, просто неверно. Я был неправ на этот счет. On Lisp немного предшествовал спецификации Common Lisp, поэтому у Грэхема не было спецификации ANSI Common Lisp, с которой можно было бы работать. Это предположение не выполняется для Common Lisp, но это не означает, что Грэм был неправ, делая это предположение в On Lisp.   -  person Joshua Taylor    schedule 15.07.2014


Ответы (1)


On Lisp немного старше Common Lisp, поэтому есть некоторые несовместимости.

On Lisp был написан до того, как Common Lisp фактически закрепился как язык, поэтому существуют некоторые несовместимости между кодом, который появляется в On Lisp, и Common Lisp. В записи CLiki в On Lisp отмечается, что эти макросы передачи продолжения на самом деле одно из мест, где есть несовместимость (выделено мной):

При определении макросов, передающих продолжение (стр. 267), Пол Грэм, кажется, предполагает, что глобальная переменная cont имеет лексическую область видимости. Это противоречит стандарту Common Lisp. В современных реализациях Common Lisp вышеупомянутые макросы просто не работают. Кроме того, эта проблема может сбить с толку новичков. Предлагаемые решения для исправления макросов (обратите внимание, что #'values ​​используется вместо #'identity - в соответствии с Errata Пола Грэма):

  • эмулировать глобальную переменную cont с лексической областью видимости, используя макрос-символ, который может быть скрыт с помощью let или lambda:
    (defvar actual-cont #'values)
    (define -symbol-macro *cont* *actual-cont*)
  • просто опустите (setq *cont* #'identity) и вызовите функцию передачи продолжения "верхнего уровня" как (=somefunc #'values...)

Это довольно краткое описание проблемы, и его стоит изучить немного подробнее, чтобы помочь новичкам, которые столкнутся с этим в будущем. Также может быть полезно просмотреть другие обсуждения этой же проблемы, в том числе:

  • стр. 268 из ‹On Lisp›... A Ветка comp.lang.lisp от 2006 года, в которой пользователь спрашивает о разнице между (setq *cont* …) и (defvar *cont* …). В этой теме люди отмечают, что On Lisp предшествует ANSI Common Lisp.

Что на самом деле происходит?

Поскольку (=bind …) расширяется до (let ((*cont* …)) …), вы совершенно правы, если *cont* — это специальная переменная (т. е. с динамическим экстентом), то, как только вы выйдете за пределы этого let, исходная привязка *cont*, т. е. identity — это то, что должно использоваться для вызовов перезапуска. Если *cont* объявлен специальным, вы получите следующее поведение:

CONTINUATIONS> (=bind (node1) (dft-node '(a (b (d h)) (c e (f i) g)))
                 (if (eq node1 'done)
                     'done
                     (=bind (node2) (dft-node '(1 (2 (3 6 7) 4 5)))
                       (list node1 node2))))
(A 1)
CONTINUATIONS> (restart)
2
CONTINUATIONS> (restart)
3
CONTINUATIONS> (restart)
6
CONTINUATIONS> (restart)
7
CONTINUATIONS> (restart)
4
CONTINUATIONS> (restart)
5
CONTINUATIONS> (restart)
B
CONTINUATIONS> (restart)
D

Это имеет смысл, потому что после получения (1) *saved* содержит две функции. Первый — это тот, который продолжит обход числового дерева на следующей ветке, а *cont*, который будет вызываться, является глобальным, #'identity, так как мы теперь вне формы =bind. Вот почему мы получаем 2, 3, … в качестве результатов. Вторая функция в *saved* на данный момент — это та, которая продолжит обход алфавитного дерева в точке B.

Причина, по которой мы не получаем кучу списков (a 1), (a 2), …, (b 1) и т. д. выше, заключается в том, что мы (разумно) предположили, что *cont* является особым, т. е. динамически связанным. Оказывается, Грэм намеревается сделать *cont* не особенным; он хочет, чтобы это была глобальная лексика. Из О Lisp, стр. 268:

Именно манипулируя *cont* мы получим эффект продолжений. Хотя *cont* имеет глобальное значение, оно редко будет использоваться: *cont* почти всегда будет параметром, захваченным =values и макросами, определенными =defun. Например, в теле add1 *cont* является параметром, а не глобальной переменной. Это различие важно, потому что эти макросы не работали бы, если бы *cont* не была локальной переменной. Вот почему *cont* получает свое начальное значение в setq, а не в defvar: последнее также объявило бы его особенным.

On Lisp немного предшествовал Common Lisp, так что это не обязательно было неверным на момент написания, но Common Lisp на самом деле не имеет глобальных лексических переменных, так что это не тот случай, когда использование ( setq *cont* …) на верхнем уровне обязательно создаст глобальную лексическую переменную. В Common Lisp точные результаты не указаны. Некоторые реализации будут рассматривать его как глобальную лексику, другие предполагают, что вы имели в виду defparameter или defvar, и в итоге вы получите глобальный специальный переменная. Как отмечает Грэм, это не сработает. Похоже, у вас есть реализация, которая делает последнее, поэтому ничего не работает.

Некоторые реализации на самом деле будут жаловаться на его код. SBCL, например, справедливо жалуется на (setq *cont* …), печатая "предупреждение: неопределенная переменная: CONTINUATIONS::*CONT*", и выдает предупреждение о стиле, когда используется *cont*, что он "использует лексический привязка символа (CONTINUATIONS::*CONT*), а не динамическая привязка, даже несмотря на то, что имя соответствует обычному соглашению об именовании (имена типа *FOO*) для специальных переменных».

Что должно произойти?

Чтобы понять, как это предполагается работать, вероятно, проще взглянуть на более простую реализацию, в которой нет всей логики, которая есть в версии On Lisp:

(defparameter *restarts* '())

(defun do-restart ()
  (if (endp *restarts*) nil
      (funcall (pop *restarts*))))

(defun traverse-tree (k tree)
  (cond
    ((null tree) (do-restart))
    ((atom tree) (funcall k tree))
    (t (push (lambda () (traverse-tree k (cdr tree))) *restarts*)
       (traverse-tree k (car tree)))))

Здесь мы не скрываем ни механизм передачи продолжения, ни список *restarts*. При этом мы получаем такое поведение:

CL-USER> (traverse-tree 'identity '((1 2) (3 4)))
1
CL-USER> (do-restart)
2
CL-USER> (do-restart)
3
CL-USER> (do-restart)
4
CL-USER> (do-restart)
NIL

Мы также можем воссоздать обход нескольких списков, и я думаю, мы получим ожидаемые результаты:

CL-USER> (let ((k (lambda (num)
                    (traverse-tree (lambda (alpha)
                                     (list num alpha))
                                   '(a (b) c)))))
           (traverse-tree k '((1 2) 3)))
(1 A)
CL-USER> (do-restart)
(1 B)
CL-USER> (do-restart)
(1 C)
CL-USER> (do-restart)
(2 A)
CL-USER> (do-restart)
(2 B)
CL-USER> (do-restart)
(2 C)
CL-USER> (do-restart)
(3 A)
CL-USER> (do-restart)
(3 B)
CL-USER> (do-restart)
(3 C)
CL-USER> (do-restart)
NIL

Разница здесь в том, что нет ссылок на *cont*, значение которых меняется, как только мы выходим из области действия let, ограничивающей для нас продолжение.

На мой взгляд, лучшей реализацией было бы просто использовать обычную лексическую переменную a для хранения продолжения (вроде k выше, но, вероятно, с именем, созданным gensym), и просто потребует, чтобы «вызовы функций передачи продолжения в конечном итоге были заключены в =bind, определяющий самое внешнее продолжение.

person Joshua Taylor    schedule 14.07.2014
comment
Существует не так много реализаций, которые рассматривают (setq var ...) как специальное объявление для новой переменной. CMUCL (и Sciener CL) известны этим. Но это можно отключить. См. ext:*top-level-auto-declare*. Я бы всегда выключил это. Вообще в CL много undefined и прочего, но реальные реализации делают выбор: TCO yes/no, Conditions как CLOS, Streams как CLOS, неопределенная переменная SETQ как специальная или нет, пакеты по умолчанию, ... - person Rainer Joswig; 14.07.2014
comment
@RainerJoswig Конечно. Если больше реализаций, сделанных (setq *cont* …) на верхнем уровне, будут делать специальное объявление, то у Грэма может не получиться работающий пример. Если бы их было меньше, то ОП, возможно, не привел бы к разрыву между кодом в книге и результатами в REPL. Что меня расстраивает, так это не обязательно использование неопределенного поведения, а использование неопределенного поведения там, где его так легко избежать. - person Joshua Taylor; 14.07.2014
comment
Я ничего не имею против создания «стандартов» по ​​соглашению. В Common Lisp слишком много неопределенного поведения в стандарте. Разработчики должны воздерживаться от использования этого и должны предоставить надежную систему с меньшим количеством сюрпризов. Я бы либо избегал, либо исправлял реализации, которые обеспечивают эти сюрпризы. - person Rainer Joswig; 14.07.2014
comment
@RainerJoswig Я согласен со стандартами по соглашению. Вот почему такие вещи, как CDR — репозиторий документов Common Lisp, великолепны. Проблема здесь в том, что текущие реализации делают разные вещи с присваиваниями верхнего уровня необъявленным переменным, и Грэм (i) предполагает, что все они делают одно и то же (не замечая, что некоторые реализации будут делать что-то другое), и (ii) злоупотребляют соглашениями. которые существуют, используя нотацию *...* для глобальной лексики, и (iii) выполняя (i) и (ii) это, когда легко вообще избежать проблемы. - person Joshua Taylor; 15.07.2014
comment
@JoshuaTaylor Обратите внимание, что On Lisp был опубликован до стандарта ANSI. Альтернативой вызову форм продолжения внутри =bind является использование deflex или символьного макроса для переносимой реализации текущей семантики. - person m-n; 15.07.2014
comment
@m-n О, это хороший момент. Думаю, я мысленно спутал его ANSI Common Lisp и On Lisp. Это, вероятно, оправдывает эту несовместимость. Некоторые сайты упоминают On Lisp как книгу по Common Lisp, но это явно не так; Common Lisp еще не существовал. Страница CLiki, посвященная On Lisp, называет ее книгой по Common Lisp, но явно указывает на проблему с этими макросами, передающими продолжение! Я собираюсь немного смягчить свою (не совсем заслуженную) критику. - person Joshua Taylor; 15.07.2014
comment
Привет, я до сих пор не могу понять, что происходит, если предположение Грэма верно. - person user1461328; 29.03.2015
comment
И еще одна путаница, когда Грэм сказал, что =bind предназначен для использования так же, как и привязка множественных значений. Он принимает список параметров, выражение и тело кода: параметры связаны со значениями, возвращаемыми выражением, и тело кода оценивается с этими связываниями.; Я не могу увидеть привязку непосредственно из самого определения макроса. Это происходит только при входе =values? - person user1461328; 29.03.2015
comment
@user1461328 user1461328 О вашем первом комментарии: вы имеете в виду предположение о том, что *cont* не будет специальной переменной, потому что она назначается с помощью setq, а не объявляется с помощью дефвар? - person Joshua Taylor; 29.03.2015
comment
@user1461328 user1461328 Реализация =bind расширит код, например (=bind (a b c) expr body...), до (let ((*cont* #'(lambda (a b c) body...))) expr). Намерение состоит в том, что *cont* в конечном итоге будет вызываться с тремя значениями, и поскольку список параметров *cont* равен (a b c), эти три значения будут привязаны к a, b и c, а затем body будет оцениваться в рамках те привязки. Итак, (=bind (a b c) expr body) ведет себя очень похоже на (multiple-value-bind (a b c) expr body). Однако вы правы в том, что если есть несколько переменных, то выражение должно - person Joshua Taylor; 29.03.2015
comment
@user1461328 (в конце концов) заканчивается на (=values ...). - person Joshua Taylor; 29.03.2015
comment
@ Джошуа Тейлор для первой части, я просто хочу знать, чего ожидал Грэм, когда писал книгу, особенно когда результат меняется с (A 5) на (B 1). В данный момент мне просто интересно, является ли cont лямбда-функцией, созданной первым =bind или вторым, если его предположение было правильным - person user1461328; 30.03.2015