|
|
В.ВодолазкийПервые шаги в Gnu Common Lisp - Программные блоки и выход из нихКонструкции block и return-from предназначены для реализации механизма выхода за пределы текущего лексического контекста. Попросту говоря, использование return-from внутри конструкции block с таким же именем обеспечивает немедленную передачу управления за пределы данного блока. В большинстве случаев этот механизм оказывается более эффективным чем передача управления на базе catch и throw, о чем мы поговорим отдельно.Специальная форма blockblock name ,form*
Конструкция block организует последовательное выполнение
всех форм form слева направо, возвращая результат
вычисления последней формы. Если, однако, во время исполнения
блока выполняется форма return или
Параметр name не оценивается и должен представлять собой
символ. Область видимости символа name лексическая ---
только
Форма
Зона видимости имени блока подчиняется общим правилам лексической
видимости и иногда может приводить к последствиям, которые, по
меньшей мере, удивляют пользователей и разработчиков других
диалектов Лиспа. Например, вызов (block loser (catch 'stuff (mapcar #'(lambda (x) (if (numberp x) (hairyfun x) (return-from loser {nil}))) items)))
Однако в ряде ситуаций Специальная форма return-fromreturn-from name [result]
Форма
Сама по себе форма Форма return
Циклы и итерацииВ составе Common Lisp реализовано несколько конструкций, ориентированных на итерационное исполнение участков кода. Прежде всего, это формаloop , которая реализует тривиальную
поддержку итерационных вычислений, и по сути представляет собой
незначительное расширение progn механизмом, который
заставляет, при выполнении некоторого условия повторять
вычисление всего фрагмента кода. Конструкции
do и do* обеспечивают контроль за изменением сразу
нескольких переменных на каждой итерации. Для реализации
специализированных итерационных процедур над элементами списка или
выполнения их ровно n-раз, предназначены функции
dolist и dotimes. Конструкция tagbody представляет
собой наиболее общий вариант, допускающий использование внутри ее
произвольных передач управления с помощью
go.
Традиционная конструкция Бесконечный циклКонструкцияloop представляет собой простейший механизм организации
циклических вычислений. Эта конструкция не предполагает использования
управляющих переменных и просто выполняет свое тело раз за разом.
Форма looploop ,form*
Каждый из входящих в состав функций параметров-форм form
оценивается последовательно: слева направо. И если оценивается
последняя форма form, то после нее возобновляется
вычисление первой формы, и так далее. В результате образуется
бесконечный цикл вычислений. Конструкция
Так же, как и большинство других итерационных механизмов,
Конечно, конструкция Общие средства для организации итерационных вычисленийВ отличие отloop , do и do* предназначены для
реализации гибкого и мощного механизма организации повторяющихся
вычислений.
Специальная форма dodo ({var | (var [init [step]])*) (end-test {result*) declaration* {tag | statement* do* ({var | (var [init [step]])*) (end-test {result*) declaration* {tag | statement*
Специальная форма
В общем случае, цикл (do ((var1 init1 step1) (var2 init2 step2) ... (varn initn stepn)) (end-test . result) *declaration . tagbody)
Цикл
Первый объект формы представляет собой список из нуля и более
спецификаторов индексных переменных. Каждый из
спецификатора представляет собой список имен переменных
var,
начальное значение init, и форма вычисления очередного
значения step. Если параметр
init пропущен, то по умолчанию он получает значение
NIL.
Если опущен параметр step, то значение переменной var
не изменяется между итерациями Спецификатор индексной переменной может также представлять собой просто имя переменной. В этом случае переменная имеет начальное значение, равное NIL и не изменяется от итерации к итерации. Рекомендуется использовать такую переменную только в тех случаях, когда эта переменная бкдет получать некоторое значение (например, с помощью setq) до того, как она впервые будет использована. Необходимо отметить, что до инициализации начальное значение равно NIL, и не является неопределенным. И все-таки рекомендуется не отдавать интерпретацию на волю системы, а явно указывать (varj NIL), если начальное значение должно означать ``false,'' или (varj '()) если начальное значение должно представлять собой пустой список. Перед тем, как начнется первая итерация, осуществляется оценка значений всех форм init, а затем каждая переменная var связывается со значением соответствующей формы init. Имейте в виду, что это не присвоение, а связывание; после окончания цикла будут восстановлены прежние значения этих переменных.
В случае
В случае
Второй параметр цикла представляет собой список, состоящий из
предиката, описывающего условие завершение цикла
end-test и произвольное количество (включая нуль) форм
result.
Фактически, этот фрагмент можно рассматривать как один из
вариантов в формате функции cond. Вначале каждой итерации,
после завершения обработки индексных переменных оценивается
значение предиката end-test. Если результат равен
NIL, то выполнение продолжается, и повторяется
вычисление
фрагмента в форме
Вначале каждой итерации, начиная со второй, индексные переменные
обновляются следующим образом. Оцениваются все формы step
слева направо, и все результирующие значения назначаются
соответствующим индексным переменным.
Любая переменная, для которой отсутствует форма
step никакого нового значения не получает.
В случае формы
Поскольку все формы step оцениваются до того, как
изменяется какая-либо из переменных, форма
step всегда использует старые значения всех
индексных переменных, даже если до этого уже были проведены
оценки каких-либо форм step.
А вот в
Если end-test формы Например: (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)))))
Оставшаяся часть формы
Вся форма
В начале тела цикла допускается использование форм
declare.
Эти формы применяются только к коду, содержащемуся в теле
Вот несколько примеров использования цикла (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)
использует механизм параллельного назначения индексных
переменных. При первой итерации значением 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)))
Эта конструкция работает точно так же, как и
(defun list-reverse (list) (do ((x list (cdr x)) (y '{() (cons (car x) y))) ((endp x) y))) В этом фрагменте интересный момент заключается в использовании предиката endp вместо null или atom. Такая реализация проверки конца списка позволяет реализовать более устойчивый код. Теперь, для рассмотрения организации вложенных циклов, предположим, что в env содержится список cons-ячеек. Car-часть каждой из ячеек представляет собой список символов, а cdr-часть --- список такой же длины, содержащий соответствующие символам значения. В принципе такая структура данных аналогична ассоциативному списку, но разделена на ``кадры''. Назовем эту структуру ``картотекой''. Теперь, для того, чтобы реализовать функцию извлечения отдельных элементов из такой структуры данных можно использовать функцию: (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))))))
Цикл (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))))
Цикл Простые итерационные конструкцииКонструкции dolist и dotimes выполняют тело цикла один раз для каждого из значений, которые последовательно принимаются одной и той же индексной переменной. Конечно, все это может быть реализовано с помощью циклаdo , но перечисленные выше
механизмы, охватывают довольно большую часть практических случаев.
Как dolist, так и dotimes осуществляют многократное вычисление набора выражений. При этом на каждой итерации осуществляется изменение некоторой индексной переменной, значение которой доступно внутри тела цикла. Функция dolist осуществляет последовательный просмотр элементов списка, а dotimes осуществляет подстановку целых чисел в диапазоне от 0 до n-1, для произвольного целого числа n, переданного как параметр при вызове функции. Значение любой из этих конструкций может быть сформировано с помощью необязательной формы result. По умолчанию обе функции возвращают NIL.
Для немедленного возврата из форм dolist и dotimes,
может использоваться оператор Само тело цикла представляет собой неявную конструкцию tagbody; оно может содержать теги, которые представляют собой параметры (цели) операторов go. Кроме того, допускается использовать в начале тела цикла объявлений declare. Форма dolistdolist (var listform [resultform]) declaration* {tag | statement*}
Функция dolist выполняет достаточно простую и прямолинейную
итерацию по каждому из элементов списка.
Вначале dolist оценивает значнеие формы listform,
которое должно привести к формированию списка. Затем
осуществляется оценка тела цикла для каждого его элемента, при
этом переменная var связывается на каждой итерации с
очередным элементом.
После того, как будет просмотрен весь список, оценивается
значение формы resultform (должна представлять собой
простою форму, и не может быть неявной Вот пример использования dolist: (dolist (x '(a b c d)) (prin1 x) (princ " ")) ; NIL ; после печати ``a b c d '' (не забудьте о пробеле)
Для немедленного прекращения выполнения цикла и возвращения
заданного значения может быть использован оператор
Форма dotimesdotimes (var countform [resultform]) {declaration* {tag | statement*}}
Функция dotimes реализует механизм для ``прямолинейного''
цикла, переменная цикла в котором принимает несколько
последовательных целочисленных значений. Выражение
(dotimes (var countform resultform) . progbody)
осуществляет оценку формы countform, значение которой
должно представлять собой целое число. Затем
производится вычисление тела progbody, которое повторяется
для переменной var, принимающей последовательно значения от
0 по count (не включая последнее значение). Если значение
countform равно нулю или меньше, то форма
progbody выполняется 0 раз, то есть не выполняется вообще.
И, в заключение, оценивается форма resultform, которая представляет собой
одну форму, и не интерпретируется автоматически как неявный
Допускается в любой момент прекратить вычисление цикла с помощью
оператора Вот пример использования dotimes для обработки строк: ;;; Предикат 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 Изменение значения var в теле цикла (например, за счет использования setq) может привести к неопределенным результатам (Конкретный результат определяется реализацией Лиспа. Что касается компилятора Common Lisp, то в таких случаях обычно выдается предупреждение). ОтображенияОтображения представляет собой класс итераций, в которых некоторая функция применяется последовательно к фрагментам одной или нескольких последовательностей. Результатом выполнения таких итерационных конструкций становится последовательность, содержащая результаты применений функции к этим фрагментам. В Лиспе предусмотрено несколько вариантов, определяющих способы, которыми осуществляется выбор фрагментов обрабатываемого списка и что происходит с результатами, получаемыми в результате приложения функции.Для осуществления подобных итераций над произвольными структурами данных предназначена общая версия функции map. Но более часто используются описываемые ниже функции, которые работают только над списками.
Функция 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 Для каждой из этих функций отображения первый аргумент представляет собой функцию, а оставшиеся должны быть списочного типа. Функция должна принимать столько аргументов, сколько списков представлено среди параметров map*. Функция mapcar работает над последовательными элементами списков. Вначале, функция применяется к car-части каждого списка, затем, к cadr-части каждого списка, и так далее.
Значение, возвращаемое mapcar представляет собой список результатов успешных вызовов функции function. Например: (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, однако они не накапливают результаты вызовов функции function.
Все эти функции используются в тех случаях, когда функция вызывается ради вносимых ею побочных эффектов, а не ради возвращаемых значений. Значение, возвращаемое 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) В этом случае функция работает как своеобразный фильтр, собственно говоря, это стандартный способ использования mapcan в Лиспе.
Не забывайте, что nconc представляет собой структуроразрушающую операцию, а следовательно такими же являются и использующие ее mapcan и mapcon; списки, возвращаемые function разрушаются при формировании итогового.
Иногда вместо использования операции отображения может оказаться
более удобным использовать Функциональный аргумент, используемый в функции отображения должен представлять собой функцию, которая допустима в качестве аргумента для apply; поэтому он может представлять собой макрокоманду или имя специальной формы. Естественно, не существует никаких противопоказаний в использовании ключевых параметров &optional и &rest.
В некоторых версиях CommonLisp function может иметь только тип
symbol или function; использование lambda-выражений
не допускается. Однако, для совместимости со старым программным
обеспечением, да и для удобства работы, можно использовать
специальную форму --- сокращение ``Program Feature''Со времен Lisp 1.5 практически во всех реализациях Лиспа был реализован некий механизм, который получил название ``the program feature''. Странное название, которое невозможно адекватно перевести на русский язык (ну разве что, как ``возможность программирования''., объясняется тем что без этого механизма просто невозможно разрабатывать программы!)
Так, конструкция
Конструкция Специальная форма tagbodytagbody {tag | statement*} Часть формы tagbody после списка переменных представляет собой ее тело. Объект в теле формы может представлять собой символ, число (в этом случае он называется тегом), либо список (а в этом случае он называется оператором). Каждый элемент тела формы обрабатывается последовательно, слева направо. При этом теги игнорируются; операторы оцениваются, а результаты оценки отбрасываются. Если достигнут конец тела, то tagbody возвращает NIL. Если оценивается конструкция (go тег), управление передается на ту часть тела формы, которая имеет метку тег. Зона видимости тегов, заданных с помощью tagbody является лексической и после выхода из конструкции tagbody использование операторов go для переходов к тегам внутри ее тела недопустимо. Однако оператор go может выполнить передачу управления в tagbody, котор ая не является самой вложенной конструкцией, содержащей этот go; теги, задаваемые в tagbody толькор затеняют другие теги с таким же именем. Конечно, лексическая область видимости точек передачи управления с помомщью go может рассматриваться как глобальная, поскольку ее общий характер приводит нередко к неприятным сюрпризам, которые удивляют как пользователей так и разработчиков других версий Лиспа. Например, в приведенном ниже фрагменте оператор go работает вполне предсказуемым образом: (tagbody (catch 'stuff (mapcar #'(lambda (x) (if (numberp x) (hairyfun x) (go lose))) items)) (return) lose (error "I lost big!")) Но в некоторых ситуациях, go в Common Lisp ведет себя совсем не так, как ожидается, и непохож на классический goto. Так, go может ``разломать'' ловушки catch, если необходимо получить доступ к точке передачи управления. Кроме того, в случае лексического замыкания, созданного с помощью function, становится возможным ссылаться на цель оператора go в пределах функции, хотя формально этот тег не должен быть иметь лексической видимости. В спецификации формы имеется ряд, пробелов, которые иногда используются дляполучения неожиданных результатов. Например, отсутствует явное запрещение многократного использования одного и того же тега внутри тела tagbody. И хотя компилятор и интерпретатор, скорее всего, будут жаловаться на такое поведение, но никаких критических для приложения действий предпринято, скорее всего, не будет. Поэтому программисты нередко используют избыточные теги, такие как --- для форматирования и внесения комментариев. Например, вот как мог бы функционировать китайский философ-созерцатель: (defun философ-созерцатель (j) (tagbody --- созерцать (unless (голоден) (go созерцать)) --- "Не могу есть без палочек." (Заточить (палочка j)) (Заточить (палочка (mod (+ j 1) 5))) --- лопать (when (голоден) (mapc #'откушать-по-кусочку '(дважды-запеченая-свинина кунг-пао-чи-динг ву-дип-ха мясо-в-апельсиновом-соусе вермишель)) (go лопать)) --- "Не могу думать с набитым желудком." (разломать (палочка j)) (разломать (палочка (mod (+ j 1) 5))) --- (if (счастлив) (go созерцать) (переквалифицироваться продавец-амулетов))))
Форма progprog ({var | (var [init])*) {declaration* {tag | statement* }} prog* ({var | (var [init])*) {declaration* {tag | statement*}}
Конструкция (prog (var1 var2 (var3 init3) var4 (var5 init5)) *declaration statement1 tag1 statement2 statement3 statement4 tag2 statement5 ... )
Список после ключевого слова
Тело
Конструкция
Вот пример использования (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 variable-list *declaration . body) ;; (block nil (let variable-list *declaration (tagbody . body)))
Существует также специальная форма prog*, которя почти
эквивалентна (prog* ((y z) (x (car y))) (return x)) возвращает car-часть значения z.
Конечно, в последние годы роль Специальная форма gogo tag Специальная форма (go tag) используется для передачи управления внутри конструкции tagbody. При этом тег tag должен представлять собой символ или целое число --- его значение не оценивается. Оператор go передает управление на точку, которая помечена тегом, равным в смысле eql аргументу tag. Если тег с таким именем отсутствует, то проверяются тела всех конструкций которые лексически содержат tagbody, если таковые, конечно, существуют. Если все же подходящего тега отыскать не удастся, фиксируется ошибка. Форма go никогда не возвращает никакого значения.
С точки зрения хорошего стиля программирования
рекомендуется
дважды подумать, прежде чем использовать в программе оператор
go. В большинстве случаев вместо go можно использовать
итерационные примитивы, вложенные условные формы или даже
|
Послать письмо voldemarus@narod.ru
|