Глава 3
Область и продолжительность видимости

При описании различных возможностей Common Lisp’а очень важными понятиями являются области и продолжительности видимости. Эти понятия возникают, когда к некоторому объекту или конструкции необходимо обратиться из некоторой части кода. Область видимости объектов отмечает пространственный или текстовый регион, в котором находящаяся внутри программа может обращаться к этим объектам. Продолжительность видимости обозначает временной интервал, в течение которого программа может обращаться к данным объектам.

Вот простой пример такой программы:

(defun copy-cell (x) (cons (car x) (cdr x)))

Областью видимости параметра с именем x является тело формы defun. Способа сослаться на этот параметр из какого-либо другого места программы нет. Продолжительностью видимости параметра x (для какого-нибудь вызова copy-cell) является интервал времени, начиная с вызова функции и заканчивая выходом из неё. (В общем случае продолжительность видимости параметра может продлиться и после завершения функции, но в данном простом случае такого не может быть.)

В Common Lisp сущность, на которую можно сослаться из кода, создаётся с помощью специальных языковых конструкций, и область и продолжительность видимости описываются в зависимости от этой конструкции и времени (выполнения конструкции) в которое эта сущность была создана. Для предмета данного описания, термин «сущность» указывает не только на объекты Common Lisp’а такие как символы и cons-ячейки, но и также на связывания переменных (обычных и специальных), ловушки, и метки переходов. Важно отметить различие между сущностью и именем для этой сущности. В определение функции, такой как:

(defun foo (x y) (* x (+ y 1)))

существует только одно имя, x, используемое для ссылки на первый параметр процедуры, когда бы они не была вызвана. Связывание — это, в частности, экземпляр параметра. Значение связанное с именем x зависит не только от области видимости, в которой данная связь возникла (в данном примере в теле функции foo связь возникла в области видимости определения параметров функции), но также, в частности, от механизма связывания. (В данном случае, значение зависит от вызова функции, в течение которого создаётся ссылка). Более сложный пример приводится в конце данной главы.

Вот некоторые виды областей и продолжительностей видимости, которые, в частности, полезны при описании Common Lisp’а:

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

Термин «динамическая область видимости» некорректен. Как бы то ни было это и устоялось, и удобно.

Сказанное выше не рассматривает возможность скрытия (shadowing). Далёкие (FIXME) ссылки на сущности осуществляются с использованием имён того или иного типа. Если две сущности имеют одинаковое имя, тогда второе имя может скрыть первое, в таком случае ссылка с помощью этого имени будет осуществлена на вторую сущность и не может быть осуществлена на первую.

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

(defun test (x z)
  (let ((z (* x 2))) (print z))
  z)

Связывание переменной z с помощью конструкции let скрывает связывание одноимённого параметра функции test. Ссылка на переменную z в форме print указывает на let связывание. Ссылка на z в конце функции указывает на параметр с именем z.

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

Ссылка по имени на сущность с динамической продолжительностью жизни всегда указывает на сущность с этим именем, что была установлена наипозднейшей и ещё не была упразднена. Например:

(defun fun1 (x)
  (catch ’trap (+ 3 (fun2 x))))

(defun fun2 (y)
  (catch ’trap (* 5 (fun3 y))))

(defun fun3 (z)
  (throw ’trap z))

Рассмотрим вызов (fun1 7). Результатом будет 10. Во время выполнения throw, существует две ловушки с именем trap: одна установлена в процедуре fun1, и другая — в fun2. Более поздняя в fun2, и тогда, из формы catch, что в fun2, возвращается значение 7. Рассматриваемая из fun3, catch в fun2 скрывает одноимённую в fun1. Если бы fun2 была определена как

(defun fun2 (y)
  (catch ’snare (* 5 (fun3 y))))

тогда бы две ловушки имели разные имена, и в таком случае одна из них из fun1 не была бы скрыта. Результатом бы стало 7.

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

Далее важные правила области и продолжительности видимости в Common Lisp’е:

Правила для лексической области видимости подразумевают, что лямбда-выражения (анонимные функции), появляющиеся в function, будут являться «замыканиями» над этими неспециальными (non-special) переменными, которые видимы для лямбда-выражения. Это значит, что функция предоставленная лямбда-выражением может ссылаться на любую лексически доступную неспециальную (non-special) переменную и получать корректное значение, даже если выполнение уже вышло из конструкции, которая устанавливала связи. Пример compose, рассмотренный в данной главе ранее, предоставлял изображение такого механизма. Правила также подразумевают, что связывания специальных переменных не «замыкаются», как может быть в некоторых других диалектах Lisp’а.

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

(defun contorted-example (f g x)
  (if (= x 0)
      (funcall f)
      (block here
         (+ 5 (contorted-example g
                                 #’(lambda ()
                                     (return-from here 4))
                                 (- x 1))))))

Рассмотрим вызов (contorted-example nil nil 2). Он вернёт результат 4. Во время исполнения, contorted-example будет вызвана три раза, чередуясь с двумя блоками:

(contorted-example nil nil 2)

  (block here1 ...)

    (contorted-example nil #’(lambda () (return-from here1 4)) 1)

      (block here2 ...)

        (contorted-example #’(lambda () (return-from here1 4))
                           #’(lambda () (return-from here2 4))
                           0)
          (funcall f)
                where f  #’(lambda () (return-from here1 4))

            (return-from here1 4)

В время выполнения funcall существует две невыполненные точки выхода block, каждая с именем here. В стеке вызовов выше, эти две точки для наглядности проиндексированы. Форма return-from, выполненная как результат операции funcall, ссылается на внешнюю невыполненную точку выхода (here1), но не на (here2). Это следствие правил лексических областей видимости: форма ссылается на ту точку выхода, что видима по тексту в точке вызова создания функции (здесь отмеченной с помощью синтаксиса #’). (FIXME)

Если в данном примере, изменить форму (funcall f) на (funcall g), тогда значение вызова (contorted-example nil nil 2) будет 9. Значение измениться по сравнению с предыдущим разом, потому что funcall вызовет выполнение (return-from here2 4), и это в свою очередь вызовет выход из внутренней точки выхода (here2). Когда это случиться, значение 4 будет возвращено из середины вызова contorted-example, к нему добавится 5 и результат окажется 9, и это значение вернётся из внешнего блока и вообще из вызова contorted-example. Цель данного примера, показать что выбор точки выхода зависит от лексической области, которая была захвачена лямбда-выражением, когда вызывался код создания этой анонимной функции.

Эта функция contorted-example работает только потому, что функция с именем f вызывается в процессе продолжительности действия точки выхода. Точки выхода из блока ведут себя, как связывания неспециальных (non-special) переменных в имеющимся лексическом окружении, но отличаются тем, что имеют динамическую продолжительность видимости, а не неограниченную. Как только выполнение покинет блок с этой точкой выхода, она перестанет существовать. Например:

(defun illegal-example ()
  (let ((y (block here #’(lambda (z) (return-from here z)))))
ф     (if (numberp y) y (funcall y 5))))

Можно предположить, что вызов (illegal-example) вернёт 5: Форма let связывает переменную y со значением выполнения конструкции block; её значение получится равным анонимной функции. Так как y не является числом, она вызывается с параметром 5. return-from тогда должны вернуть данное значение с помощью точки выхода here, тогда осуществляется выход из блока ещё раз и y получает значение 5, которое будучи числом, возвращается в качестве значения для illegal-example.

Рассуждения выше неверны, потому что точки выхода определяемые в Common Lisp’е имеют динамическую продолжительность видимости. Аргументация верна только до вызова return-from. Вызов формы return-from является ошибкой, не потому что она не может сослаться на точку выхода, а потому что она корректно ссылается на точку выхода и эта точка выхода уже была упразднена.