如何在Lisp中实现重做语句(如在Perl和Ruby中)

Jis*_*Yoo 4 lisp common-lisp control-structure redo

需要使用其他语言的break语句或continue语句的代码可以使用block&return-fromcatch/ throw在Common Lisp和Emacs Lisp中完成.然后有代码需要redo声明,或至少最好用redo.和redo语句不必须对循环.我怎么能redo在Lisp中做?

如果redo在Lisp中有一个等价物,我认为它会像这样工作:特殊形式with-redo,它采用符号和形式,并redo采用符号.表格(with-redo 'foo BODY-FORMS...)可以包含(redo 'foo)在其BODY-FORMS中,并将(redo 'foo)控制权转移回BODY-FORMS的开头.

Rai*_*wig 7

在Common Lisp中:

(tagbody
   start
   (do-something)
   (go start))


(dotimes (i some-list)
  redo
  (when (some-condition-p)
    (go redo))
  (some-more))
Run Code Online (Sandbox Code Playgroud)

  • 应该补充的是,一些宏(比如`dotimes`,或者更常见的是以`do`开头的所有循环宏)隐含地将它们的主体包含在标签体中.这就是上面第二个例子中所展示的内容. (5认同)

Jos*_*lor 5

Rainer的回答说明了使用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
Run Code Online (Sandbox Code Playgroud)

现在这实际上是一个漏洞抽象,因为它body可以为隐式定义其他标签tagbody,并且可以使用go而不是redo等等.这可能是可取的; 许多内置的迭代结构(例如do,do*)使用隐式tagbody,所以它可能没问题.但是,由于您还要添加自己的控制流操作符,因此redo您可能希望确保它只能与定义的标记一起使用with-redo.事实上,虽然Perlredo可以使用或不使用标签,但Rubyredo似乎不允许使用标签.无标签的情况允许跳回到最里面的封闭循环的行为(或者,在我们的情况下,最里面的循环with-redo).我们可以解决漏洞抽象,以及同时嵌套redos 的能力.

(defmacro with-redo (&body body)
  `(macrolet ((redo () `(go #1=#:hidden-label)))
     (tagbody
        #1#
        ((lambda () ,@body)))))
Run Code Online (Sandbox Code Playgroud)

这里我们定义的标签与使用with-redo其他的事情不应该知道(也不可能找到,除非他们macroexpand一些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
Run Code Online (Sandbox Code Playgroud)

当然,既然您可以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
Run Code Online (Sandbox Code Playgroud)

其次,使用#1=#:hidden-label#1#表示用于重做的go标记是一个未加密的符号(这减少了抽象泄漏的可能性),但是在扩展中也是相同的符号with-redo.在下面的代码片段中tag1,tag2是来自两个不同扩展的go-tags 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
Run Code Online (Sandbox Code Playgroud)

对于每个宏扩展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
Run Code Online (Sandbox Code Playgroud)

现在,值得一提的是,这是否会产生实际的影响,如果是这样,在哪些情况下,这是好的还是差别?坦白说,我并不完全确定.

如果你在一个表单的内部宏展开之后执行一些复杂的代码操作,那么表单1已经被转换为,这意味着移动到另一个表单的主体,表单2,它仍然会有重启的效果形式2中的迭代.在我看来,这更像是一个可以从b 1传输到另一个b 2的唯一区别,它现在从b 2而不是b返回(with-redo ...)(redo)(go #1#)(go #1#)(with-redo ...)returnblock block 1.我这是可取的,因为我们正试图将无标签with-redoredo原始控制结构视为一种.

  • 有趣地使用#:read宏和#1#来创建一个可以在以后引用的新的未处理符号.我以前从未见过这个.与我经常看到的典型(let(foo(gensym))`(...))方法相比,我无法决定我是否更喜欢这个.为什么一个更好/更适合防止变量捕获的任何原因,或者仅仅是使用一个或另一个的风格问题? (2认同)