Первые шаги в Gnu Common Lisp - Программные блоки и выход из них
Конструкции block и return-from предназначены для реализации механизма выхода за пределы текущего лексического контекста. Попросту говоря, использование return-from внутри конструкции block с таким же именем обеспечивает немедленную передачу управления за пределы данного блока. В большинстве случаев этот механизм оказывается более эффективным чем передача управления на базе catch и throw, о чем мы поговорим отдельно.Специальная форма block
block name ,form*
Конструкция block организует последовательное выполнение
всех форм form слева направо, возвращая результат
вычисления последней формы. Если, однако, во время исполнения
блока выполняется форма return или return-from
, имеющая
в качестве аргумента то же имя блока, что и задано в параметре
name, то в качестве результата всей конструкции block
возвращается значение, указываемое с помощью return
или
return-from
. Именно этим block
отличается от
progn
; конструкция progn
на форму
return
никак не реагирует.
Параметр name не оценивается и должен представлять собой
символ. Область видимости символа name лексическая ---
только return
или return-from
, которые текстуально
содержатся внутри одной из форм блока, могут обеспечить выход из
него. Это приводит к тому, что выйти из любого блока (или его
экземпляра) можно только один раз --- либо по завершению его
работы, либо в результате выполнения явной команды возврата.
Форма defun
практически неявно помещает тело определяемой
функции внутрь конструкции block
; этот блок имеет имя,
совпадающее с именем функции. Поэтому допускается использовать
выход из функции с помощью вызова return-from
.
Зона видимости имени блока подчиняется общим правилам лексической
видимости и иногда может приводить к последствиям, которые, по
меньшей мере, удивляют пользователей и разработчиков других
диалектов Лиспа. Например, вызов return-from
в приведенном
ниже примере ведет себя вполне предсказуемо:
(block loser (catch 'stuff (mapcar #'(lambda (x) (if (numberp x) (hairyfun x) (return-from loser {nil}))) items)))
Однако в ряде ситуаций return
в Common Lisp может оказаться
заметно сложнее. Так, return
может приводить к разрушению
механизма ловушек прерываний, а также существует вполне реальная
возможность ссылки на несуществующее имя блока, если блок был
``закрыт'' в результате лексического замыкания.
Специальная форма return-from
return-from name [result]
Форма return-from
используется для возврата из блока
block
, или из подобных ей конструкций do
и prog
,
которые неявно используют механизм block
.
Имя name не оценивается и должно представлять собой символ.
При этом данная форма должна находиться внутри конструкции block
с таким же именем; если в форме присутствует необязательный
параметр result, то его значение передается как результат
всего блока при выходе. Если же форма result отсутствует,
то возвращаемый результат будет равен nil. Как правило, такой
вариант используется в тех случаях, когда возвращаемое значение
не представляет никакого интереса.
Сама по себе форма return-from
никогда не возвращает, да и
не имеет собственного значения --- она лишь передает результат
вычислений во внешний мир за пределы конструкции block
.
Если результат вычисления result приводит к генерации
нескольких значений, то при выходе возвращаются все.
Форма return
return [result]
Форма (return form) по смыслу идентична
конструкции (return-from nil form); она
обеспечивает возврат из безымянного блока (именем
которого является nil). Такие блоки неявно
создаются в итерационных конструкциях,
таких как do
, что позволяет
использовать в них return
для
корректного выхода.
Циклы и итерации
В составе Common Lisp реализовано несколько конструкций, ориентированных на итерационное исполнение участков кода. Прежде всего, это формаloop
, которая реализует тривиальную
поддержку итерационных вычислений, и по сути представляет собой
незначительное расширение progn
механизмом, который
заставляет, при выполнении некоторого условия повторять
вычисление всего фрагмента кода. Конструкции
do
и do*
обеспечивают контроль за изменением сразу
нескольких переменных на каждой итерации. Для реализации
специализированных итерационных процедур над элементами списка или
выполнения их ровно n-раз, предназначены функции
dolist и dotimes. Конструкция tagbody представляет
собой наиболее общий вариант, допускающий использование внутри ее
произвольных передач управления с помощью
go.
Традиционная конструкцияprog
представляет собой результат объединения tagbody,block
, и let.
Бесконечный цикл
Конструкцияloop
представляет собой простейший механизм организации
циклических вычислений. Эта конструкция не предполагает использования
управляющих переменных и просто выполняет свое тело раз за разом.
Форма loop
loop ,form*
Каждый из входящих в состав функций параметров-форм form
оценивается последовательно: слева направо. И если оценивается
последняя форма form, то после нее возобновляется
вычисление первой формы, и так далее. В результате образуется
бесконечный цикл вычислений. Конструкция loop
, по вполне
понятным причинам, никогда не возвращает значений. Выполнение ее
можно прервать только с помощью явной команды, такой как,
например, return
или throw.
Так же, как и большинство других итерационных механизмов,
loop
создает неявный блок с именем {\nil.
Поэтому для выхода из loop
может использоваться вызов
return
, которому можно передать необходимые результаты.
Конечно, конструкция loop
имеет смысл только в том
случае, если каждая форма form отлична от атома (то есть
является списком). Вариант, при котором форма form
представляет собой атомарное значение, используется в некоторых
современных версиях системы.
Общие средства для организации итерационных вычислений
В отличие отloop
, do
и 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
будет прекращено,
а в качестве значения будет возвращено значение, указанное в
параметре result.
В общем случае, цикл do
выглядит следующим образом :
(do ((var1 init1 step1) (var2 init2 step2) ... (varn initn stepn)) (end-test . result) *declaration . tagbody)
Цикл do*
выглядит точно так же, отличаясь только ключевым
словом do*
вначале.
Первый объект формы представляет собой список из нуля и более
спецификаторов индексных переменных. Каждый из
спецификатора представляет собой список имен переменных
var,
начальное значение init, и форма вычисления очередного
значения step. Если параметр
init пропущен, то по умолчанию он получает значение
NIL.
Если опущен параметр step, то значение переменной var
не изменяется между итерациями do
. Впрочем, это не запрещает
внутри цикла do
изменять значения этих переменных с помощью
функции setq.
Спецификатор индексной переменной может также представлять собой просто имя переменной. В этом случае переменная имеет начальное значение, равное NIL и не изменяется от итерации к итерации. Рекомендуется использовать такую переменную только в тех случаях, когда эта переменная бкдет получать некоторое значение (например, с помощью setq) до того, как она впервые будет использована. Необходимо отметить, что до инициализации начальное значение равно NIL, и не является неопределенным. И все-таки рекомендуется не отдавать интерпретацию на волю системы, а явно указывать (varj NIL), если начальное значение должно означать ``false,'' или (varj '()) если начальное значение должно представлять собой пустой список.
Перед тем, как начнется первая итерация, осуществляется оценка значений всех форм init, а затем каждая переменная var связывается со значением соответствующей формы init. Имейте в виду, что это не присвоение, а связывание; после окончания цикла будут восстановлены прежние значения этих переменных.
В случае do
, все формы init
оцениваются до того как будет проведено связывание
какой-либо переменной var; поэтому все формы
init при обращении к переменным с такими именами ссылаются
на их старые связи, то есть на значения, видимые до начала
выполнения конструкции do
.
В случае
Второй параметр цикла представляет собой список, состоящий из
предиката, описывающего условие завершение цикла
end-test и произвольное количество (включая нуль) форм
result.
Фактически, этот фрагмент можно рассматривать как один из
вариантов в формате функции cond. Вначале каждой итерации,
после завершения обработки индексных переменных оценивается
значение предиката end-test. Если результат равен
NIL, то выполнение продолжается, и повторяется
вычисление
фрагмента в форме
Вначале каждой итерации, начиная со второй, индексные переменные
обновляются следующим образом. Оцениваются все формы step
слева направо, и все результирующие значения назначаются
соответствующим индексным переменным.
Любая переменная, для которой отсутствует форма
step никакого нового значения не получает.
В случае формы
Поскольку все формы step оцениваются до того, как
изменяется какая-либо из переменных, форма
step всегда использует старые значения всех
индексных переменных, даже если до этого уже были проведены
оценки каких-либо форм step.
А вот в
Если end-test формы
Например:
Оставшаяся часть формы
Вся форма
В начале тела цикла допускается использование форм
declare.
Эти формы применяются только к коду, содержащемуся в теле
Вот несколько примеров использования цикла
Конструкция
использует механизм параллельного назначения индексных
переменных. При первой итерации значением oldx является
предыдущее значение x, которое имела эта переменная до
начала выполнения цикла
Довольно часто итерационный алгоритм может быть наиболее просто и
ясно реализован непосредственно внутри форм
step функции
Эта конструкция работает точно так же, как и
В этом фрагменте интересный момент
заключается в использовании
предиката endp вместо null или atom. Такая
реализация проверки конца списка позволяет реализовать более
устойчивый код.
Теперь, для рассмотрения организации вложенных циклов,
предположим, что в env содержится список cons-ячеек.
Car-часть каждой из ячеек представляет собой список
символов, а cdr-часть --- список такой же длины, содержащий
соответствующие символам значения. В принципе такая структура
данных аналогична ассоциативному списку, но разделена на
``кадры''. Назовем эту структуру ``картотекой''. Теперь, для
того, чтобы реализовать функцию извлечения отдельных элементов из
такой структуры данных можно использовать функцию:
Цикл
Цикл
Как dolist, так и dotimes осуществляют многократное
вычисление набора выражений. При этом на каждой итерации
осуществляется изменение некоторой индексной переменной, значение
которой доступно внутри тела цикла. Функция dolist
осуществляет последовательный просмотр элементов списка, а
dotimes осуществляет подстановку целых чисел в диапазоне от
0 до n-1, для произвольного целого числа n,
переданного как параметр при вызове функции.
Значение любой из этих конструкций может быть сформировано с
помощью необязательной формы result. По умолчанию обе
функции возвращают NIL.
Для немедленного возврата из форм dolist и dotimes,
может использоваться оператор
Само тело цикла представляет собой неявную конструкцию
tagbody; оно может содержать теги, которые представляют
собой параметры (цели) операторов go. Кроме того,
допускается использовать в начале тела цикла объявлений
declare.
Функция dolist выполняет достаточно простую и прямолинейную
итерацию по каждому из элементов списка.
Вначале dolist оценивает значнеие формы listform,
которое должно привести к формированию списка. Затем
осуществляется оценка тела цикла для каждого его элемента, при
этом переменная var связывается на каждой итерации с
очередным элементом.
После того, как будет просмотрен весь список, оценивается
значение формы resultform (должна представлять собой
простою форму, и не может быть неявной
Вот пример использования dolist:
Для немедленного прекращения выполнения цикла и возвращения
заданного значения может быть использован оператор
Функция dotimes реализует механизм для ``прямолинейного''
цикла, переменная цикла в котором принимает несколько
последовательных целочисленных значений. Выражение
(dotimes (var countform resultform) . progbody)
осуществляет оценку формы countform, значение которой
должно представлять собой целое число. Затем
производится вычисление тела progbody, которое повторяется
для переменной var, принимающей последовательно значения от
0 по count (не включая последнее значение). Если значение
countform равно нулю или меньше, то форма
progbody выполняется 0 раз, то есть не выполняется вообще.
И, в заключение, оценивается форма resultform, которая представляет собой
одну форму, и не интерпретируется автоматически как неявный
Допускается в любой момент прекратить вычисление цикла с помощью
оператора
Вот пример использования dotimes для обработки строк:
Изменение значения var в теле цикла (например, за счет
использования setq) может привести к неопределенным
результатам (Конкретный результат определяется
реализацией Лиспа. Что касается компилятора Common Lisp, то в
таких случаях обычно выдается предупреждение).
Для осуществления подобных итераций над произвольными структурами
данных предназначена общая версия функции map. Но более
часто используются описываемые ниже функции, которые работают
только над списками.
Для каждой из этих функций отображения первый аргумент
представляет собой функцию, а оставшиеся должны быть списочного
типа. Функция должна принимать столько аргументов, сколько
списков представлено среди параметров map*.
Функция mapcar работает над последовательными элементами
списков. Вначале, функция применяется к car-части каждого
списка, затем, к cadr-части каждого списка, и так далее.
Значение, возвращаемое mapcar представляет собой список
результатов успешных вызовов функции function.
Например:
Функция maplist работает так же, как и mapcar с тем
отличием, что функция применяется к спискам целиком и
последовательно вычисляемым их cdr-частям, а не к их
отдельным элементам. Например:
Точно так же функции mapl и mapc работают аналогично
maplist и mapcar, однако они не накапливают результаты
вызовов функции function.
Все эти функции используются в тех случаях, когда функция
вызывается ради вносимых ею побочных эффектов, а не ради
возвращаемых значений. Значение, возвращаемое
mapl или mapc это ее второй аргумент, то есть первый
аргумент последовательности.
Функции mapcan и mapcon работают аналогично mapcar
и maplist, соответственно, за исключением того, что они
комбинируют результаты вызовов функции с помощью
структуроразрушающей nconc вместо list. Поэтому,
То же самое справедливо для mapcan и mapcar.
Образно говоря, эти функции позволяют отображаемой функции
возвращать переменное количество объектов, которые должны
помещаться в выходной список. Это оказывается особенно полезно
если должен быть возвращен всего один объект или даже ни одного:
В этом случае функция работает как своеобразный фильтр, собственно
говоря, это стандартный способ использования mapcan в Лиспе.
Не забывайте, что nconc представляет собой
структуроразрушающую операцию, а следовательно такими же являются
и использующие ее mapcan и mapcon;
списки, возвращаемые function разрушаются при формировании
итогового.
Иногда вместо использования операции отображения может оказаться
более удобным использовать
Функциональный аргумент, используемый в функции отображения
должен представлять собой функцию, которая допустима в качестве
аргумента для apply; поэтому он может представлять собой
макрокоманду или имя специальной формы. Естественно, не
существует никаких противопоказаний в использовании ключевых
параметров &optional и &rest.
В некоторых версиях CommonLisp function может иметь только тип
symbol или function; использование lambda-выражений
не допускается. Однако, для совместимости со старым программным
обеспечением, да и для удобства работы, можно использовать
специальную форму --- сокращение
Так, конструкция
Конструкция
Часть формы tagbody после списка переменных представляет
собой ее тело. Объект в теле формы может представлять собой
символ, число (в этом случае он называется тегом),
либо список (а в этом случае он называется
оператором).
Каждый элемент тела формы обрабатывается последовательно, слева
направо. При этом теги игнорируются; операторы
оцениваются, а результаты оценки отбрасываются. Если достигнут
конец тела, то tagbody возвращает NIL.
Если оценивается конструкция (go тег), управление
передается на ту часть тела формы, которая имеет метку
тег.
Зона видимости тегов, заданных с помощью tagbody является
лексической и после выхода из конструкции tagbody
использование операторов go для переходов к
тегам внутри ее тела недопустимо. Однако оператор
go может выполнить передачу управления в tagbody, котор
ая не является самой вложенной конструкцией, содержащей этот
go; теги, задаваемые в tagbody толькор затеняют другие
теги с таким же именем.
Конечно, лексическая область видимости точек передачи управления
с помомщью go может рассматриваться как глобальная,
поскольку ее общий характер приводит нередко к неприятным
сюрпризам, которые удивляют как пользователей так и разработчиков
других версий Лиспа. Например, в приведенном ниже фрагменте
оператор go работает вполне предсказуемым образом:
Но в некоторых ситуациях, go в Common Lisp ведет себя совсем
не так, как ожидается, и непохож на классический goto.
Так, go может ``разломать'' ловушки catch, если необходимо
получить доступ к точке передачи управления. Кроме того, в случае
лексического замыкания, созданного с помощью function,
становится возможным ссылаться на цель оператора go в
пределах функции, хотя формально этот тег не должен быть иметь
лексической видимости.
В спецификации формы имеется ряд, пробелов, которые иногда
используются дляполучения неожиданных результатов. Например,
отсутствует явное запрещение многократного использования одного и
того же тега внутри тела tagbody. И хотя компилятор и
интерпретатор, скорее всего, будут жаловаться на такое поведение,
но никаких критических для приложения действий предпринято,
скорее всего, не будет. Поэтому программисты нередко используют
избыточные теги, такие как --- для форматирования и внесения
комментариев. Например, вот как мог бы функционировать китайский
философ-созерцатель:
Конструкция
Список после ключевого слова
Тело
Конструкция
Вот пример использования
которое может быть переписано в более ясной форме:
Конструкция
Существует также специальная форма prog*, которя почти
эквивалентна
возвращает car-часть значения z.
Конечно, в последние годы роль
Специальная форма (go tag) используется для передачи
управления внутри конструкции tagbody. При этом тег tag
должен представлять собой символ или целое число --- его значение
не оценивается. Оператор go передает управление на точку,
которая помечена тегом, равным в смысле eql аргументу
tag. Если тег с таким именем отсутствует, то проверяются
тела всех конструкций которые лексически содержат tagbody,
если таковые, конечно, существуют. Если все же подходящего тега
отыскать не удастся, фиксируется ошибка.
Форма go никогда не возвращает никакого значения.
С точки зрения хорошего стиля программирования
рекомендуется
дважды подумать, прежде чем использовать в программе оператор
go. В большинстве случаев вместо go можно использовать
итерационные примитивы, вложенные условные формы или даже
do*
, вначале оценивается первая форма
init,
затем с этим значением связывается первая переменная
var, после этого оценивается вторая форма
init и с ее результатом связывается вторая
переменная var, и так далее;
в общем случае форма initj может ссылаться на новое
связывание vark, если kdo
(или do*
). Если же результат не
равен NIL, оцениваются формы result, в соответствии с
неявным progn
, и после этого
do
завершает работу. Форма do
возвращает результаты
оценки последней формы result.
В случае, если формы result отсутствуют, значением do
становится NIL. Конечно, такое поведение не полностью
аналогично работе оператора cond form, поскольку вариант cond
ьез форм result возвращает результат теста, который, как ис
ледует ожидать, отличен от NIL.
do
, все формы step оцениваются до того,
как будет обновлено значение какой-либо переменной; назначение
значений переменным выполняется параллельно, как при
использовании вызова psetq.
do*
, оценивается первая форма
step,
затем это значение связывается с первой переменной
var,
после чего оуенивается вторая форма step и ее значение
присваивается второй переменной var, и так далее;
присвоение значений переменным осуществляется последовательно,
как при использовании setq.
В обоих версиях --- do
и do*
, после обновления значений
индексных переменных производится оценка
end-test, после чего начинается собственно выполнение
очередной итерации.
do
равен NIL, то условие
теста никогда не будет выполнено. В этом случае тело цикла do
будет выполняться вечно. Выполнение бесконечного цикла может быть
прервано с помощью 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 внутри тела
цикла. После того, как будет достигнут конец тела
do
бужет инициировано выполнение следующей итерации,
начинающееся с оценки значений форм step.
do
заключается в неявную конструкцию block
с
именем nil. Поэтому в любой момент для выхода из цикла можно
использовать оператор return
.
do
, а именно, к связям переменных do
, формам
init и step, а также к
формам end-test и result.
do
:
(do ((i 0 (+ i 1)) ; Устанавливает все NULL-элементы a-vector в нуль
(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)
do
. По мере выполнения
очередных итераций это значение теряется и oldx содержит то значение,
которое имела переменная x на предыдущей итерации.
do
, и само тело цикла body просто
остается пустым. Например:
(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 для преобразования
накапливаемого результата работы цикла do
в нужное
представление.
(defun list-reverse (list)
(do ((x list (cdr x))
(y '{() (cons (car x) y)))
((endp x) y)))
(defun картотека-поиск (sym картотека)
(do ((r картотека (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 картотека-поиск (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.
Простые итерационные конструкции
Конструкции dolist и dotimes выполняют тело цикла один
раз для каждого из значений, которые последовательно принимаются
одной и той же индексной переменной. Конечно, все это может быть
реализовано с помощью цикла do
, но перечисленные выше
механизмы, охватывают довольно большую часть практических случаев.
return
, который отменяет
выполнение всех следующих за ним функций; фактически, это
означает, что все тело цикло окружается неявным блоком block
с именем nil.
Форма dolist
dolist (var listform [resultform])
declaration* {tag | statement*}
progn
),
которая возвращается как результат всего вызова dolist.
Во время оценки значения resultform управляющая переменная
цикла var по-прежнему является связанной и имеет значение
nil. Если resultform опущена, функция возвращает NIL.
(dolist (x '(a b c d)) (prin1 x) (princ " ")) ; NIL
; после печати ``a b c d '' (не забудьте о пробеле)
return
.
Форма dotimes
dotimes (var countform [resultform])
{declaration* {tag | statement*}}
progn
-блок, и ее результат становится результатом всей формы
dotimes. Во время оценки resultform управляющая
переменная var по-прежнему считается связанной, и ее
значение равно количеству выполненных циклов.
Если resultform опущена, результат вычисления всей формы
равен NIL.
return
, который позволяет вернуть произвольное
значение.
;;; Предикат palindromep принимает значение
;;; True если его аргумент представляет собой палиндром
;;; (строку, одинаково читающуюся в оба направления).
(defun palindromep (string &optional
(start 0)
(end (length string)))
(dotimes (k (floor (- end start) 2) {\true)
(unless (char-equal (char string (+ start k))
(char string (- end k 1)))
(return NIL))))
(palindromep "Able was I ere I saw Elba") ; true
(palindromep "А роза упала на лапу Азора") ; 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!")) ;true
(palindromep
(remove-if-not
#'alpha-char-p
"Unremarkable was I ere I saw Elba Kramer, nu?")) ; true
(palindromep
(remove-if-not
#'alpha-char-p
"A man, a plan, a cat, a ham, a yak,
a yam, a hat, a canal--Panama!")) ; true
(palindromep
(remove-if-not
#'alpha-char-p
"Ja-da, ja-da, ja-da ja-da jing jing jing")) ; nil
Отображения
Отображения представляет собой класс итераций, в которых
некоторая функция применяется последовательно к фрагментам одной
или нескольких последовательностей. Результатом выполнения таких
итерационных конструкций становится последовательность,
содержащая результаты применений функции к этим фрагментам. В
Лиспе предусмотрено несколько вариантов, определяющих способы,
которыми осуществляется выбор фрагментов обрабатываемого списка и
что происходит с результатами, получаемыми в результате
приложения функции.
Функция 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 #'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 #'(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 если соответствующий элемент входного
;; списка представлял собой последнеий экземпляр этого элемента
;; во входном списке.
Практически во всех Лисп-системах, нначиная с первой, которая
получила распространение в СССР Lisp 1.5, функция
mapl называлась map. В главе, посвященной работе с
последовательностями, мы рассмотрим, почему такой выбор
представляется неудачным. Поэтому в CommonLisp имя функции map
используется общей функцией отображения (generic sequence mapper).
(mapcon f x1 ... xn)
; то же что и (apply #'nconc (maplist f x1 ... xn))
(mapcan #'(lambda (x) (and (numberp x) (list x)))
'(a 1 b c 3 4 d 5))
;; (1 3 4 5)
В этом случае, однако, более предпочтительной может оказаться
функция remove-if-not.
do
или простую рекурсию; а
функции отображения используются в тех случаях, когда это
способствует упрощению программы и повышает читаемость кода.
#'
, помещаемое перед
lambda-выражением, которое записывается в виде аргумента этой
формы.
``Program Feature''
Со времен Lisp 1.5 практически во всех реализациях Лиспа был
реализован некий механизм, который получил название
``the program feature''. Странное название, которое невозможно
адекватно перевести на русский язык (ну разве что, как
``возможность программирования''., объясняется тем что без этого
механизма просто невозможно разрабатывать программы!)
prog
позволяет создавать Алгол- или
Фортран-подобные программы, записываемые в процедурном стиле, с
использованием операторов передачи управления go, которые
ссылаются на теги, в теле prog
. Однако современный стиль
программирования на Лиспе склоняется к отказу от постоянного
использования prog
. Разнообразные итерационные средства,
такие как do
, предоставляют возможность без труда реализовывать
prog
-подобные конструкции (В то же время, следует
признать, что использование do без итерационных механизмов
на практике встречается исключительно редко...)
prog
осуществляет выполнение трех различных
операций: она осуществляет связывание локальных переменных,
разрешает использование оператора return
, и разрешает
использование оператора go. В Common Lisp все эти три
операции разделены на три непересекающихся механизма:
let, block
, и tagbody. Эти три средства
используются незавсисимо друг от друга как строительные
кирпичики, составляющие основу других, более сложных конструкций.
Специальная форма tagbody
tagbody {tag | statement*}
(tagbody
(catch 'stuff
(mapcar #'(lambda (x) (if (numberp x)
(hairyfun x)
(go lose)))
items))
(return)
lose
(error "I lost big!"))
(defun философ-созерцатель (j)
(tagbody ---
созерцать (unless (голоден) (go созерцать))
---
"Не могу есть без палочек."
(Заточить (палочка j))
(Заточить (палочка (mod (+ j 1) 5)))
---
лопать (when (голоден)
(mapc #'откушать-по-кусочку
'(дважды-запеченая-свинина кунг-пао-чи-динг
ву-дип-ха мясо-в-апельсиновом-соусе
вермишель))
(go лопать))
---
"Не могу думать с набитым желудком."
(разломать (палочка j))
(разломать (палочка (mod (+ j 1) 5)))
---
(if (счастлив) (go созерцать)
(переквалифицироваться продавец-амулетов))))
Форма prog
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, и т.д., которые представляют собой
временные переменные, связывание которых действительно только
внутри prog
. Этот список обрабатывается точно так же, как и
список в форме let: вначале в направлении слева направо
оцениваются все формы init (где любая пропущенная форма
init считается равной NIL), а затем все переменные
связваются с полученными результатами одновременно.
Любое объявление declaration, появляющееся в теле prog
используется, так, как если бы они были размещены в начале тела
let.
prog
выполняется аналогично конструкции tagbody;
поэтому внутри его может быть использован оператор go.
prog
создает неявный блок block
с именем
nil, окружающий всю эту конструкцию, что позволяет
использовать оператор return
для передачи управления за
пределы конструкции prog
.
prog
:
(defun король-конфузов (w)
"Берет в качестве аргумента cons-ячейку из двух списков и
создает из нее список cons-ячеек. Можно рассматривать
эту функцию как своеобразную молнию."
(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 "Лишние символы соединяются сами с собой."
"Несовпадение длин списков! S" y)
(setq z y)
(go rejoin)))
(defun королева-ясности (w)
"Берет в качестве аргумента cons-ячейку из двух списков и
создает из нее список cons-ячеек. Можно рассматривать
эту функцию как своеобразную молнию."
(do ((y (car w) (cdr y))
(z (cdr w) (cdr z))
(x '{\empty (cons (cons (car y) (car z)) x)))
((null y) x)
(when (null z)
(cerror "Лишние символы соединяются сами с собой."
"Несовпадение длин списков! 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))
prog
заметно уменьшилась и
программисты предпочитают использовать специфические конструкции.
Однако общий механизм тем и отличается, что позволяет реализовать
любой частный случай.
Специальная форма go
go tag
return-from
. Если же использование go кажется
неизбежным, то управляющая конструкция, реализуемая с помощью
go должна быть упакована в макроопределение.