Rainer 的回答说明了使用tagbody
which 可能是实现这种构造(一种特定类型的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
,并且可以使用go
代替redo
,等等。这可能是可取的;许多内置的迭代结构(例如 , do
)do*
使用隐含的tagbody
,所以它可能没问题。但是,由于您还添加了自己的控制流运算符 ,redo
因此您可能需要确保它只能与 . 定义的标签一起使用with-redo
。事实上,虽然Perlredo
可以使用或不使用标签,但Rubyredo
似乎不允许使用标签。无标签情况允许跳回最里面的封闭循环(或者,在我们的例子中,最里面的with-redo
)。我们可以解决泄漏抽象,以及同时嵌套redo
s 的能力。
(defmacro with-redo (&body body)
`(macrolet ((redo () `(go #1=#:hidden-label)))
(tagbody
#1#
((lambda () ,@body)))))
在这里,我们定义了一个标签,用于with-redo
其他东西不应该知道的(并且无法找到,除非它们对某些with-redo
形式进行宏扩展,并且我们已经将 包装body
在一个lambda
函数中,这意味着,例如,一个符号thebody
是要评估的表单,而不是 的标记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
伪装 a ,但仍然是隐式标签体,以便您可以定义其他标签并使用 跳转到它们;您也可以调整此处的代码来做到这一点。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
and#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
现在,值得一问的是,这是否会产生实际影响,如果有,在哪些情况下,是好是坏?坦率地说,我不完全确定。
如果你在一个表单内部宏展开后进行一些复杂的代码操作,表单1已经变成了,这意味着将 移动到另一个表单的主体中表单2,它仍然会有重新启动的效果形式2中的迭代。在我看来,这使它更像 a可以从 a b 1传输到不同的b 2,唯一的区别是它现在从b 2而不是b 1返回。我认为(with-redo ...)
(redo)
(go #1#)
(go #1#)
(with-redo ...)
return
block
block
这是可取的,因为我们试图将无标签with-redo
视为redo
原始控制结构。