4

需要break语句或continue其他语言语句的代码可以在 Common Lisp 和 Emacs Lisp 中使用block&return-fromcatch&来完成。throw然后是需要redo语句的代码,或者至少最好用redo. redo语句不一定是关于循环的。redo在 Lisp中我该怎么做?

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

4

3 回答 3

7

在 Common Lisp 中:

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


(dotimes (i some-list)
  redo
  (when (some-condition-p)
    (go redo))
  (some-more))
于 2013-06-28T09:06:34.573 回答
5

Rainer 的回答说明了使用tagbodywhich 可能是实现这种构造(一种特定类型的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,等等。这可能是可取的;许多内置的迭代结构(例如 , dodo*使用隐含的tagbody,所以它可能没问题。但是,由于您还添加了自己的控制流运算符 ,redo因此您可能需要确保它只能与 . 定义的标签一起使用with-redo。事实上,虽然Perlredo可以使用或不使用标签,但Rubyredo似乎不允许使用标签。无标签情况允许跳回最里面的封闭循环(或者,在我们的例子中,最里面的with-redo)。我们可以解决泄漏抽象,以及同时嵌套redos 的能力。

(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 ,但仍然是隐式标签体,以便您可以定义其他标签并使用 跳转到它们;您也可以调整此处的代码来做到这一点。gowith-redogo

一些实施注意事项

这个这个答案产生了一些评论,我想对实现做更多的注释。使用标签实现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-labeland#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 ...)returnblock block 这是可取的,因为我们试图将无标签with-redo视为redo原始控制结构。

于 2013-06-28T11:52:40.260 回答
1

更新:Emacs 24.4(即将发布)有 tagbody。Emacs 24.4 附带的 cl-lib 包括 cl-tagbody。

对于没有 tagbody 的 Lisp 方言,只要方言具有 catch/throw 等价物,仍然可以实现重做。

对于 Emacs Lisp:

;; with-redo version 0.1
(defmacro with-redo (tag &rest body)
  "Eval BODY allowing jumps using `throw'.
TAG is evalled to get the tag to use; it must not be nil.
Then the BODY is executed.
Within BODY, a call to `throw' with the same TAG and a non-nil VALUE causes a jump to the beginning of BODY.
A call to `throw' with the same TAG and nil as VALUE exits BODY and this `with-redo'.
If no throw happens, `with-redo' returns the value of the last BODY form."
  (declare (indent 1))
  (let ((ret (make-symbol "retval")))
    `(let (,ret)
       (while
           (catch ,tag
             (setq ,ret (progn ,@body))
             nil))
       ,ret)))
(defun redo (symbol)
  (throw symbol t))

使用示例(所有示例都在 Emacs Lisp 中):

(with-redo 'question
  (let ((name (read-string "What is your name? ")))
    (when (equal name "")
      (message "Zero length input. Please try again.")
      (beep)
      (sit-for 1)
      (redo 'question))
    name))

将相同的示例编写为中间测试循环:

(require 'cl-lib)
(let (name)
  (cl-loop do
           (setq name (read-string "What is your name? "))
           while
           (equal name "")
           do
           (message "Zero length input. Please try again.")
           (beep)
           (sit-for 1))
  name)

同样的例子写成一个无限循环,用一个 throw 代替:

(let (name)
  (catch 'question
    (while t
      (setq name (read-string "What is your name? "))
      (unless (equal name "")
        (throw 'question name))
      (message "Zero length input. Please try again.")
      (beep)
      (sit-for 1))))

实现with-lex-redo-anonand lex-redo, where会导致跳转到文本/词汇最内层形式(lex-redo)的正文开头:with-lex-redo-anon

;; with-lex-redo-anon version 0.1
(require 'cl-lib)
(defmacro with-lex-redo-anon (&rest body)
  "Use with `(lex-redo)'."
  (let ((tag (make-symbol "lex-redo-tag"))
        (ret (make-symbol "retval")))
    `(cl-macrolet ((lex-redo () '(cl-return-from ,tag t)))
       (let (,ret)
         (while
             (cl-block ,tag
               (setq ,ret (progn ,@body))
               nil))
         ,ret))))

示例测试:

(let ((i 0) (j 0))
  (with-lex-redo-anon
    (with-lex-redo-anon
      (print (list i j))
      (when (< j 2)
        (incf j)
        (lex-redo)))
    (when (< i 2)
      (incf i)
      (lex-redo))))

与另一个答案中的输出相同。

于 2013-06-29T09:57:34.767 回答