Динамические и Лексические переменные в Common Lisp - PullRequest
38 голосов
/ 21 января 2009

Я читаю книгу Питера Сейбеля «Практический общий Лисп».

В главе 6, разделы «Переменные» "Лексические переменные и замыкания" и "Динамические, a.k.a. Специальные, переменные". http://www.gigamonkeys.com/book/variables.html

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

Я понимаю, как работают замыкания, но я не понимаю, что такого особенного в данном примере: let

(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 объявляется автоматически глобально особенный.

Что бы произошло, если бы let связал бы x , используя "нормальное лексическое связывание" ? В общем, каковы различия между динамическим и лексическим связыванием и чем этот пример особенный в отношении динамического связывания?

Ответы [ 5 ]

47 голосов
/ 05 марта 2010

Что происходит?

Вы говорите: чувствую, что здесь ничего особенного не происходит. Внешний 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* было объявлено special через DEFVAR выше в вашем коде, конструкция LET вводит новое динамическое связывание для *x*. FOO затем вызывается. Поскольку внутри FOO *x* используется динамическое связывание , он ищет текущее и обнаруживает, что *x* динамически связан с 20.

Значение переменной special найдено в текущей динамической привязке.

Местные СПЕЦИАЛЬНЫЕ декларации

Существуют также локальные special объявления:

(defun foo-s ()
  (declare (special *x*))
  (+ *x* 1))

Если переменная была объявлена ​​ special с помощью 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.

25 голосов
/ 21 января 2009

Когда переменная лексически ограничена , система ищет, где функция определена , чтобы найти значение для свободной переменной. Когда переменная динамически ограничена , система ищет, где функция называется , чтобы найти значение для свободной переменной. Переменные в 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

Как let узнает, является ли переменная лексической или динамической? Это не так. С другой стороны, когда foo идет, чтобы найти значение X, оно сначала найдет лексическое значение, определенное на верхнем уровне. Затем он проверяет, должна ли переменная быть динамической. Если это так, то foo ищет вызывающую среду, которая в этом случае использует let, чтобы затмить значение X равным 4.

(примечание: это упрощение, но оно поможет визуализировать разницу между различными правилами определения объема)

9 голосов
/ 13 февраля 2009

Может быть, этот пример поможет.

;; 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
7 голосов
/ 13 февраля 2009

Вы также можете указать своему Лиспу динамически связывать локальные переменные:

(let ((dyn 5))
  (declare (special dyn))
  ... ;; DYN has dynamic scope for the duration of the body
  )
6 голосов
/ 22 сентября 2012

Пример перезаписи с 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

Еще одно замечательное объяснение от На Лиспе , глава 2.5 Область применения:

Common Lisp - это лексически ограниченный Lisp. Схема - самый старый диалект с лексической областью; до Схемы динамическая область видимости считалась одной из определяющих особенностей Lisp.

Разница между лексической и динамической областью видимости заключается в том, как реализация работает со свободными переменными. Символ связывается в выражении, если он был установлен как переменная, либо путем появления в качестве параметра, либо с помощью операторов привязки к переменной, таких как let and do. Символы, которые не связаны, называются свободными. В этом примере область действия вступает в игру:

(let ((y 7)) 
  (defun scope-test (x)
  (list x y)))

В выражении defun х связан, а у свободен. Свободные переменные интересны, потому что не очевидно, какими должны быть их значения. Нет никакой неопределенности в отношении значения связанной переменной - при вызове scope-test значением x должно быть то, что передано в качестве аргумента. Но каково должно быть значение y? На этот вопрос отвечают правила охвата диалекта.

В динамически ограниченном Лиспе, чтобы найти значение свободной переменной при выполнении scope-test, мы оглядываемся назад по цепочке функций, которые ее вызвали. Когда мы найдем среду, в которой y был связан, эта привязка y будет той, что используется в scope-test. Если мы не найдем ничего, мы примем глобальное значение y. Таким образом, в динамически ограниченном Лиспе y будет иметь значение, которое оно имеет в вызывающем выражении:

> (let ((y 5)) (scope-test 3))
    (3 5)

С динамической областью видимости ничего не значит, что y был связан с 7, когда был определен контекст проверки. Все, что имеет значение, это то, что у было значение 5 при вызове scope-test.

В лексически ограниченном Лиспе вместо того, чтобы оглядываться назад на цепочку вызывающих функций, мы оглядываемся на окружающие среды во время определения функции. В лексически ограниченном Лиспе наш пример поймал бы привязку y, где был определен scope-test. Вот что происходит в Common Lisp:

> (let ((y 5)) (scope-test 3))
    (3 7)

Здесь привязка y к 5 во время вызова не влияет на возвращаемое значение.

Хотя вы все равно можете получить динамическую область видимости, объявив переменную специальной, лексическая область видимости по умолчанию в Common Lisp. В целом сообщество Lisp, похоже, рассматривает передачу динамической области действия с небольшим сожалением. Во-первых, это приводило к ужасно неуловимым ошибкам. Но лексическая сфера - это больше, чем способ избежать ошибок. Как будет показано в следующем разделе, это также делает возможными некоторые новые методы программирования.

...