[Картонная Армия - от галеры до ракеты!]

Домой Вверх Содержание

Программные блоки
Lisp - первые шаги Lisp -атомы и списки Lisp - переменные Lisp - вызов функций Программные блоки Структуры данных

 

 

horizontal rule

В.Водолазкий

Первые шаги в 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.

В случае do*, вначале оценивается первая форма init, затем с этим значением связывается первая переменная var, после этого оценивается вторая форма init и с ее результатом связывается вторая переменная var, и так далее; в общем случае форма initj может ссылаться на новое связывание vark, если k<j, в противном случае ссылка производится на старую связь vark.

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

Вначале каждой итерации, начиная со второй, индексные переменные обновляются следующим образом. Оцениваются все формы step слева направо, и все результирующие значения назначаются соответствующим индексным переменным. Любая переменная, для которой отсутствует форма step никакого нового значения не получает. В случае формы do, все формы step оцениваются до того, как будет обновлено значение какой-либо переменной; назначение значений переменным выполняется параллельно, как при использовании вызова psetq.

Поскольку все формы step оцениваются до того, как изменяется какая-либо из переменных, форма step всегда использует старые значения всех индексных переменных, даже если до этого уже были проведены оценки каких-либо форм step. А вот в do*, оценивается первая форма step, затем это значение связывается с первой переменной var, после чего оуенивается вторая форма step и ее значение присваивается второй переменной var, и так далее; присвоение значений переменным осуществляется последовательно, как при использовании setq. В обоих версиях --- do и do*, после обновления значений индексных переменных производится оценка end-test, после чего начинается собственно выполнение очередной итерации.

Если 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.

В начале тела цикла допускается использование форм declare. Эти формы применяются только к коду, содержащемуся в теле 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)

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

Довольно часто итерационный алгоритм может быть наиболее просто и ясно реализован непосредственно внутри форм step функции 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)))

В этом фрагменте интересный момент заключается в использовании предиката 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))))))

Цикл 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, но перечисленные выше механизмы, охватывают довольно большую часть практических случаев.

Как dolist, так и dotimes осуществляют многократное вычисление набора выражений. При этом на каждой итерации осуществляется изменение некоторой индексной переменной, значение которой доступно внутри тела цикла. Функция dolist осуществляет последовательный просмотр элементов списка, а dotimes осуществляет подстановку целых чисел в диапазоне от 0 до n-1, для произвольного целого числа n, переданного как параметр при вызове функции.

Значение любой из этих конструкций может быть сформировано с помощью необязательной формы result. По умолчанию обе функции возвращают NIL.

Для немедленного возврата из форм dolist и dotimes, может использоваться оператор return, который отменяет выполнение всех следующих за ним функций; фактически, это означает, что все тело цикло окружается неявным блоком block с именем nil.

Само тело цикла представляет собой неявную конструкцию tagbody; оно может содержать теги, которые представляют собой параметры (цели) операторов go. Кроме того, допускается использовать в начале тела цикла объявлений declare.

Форма dolist

dolist (var listform [resultform])
       declaration* {tag | statement*}

Функция dolist выполняет достаточно простую и прямолинейную итерацию по каждому из элементов списка. Вначале dolist оценивает значнеие формы listform, которое должно привести к формированию списка. Затем осуществляется оценка тела цикла для каждого его элемента, при этом переменная var связывается на каждой итерации с очередным элементом. После того, как будет просмотрен весь список, оценивается значение формы resultform (должна представлять собой простою форму, и не может быть неявной progn), которая возвращается как результат всего вызова dolist. Во время оценки значения resultform управляющая переменная цикла var по-прежнему является связанной и имеет значение nil. Если resultform опущена, функция возвращает NIL.

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

(dolist (x '(a b c d)) (prin1 x) (princ " "))  ; NIL 
   ; после печати ``a b c d '' (не забудьте о пробеле)

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

Форма dotimes

dotimes (var countform [resultform])
        {declaration* {tag | statement*}}

Функция dotimes реализует механизм для ``прямолинейного'' цикла, переменная цикла в котором принимает несколько последовательных целочисленных значений. Выражение (dotimes (var countform resultform) . progbody) осуществляет оценку формы countform, значение которой должно представлять собой целое число. Затем производится вычисление тела progbody, которое повторяется для переменной var, принимающей последовательно значения от 0 по count (не включая последнее значение). Если значение countform равно нулю или меньше, то форма progbody выполняется 0 раз, то есть не выполняется вообще. И, в заключение, оценивается форма resultform, которая представляет собой одну форму, и не интерпретируется автоматически как неявный progn-блок, и ее результат становится результатом всей формы dotimes. Во время оценки resultform управляющая переменная var по-прежнему считается связанной, и ее значение равно количеству выполненных циклов. Если resultform опущена, результат вычисления всей формы равен NIL.

Допускается в любой момент прекратить вычисление цикла с помощью оператора return, который позволяет вернуть произвольное значение.

Вот пример использования 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.

Практически во всех Лисп-системах, нначиная с первой, которая получила распространение в СССР Lisp 1.5, функция mapl называлась map. В главе, посвященной работе с последовательностями, мы рассмотрим, почему такой выбор представляется неудачным. Поэтому в CommonLisp имя функции map используется общей функцией отображения (generic sequence mapper).

Все эти функции используются в тех случаях, когда функция вызывается ради вносимых ею побочных эффектов, а не ради возвращаемых значений. Значение, возвращаемое 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 в Лиспе.

В этом случае, однако, более предпочтительной может оказаться функция remove-if-not.

Не забывайте, что nconc представляет собой структуроразрушающую операцию, а следовательно такими же являются и использующие ее mapcan и mapcon; списки, возвращаемые function разрушаются при формировании итогового.

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

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

В некоторых версиях CommonLisp function может иметь только тип symbol или function; использование lambda-выражений не допускается. Однако, для совместимости со старым программным обеспечением, да и для удобства работы, можно использовать специальную форму --- сокращение #', помещаемое перед 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 после списка переменных представляет собой ее тело. Объект в теле формы может представлять собой символ, число (в этом случае он называется тегом), либо список (а в этом случае он называется оператором).

Каждый элемент тела формы обрабатывается последовательно, слева направо. При этом теги игнорируются; операторы оцениваются, а результаты оценки отбрасываются. Если достигнут конец тела, то 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 созерцать) 
               (переквалифицироваться продавец-амулетов))))

Форма 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. Единственное отличие состоит в том, что prog* осуществляет связывание и инициализацию временных переменных последовательно, а поэтому форма init для каждой из них может использовать значения, вычисленные для предыдущих переменных. Поэтому prog* относится к prog как let* к let. Например,

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

возвращает car-часть значения z.

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

Специальная форма go

go tag

Специальная форма (go tag) используется для передачи управления внутри конструкции tagbody. При этом тег tag должен представлять собой символ или целое число --- его значение не оценивается. Оператор go передает управление на точку, которая помечена тегом, равным в смысле eql аргументу tag. Если тег с таким именем отсутствует, то проверяются тела всех конструкций которые лексически содержат tagbody, если таковые, конечно, существуют. Если все же подходящего тега отыскать не удастся, фиксируется ошибка.

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

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

 

horizontal rule

 

 

 

 

Послать письмо voldemarus@narod.ru  
Авторские права © 2003-2010 Картонная армия
Последнее изменение: июля 18, 2010