7.8 Формы циклов

Common Lisp предоставляет некоторые конструкции для циклов. Конструкция loop предоставляет простую функциональность. Она слегка больше, чем progn, и имеет ветку для переноса управления снизу вверх. Конструкции do и do* предоставляют общую функциональность для управления на каждом цикле изменением нескольких переменных. Для специализированных циклов над элементами списка или n последовательных чисел предоставляются формы dolist и dotimes. Конструкция tagbody наиболее общая конструкция, которая внутри себя позволяет использование выражений go. (Традиционная конструкция prog — это синтез tagbody, block и let.) Большинство конструкций циклов позволяют определённые статически нелокальные выходы (смотрите return-from и return).

7.8.1 Бесконечный цикл

Конструкция loop является наипростейшей функциональностью для итераций. Она не управляет переменными, и просто циклично выполняет своё тело.

[Макрос] loop {form}*

Каждая форма form выполняется последовательно слева направо. Когда вычислена последняя форма, тогда вычисляется первая форма и так далее, в безостановочном цикле. Конструкция loop никогда не возвращает значение. Её выполнение может быть остановлено явно, с помощью return или throw, например.

loop, как и многие конструкции циклов, устанавливает неявный блок с именем nil. Таким образом, return с заданным результатом может использоваться для выхода и loop.


7.8.2 Основные формы циклов

В отличие от loop, do и do* предоставляют мощный механизм для повторных вычислений большого количества переменных.

[Макрос] do ({var | (var [init [step]])}*)(end-test {result}*){declaration}* {tag | statement}*

[Макрос] do* ({var | (var [init [step]])}*)(end-test {result}*){declaration}* {tag | statement}*

Оператор do представляет общую функциональность цикла, с произвольным количеством «переменных-индексов». Эти переменные связываются при входе в цикл и параллельно наращиваются, как это было задано. Они могут быть использованы, как для генерации необходимых последовательных чисел (как, например, последовательные целые числа), так и для накопления результата. Когда условие окончания цикла успешно выполнилось, тогда цикл завершается с заданным значением.

В общем виде do выглядит так:

(do ((var1 init1 step1)
     (var2 init2 step2)
     ...
     (varn initn stepn))
    (end-test . result)
   {declaration}*
  . tagbody)

Цикл do* выглядит также, кроме изменения имени с do на do*.

Первый элемент формы является списком нуля и более спецификаторов переменных-индексов. Каждый спецификатор является списком из имени переменной var, первоначального значения init, и форма приращения step. Если init опущен, используется первоначальное значение nil. Если step опущен, var не изменяется на итерациях цикла (но может изменяться в теле цикла с помощью формы setq).

Спецификатор переменной-индекса может также быть просто именем переменной. В этом случае переменная будет иметь первоначальное значение nil и не будет изменяться при итерациях цикла. В целях стиля, использовать просто имя переменной рекомендуется, только если перед первым использованием для неё устанавливается значение с помощью setq. Если необходимо чтобы первоначальное значение было nil, а не неопределённое, то лучше указывать это явно, если нужна ложь так: (varj nil) или если нужен пустой список так: (varj ’()).

Перед первой итерацией вычисляются все формы init, и каждая переменная var связывается с соответствующим результатом вычислений init. Используется именно связывание, а не присвоение. Когда цикл завершается, старые значения этих переменных восстанавливаются. Для do, все формы init вычисляется перед тем, как будут связаны переменные var. Таким образом все формы могут ссылаться на старые связывания этих переменных (то есть на значения, которые были видимы до начала выполнения конструкции do). Для do* вычисляется первая форма init, затем первая переменная связывается с результатом этих вычислений. Затем вычисляется вторая форма init и вторая переменная var связывается с этим значением, и так далее. В целом, форма initj может ссылаться на новые связывания vark, если k < j, иначе ссылка происходит на старое связывание.

Второй элемент конструкции цикла это список из формы предиката-выхода end-test и нуля и более форм результата result. Этот элемент напоминает подвыражение cond. В начале каждой итерации, после обработки всех переменных, вычисляется форма end-test. Если результат nil, выполняется тело формы do (или do*). Если результат не nil, последовательно вычисляются формы result, как неявный progn, и затем do возвращает управление. do возвращает результаты вычисления последней формы result. Если таких форм не быть, значением do становиться nil. Следует отметить, что аналогия с подвыражениями cond не полная, так как cond в этом случае возвращает результат формы условия.

Переменные-индексы изменяются в начале каждой непервой итерации так, как написано далее. Слева направо вычисляются все формы step, и затем результаты присваиваются переменным-индексам. Если такой формы step для переменной указано не было, то переменная и не изменяется. Для do, все формы step вычисляются перед там, как будут изменены переменные. Присваивания переменным осуществляются параллельно, как в psetq. Так как все формы step вычисляются перед тем, как будет изменена хоть одна переменных, форма step при вычислении всегда ссылается на старые значения всех переменных-индексов, даже если другие формы step были выполнены. Для do*, вычисляется первая форма step, затем полученное значение присваивается первой переменной-индексом, затем вычисляется вторая форма step, и полученное значение присваивается второй переменной, и так далее. Присваивание происходит последовательно, как в setq. И для do, и для do* после того как переменные были изменены, вычисляется end-test так, как уже было описано выше. Затем продолжаются итерации.

Если end-test формы do равен nil, тогда предикат всегда ложен. Таким образом получается «бесконечный цикл»: тело body do выполняется циклично, переменные-индексы изменяются как обычно. (Конструкция loop также является «бесконечным циклом», только без переменных-индексов.) Бесконечный цикл может быть остановлен использованием return, return-from, go на более высокий уровень или throw. Например:

(do ((j 0 (+ j 1)))
    (nil)                        ;Выполнять вечно
  (format t "~%Input ~D:" j)
  (let ((item (read)))
    (if (null item) (return)     ;Обрабатывать элементы пока не найден nil
        (format t "~&Output ~D: ~S" j (process item)))))

Оставшаяся часть do оборачивается в неявный tagbody. Теги могут использоваться внутри тела цикла do для того, чтобы затем использовать выражения go. На такие выражения go не могут использоваться в спецификаторах переменных-индексов, в предикате end-test и в формах результата result. Когда управление достигает конца тела цикла do, наступает следующая цикл итерации (начинающийся с вычисления форм step).

Неявный block с именем nil окружает всю форму do. Выражение return может использоваться в любом месте для немедленного выхода из цикла.

Формы declare могут использоваться в начала тела do. Они применяются к коду внутри тела do, для связываний переменных-индексов, для форм init, для форм step, для предиката end-test и для форм результата result.

Вот парочка примеров использования do:

(do ((i 0 (+ i 1))     ;Sets every null element of a-vector to zero
     (n (length a-vector)))
    ((= i n))
  (when (null (aref a-vector i))
    (setf (aref a-vector i) 0)))

Конструкция

(do ((x e (cdr x))
     (oldx x x))
    ((null x))
  body)

использует параллельное присваивание переменным-индексам. На первой итерации значение oldx получает значение x, которое было до входа в цикл. При выходе из цикла oldx будет содержать значение x, которое было на предыдущей итерации.

Очень часто алгоритм цикла может быть по большей части выражен в формах step и тело при этом останется пустым. Например,

(do ((x foo (cdr x))
     (y bar (cdr y))
     (z ’() (cons (f (car x) (car y)) z)))
    ((or (null x) (null y))
     (nreverse z)))

делает то же, что и (mapcar #’f foo bar). Следует отметить, что вычисление step для z использует тот факт, что переменные переприсваиваются параллельно. Тело функции пустое. Наконец, использование nreverse в форме возврата результата, переставляет элементы списка для правильного результата. Другой пример:

(defun list-reverse (list)
       (do ((x list (cdr x))
            (y ’() (cons (car x) y)))
           ((endp x) y)))

Нужно заметить, что используется endp вместо null или atom для проверки конца списка. Это даёт более надёжный алгоритм.

В качестве примера вложенных циклов, предположим что env содержит список cons-ячеек. car элемента каждой cons-ячейки является списком символов, и cdr каждой cons-ячейки является списком такой же длины с соответствующими значениями. Такая структура данных похожа на ассоциативный список, но она в отличие разделена на «кадры». Общая структура напоминает грудную клетку. Функция поиска по такой структуре может быть такой:

(defun ribcage-lookup (sym ribcage)
       (do ((r ribcage (cdr r)))
           ((null r) nil)
         (do ((s (caar r) (cdr s))
              (v (cdar r) (cdr v)))
             ((null s))
           (when (eq (car s) sym)
             (return-from ribcage-lookup (car v))))))

(Примечание, использование отступов в примере выше выделяет тела вложенных циклов.)

Цикл do может быть выражен в терминах более примитивных конструкций block, return, let, loop, tagbody и psetq:

(block nil
  (let ((var1 init1)
        (var2 init2)
        ...
        (varn initn))
     {declaration}*
    (loop (when end-test (return (progn . result)))
          (tagbody . tagbody)
          (psetq var1 step1
                 var2 step2
                 ...
                 varn stepn))))

do* почти то же, что и do за исключением того, что связывание и наращение переменных происходит последовательно, а не параллельно. Таким образом, в вышеприведённой конструкции, let будет заменена на let* и psetq на setq.


7.8.3 Простые формы циклов

Конструкции dolist и dotimes для каждого значения взятого для одной переменной выполняют тело один раз. Они хоть и выражаются в терминах do, но захватывают очень простые шаблоны использования.

И dolist и dotimes циклично выполняют тело. На каждой итерации заданная переменная связывается с элементом, которая затем может использоваться в теле. dolist использует элементы списка, и dotimes использует целые числа от 0 по n− 1, при некотором указанном положительном целом n.

Результат двух этих конструкций может быть указан с помощью необязательной формы результата. Если эта форма опущена результат равен nil.

Выражение return может быть использовано для немедленного возврата из форм dolist или dotimes, игнорируя все оставшиеся итерации, которые должны были быть выполнены. block с именем nil окружает конструкцию. Тело цикла неявно обернуто конструкцией tagbody. Таким образом тело может содержать теги и go выражения. Декларации могут быть указаны перед телом цикла.

[Макрос] dolist (var listform [resultform]){declaration}* {tag | statement}*

dolist предоставляет прямой цикл по списку элементов. Сначала dolist вычисляет форму listform, которая должна вернуть список. Затем для каждого элемента вычисляется тело цикла. Данный элемент на каждой итерации связывается с переменной var. Затем вычисляется resultform (одна форма, не неявный progn). (Когда вычисляется resultform, переменная var все ещё связана, и имеет значение nil.) Если resultform опущена, то результат равен nil.

(dolist (x ’(a b c d)) (prin1 x) (princ " "))  nil
   вывод «a b c d » (в том числе пробел в конце)

Для завершения цикла и возврата заданного значения может использоваться явное выражение return.

Пользователь ограничен в создании побочных действий так, как это описано в разделе 7.9


[Макрос] dotimes (var countform [resultform]){declaration}* {tag | statement}*

dotimes предоставляет цикл над последовательностью целых чисел. Выражение (dotimes (var countform resultform) . progbody) вычисляет форму countform, которая должна вернуть целое число. Затем тело цикла выполняется по порядку один раз для каждого число от нуля (включительно) до count (исключая). При этом переменная var связывается с текущим целым числом. Если значение countform отрицательно или равно нулю, тогда progbody не выполняется ни разу. Наконец выполняется resultform (одна форма, не неявный progn), и полученный результат возвращается из формы цикла. (Когда result вычисляется, переменная-индекс var все ещё связана и содержит количество выполненных итераций.) Если resultform опущена, то результат равен nil.

Для завершения цикла и возврата заданного значения может использоваться выражение return.

Пример использования dotimes для обработки строк:

;;; True if the specified subsequence of the string is a
;;; palindrome (reads the same forwards and backwards).

(defun palindromep (string &optional
                           (start 0)
                           (end (length string)))
  (dotimes (k (floor (- end start) 2) t)
    (unless (char-equal (char string (+ start k))
                        (char string (- end k 1)))
      (return nil))))

(palindromep "Able was I ere I saw Elba")  t

(palindromep "A man, a plan, a canal–Panama!")  nil

(remove-if-not #’alpha-char-p     ;Удалить знаки препинания
               "A man, a plan, a canal–Panama!")
    "AmanaplanacanalPanama"

(palindromep
 (remove-if-not #’alpha-char-p
                "A man, a plan, a canal–Panama!"))  t

(palindromep
 (remove-if-not
   #’alpha-char-p
   "Unremarkable was I ere I saw Elba Kramer, nu?"))  t

(palindromep
 (remove-if-not
   #’alpha-char-p
   "A man, a plan, a cat, a ham, a yak,
                   a yam, a hat, a canal–Panama!"))  t
(palindromep
 (remove-if-not
   #’alpha-char-p
   "Ja-da, ja-da, ja-da ja-da jing jing jing"))  nil

Изменение значения переменной var в теле цикла (с помощью setq например) будет иметь непредсказуемые последствия, возможно зависящие от реализации. Компилятор Common Lisp’а может вывести предупреждение о том, что переменная-индекс используется в setq.


Смотрите также do-symbols, do-external-symbols и do-all-symbols.

7.8.4 Отображение

Отображение — это тип цикла, в котором заданная функция применяется к частям одной или более последовательностей. Результатом цикла является последовательность, полученная из результатов выполнения это функции. Существует несколько опции для указания того, какие части списка будут использоваться в цикле, и что будет происходить с результатом применения функции.

Функция map может быть использована для отображения любого типа последовательности. Следующие же функции оперируют только списками.

[Функция] mapcar function list &rest more-lists
[Функция] maplist function list &rest more-lists
[Функция] mapc function list &rest more-lists
[Функция] mapl function list &rest more-lists
[Функция] mapcan function list &rest more-lists
[Функция] mapcon function list &rest more-lists

Для каждой из этих функций отображения, первый аргумент является функцией и оставшиеся аргументы должны быть списками. Функция в первом аргументе должно принимать столько аргументов, сколько было передано списков в функцию отображения.

mapcar последовательно обрабатывает элементы списков. Сначала функция применяется к car элементу каждого списка, затем к cadr элементу, и так далее. (Лучше всего, чтобы все переданные списки имели одинаковую длину. Если это не так, то цикл завершиться, как только закончится самый короткий список, и все оставшиеся элементы в других списках будут проигнорированы.) Значение, возвращаемое mapcar, является списком результатов последовательных вызовов функции из первого параметра. Например:

(mapcar #’abs ’(3 -4 2 -5 -6))  (3 4 2 5 6)
(mapcar #’cons ’(a b c) ’(1 2 3))  ((a . 1) (b . 2) (c . 3))

maplist похожа на mapcar за исключением того, что функция применяется к спискам и последующим cdr элементам этих списков, а не последовательно к элементам спискам. Например:

(maplist #’(lambda (x) (cons ’foo x))
         ’(a b c d))
    ((foo a b c d) (foo b c d) (foo c d) (foo d))

(maplist #’(lambda (x) (if (member (car x) (cdr x)) 0 1)))
         ’(a b a c d b c))
    (0 0 1 0 1 1 1)
   ;Возвращается 1, если соответствующий элемент входящего списка
   ;  появлялся последний раз в данном списке.

mapl и mapc похожи на maplist и mapcar, соответственно, за исключением того, что они не накапливают результаты вызова функций.

Эти функции используются, когда функция в первом параметре предназначена для побочных эффектов, а не для возвращаемого значения. Значение возвращаемое mapl или mapc является вторым аргументом, то есть первой последовательностью для отображения.

mapcan и mapcon похожи на mapcar и maplist соответственно, за исключением того, что результат создаётся с помощью функции nconc, а не list. То есть,

(mapcon f x1 ... xn)
    (apply #’nconc (maplist f x1 ... xn))

Такое же различие между mapcan и mapcar. Концептуально, эти функции позволяют функции отображения возвращать переменное количество элементов. Таким образом длина результата может быть не равна длине входного списка. Это, в частности, полезно для возврата нуля или одного элемента:

(mapcan #’(lambda (x) (and (numberp x) (list x)))
        ’(a 1 b c 3 4 d 5))
    (1 3 4 5)

В этом случае функция действует, как фильтр. Это стандартная Lisp’овая идиома использования mapcan. (Однако, в этом контексте функция remove-if-not также может быть полезна.) Помните, что nconc деструктивная операция, следовательно и mapcan и mapcon также деструктивны. Список возвращаемый функцией function изменяется для соединения и возврата результата.

Иногда do или прямая последовательная рекурсия удобнее, чем функции отображения. Однако, функции отображения должны быть использованы везде, где они действительно необходимы, так как они увеличивают ясность кода.

Функциональный аргумент функции отображения должен быть подходящим для функции apply. Он не может быть макросом или именем оператора. Кроме того, в качестве функционального аргумента можно использовать функцию, имеющую &optional и &rest параметры.

Пользователь ограничен в создании побочных действий так, как это описано в разделе 7.9


7.8.5 Использование «GOTO»

Реализации Lisp’а начиная с Lisp’а 1.5 содержат то, что изначально называлось «the program feature», как будто без этого невозможно писать программы! Конструкция prog позволяет писать в Algol- или Fortran- императивном стиле, используя выражения go, которые могут ссылаться на теги в теле prog. Современный стиль программирования на Lisp’е стремится снизить использование prog. Различные конструкции циклов, как do, имеют тела с характеристиками prog. (Тем не менее, возможность использовать выражения go внутри конструкции цикла очень редко используется на практике.)

prog предоставляет три различные операции: связывание локальный переменных, использование выражения return и использование выражения go. В Common Lisp’е эти три операции были разделены на три конструкции: let, block и tagbody. Эти три конструкции могут использоваться независимо, как строительные кирпичики для других типов конструкций.

[Специальный оператор] tagbody {tag | statement}*

Часть tagbody после списка переменные называется телом. Элемент тела может быть символов или целым числом, и называться в этом случае тег. Также элемент тела может быть списком, и называться в этом случае выражением.

Каждый элемент тела обрабатывается слева направо. Теги игнорируются. Выражения вычисляются и их результаты игнорируются. Если управление достигает конца тела, tagbody возвращает nil.

Если вычисляется форма (go tag), управление перемещается на часть тела, обозначенную тегом.

Область видимости тегов, устанавливаемых в tagbody, является лексической. Продолжительность видимости тегов динамическая. Когда управление вышло из конструкции tagbody, ссылка go на теги в её теле невозможны. Существует возможность для go прыжка в tagbody, которая не находится внутри конструкции tagbody, которая и содержала этот go. То есть возможны прыжки в родительский tagbody. Теги устанавливаемые tagbody будут только скрывать другие одноимённые теги.

Лексическая область видимости для тегов (целей go) полностью полноправно и последствия могут быть сюрпризом для пользователей и разработчиков других Lisp систем. Например, go в следующем примере, работает в Common Lisp так, как это и ожидается:

(tagbody
   (catch ’stuff
      (mapcar #’(lambda (x) (if (numberp x)
                                (hairyfun x)
                                (go lose)))
              items))
   (return)
 lose
   (error "I lost big!"))

В зависимости от ситуации, go в Common Lisp’е не обязательно похож на простую машинную инструкцию «jump». Если необходимо, go может перепрыгивать ловушки исключений. Возможно так, что «замыкание», созданное с помощью function, для лямбда-выражения ссылается на тег (цель go) так долго, сколько лексически доступен данный тег. Смотрите 3 для понимания этого примера.


[Макрос] prog ({var | (var [init])}*) {declaration}* {tag | statement}*

[Макрос] prog* ({var | (var [init])}*) {declaration}* {tag | statement}*

Конструкция prog является синтезом let, block и tagbody, позволяющая связывать переменные, использовать return и go в одной конструкции. Обычно конструкция prog выглядит так:

(prog (var1 var2 (var3 init3) var4 (var5 init5))
       {declaration}*
      statement1
 tag1
      statement2
      statement3
      statement4
 tag2
      statement5
      ...
      )

Список после ключевого символа prog является множеством спецификаторов для связывания переменных var1, var2. Этот список обрабатывается так же, как и в выражении let: сначала слева направо выполняются все формы init (если формы нет, берётся значение nil), и затем переменные параллельно связываются с полученными ранее значениями. Возможно использовать декларации в начале дела prog так же, как и в let.

Тело prog выполняется, как обернутое в tagbody. Таким образом, для перемещения управления к тегу могут использоваться выражения go.

prog неявно устанавливает вокруг тела block с именем nil. Это значит, можно в любое время использовать return для выхода из конструкции prog.

Вот небольшой пример того, что можно сделать с помощью prog:

(defun king-of-confusion (w)
  "Take a cons of two lists and make a list of conses.
   Think of this function as being like a zipper."
  (prog (x y z)     ;Инициализировать x, y, z в nil
        (setq y (car w) z (cdr w))
   loop
        (cond ((null y) (return x))
              ((null z) (go err)))
   rejoin
        (setq x (cons (cons (car y) (car z)) x))
        (setq y (cdr y) z (cdr z))
        (go loop)
   err
        (cerror "Will self-pair extraneous items"
                "Mismatch - gleep!  S" y)
        (setq z y)
        (go rejoin)))

которые делает то же, что и:

(defun prince-of-clarity (w)
  "Take a cons of two lists and make a list of conses.
   Think of this function as being like a zipper."
  (do ((y (car w) (cdr y))
       (z (cdr w) (cdr z))
       (x ’() (cons (cons (car y) (car z)) x)))
      ((null y) x)
    (when (null z)
      (cerror "Will self-pair extraneous items"
              "Mismatch - gleep!  S" y)
      (setq z y))))

Конструкция prog может быть выражена в терминах более простых конструкций block, let и tagbody:

(prog variable-list {declaration}* . body)
    (block nil (let variable-list {declaration}* (tagbody . body)))

Оператор prog* очень похож на prog. Одно отличие в том, что связывание и инициализация переменных осуществляется последовательно, тем самым форма init использовать значения ранее связанных переменных. Таким образом prog* относится к prog, как let* к let. Например,

(prog* ((y z) (x (car y)))
       (return x))

возвращает car элемент значения z.


[Специальный оператор] go tag

Оператор (go tag) используется для применения «goto» внутри конструкции tagbody. tag должен быть символов или целым числом. tag не вычисляется. go переносит управление на точку тела, которая была помечена тегом равным eql заданному. Если такого тега в теле нет, поиск осуществляется в лексически доступном теле другой конструкции tagbody. Использоваться go с тегом, которого нет, является ошибкой.

Форма go никогда не возвращает значение.

В целях хорошего стиля, рекомендуется дважды подумать, прежде чем использовать go. Большинство функций go могут быть заменены циклами, вложенными условными формами или return-from. Если использование go неизбежно, рекомендуется управляющую структуру реализованную с помощью go «упаковать» в определении макроса.