12.3 Локальные переменные

Глобальные переменные имеют значения, которые сохраняются до тех пор, пока явно не будут заменены новыми значениями. Иногда полезно присвоить переменной локальное значение — значение, которое действует только в определённой части программы на Lisp. Когда переменная имеет локальное значение, говорят, что она локально связана с этим значением и что она является локальной переменной.

Например, когда вызывается функция, её аргументы-переменные получают локальные значения, которыми являются фактические аргументы, переданные при вызове функции; эти локальные привязки действуют внутри тела функции. Другой пример: специальная форма let явно устанавливает локальные привязки для конкретных переменных, которые действуют только внутри тела формы let.

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

Установка локальной привязки сохраняет предыдущее значение переменной (или его отсутствие). Говорят, что предыдущее значение затеняется. Могут затеняться как глобальные, так и локальные значения. Если действует локальная привязка, использование setq для локальной переменной сохраняет указанное значение в локальной привязке. Когда эта локальная привязка перестаёт действовать, ранее затенённое значение (или его отсутствие) восстанавливается.

Переменная может иметь одновременно несколько локальных привязок (например, если есть вложенные формы let, связывающие эту переменную). Текущая привязка — это локальная привязка, которая действительно действует. Она определяет значение, возвращаемое при вычислении символа переменной, и именно с ней работает setq.

В большинстве случаев можно считать текущей привязкой самую внутреннюю локальную привязку или глобальную привязку, если локальной нет. Более точно, правило, называемое правилом области видимости, определяет, где в программе действует локальная привязка. Правило области видимости по умолчанию в Emacs Lisp называется динамической областью видимости, которое просто утверждает, что текущая привязка в любой точке выполнения программы — это самая недавно созданная привязка для этой переменной, которая всё ещё существует. Подробности о динамической области видимости и альтернативном правиле, называемом лексической областью видимости, см. в Правила области видимости для привязок переменных. В последнее время Emacs всё больше переходит на использование лексических привязок во всё большем числе мест с целью сделать лексическую привязку поведением по умолчанию. В частности, все исходные файлы Emacs Lisp и буфер *scratch* используют лексическую область видимости.

Специальные формы let и let* предназначены для создания локальных привязок:

Специальная форма: let (привязки…) формы…

Эта специальная форма устанавливает локальные привязки для определённого набора переменных, как указано в привязках, а затем вычисляет все формы в текстовом порядке. Её возвращаемое значение — это значение последней формы из форм. Локальные привязки, установленные let, действуют только внутри тела форм.

Каждая из привязок — это либо (i) символ, и в этом случае этот символ локально связывается с nil; либо (ii) список вида (символ форма-значения), в котором символ локально связывается с результатом вычисления формы-значения. Если форма-значения опущена, используется nil.

Все формы-значения в привязках вычисляются в том порядке, в котором они перечислены, и до привязывания каких-либо символов к ним. Вот пример: z привязывается к старому значению y, равному 2, а не к новому значению y, равному 1.

(setq y 2)
     ⇒ 2

(let ((y 1)
      (z y))
  (list y z))
     ⇒ (1 2)

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

(let ((x 1)
      (x 2))
  (print x))

Поэтому избегайте связывать одну переменную более одного раза в одной форме let.

Специальная форма: let* (привязки…) формы…

Эта специальная форма похожа на let, но связывает каждую переменную сразу после вычисления её локального значения, перед вычислением локального значения для следующей переменной. Поэтому выражение в привязках может ссылаться на предыдущие символы, связанные в этой форме let*. Сравните следующий пример с примером выше для let.

(setq y 2)
     ⇒ 2

(let* ((y 1)
       (z y))    ; Использует только что установленное значение y.
  (list y z))
     ⇒ (1 1)

По сути, привязка x и y в let* в предыдущем примере эквивалентна использованию вложенных привязок let:

(let ((y 1))
  (let ((z y))
    (list y z)))
Специальная форма: letrec (привязки…) формы…

Эта специальная форма похожа на let*, но все переменные связываются до вычисления каких-либо локальных значений. Затем значения присваиваются локально связанным переменным. Это полезно только при действии лексической привязки и когда вы хотите создать замыкания, которые ссылаются на привязки, которые иначе ещё не действовали бы при использовании let*.

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

(letrec ((hookfun (lambda ()
                    (message "Выполнено один раз")
                    (remove-hook 'post-command-hook hookfun))))
  (add-hook 'post-command-hook hookfun))
Специальная форма: dlet (привязки…) формы…

Эта специальная форма похожа на let, но связывает все переменные динамически. Это редко бывает полезно — обычно вы хотите связывать обычные переменные лексически, а специальные переменные (т.е. переменные, определённые с помощью defvar) динамически, и именно это делает let.

dlet может быть полезен при взаимодействии со старым кодом, который предполагает, что определённые переменные связаны динамически (см. Динамическая привязка), но практически невозможно объявить эти переменные через defvar. dlet временно сделает связываемые переменные специальными, выполнит формы, а затем снова сделает переменные обычными.

Специальная форма: named-let имя привязки &rest тело

Эта специальная форма — циклическая конструкция, вдохновлённая языком Scheme. Она похожа на let: связывает переменные в привязках, а затем вычисляет тело. Однако named-let также связывает имя с локальной функцией, формальными аргументами которой являются переменные из привязок, а телом — тело. Это позволяет телу вызывать себя рекурсивно через имя, при этом аргументы, переданные имени, используются как новые значения связанных переменных в рекурсивном вызове.

Пример цикла, суммирующего список чисел:

(named-let sum ((numbers '(1 2 3 4))
                (running-sum 0))
  (if numbers
      (sum (cdr numbers) (+ running-sum (car numbers)))
    running-sum))
⇒ 10

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

Вызов функции находится в хвостовой позиции, если он выполняется самым последним действием, так что значение, возвращаемое вызовом, является значением самого тела, как в случае рекурсивного вызова sum выше.

named-let может использоваться только при включённой лексической привязке. См. Лексическая привязка.

Вот полный список других средств, создающих локальные привязки:

Переменные также могут иметь буферно-локальные привязки (см. Буферно-локальные переменные); некоторые переменные имеют терминально-локальные привязки (см. Несколько терминалов). Эти виды привязок работают подобно обычным локальным привязкам, но локализуются в зависимости от того, где вы находитесь в Emacs.