?

Log in

No account? Create an account

Переменные в Common Lisp - lispnik

фев. 1, 2004

06:26 pm - Переменные в Common Lisp

Previous Entry Поделиться Next Entry

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

Сделаю важное замечание: в CL любая переменная должна быть тем или иным способом объявлена: с помощью defvar/defparameter или с помощью let. В стандарте ANSI указано, что последствия использования необъявленных переменных не определены, то есть конкретная реализация может делать в этом случае всё, что захочет: может выдать сообщение об ошибке, может молча объявить эту переменную как лексическую или (более вероятно) как динамическую. Так что примеры в двухтомнике Хювеннена-Сеппянена и в книге Пола Грэхэма ANSI Common Lisp, в которых в командной строке с помощью setf/setq присваиваются значения необъявленным переменным, не корректны. Однако извинением авторам служит то, что книги были вышли до принятия окончательной версии стандарта ANSI Common Lisp, в котором это было зафиксировано.

Самые первые версии Лиспа имели только динамические переменные. Насколько мне известно, первым диалектом Лиспа с лексическими переменными была Схема (Scheme). В Common Lisp переменные по умолчанию являются лексическими, а динамические переменные должны быть соответствующим образом объявлены.

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

  1. Указать её имя в defvar или defparameter:
    > (defvar *special-var1* nil)
    > (defparameter *special-var2* nil)
    
  2. Указать её имя в декларации special.
    > (let ((*test* nil))
               (declare (special *test*))
             (do-something-special))
    ;;; Или даже так:
    > (defun something-special2 (lex-var1 *test* lex-var2)
               (declare (special *test* *test2*))
             (do-something-special lex-var1 *test* *test2* lex-var2))
    

Все остальные переменные будут лексическими.

Имена динамических переменных обрамляются звёздочками по традиции, чтобы можно по имени переменной понять, является ли она динамической.

А теперь рассмотрим различия в поведении лексических и динамических переменных.

Главное различие — область видимости. Лексическая переменная видна только в теле той формы, в которой она была объявлена (в теле функции в случае defun, в теле let в случае let, и т.д.). Динамическая переменная, даже если она была создана локально с помощью let, видна во всех вызываемых функциях пока действительно связывание с помощью let:

> (defun test1 (a)
      (declare (special *somevar*))
     (+ a *somevar*))

> (defun test2 (b)
     (let ((*somevar* 10) (anothervar 20))
          (declare (special *somevar*))
        (test1 (1+ b))))

В данном примере значение переменная *somevar*, которое было ей присвоено в test2, видно и в функции test1, а вот значение лексической переменной anothervar в test1 нельзя узнать вообще никак.

Интересным является то, что связывания динамических переменных, созданные с помощью let (а так же с другими формами, осуществляющими связывание, например with-open-file или defun и lambda, создающие связывания для своих аргументов) влияют на все вложенные вызовы функций (если они, конечно, не сами не связывают те же переменные). После выхода из let восстанавливается предыдущее связывание, если оно было. Вот как это можно использовать: допустим, у нас есть функция funny-print, которая что-то печатает на экране, используя стандартную динамическую переменную *output-stream*. Используя let, мы можем "подменить" её значение, заставив функцию печатать в файл или строку:

> (with-output-to-string (*output-stream*)
       (funny-print some-object))

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

Можно считать, что с каждой динамической переменной связан специальный стек связываний. Вычисления и присваивания (setf/setq/set) работают со связыванием, которое расположено на вершине стека. Каждая форма let (и эквивалентные ей), в которой указана динамическая переменная, в начале помещает на стек новое связывание и удаляет его в конце выполнения. При этом все манипуляции, которые выполнялись над этим связыванием, влияют только на это связывание, поэтому иногда можно увидеть код, подобный следующему:

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

Лексические и динамические переменные ведут себя по-разному в замыканиях. А именно, связывания лексических переменных запоминаются при создании замыкания (как говорят, сохраняется лексическое окружение), а связывания динамических переменных — нет, при каждом вызове замыкания значения берутся из вызываемого окружения. Вот простой пример:

> (defvar *shift1* 100)  ; динамическая переменная, которую мы
                       ; будем использовать в замыкании

;;; Создаём замыкание, в котором лексический контекст состоит
;;; из переменной shift2.  Отметим, что в теле лямбда-выражения
;;; переменная x также является лексической
> (defvar *test-closure*    ; тоже динамическая, но это не важно
    (let ((shift2 20))
          (lambda (x) (+ x *shift1* shift2))))

;;; Пробуем
> (funcall *test-closure* 3)
=> 123

;;; Создаём новое связывание *shift1*:
> (let ((*shift1* 400))
    (funcall *test-closure* 3))
=> 423

;;; Создаём новое связывание shift2:
> (let ((shift2 40))
    (funcall *test-closure* 3))
=> 123
Как видим, в последнем примере результат не изменился!

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

Если же нужно запомнить в замыкании значение динамической переменной во время создания замыкания (значение, а не связывание, которое запомнить невозможно), то можно воспользоваться временной лексической переменной:

> (let ((temp-lexical *dynamic-var*)) ; запоминаем
     (lambda (x)
         (let ((*dynamic-var* temp-lexical)) ; восстанавливаем
             (some-fun-that-uses-dinamyc-var x))))

И последнее отличие, которое я упомяну, заключается в том, что динамическая переменная может не иметь значения, в то время как у лексической переменной всегда есть какое-то значение. Это происходит тогда, когда переменная объявляется динамической с помощью defvar или декларации special, но никакое значение ей не присваивается.

Обновлено и исправлено 3 февраля.
Исправлено 4 февраля (ошибку указал Anton Kovalenko).
Исправлено 5 февраля (ошибку указал anonymous).
Исправлено 27 мая 2010 г. (ошибки указал anonymous).

Настроение: workingworking
Музыка: Реклама на Радио ОТС

Comments:

[User Picture]
From:alexott
Date:Февраль 1, 2004 09:28 pm
(Link)
хороший рассказ
(Ответить) (Thread)
[User Picture]
From:lispnik
Date:Февраль 2, 2004 01:28 am

Re:

(Link)
Стараемся помаленьку :)
(Ответить) (Parent) (Thread)
From:(Anonymous)
Date:Февраль 3, 2004 01:51 pm
(Link)
В замыканиях сохраняются не значения, а связывания.

У двух финнов, кажется, был пример вроде такого:


(let ((pair
       (let ((x 0))
         (cons (lambda (a) (setq x a))
               (lambda () x)))))
  (setf (symbol-function 'setit) (car pair))
  (setf (symbol-function 'getit) (cdr pair)))

(getit)

(setit 1)

(getit)


Где вызов setit влияет на getit, т.к. они видят одно и то же связывание для x.
(Ответить) (Thread)
[User Picture]
From:lispnik
Date:Февраль 3, 2004 09:03 pm

Re:

(Link)
Если ты про пример с temp-lexical, то связывание динамической переменной ты никак не сохранишь, только значение...
(Ответить) (Parent) (Thread)
[User Picture]
From:lispnik
Date:Февраль 3, 2004 09:04 pm

Re:

(Link)
Ага, действительно у меня была ошибка, исправлю...
(Ответить) (Parent) (Thread)
From:(Anonymous)
Date:Май 26, 2010 04:03 pm

не то чтобы ошибка...

(Link)
но слово dynamic в статье имеет как минимум два неправильных написания (:

еще в нагрузку две очепятки -- за номером 1:
> (defun test2 (b)
     (let ((*somevar 10) (anothervar 20))
          (declare (special *somevar*))
        (test1 (1+ b))))

тут пропущена звездочка у somevar; и за номером 2:

"В данном примере значение переменная *somevar*, которое было ей присвоено в tes2" -- тут пропущена буква t.
(Ответить) (Thread)
[User Picture]
From:lispnik
Date:Май 27, 2010 01:41 am

Re: не то чтобы ошибка...

(Link)
Спасибо, исправил!
(Ответить) (Parent) (Thread)