Как реализовать оператор повтора (как в Perl и Ruby) в Lisp

Код, требующий операторов break или операторов continue на других языках, можно выполнить с помощью операторов block и return-from или catch и throw в Common Lisp и Emacs Lisp. Затем есть код, который требует redo операторов или, по крайней мере, лучше всего писать с redo. И операторы redo не обязательно должны быть связаны с циклами. Как я могу сделать redo в Лиспе?

Если бы в Лиспе был эквивалент redo, я думаю, он работал бы так: специальная форма with-redo, которая принимает символ и формы, и redo, которая принимает символ. Форма (with-redo 'foo BODY-FORMS...) может содержать (redo 'foo) в своих BODY-FORMS, а (redo 'foo) передает управление обратно в начало BODY-FORMS.


person Jisang Yoo    schedule 28.06.2013    source источник


Ответы (3)


В общем Лиспе:

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


(dotimes (i some-list)
  redo
  (when (some-condition-p)
    (go redo))
  (some-more))
person Rainer Joswig    schedule 28.06.2013
comment
Следует добавить, что некоторые макросы (такие как dotimes или вообще все циклические макросы, начинающиеся с do) неявно заключают свои тела в тело тега. Это то, что продемонстрировано во втором примере выше. - person Svante; 28.06.2013

Ответ Райнера иллюстрирует использование 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
comment
Интересное использование макроса #: read и #1# для создания нового неинтернированного символа, на который можно ссылаться позже. Я никогда не видел этого раньше. Я не могу решить, нравится ли мне это больше по сравнению с типичным (let (foo (gensym)) `(...)) подходом, который я видел гораздо чаще. Любая причина, по которой лучше/более подходит для предотвращения захвата переменных, или это просто вопрос стиля использования одного или другого? - person Clayton Stanley; 29.06.2013
comment
@ClaytonStanley Использование символа ubintenred (read) позволяет получить красивый код, но может вызвать некоторую путаницу при просмотре расширенных макросов (если вы расширите этот (with-redo .... (with-redo ...) ...), неясно, какой #:hidden-label есть какой, но использование идиомы LET и (gensym 'hidden-label) должно привести к чтобы неинтернированные символы назывались по-разному (#:hidden-symbol0001, ...). - person Vatine; 29.06.2013
comment
@Ватин Верно. Это то, что сначала сбило меня с техники #:foo. Я знаю, что вы не можете полагаться на операторы печати символов, чтобы определить, являются ли они эквивалентными, но, по крайней мере, с помощью метода gensym вы получите некоторую визуальную обратную связь, которая говорит, что они, вероятно, не являются эквивалентными. - person Clayton Stanley; 29.06.2013
comment
@Vatine @ClaytonStanley В данном случае нам нужен один и тот же неинтернированный символ во всех всех расширениях with-redo, чтобы мы могли с уверенностью сказать, что redo возвращает нас к лексически заключенному самый внутренний with-redo. Альтернативой может быть (let ((hidden-tag (gensym …))) (defmacro …)), но у него есть разрешение верхнего уровня, которое я нахожу немного уродливым (но это не совсем проблема), или (defvar *hidden-tag* …), но тогда мы определили что-то, что может привлечь чье-то внимание (но это не совсем тоже проблема; если тыкнешь внутренности, то можешь что-то сломать). - person Joshua Taylor; 29.06.2013

Обновление: Emacs 24.4 (скоро будет выпущен) имеет тело тега. cl-lib, входящая в состав Emacs 24.4, включает cl-tagbody.

Для диалекта Лиспа, в котором нет тела тега, можно реализовать повтор, если в диалекте есть эквивалент перехвата/выброса.

Для 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)

Тот же пример, написанный как бесконечный цикл с броском вместо этого:

(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-anon и lex-redo, где (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))))

Тот же вывод, что и в другом ответе.

person Jisang Yoo    schedule 29.06.2013
comment
В Common Lisp catch и throw имеют динамическую связь (throw просто должно произойти, пока соответствующий catch находится выше в стеке), тогда как tagbody и go являются лексическими. Например, (flet ((foo () (go away))) (tagbody away (foo))) — ошибка, а (flet ((foo () (throw 'away))) (catch 'away (foo))) — нормально. Со свежими символами решение на основе catch может работать, но redo по-прежнему нуждается в tag в качестве аргумента, что разрешено вопросом, но меньше похоже на redo без меток в Perl и Ruby. Можно ли это адаптировать, чтобы разрешить redo без метки, который всегда переносится на… - person Joshua Taylor; 29.06.2013
comment
… лексически самый внутренний объемлющий with-redo? - person Joshua Taylor; 29.06.2013
comment
Я добавил определение with-lex-redo-anon к ответу. Это зависит от лексических cl-block и cl-return-from, которые реализованы в cl-lib с использованием динамических catch и throw. Не уверен, как cl-lib справляется с этим, но, похоже, они работают. - person Jisang Yoo; 29.06.2013