Ответ Райнера иллюстрирует использование tagbody
, что, вероятно, является самым простым способом реализации конструкции такого типа (особый вид goto
или безусловный переход). Я подумал, что было бы неплохо указать, что если вы не хотите использовать явное тело тега или неявное тело тега, предоставляемое одной из стандартных конструкций, вы также можете создать with-redo
, как вы предложили. Единственная разница в этой реализации заключается в том, что мы не будем ставить теги в кавычки, так как они не оцениваются в tagbody
, а совместимость с другими конструкциями тоже хороша.
(defmacro with-redo (name &body body)
`(macrolet ((redo (name)
`(go ,name)))
(tagbody
,name
,@body)))
CL-USER> (let ((x 0))
(with-redo beginning
(print (incf x))
(when (< x 3)
(redo beginning))))
1
2
3
; => NIL
На самом деле это дырявая абстракция, поскольку body
может определять другие метки для неявного tagbody
, а вместо redo
можно использовать go
и так далее. Это может быть желательно; множество встроенных итерационных конструкций (например, do
, do*
) используйте неявный tagbody
, так что это может быть в порядке. Но, поскольку вы также добавляете свой собственный оператор потока управления, redo
, вы можете убедиться, что его можно использовать только с тегами, определенными with-redo
. На самом деле, в то время как Perl redo
можно использовать с меткой или без нее, Ruby redo
не позволяет использовать метки. Случаи без меток допускают возврат к самому внутреннему охватывающему циклу (или, в нашем случае, к самому внутреннему with-redo
). Мы можем устранить дырявую абстракцию, а также возможность одновременного вложения redo
.
(defmacro with-redo (&body body)
`(macrolet ((redo () `(go #1=#:hidden-label)))
(tagbody
#1#
((lambda () ,@body)))))
Здесь мы определили тег для использования с with-redo
, о котором другие вещи не должны знать (и не могут узнать, если они не макрорасширят некоторые формы with-redo
, и мы обернули body
в функцию lambda
, что означает, что, например, , символ в body
— это оцениваемая форма, а не тег для tagbody
. Вот пример, показывающий, что redo
переходит обратно к ближайшему лексически охватывающему with-redo
:
CL-USER> (let ((i 0) (j 0))
(with-redo
(with-redo
(print (list i j))
(when (< j 2)
(incf j)
(redo)))
(when (< i 2)
(incf i)
(redo))))
(0 0)
(0 1)
(0 2)
(1 2)
(2 2)
; => NIL
Конечно, поскольку вы можете определить with-redo
самостоятельно, вы можете принимать решения о том, какой дизайн вы хотите принять. Возможно, вам нравится идея redo
без аргументов (и маскировки go
секретной меткой, но with-redo
по-прежнему является неявным телом тега, так что вы можете определять другие теги и переходить к ним с помощью go
; вы можете адаптировать код здесь, чтобы сделать просто это тоже.
Некоторые замечания по реализации
Этот ответ вызвал несколько комментариев, я хотел сделать еще пару заметок о реализации. Реализация with-redo
с метками довольно проста, и я думаю, что все опубликованные ответы касаются этого; случай без метки немного сложнее.
Во-первых, использование локального макролета — это удобство, которое даст нам предупреждения, когда redo
используется за пределами некоторого лексически включающего with-redo
. Например, в SBCL:
CL-USER> (defun redo-without-with-redo ()
(redo))
; in: DEFUN REDO-WITHOUT-WITH-REDO
; (REDO)
;
; caught STYLE-WARNING:
; undefined function: REDO
Во-вторых, использование #1=#:hidden-label
и #1#
означает, что тег go для повторного выполнения является неинтернированным символом (что снижает вероятность утечки абстракции), но также является одним и тем же символом в расширениях with-redo
. В следующем фрагменте tag1
и tag2
— это переходные теги из двух разных расширений with-redo
.
(let* ((exp1 (macroexpand-1 '(with-redo 1 2 3)))
(exp2 (macroexpand-1 '(with-redo a b c))))
(destructuring-bind (ml bndgs (tb tag1 &rest rest)) exp1 ; tag1 is the go-tag
(destructuring-bind (ml bndgs (tb tag2 &rest rest)) exp2
(eq tag1 tag2))))
; => T
Альтернативная реализация with-redo
, которая использует новый gensym
для каждого макрорасширения, не имеет этой гарантии. Например, рассмотрим with-redo-gensym
:
(defmacro with-redo-gensym (&body body)
(let ((tag (gensym "REDO-TAG-")))
`(macrolet ((redo () `(go ,tag)))
(tagbody
,tag
((lambda () ,@body))))))
(let* ((exp1 (macroexpand-1 '(with-redo-gensym 1 2 3)))
(exp2 (macroexpand-1 '(with-redo-gensym a b c))))
(destructuring-bind (ml bndgs (tb tag1 &rest rest)) exp1
(destructuring-bind (ml bndgs (tb tag2 &rest rest)) exp2
(eq tag1 tag2))))
; => NIL
Теперь стоит спросить, имеет ли это практическое значение, и если да, то в каких случаях, и в лучшую или в худшую сторону? Честно говоря, я не совсем уверен.
Если вы выполняли какие-то сложные манипуляции с кодом после внутреннего макрорасширения формы (with-redo ...)
, form1, так что (redo)
уже превратилось в (go #1#)
, это означает, что перемещение (go #1#)
в тело другой формы (with-redo ...)
, form2, по-прежнему приведет к перезапуску итерации в form 2. На мой взгляд, это больше похоже на return
, который может быть переносится из block
b1 в другой block
b2, с той лишь разницей, что теперь он возвращается из < em>b2 вместо b1. Я думаю, что это желательно, поскольку мы пытаемся рассматривать with-redo
и redo
без меток как примитивные управляющие структуры.
person
Joshua Taylor
schedule
28.06.2013