Динамические и лексические переменные в Common Lisp
Я читаю книгу "Практический общий Lisp" Питера Сейбеля.
В главе 6 разделы "Переменные"
"Лексические переменные и замыкания" и "Динамические, a.k.a. Special, Variables".
http://www.gigamonkeys.com/book/variables.html
Моя проблема в том, что примеры в обоих разделах показывают, как (пусть...) может теневать глобальные переменные и на самом деле не говорит о различии между динамическими и лексическими варами.
Я понимаю, как работают замыкания, но на самом деле я не понимаю, что в этом примере есть что-то особенное:
(defvar *x* 10)
(defun foo ()
(format t "Before assignment~18tX: ~d~%" *x*)
(setf *x* (+ 1 *x*))
(format t "After assignment~18tX: ~d~%" *x*))
(defun bar ()
(foo)
(let ((*x* 20)) (foo))
(foo))
CL-USER> (foo)
Before assignment X: 10
After assignment X: 11
NIL
CL-USER> (bar)
Before assignment X: 11
After assignment X: 12
Before assignment X: 20
After assignment X: 21
Before assignment X: 12
After assignment X: 13
NIL
Я чувствую, что здесь ничего особенного не происходит. Внешний foo в баре увеличивает глобальный x, а foo, окруженный буквой let, увеличивает затененный x. Какая большая сделка? Я не понимаю, как это должно объяснить разницу между лексическими и динамическими варами. Однако книга продолжается следующим образом:
Итак, как это работает? Как LET знайте, что когда он связывает x, это предполагается создать динамическую привязку а не нормальное лексическое связывание? Он знает, потому что имя было объявлено специальным .12 Название каждого переменная, определенная с помощью DEFVAR и DEFPARAMETER автоматически объявляется глобально специальные.
Что произойдет, если бы связать x с помощью "нормального лексического привязки" ? В целом, каковы различия между динамической и лексической привязкой и как этот пример особенный относительно динамической привязки?
Ответы
Ответ 1
Если переменная лексически ограничена, система ищет, где функция определена, чтобы найти значение для свободной переменной. Когда переменная динамически ограничена, система ищет, где функция называется, чтобы найти значение для свободной переменной. Переменные в Common Lisp по умолчанию лексические; однако динамически измененные переменные могут быть определены на верхнем уровне с помощью defvar или defparameter.
Простейший пример
лексическое охват (с помощью setq):
(setq x 3)
(defun foo () x)
(let ((x 4)) (foo)) ; returns 3
динамическое масштабирование (с defvar):
(defvar x 3)
(defun foo () x)
(let ((x 4)) (foo)) ; returns 4
Как известно, если переменная лексическая или динамическая? Это не так. С другой стороны, когда foo отправляется на поиск значения X, он сначала найдет лексическое значение, определенное на верхнем уровне. Затем он проверяет, должна ли переменная быть динамической. Если это так, то foo смотрит на вызывающую среду, которая в этом случае использует, чтобы затмить значение X равным 4.
(обратите внимание: это упрощение, но это поможет визуализировать разницу между различными правилами определения области)
Ответ 2
Что происходит?
Вы говорите: чувствую, что здесь ничего особенного не происходит. Внешний foo
в bar
увеличивает глобальный x
, а foo
, окруженный let
в bar
, увеличивает затененный x
. Какая большая сделка?
Особый смысл здесь заключается в том, что let
может изменить значение *x*
. Невозможно использовать лексические переменные.
Код объявляет *x*
специальным с помощью DEFVAR
.
В foo
теперь значение *x*
отображается динамически. foo
примет текущее динамическое связывание *x*
или, если его нет, значение символа символа *x*
. Например, новое динамическое связывание может быть введено с помощью let
.
С другой стороны, лексическая переменная должна присутствовать в лексической среде где-то. let
, LAMBDA
, DEFUN
, а другие могут вводить такие лексические переменные. См. Здесь лексическую переменную x
, представленную тремя различными способами:
(let ((x 3))
(* (sin x) (cos x)))
(lambda (x)
(* (sin x) (cos x)))
(defun baz (x)
(* (sin x) (cos x)))
Если наш код:
(defvar x 0)
(let ((x 3))
(* (sin x) (cos x)))
(lambda (x)
(* (sin x) (cos x)))
(defun baz (x)
(* (sin x) (cos x)))
Тогда x
были специальными во всех трех случаях, из-за объявления DEFVAR
, объявляющего x
как особенное - глобально для всех уровней. Из-за этого существует соглашение о объявлении специальных переменных как *x*
. Таким образом, только переменные со звездами вокруг них являются специальными - соглашение. Это полезное соглашение.
В вашем коде у вас есть:
(defun bar ()
(foo)
(let ((*x* 20))
(foo))
(foo))
Так как *x*
объявлен специальным с помощью DEFVAR
выше в вашем коде, конструктор let
вводит новую динамическую привязку для *x*
. foo
затем вызывается. Поскольку внутри foo
используется *x*
динамическое связывание, он просматривает текущий и обнаруживает, что *x*
динамически привязан к 20
.
Значение специальной переменной находится в текущей динамической привязке.
Локальные объявления SPECIAL
Существуют также локальные объявления special
:
(defun foo-s ()
(declare (special *x*))
(+ *x* 1))
Если переменная была объявлена специальной с помощью DEFVAR
или DEFPARAMETER
, то локальное объявление special
может быть опущено.
Лексическая переменная напрямую ссылается на привязку переменной:
(defun foo-l (x)
(+ x 1))
Посмотрите на практике:
(let ((f (let ((x 10))
(lambda ()
(setq x (+ x 1))))))
(print (funcall f)) ; form 1
(let ((x 20)) ; form 2
(print (funcall f))))
Здесь все переменные лексические. В форме 2 let
не будет теневой x
в нашей функции f
. Это невозможно. Функция использует лексическую связанную переменную, введенную LET ((X 10)
. Окружение вызова другой лексически связанной x
в форме 2 не влияет на нашу функцию.
Попробуйте специальные переменные:
(let ((f (let ((x 10))
(declare (special x))
(lambda ()
(setq x (+ x 1))))))
(print (funcall f)) ; form 1
(let ((x 20)) ; form 2
(declare (special x))
(print (funcall f))))
Что теперь? Это работает?
Это не так!
Первая форма вызывает функцию, и она пытается найти динамическое значение x
, и ее нет. Мы получаем ошибку в форме 1: x
является несвязанной, поскольку в ней нет динамического связывания.
Форма 2 будет работать, поскольку let
с объявлением special
вводит динамическую привязку для x
.
Ответ 3
Возможно, этот пример поможет.
;; the lexical version
(let ((x 10))
(defun lex-foo ()
(format t "Before assignment~18tX: ~d~%" x)
(setf x (+ 1 x))
(format t "After assignment~18tX: ~d~%" x)))
(defun lex-bar ()
(lex-foo)
(let ((x 20)) ;; does not do anything
(lex-foo))
(lex-foo))
;; CL-USER> (lex-bar)
;; Before assignment X: 10
;; After assignment X: 11
;; Before assignment X: 11
;; After assignment X: 12
;; Before assignment X: 12
;; After assignment X: 13
;; the dynamic version
(defvar *x* 10)
(defun dyn-foo ()
(format t "Before assignment~18tX: ~d~%" *x*)
(setf *x* (+ 1 *x*))
(format t "After assignment~18tX: ~d~%" *x*))
(defun dyn-bar()
(dyn-foo)
(let ((*x* 20))
(dyn-foo))
(dyn-foo))
;; CL-USER> (dyn-bar)
;; Before assignment X: 10
;; After assignment X: 11
;; Before assignment X: 20
;; After assignment X: 21
;; Before assignment X: 11
;; After assignment X: 12
;; the special version
(defun special-foo ()
(declare (special *y*))
(format t "Before assignment~18tX: ~d~%" *y*)
(setf *y* (+ 1 *y*))
(format t "After assignment~18tX: ~d~%" *y*))
(defun special-bar ()
(let ((*y* 10))
(declare (special *y*))
(special-foo)
(let ((*y* 20))
(declare (special *y*))
(special-foo))
(special-foo)))
;; CL-USER> (special-bar)
;; Before assignment X: 10
;; After assignment X: 11
;; Before assignment X: 20
;; After assignment X: 21
;; Before assignment X: 11
;; After assignment X: 12
Ответ 4
Вы можете указать вашему Lisp также динамически связывать локальные переменные:
(let ((dyn 5))
(declare (special dyn))
... ;; DYN has dynamic scope for the duration of the body
)
Ответ 5
Перепишите пример из PCL.
;;; Common Lisp is lexically scoped by default.
λ (setq x 10)
=> 10
λ (defun foo ()
(setf x (1+ x)))
=> FOO
λ (foo)
=> 11
λ (let ((x 20))
(foo))
=> 12
λ (proclaim '(special x))
=> NIL
λ (let ((x 20))
(foo))
=> 21
Еще одно замечательное объяснение от Вкл Lisp, глава 2.5 Область действия:
Общий Lisp - лексически ограниченный Lisp. Схема - самый древний диалект с лексической сферой; перед Схемой динамическая область рассматривалась как одна из определяющих черт Lisp.
Разница между лексической и динамической областью сводится к тому, как реализация имеет дело со свободными переменными. Символ связан в выражении, если он был установлен как переменная, либо появляясь в качестве параметра, либо операторами смены переменных, такими как let и do. Символы, которые не связаны, считаются свободными. В этом примере область действия вступает в игру:
(let ((y 7))
(defun scope-test (x)
(list x y)))
В выражении defun х привязывается и y является свободным. Свободные переменные интересны, потому что неясно, каковы должны быть их ценности. Нет никакой неопределенности в отношении значения связанной переменной - при вызове scope-test значение x должно быть передано как аргумент. Но какова должна быть величина y? Это вопрос, на который отвечают правила области диалектов.
В динамически ограниченном Lisp, чтобы найти значение свободной переменной при выполнении проверки области видимости, мы оглянемся назад на цепочку функций, которые ее назвали. Когда мы находим среду, в которой y была привязана, эта привязка y будет той, которая используется в тесте scope-test. Если мы не найдем его, мы возьмем глобальное значение y. Таким образом, в динамическом диапазоне Lisp y будет иметь значение, которое оно имело в вызывающем выражении:
> (let ((y 5)) (scope-test 3))
(3 5)
С динамической областью это означает, что y не было привязано к 7, когда был определен метод проверки. Все, что имеет значение, - это то, что у было значение 5, когда был вызван метод проверки.
В лексическом диапазоне Lisp вместо того, чтобы оглядываться по цепочке вызывающих функций, мы смотрим назад на среду, содержащуюся во время определения функции. В лексическом диапазоне Lisp наш пример поймал бы привязку y, где был задан метод определения области. Так вот что произойдет в Common Lisp:
> (let ((y 5)) (scope-test 3))
(3 7)
Здесь привязка y к 5 во время вызова не влияет на возвращаемое значение.
Хотя вы все же можете получить динамическую область, объявив, что переменная является специальной, лексическая область по умолчанию используется в Common Lisp. В целом сообщество Lisp, похоже, с небольшим сожалением рассматривает переход динамической области. Во-первых, это приводило к ужасно неуловимым ошибкам. Но лексический охват - это больше, чем способ избежать ошибок. Как будет показано в следующем разделе, это также позволяет использовать некоторые новые методы программирования.