Регистрация | Войти
Lisp — программируемый язык программирования
Предыдущая Оглавление Следующая

22. LOOP для мастеров с черным поясом.

В главе 7 я кратко описал расширенный макрос LOOP. Как я упоминал тогда, LOOP по существу предоставляет язык специального назначения для написания конструкций итерирования.

Это может показаться весьма хлопотным: изобретение целого языка лишь для написания циклов. Но, если вы задумаетесь о способах использования циклов в программах, эта идея действительно станет обретать смысл. Любая программа любого размера всегда будет содержать циклы. И хотя все они не будут одинаковыми, они также не будут и совершенно различными; при детальном рассмотрении будут выделены образцы (в частности, если включать в них код непосредственно предшествующий и следующий за циклами): образцы инициализации перед циклом, образцы действий внутри цикла и образцы действий после завершения цикла. Язык LOOP фиксирует эти образцы, так что вы можете выражать их явно.

Макрос LOOP имеет множество частей: одной из главных претензий противников LOOP является то, что он слишком сложен. В этой главе я покажу, что это не так, дав вам систематическое описание различных частей LOOP и того, как эти части использовать вместе.

Части LOOP

Вы можете делать в LOOP следующее:

  • Итерировать переменную численно или по различным структурам данных.
  • Собирать, подсчитывать, суммировать, искать максимальное и минимальное значения по данным, просматриваемым во время цикла.
  • Выполнять произвольные выражения Lisp.
  • Решать, когда остановить цикл.
  • Осуществлять определенные действия при заданных условиях.

Вдобавок, LOOP предоставляет синтакс для следующего:

  • Создание локальных переменных для использования внутри цикла.
  • Задание произвольных выражений Lisp для выполнения перед и после цикла.

Базовой структурой LOOP является набор предложений (clauses), каждое их которых начинается с ключевого слова loop1). То, как каждое предложение анализируется макросом LOOP, зависит от такого ключевого слова. Некоторые из главных ключевых слов, которые вы видели в главе 7, следующие: for, collecting, summing, counting, do и finally.

Управление итерированием

Большинство из так называемых предложений управления итерированием начинаются с ключевого слова loop for или его синонима as2), за которыми следует имя переменной. Что следует за именем переменной, зависит от типа предложения for.

Подвыражения (subclauses) предложений for могут итерировать по следующему:

  • Численные интервалы, вверх или вниз.
  • Отдельные элементы списка.
  • cons-ячейки, составляющие список.
  • Элементы вектора, включая подтипы, такие как строки и битовые векторы.
  • Пары хэш-таблицы.
  • Символы пакета.
  • Результаты повторных вычислений заданной формы.

Одиночный цикл может содержать несколько предложений for, каждое из которых именует собственную переменную. Если цикл содержит несколько предложений for, он завершается как только любое из них достигает своего условия завершения. Например, следующий цикл:

(loop
  for item in list
  for i from 1 to 10
  do (something)
)

выполнится максимум 10 раз, но может завершиться и раньше, если список содержим менее десяти элементов.

Подсчитывающие циклы (Counting Loops)

Предложения арифметического итерирования управляют числом раз, которое будет выполнено тело цикла, путем изменения переменной в пределах интервала чисел, выполняя тело на каждом шаге. Такие предложения состоят из от одного до трех следующих предложных оборотов (prepositional phrases), идущих после for (или as): оборот откуда (from where), оборот докуда (to where) и оборот по сколько (by how much).

Оборот откуда задает начальное значение для переменной предложения. Он состоит из одного из предлогов (prepositions) from, downfrom или upfrom, за которыми следует форма, предоставляющая начальное значение (число).

Оборот докуда задает точку останова цикла и состоит из одного из предлогов to, upto, below, downto или above, за которыми следует форма, предоставляющая точку останова. С upto и downto цикл завершится (без выполнения тела) когда переменная перейдет точку останова; с below и above он завершится на итерацию ранее.

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

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

(loop for i upto 10 collect i)

накапливает первые одиннадцать целых чисел (с нуля до десяти), но поведение этого:

(loop for i downto -10 collect i)         ; неверно

не определено. Вместо этого вам нужно написать так:

(loop for i from 0 downto -10 collect i)

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

(loop for i from 10 to 20 ...) 

работает хорошо, используя значение приращения по умолчанию. Но это:

(loop for i from 20 to 10 ...)

не знает, что нужно считать от двадцати до десяти. Хуже того, это выражение не выдаст вам никакой ошибки: оно просто не выполнит цикл, так как i уже больше десяти. Вместо этого вы должны написать так:

(loop for i from 20 downto 10 ...)

или так:

(loop for i downfrom 20 to 10 ...)

Наконец, если вам просто нужен цикл, повторяющийся определенное число раз, вы можете заменить предложение следующей формы:

for i from 1 to number-form

на предложение repeat следующего вида:

repeat number-form

Эти предложения идентичны по своему действию, за исключением того, что предложение repeat не создает явной переменной цикла.

Организация циклов по коллекциям и пакетам

Предложения for для итерирования по спискам гораздо проще, чем арифметические предложения. Они поддерживают только два предложных оборота: in и on.

Оборот такой формы:

for var in list-form

итерирует переменную по всем элементам списка, являющегося результатом вычисления list-form.

(loop for i in (list 10 20 30 40) collect i) ==> (10 20 30 40)

Иногда это предложение дополняется оборотом by, который задает функцию для продвижения по списку. Значением по умолчанию является CDR, но можно использовать любую функцию, принимающую список и возвращающую подсписок. Например, вы можете накапливать каждый второй элемент списка с помощью loop следующим образом:

(loop for i in (list 10 20 30 40) by #'cddr collect i) ==> (10 30)

Предложный оборот on используется для итерирования по cons-ячейкам, составляющим список.

(loop for x on (list 10 20 30) collect x) ==> ((10 20 30) (20 30) (30))

Этот оборот также принимает предлог by:

(loop for x on (list 10 20 30 40) by #'cddr collect x) ==> ((10 20 30 40) (30 40))

Итерирование по элементам вектора (что включает строки и битовые векторы) подобно итерированию по элементам списка, за исключением использования предлога across вместо in3). Например:

(loop for x across "abcd" collect x) ==> (#\a #\b #\c #\d)

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

(loop for var being the things in hash-or-package ...)

Для хэш-таблиц возможными значениями для things являются hash-keys и hash-values, означающие, что var будет связываться с последовательными значениями ключей или самими значениями хэш-таблицы, соответственно. Форма hash-or-package вычисляется лишь один раз для получения значения, которое должно быть хэш-таблицей.

Для итерирования по пакету things может быть symbols, present-symbols и external-symbols, и var будет связываться с каждым символом, доступным в пакете, каждым символом, присутствующем в пакете (другими словами, интернированным или импортированным в этот пакет), или с каждым символом, экспортированным из пакета, соответственно. Форма hash-or-package вычисляется для предоставления имени пакета, который будет искаться как с помощью FIND-PACKAGE, или объекта пакета. Для частей предложения for также доступны синонимы. На месте the вы можете использовать each, вместо inof, а также things можно записывать в единственном числе (например, hash-key или symbol).

И наконец, так как часто при итерировании по хэш-таблицам нужны и ключи, и сами значения, предложения для хэш-таблиц поддерживают использование специального using-подпредложения.

(loop for k being the hash-keys in h using (hash-value v) ...)
(loop for v being the hash-values in h using (hash-key k) ...)

Оба этих цикла будут связывать k с каждым ключем в хэш-таблице, а v – с соответствующим значением. Обратите внимание, что первый элемент using-подпредложения должен быть записан в единственном числе4).

Equals-Then итерирование

Если ни одно из остальных предложений for не предоставляет именно ту форму итерирования переменной, которая вам нужна, вы можете получить полный контроль над итерированием, используя предложение equals-then. Это предложение подобно связывающим предложениям (binding clauses) в циклах DO, преведенных к более Algol-подобному синтаксису. Образец использования следующий:

(loop for var = initial-value-form [ then step-form ] ...)

Как обычно, var – имя итерируемой переменной. Ее начальное значение получается путем однократного вычисления initial-value-form перед первой итерацией. На каждой последующей итерации вычисляется step-form и ее значение становится новым значением var. В отсутствие then-части предложения initial-value-form перевычисляется на каждой итерации для предоставления нового значения. Заметьте, что это отличается от связывающего проедложения DO без step-формы.

step-form может ссылать на другие переменные loop, включая переменные, созданные другими предложениями for цикла loop. Например:

(loop repeat 5 
      for x = 0 then y
      for y = 1 then (+ x y)
      collect y
)
==> (1 2 4 8 16)

Заметьте, однако, что каждое предложение for вычисляется отдельно в порядке своего появления. Поэтому в предыдущем цикле на второй итерации x устанавливается в значение y до того, как y изменится (другими словами, в 1). Но y затем устанавливает в значение суммы своего старого значения (все еще 1) и нового значения x. Если порядок предложений for изменить, результат изменится.

(loop repeat 5
      for y = 1 then (+ x y)
      for x = 0 then y
      collect y
)
==> (1 1 2 4 8)

Часто, однако, вам нужно, чтобы step-формы для нескольких переменных были вычислены перед тем, как любая из этих переменных получит свое новое значение (подобно тому как это происходит в DO). В этом случае вы можете объединить несколько предложений for, заменив все кроме первого for на and. Вы уже видели такую запись в LOOP-версии вычисления чисел Фиббоначи в главе 7. Вот другой вариант, основанный на двух предыдущих примерах:

(loop repeat 5 
      for x = 0 then y
      and y = 1 then (+ x y)
      collect y
)
==> (1 1 2 3 5)

Локальные переменные

В то время как главные переменные, необходимые внутри цикла, обычно явно объявляются в предложениях for, иногда вам понадобятся вспомогательные переменные, которые вы можете объявить с помощью предложений with.

with var [ = value-form ]

Имя var станет именем локальной переменной, которая перестанет существовать после завершения цикла. Если предложение with содержит часть = value-form, то перед первой итерацией цикла переменная будет проинициализирована значением value-form.

В loop может быть несколько предложений with; каждое предложение вычисляется независимо в порядке их появления, и значение присваивается перед началом обработки следующего предложения, что позволяет последующим переменным зависеть от значения уже объявленных переменных. Взаимно независимые переменные могут быть объявлены в одном предложении with с использованием and между такими декларациями.

Деструктурирование переменных

Очень удобной возможностью LOOP, о которой я ранее не упоминал, является возможность деструктурирования списковых значений, присваемых переменным цикла. Это позволяет разбирать на части значение списков, которые иначе присваивались бы переменной цикла, подобно тому, как работает DESTRUCTURING-BIND, но немного более простым способом. В общем, вы можете заменить любую переменную цикла в предложениях for или with деревом символов, и списковое значение, которое было бы присвоено простой переменной, будет деструктурировано на переменные, именованные символами дерева. Простой пример выглядит следующим образом:

CL-USER> (loop for (a b) in '((1 2) (3 4) (5 6))
do (format t "a: ~a; b: ~a~%" a b))
a: 1; b: 2
a: 3; b: 4
a: 5; b: 6
NIL

Такое дерево также может включать в себя точечные пары. В этом случае имя после точки работает как &rest параметр: с ним будет связан список, содержащий все оставшиеся элементы списка. Это особенно полезно с for/on циклом, так как значением всегда является список. Например, этот LOOP (который я использовал в главе 18 для вывода элементов списка, разделенных запятыми):

(loop for cons on list
    do (format t "~a" (car cons))
    when (cdr cons) do (format t ", ")
)

может также быть записан следующим образом:

(loop for (item . rest) on list
    do (format t "~a" item)
    when rest do (format t ", ")
)

Если вы хотите игнорировать значение деструктурированного списка, вы можете использовать NIL на месте имени переменной.

(loop for (a nil) in '((1 2) (3 4) (5 6)) collect a) ==> (1 3 5)

Если список деструктурирования содержит больше переменных, чем значений в списке, лишние переменные получают значение NIL, что делает переменные по существу похожими на &optional параметры. Не существует, однако, эквивалента &key параметрам.

Накопление значения

Предложения накопления значения вероятно являются наиболее мощной частью LOOP. Хотя предложения управления итерированием предоставляют лаконичный синтаксис для выражения базовых механизмов итерирования, они не отличаются разительно от подобных механизмов, предоставляемых DO, DOLIST и DOTIMES.

С другой стороны, операторы накопления значения предоставляют возможность лаконичной записи общих идиом накопления во время итерирования. Каждое предожение накопления начинается с глагола и следует следующему образцу:

verb form [ into var ]

Каждый раз, при прохождении цикла, предложение накопления вычисляет form и сохраняет значение способом, определяемым глаголом verb. С подпредложением into значение сохраняется в переменную под именем var. Переменная является локальной в цикле, как если бы она была объявлена в предложении with. Без подпредложения into предложение накопления накапливает значения в переменную по умолчанию для всего выражения цикла.

Возможными глаголами являются collect, append, nconc, count, sum, maximize и minimize. Также доступны синонимы в форме причастий настоящего времени: collecting, appending, nconcing, counting, summing, maximizing и minimizing.

Предложение collect строит список, содержащий все значения form в порядке их просмотра. Эта конструкция особенно полезна, так как код, который вы бы написали для накопления списка, равный по эффективности сгенерированному LOOP коду, будет гораздо более сложным, чем вы обычно пишите вручную5). Родственными collect являются глаголы append и nconc. Эти глаголы также накапливают значения в список, но они объединяют значения, которые должны быть списками, в единый список как с помощью функций APPEND и NCONC6).

Остальные предложения накопления значения используются для накопления численных значений. Глагол count подсчитывает число раз, которое форма form была истинна, sum подсчитывает сумму значений, которые принимала форма form, maximize подсчитывает максимальное из этих значения, а minimize — минимальное. Представим, например, что вы определили переменную *random*, содержащую список случайных чисел.

(defparameter *random* (loop repeat 100 collect (random 10000)))

Следующий цикл вернет список, содержащий различную сводную информацию о числах из *random*:

(loop for i in *random*
   counting (evenp i) into evens
   counting (oddp i) into odds
   summing i into total
   maximizing i into max
   minimizing i into min
   finally (return (list min max total evens odds))
)

Безусловное выполнение

Хоть и удобная для конструкций накопления значения, LOOP не была бы очень хорошим средством итерации общего назначения, если бы не предоставляла способа выполнения произвольного кода в теле цикла.

Самым простым способом выполнения произвольного кода внутри тела цикла является использование предложения do. По сравнению с вышеописанными предложениями со всеми их предлогами и подвыражениями, do следует модели простоты по Йоде7). Предложение do состоит из слова do (или doing), за которым следует одна или более форм Lisp, которые вычисляются при вычислении предложения do. Предложение do заканчивается закрывающей скобкой цикла loop или следующим ключевым словом loop.

Например, для печати чисел от одного до десяти, вы можете записать следующее:

(loop for i from 1 to 10 do (print i))

Еще одной формой непосредственного выполнения является предложение return. Это предложение состоит из слова return, за которым следует одна форма Lisp, которая вычисляется, а результат немедленно возвращается как значение цикла loop.

Вы также можете прервать цикл из предложения do путем использования любого обычного оператора управления потоком вычислений Lisp, таких как RETURN и RETURN-FROM. Обратите внимание, что предложение return всегда возвращает управление из непосредственно охватывающего выражения LOOP, в то время как с помощью RETURN и RETURN-FROM в предложении do можно вернуть управление из любого охватывающего выражения. Например, сравните следующее:

(block outer
  (loop for i from 0 return 100) ; 100 возвращается из LOOP
 (print "This will print")
  200
)
==> 200

с этим:

(block outer
  (loop for i from 0 do (return-from outer 100)) ; 100 возвращается из BLOCK
 (print "This won't print")
  200
)
==> 100

Предложения do и return вместе называются предложениями безусловного выполнения.

Условное выполнение

Так как предложение do может содержать произвольные формы Lisp, вы можете использовать любые выражения Lisp, включая конструкции управления, такие как IF и WHEN. Таким образом, следующее является одним из способов написания цикла, печатающего только четные числа от одного до десяти:

(loop for i from 1 to 10 do (when (evenp i) (print i)))

Однако иногда вам понадобится условное управление на уровне предложений цикла loop. Например, представим, что вам нужно просуммировать только четные числа от одного до десяти путем использования предложения summing. Вы не сможете написать такой цикл с помощью предложения do, так как не существует способа "вызвать" sum i в середине обычной формы Lisp. В случаях, подобных этому, вам нужно использовать одно из собственных условных выражений LOOP:

(loop for i from 1 to 10 when (evenp i) sum i) ==> 30

LOOP предоставляет три условных конструкции, и все они следуют этому базовому образцу:

  conditional test-form loop-clause

Условный оператор conditional может быть if, when или unless. test-form — это любая обычная форма Lisp, а предложение loop-clause может быть предложением накопления значения (count, collect и так далее), предложением безусловного выполнения или другим предложением условного выполнения. Несколько предложений цикла могут быть объединены в одну условную конструкцию путем соединения их с помощью and.

Несколько условных предложений могут быть объединены в одно условное, путем соединения их с помощью and.

Дополнительным синтаксическим сахаром является возможность использования в первом предложении loop после формы условия переменной it для ссылки на значение, возвращенное этой формой условия. Например, следующий цикл накапливает не равные NIL значения, найденные в some-hash по ключам из some-list:

(loop for key in some-list when (gethash key some-hash) collect it)

Условное выражение выполняется при каждой итерации цикла. Предложения if и when выполняют свои предложения loop если форма test-form вычисляется в истину. unless же выполняет предложения только если test-form вычисляется в NIL. В отличие от так же названных операторов Common Lisp, if и when в LOOP являются синонимами — в их поведении нет никакой разницы.

Все три условных предложения могут также принимать ветвь else, в которой за else следует другое предложение loop либо несколько предложений, объединенных and. Если условные предложения являются вложенными, множество предложений, связанных с внутренним условным предложением, может быть завершено с помощью слова end. end является необязательным, если оно не нужно для разрешения неоднозначности с вложенными условными предложениями: конец условного предложения будет определен по концу цикла либо по началу другого предложения, не присоединенного с помощью and

Следующий довольно глупый цикл демонстрирует различные формы условных предложений LOOP. Функция update-analysis будет вызываться на каждой итерации цикла с последними значениями различных переменных, накапливаемых предложениями внутри условных предложений.

(loop for i from 1 to 100
      if (evenp i)
        minimize i into min-even and
        maximize i into max-even and
        unless (zerop (mod i 4))
          sum i into even-not-fours-total
        end
        and sum i into even-total
      else
        minimize i into min-odd and
        maximize i into max-odd and
        when (zerop (mod i 5))
          sum i into fives-total
        end
        and sum i into odd-total
      do (update-analysis min-even
                          max-even
                          min-odd
                          max-odd
                          even-total
                          odd-total
                          fives-total
                          even-not-fours-total
)
)

Начальные установки и подытоживание

Одним из ключевых озарений проектировщиков языка LOOP было осознание того, что циклы часто предваряются некоторым кодом, занимающимся начальной установкой каких-то вещей, и завершаются кодом, осуществляющим что-то со значениями, вычисленными в цикле. Простой пример на Perl8) мог бы выглядеть так:

my $evens_sum = 0;
my $odds_sum  = 0;
foreach my $i (@list_of_numbers) {
 if ($i % 2) {
   $odds_sum += $i;
 } else {
   $evens_sum += $i;
 }
}
if ($evens_sum > $odds_sum) {
 print "Sum of evens greater\n";
} else {
 print "Sum of odds greater\n";
}

Циклической сущностью в этом коде является инструкция foreach. Но сам цикл foreach не является независимым: код в теле цикла ссылается на переменные, объявленные в двух строках перед циклом9). А работа, осуществляемая циклом является абсолютно бесполезной без инструкции if после цикла, которая фактически сообщает о результате. В Common Lisp, к тому же, конструкция LOOP является выражением, возвращающим значение, и поэтому потребность в осуществлении чего-либо, а именно генерации возвращаемого значения, даже больше.

Поэтому проектировщики LOOP предоставили возможность включения такого, на самом деле являющегося частью цикла, кода в сам цикл. Для этого LOOP предоставляет два ключевых слова, initially и finally, которые вводят код для запуска снаружи главного тела цикла.

После слов initially или finally эти предложения включают все формы Lisp до начала следующего предложения цикла либо до его конца. Все формы initially комбинируются в единую вводную часть (prologue), которая запускается однократно непосредственно после инициализации всех локальных переменных цикла и перед его телом. Формы finally схожим образом комбинируются в заключительную часть (epilogue) и выполняются после последней итерации цикла. И вводная, и заключительная части могут ссылаться на локальные пемеренные цикла.

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

  • Выполнение предложения return.
  • RETURN, RETURN-FROM или другая конструкция передачи управления была вызвана из формы Lisp, находящейся в теле цикла10).
  • Цикл завершается по предложению always, nerver или thereis, которые я обсужу в следующей секции.

Внутри кода заключительной части для явного предоставления возвращаемого циклом значения могут использоваться RETURN или RETURN-FROM. Это явно возвращаемое значение имеет приоритет над любым значением, которое может иначе предоставляться предложениями накопления или критерия остановки.

Для возможности использования RETURN-FROM для возврата из указываемого цикла (полезно при вложенных выражениях LOOP) вы можете дать LOOP имя с помощью ключевого слова loop named. Если предложение named используется в цикле, оно должно идти первым. В качестве простого примера предположим что у вас есть список списков и вы хотите найти в одном из вложенных списков элемент, который удовлетворяет некоторому критерию. Вы можете найти его с помощью пары вложенных циклов подобным образом:

(loop named outer for list in lists do
     (loop for item in list do
          (if (what-i-am-looking-for-p item)
            (return-from outer item)
)
)
)

Критерии завершения

Хотя предложения for и repeat предоставляют базовую инфраструктуру для управления числом итераций, иногда вам понадобится прервать цикл до его завершения. Вы уже видели, как с помощью предложения return или операторов RETURN и RETURN-FROM внутри предложения do можно немедленно прервать цикл; но как есть общие образцы для накопления значений, так существуют и общие образцы для принятия решений, когда останавливать цикл. Такие образцы поддерживаются в LOOP с помощью предложений завершения while, until, always, never и thereis. Все они следуют одинаковому образцу:

  loop-keyword test-form

Все эти предложения вычисляют форму test-form на каждой итерации и на основе возвращаемого ей значения принимают решение, завершить ли выполнение цикла. Они отличаются в том, что происходит при завершении ими цикла (если он завершается), и как они определяют необходимость такого завершения.

Ключевые слова loop while и until предоставляют "мягкие" предложения завершения. Если они решают завершить цикл, управление передается в заключительную часть, пропуская оставшуюся часть тела цикла. Затем заключительная часть может вернуть значение или сделать еще что-либо для завершения цикла. Предложение while останавливает цикл как только контрольная форма test-form вычисляется в ложное значение, а until, наоборот, - как только в истинное.

Другая форма мягкго завершения предоставляется макросом LOOP-FINISH. Это обычная форма Lisp, не предложение loop, поэтому она может использоваться в любом месте внутри форм Lisp предложения do. LOOP-FINISH также приводит к немедленному переходу к заключительной части, и может быть полезен, когда решение о прерывании цикла не может быть легко умещено в единственную форму, могущую использоваться в предложениях while или until.

Остальные три предложения, always, never и thereis, останавливают цикл гораздо более жестко: они приводят к немедленному возврату из цикла, пропуская не только все последующие предложения loop, но и заключительную часть. Они также предоставляют значение по умолчанию даже если не приводят к завершению цикла. Однако, если цикл не завершается ни по одному из этих критериев, заключительная часть запускается и может вернуть значение, отличное от значения по умолчанию, предоставляемого предложениями завершения.

Так как эти предложения предоставляют свои собственные возвращаемые значения, они не могут комбинироваться с предложениями накопления за исключением содержащих подвыражение into. Иначе компилятор (или интерпретатор) должен просигнализировать ошибку во время выполнения. Предложения always и never возвращают только булевы значения, поэтому они наиболее полезны в случае, если вам нужно использовать выражение цикла как часть предиката. Вы можете использовать always для проверки того, что контрольная форма вычисляется в истинное значение на каждой итерации цикла. И наоборот, never проверяет, что контрольная форма на каждой итерации вычисляется в NIL. Если контрольная форма "не срабатывает" (возвращает NIL в предложении always или не NIL в предложении never), цикл немедленно прерывается, возвращая NIL. Если же цикл выполняется до конца, предоставляется значение по умолчанию: T.

Например, если вы хотите проверить, что все числа в списке numbers являются четными, вы можете написать следующее:

(if (loop for n in numbers always (evenp n))
    (print "All numbers even.")
)

Также вы можете записать следующее:

(if (loop for n in numbers never (oddp n))
    (print "All numbers even.")
)

Предложение thereis используется для проверки, вычисляется ли контрольная форма в истинное значение хотя бы раз. Как только контрольная форма возвращает значение не равное NULL, цикл останавливается, возвращая это значение. Если же цикл доходит до конца, предложение thereis предоставляет возвращаемое значение по умолчанию: NIL.

(loop for char across "abc123" thereis (digit-char-p char)) ==> 1
(loop for char across "abcdef" thereis (digit-char-p char)) ==> NIL

Сложим все вместе

Вы увидели все основные возможности LOOP. Вы можете комбинировать все выше обсужденные предложения следуя следующим правилам:

  • Предложение named, если указывается, должно быть первым предложением.
  • После предложения named идут все остальные предложения initially, with, for и repeat.
  • Затем идут предложения тела: условного и безусловного выполнения, накопления, критериев завершения11).
  • Завершается цикл предложениями finally.

Макрос LOOP раскрывается в код, который осуществляет следующие действия:

  • Инициализирует все локальные переменные цикла, которые объявлены в предложениях with или for, а также неявно созданы предложениями накопления. Начальные значения форм вычисляются в порядке появления соответствующих предложений в цикле.
  • Выполняет формы, предоставляемые предложениями initially (вводная часть), в порядке их появления в цикле.
  • Итерирует, выполняя тело цикла как описано в следующем абзаце.
  • Выполняет формы, предоставляемые предложениями finally (заключительная часть), в порядке их появления в цикле.

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

И это описывает почти все, связанное с LOOP12). Вы будете использовать LOOP далее в этой книге довольно часто, поэтому стоило получить некоторое представление о нем. Ну а после вам самим решать, насколько интенсивно использовать LOOP.

И теперь вы готовы к погружению в практические главы, составляющие оставшуюся часть этой книги. Для начала мы напишем антиспамовый фильтр.

1)Термин ключевое слово loop является несколько неудачным, так как ключевые слова loop не являются ключевыми словами в обычном смысле, то есть символами пакета KEYWORD. На самом деле ими могут быть любые символы с подходящими именами из любых пакетов: макрос LOOP заботится только об их именах. Обычно же они записываются без спецификатора пакета и поэтому считываются (и интернируются при необходимости) в текущий пакет
2)Так как одной из целей LOOP является возможность записи выражений итерирования в синтаксисе, близком к английскому, многие ключевые слова имеют синонимы, которые трактуются LOOP как одинаковые, но дают при этом некоторую свободу в выражении вещей на более естественном английском языке учитывая различные контексты.
3)Вас может удивить, почему LOOP не может определить, итерирует ли он по списку или по вектору, без указания различных предлогов. Это еще одно следствие того, что LOOP является макросом: то, является значение списком или вектором, не может быть известно до времени выполнения, а LOOP, как макрос, должен сгенерировать код во время компиляции. Также создатели LOOP ставили целью генерацию максимально эффективного кода. Для генерации эффективного кода для итерирования, например, по вектору необходимо знать во время компиляции, что значением во время выполнения будет вектор, поэтому и нужны различные предлоги.
4)Даже не спрашивайте меня, почему авторы LOOP отступили от стиля без скобок для using-подпредложения.
5)Трюк заключается в удержании хвоста списка и добавления новых cons-ячеек путем SETF CDR'а хвоста. Написанный вручную эквивалент кода, генерируемого (loop for i upto 10 collect i) будет выглядеть подобным образом:
(do ((list nil) (tail nil) (i 0 (1+ i)))
    ((> i 10) list)
  (let ((new (cons i nil)))
    (if (null list)
        (setf list new)
        (setf (cdr tail) new)
)

    (setf tail new)
)
)

Конечно вы редко, если вообще, будете писать подобный код. Вы будете использовать либо LOOP, либо, если по каким-то причинам вы не захотите использовать LOOP, стандартную идиому PUSH/NREVERSE накопления значений.
6)Напомним, что NCONC является деструктивной версией APPEND — использование предложения nconc безопасно только в том случае, если накапливаемые вами значения являются новыми списками, которые не разделяют какую либо свою структуру с другими списками. Например, это безопасно:
(loop for i upto 3 nconc (list i i)) ==> (0 0 1 1 2 2 3 3)
Но это доставит вам хлопот:
(loop for i on (list 1 2 3) nconc i) ==> неопределено
Последнее наиболее вероятно зациклится навечно, так как различные части списка, созданного с помощью (list 1 2 3), будут деструктивно модифицированы, указывая друг на друга. Но даже это не гарантируется — поведение просто неопределено.
7)"Нет! Не пытайся. Делай... или не делай. Но не пытайся." – Йода, Империя наносит ответный удар.
8)Я не придираюсь к Perl здесь — этот пример выглядел бы примерно так же на любом языке с основанным на C синтаксисом.
9)Perl позволяет вам не объявлять переменные, если вы не используете режим strict. Но вам следует всегда использовать его в Perl. Эквивалентный код на Python, Java или C потребовал бы обязательного объявления переменных.
10)Вы можете нормально, с запуском заключительной части, завершить цикл из кода Lisp, выполняемого как часть тела цикла, с помощью локального макроса LOOP-FINISH.
11)Некоторые реализации Common Lisp позволяют вам смешивать предложения тела и предложения for, но это неспецифицировано, поэтому другие реализации отвергнут такие циклы.
12)Одним из аспектов LOOP, которого я даже не касался, является синтаксис объявления типов переменных цикла. Конечно, я также не обсуждал и объявление типов вне LOOP. Последнее я вкратце обсужу в главе 32. Для информации же о том, как декларации типов работают с LOOP, обратитесь к вашему любимому справочнику по Common Lisp.
Предыдущая Оглавление Следующая
@2009-2013 lisper.ru