Регистрация | Войти
Lisp — программируемый язык программирования
Предыдущая Оглавление Следующая

6. Переменные

Следующий базовый строительный блок, с которым нам нужно ознакомиться, — переменные. Common Lisp поддерживает два вида переменных: лексические и динамические1). Эти два типа переменных примерно соответствуют "локальным" и "глобальным" переменным других языков. Однако, это соответствие лишь приблизительное. С одной стороны "локальные" переменные некоторых языков в действительности гораздо ближе к динамическим переменным Common Lisp 2). И, с другой, локальные переменные некоторых других языков имеют лексическую область видимости не предоставляя всех возможностей, предоставляемых лексическими переменными Common Lisp. В частности, не все языки, предоставляющие переменные, имеющие лексическую область видимости, поддерживают замыкания.

Чтобы сделать все еще более запутанным, многие формы, которые работают с переменными, могут использоваться как с лексическими, так и с динамическими переменными. Поэтому я начну с обсуждения некоторых аспектов переменных Lisp, которые применимы к обоим видам переменных, а затем рассмотрю специфические характеристики лексических и динамических переменных. Далее я обсужу оператор присваивания общего назначения Common Lisp — SETF, который используется для присваивания новых значений переменным, а также – почти любым местам, которые могут хранить значения.

Основы переменных

Как и в других языках, в Common Lisp переменные являются именованными местами, которые могут содержать значения. Однако, в Common Lisp переменные не типизированы таким же образом, как в таких языках, как Java или C++. То есть вам не нужно описывать тип объектов, которые может содержать каждая переменная. Вместо этого, переменная может содержать значения любого типа и сами значения содержат информацию о типе, которая может быть использована для проверки типов во время выполнения. Таким образом, Common Lisp является динамически типизированным: ошибки типов выявляются динамически. Например, если вы передадите не число в функцию +, Common Lisp сообщит вам об ошибке типов. С другой стороны, Common Lisp является строго типизированным языком в том смысле, что все ошибки типов будут обнаружены: нет способа представить объект в качестве экземпляра класса, которым он не является3).

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

Один из способов введения новой переменной вы уже использовали при определении параметров функции. Как вы видели в предыдущей главе, при определении функции с помощью DEFUN список параметров определяет переменные, которые будут содержать аргументы, переданные функции при вызове. Например, следующая функция определяет три переменные для хранения своих аргументов: x, y и z.

(defun foo (x y z) (+ x y z))

При каждом вызове функции, Lisp создает новые привязки (bindings) для хранения аргументов, переданных при вызове этой функции. Привязка является проявлением переменной во время выполнения. Отдельная переменная — сущность, на которую вы можете сослаться в исходном коде своей программы — может иметь множество различных привязок за время выполнения программы. Отдельная переменная даже может иметь множество привязок в одно и то же время: параметры рекурсивной функции, например, связываются заново (rebound) при каждом вызове этой функции.

Другой формой, позволяющей вводить новые переменные, является специальный оператор LET. Шаблон формы LET имеет следующий вид:

(let (variable*)
body-form*)

где каждая variable является формой инициализации переменной. Каждая форма инициализации является либо списком, содержащим имя переменной и форму начального значения, либо, как сокращение для инициализации переменной в значение NIL, просто именем переменной. Следующая форма LET, например, связывает три переменные x, y и z с начальными значениями 10, 20 и NIL:

(let ((x 10) (y 20) z)
  ...
)

При вычислении формы LET сначала вычисляются все формы начальных значений. Затем, перед выполнением форм тела, создаются и инициализируются в соответствующие начальные значения новые привязки. Внутри тела LET имена переменных ссылаются на только что вновь созданные привязки. После LET имена продолжают ссылаются на то, на что они ссылались перед LET (если они на что-то ссылались).

Значение последнего выражения тела возвращается как значение выражения LET. Как и параметры функций, переменные, вводимые LET, связываются заново (rebound) каждый раз, когда поток управления заходит в LET5).

Область видимости (scope) параметров функций и переменных LET — область программы, где имя переменной может быть использовано для ссылки на привязку переменной — ограничивается формой, которая вводит переменную. Такая форма (определение функции или LET) называется связывающей формой (binding form). Как вы скоро увидите, два типа переменных (лексические и динамические) используют два несколько отличающихся механизма области видимости, но в обоих случаях область видимости ограничена связывающей формой.

Если вы записываете вложенные связывающие формы, которые вводят переменные с одинаковыми именами, то привязки внутренних переменных скрывают внешние привязки. Например, при вызове следующей функции для параметра x создается привязка для хранения аргумента функции. Затем первая LET создает новую привязку с начальным значением 2, а внутренняя LET создает еще одну привязку с начальным значением 3. Комментарии справа указывают область видимости каждой привязки.

(defun foo (x)
  (format t "Параметр: ~a~%" x)            ; |<------ x - аргумент
 (let ((x 2))                             ; |
   (format t "Внешний LET: ~a~%" x)       ; | |<---- x = 2
   (let ((x 3))                           ; | |
     (format t "Внутренний LET: ~a~%" x)
)
; | | |<-- x = 3
   (format t "Внешний LET: ~a~%" x)
)
     ; | |
 (format t "Параметр: ~a~%" x)
)
          ; |

Каждое обращение к x будет ссылаться на привязку с наименьшей окружающей областью видимости. Как только поток управления покидает область видимости какой-то связывающей формы, привязка из непосредственно окружающей области видимости перестает скрываться и x ссылается уже на нее. Таким образом, результатом вызова foo будет следующий вывод:

CL-USER> (foo 1)
Параметр: 1
Внешний LET: 2
Внутренний LET: 3
Внешний LET: 2
Параметр: 1
NIL

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

Например, в главе 7 вы встретите цикл DOTIMES, простой цикл-счетчик. Он вводит переменную, которая содержит значение счетчика, увеличивающегося на каждой итерации цикла. Например, следующий цикл, печатающий числа от 0 до 9, связывает переменную x:

(dotimes (x 10) (format t "~d " x))

Ещё одной связывающей формой является вариант LET: LET*. Различие состоит в том, что в LET имена переменных могут быть использованы только в теле LET (части LET, идущей после списка переменных), а в LET* формы начальных значений для каждой переменной могут ссылаться на переменные, введенные ранее в списке переменных. Таким образом, вы можете записать следующее:

(let* ((x 10)
       (y (+ x 10))
)

  (list x y)
)

но не так:

(let ((x 10)
      (y (+ x 10))
)

  (list x y)
)

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

(let ((x 10))
  (let ((y (+ x 10)))
    (list x y)
)
)

Лексические переменные и замыкания

По умолчанию все связывающие формы в Common Lisp вводят переменные лексической области видимости (lexically scoped). На переменные лексической области видимости можно ссылаться только в коде, который текстуально находится внутри связывающей формы. Лексическая область видимости должна быть знакома каждому, кто программировал на Java, C, Perl или Python, так как все они предоставляют "локальные" переменные, имеющие лексическую область видимости. Программисты на Algol также должны чувствовать себя хорошо, так как этот язык первым ввел лексическую область видимости в 1960-х.

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

(let ((count 0)) #'(lambda () (setf count (+ 1 count))))

ссылка на count внутри формы LAMBDA допустима в соответствии с правилами лексической области видимости. Однако, анонимная функция, содержащая ссылку, будет возвращена как значение формы LET, и она может быть вызвана с помощью FUNCALL кодом, который не находится в области видимости LET. Так что же произойдет? Как выясняется, если count является лексической переменной, все работает. Привязка count, созданная когда поток управления зашел в форму LET, остается столько, сколько это необходимо, в данном случае до тех пор, пока что-то сохраняет ссылку на функциональный объект, возвращенный формой LET. Анонимная функция называется замыканием (closure), потому что она "замыкается вокруг" привязки, созданной LET.

Ключевым моментом для понимания замыканий является то, что захватывается не значение переменной, а привязка. Поэтому замыкание может не только иметь доступ к значению переменной, вокруг которой оно "замкнуто", но и присваивать ей новые значения, которые будут сохраняться между вызовами замыкания. Например, вы можете захватить замыкание, созданное предыдущим выражением, в глобальную переменную следующим образом:

(defparameter *fn* (let ((count 0)) #'(lambda () (setf count (1+ count)))))

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

CL-USER> (funcall *fn*)
1
CL-USER> (funcall *fn*)
2
CL-USER> (funcall *fn*)
3

Отдельное замыкание может "замыкаться вокруг" нескольких привязок переменных просто ссылаясь на них. Также множество замыканий могут захватывать одну и ту же привязку. Например, следующее выражение возвращает список трех замыканий, первое из которых увеличивает значение привязки count, вокруг которой оно "замкнуто", второе – уменьшает его, а третье – возвращает текущее значение:

(let ((count 0))
  (list
   #'(lambda () (incf count))
   #'(lambda () (decf count))
   #'(lambda () count))
)

Динамические (специальные) переменные

Привязки с лексической областью видимости помогают поддерживать код понятным, путём ограничения области видимости, в которой, буквально говоря, данное имя имеет смысл. Вот почему большинство современных языков программирования используют лексическую область видимости для локальных переменных. Однако, иногда вам действительно может понадобиться глобальная переменная – переменная, к который вы можете обратиться из любой части своей программы. Хотя неразборчивое использование глобальных переменных может привести к "спагетти-коду" также быстро как и неумеренное использование goto, глобальные переменные имеют разумное использование и существуют в том или ином виде почти в каждом языке программирования6). И, как вы сейчас увидите, глобальные переменные Lisp – динамические переменные – одновременно и более удобны, и более гибки.

Common Lisp предоставляет два способа создания глобальных переменных: DEFVAR и DEFPARAMETER. Обе формы принимают имя переменной, начальное значение и опциональную строку документации. После создания переменной с помощью DEFVAR или DEFPARAMETER имя может быть использовано где угодно для ссылки на текущую привязку этой глобальной переменной. Как вы заметили в предыдущих главах, по соглашению, глобальные переменные именуются именами, начинающимися и заканчивающимися *. Далее в этой главе вы увидите, почему очень важно следовать этому соглашению по именованию. Примеры DEFVAR и DEFPARAMETER выглядят следующим образом:

(defvar *count* 0
  "Число уже созданных виджетов."
)


(defparameter *gap-tolerance* 0.001
  "Допустимое отклонение интервала между виджетами."
)

Различие между этими двумя формами состоит в том, что DEFPARAMETER всегда присваивает начальное значение названной переменной, а DEFVAR делает это только если переменная не определена. Форма DEFVAR также может использоваться без начального значения для определения глобальной переменной без установки ее значения. Такая переменная называется несвязанной (unbound).

На деле, вам следует использовать DEFVAR для определения переменных, которые будут содержать данные, которые вы хотите сохранять даже при изменениях исходного кода, использующего эту переменную. Например, представьте, что две переменные, определенные ранее, являются частью приложения управления "фабрикой виджетов"7). Правильным будет определить переменную *count* с помощью DEFVAR, так как число уже созданных виджетов не становится недействительным лишь потому, что мы сделали некоторые изменения в коде создания виджетов8).

С другой стороны, переменная *gap-tolerance* вероятно влияет некоторым образом на поведение самого кода создания виджетов. Если вы решите, что вам нужно меньшее или большее допустимое отклонение и, следовательно, измените значение в форме DEFPARAMETER, вы захотите, чтобы изменение вступило в силу при перекомпиляции и перезагрузке файла.

После определения переменной с помощью DEFVAR или DEFPARAMETER вы можете ссылаться на нее откуда угодно. Например, вы можете определить следующую функцию для увеличения числа созданных виджетов:

(defun increment-widget-count () (incf *count*))

Преимуществом глобальных переменных является то, что вам не нужно передавать их в функции. Большинство языков программирования хранят потоки стандартного ввода и вывода в глобальных переменных именно по этой причине: вы никогда не знаете, когда именно вы захотите напечатать что-либо на стандартный вывод, и вы не хотите, чтобы каждая функция принимала и передавала далее аргументы, содержащие эти потоки, только потому, что нижележащему коду они могут понадобиться.

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

Например, представьте, что вы работаете над программой, содержащей некоторые низкоуровневые функции журналирования, печатающие в поток, хранящийся в глобальной переменной *standard-output*. Теперь представьте, что в какой-то части программы вы хотите перенаправить весь вывод, генерируемый этими функциями в файл. Вы можете открыть файл и присвоить полученный поток переменной *standard-output*. После этого вышеупомянутые функции будут слать свой вывод в этот файл.

Это работает замечательно, пока вы не забудете восстановить исходное значение *standard-output* после завершения действий. Если вы забудете восстановить *standard-output*, весь остальной код программы, использующий *standard-output*, также будет слать свой вывод в файл9).

Но похоже, что то, что вам действительно нужно, это способ обернуть часть кода во что-то говорящее: "Весь нижележащий код (все функции, которые он вызывает, все функции, которые вызывают эти функции, и так далее до функций самого низкого уровня) должны использовать это значение для глобальной переменной *standard-output*". А затем, по завершении работы функции верхнего уровня, старое значение *standard-output* должно быть автоматически восстановлено.

Оказывается, что это именно те возможности, что предоставляет вам другой вид переменных Common Lisp: динамические переменные. Когда вы связываете динамическую переменную, например с LET-переменной или с параметром функции, привязка, создаваемая во время входа в связывающую форму, заменяет глобальную привязку на все время выполнения связывающей формы. В отличие от лексических привязок, к которым можно обращаться только из кода, находящегося в лексической области видимости связывающей формы, к динамическим привязкам можно обращаться из любого кода, вызываемого во время выполнения связывающей формы10). И оказывается, что все глобальные переменные на самом деле являются динамическими.

Таким образом, если вы хотите временно переопределить *standard-output*, это можно сделать просто пересвязав ее, например, с помощью LET.

(let ((*standard-output* *some-other-stream*))
  (stuff)
)

В любом коде, который выполняется в результате вызова stuff, ссылки на *standard-output* будут использовать привязку, установленную с помощью LET. А после того как stuff завершится и поток управления покинет LET, новая привязка *standard-output* исчезнет и последующие обращения к *standard-output* будут видеть привязку, бывшую до LET. В любой момент времени самая последняя установленная привязка скрывает все остальные. Можно представить, что каждая новая привязка данной динамической переменной помещается в стек привязок этой переменной и ссылки на эту переменную всегда используют последнюю установленную привязку. После выхода из связывающей формы созданные в ней привязки убираются из стека, делая видимыми предыдующие привязки11).

Простой пример показывает как это работает:

(defvar *x* 10)
(defun foo () (format t "X: ~d~%" *x*))

DEFVAR создает глобальную привязку переменной *x* со значением 10. Обращение к *x* в foo будет искать текущую привязку динамически. Если вы вызовете foo на верхнем уровне (from the top level), глобальная привязка, созданная DEFVAR, будет единственной доступной привязкой, поэтому будет напечатано 10.

CL-USER> (foo)
X: 10
NIL

Но вы можете использовать LET для создания новой привязки, которая временно скроет глобальную привязку, и foo напечатает другое значение.

CL-USER> (let ((*x* 20)) (foo))
X: 20
NIL

Теперь снова вызовем foo без LET, она опять будет видеть глобальную привязку.

CL-USER> (foo)
X: 10
NIL

Теперь определим новую функцию.

(defun bar ()
  (foo)
  (let ((*x* 20)) (foo))
  (foo)
)

Обратите внимание, что средний вызов foo находится внутри LET, которая связывает *x* с новым значением 20. При вызове bar вы получите следующий результат:

CL-USER> (bar)
X: 10
X: 20
X: 10
NIL

Как вы можете заметить, первый вызов foo видит глобальную привязку со значением 10. Средний вызов видит новую привязку со значением 20. А после LET, foo снова видит глобальную привязку.

Как и с лексической привязкой, присваивание нового значения влияет только на текущую привязку. Чтобы увидеть это, вы можете переопределить foo, добавив присваивание значения переменной *x*.

(defun foo ()
  (format t "Перед присваиванием~18tX: ~d~%" *x*)
  (setf *x* (+ 1 *x*))
  (format t "После присваивания~18tX: ~d~%" *x*)
)

Теперь foo печатает значение *x*, увеличивает его на единицу, а затем печатает его снова. Если вы просто запустите foo, вы увидите следующее:

CL-USER> (foo)
Перед присваиванием X: 10
После присваивания X: 11
NIL

Ничего удивительного. Теперь запустим bar.

CL-USER> (bar)
Перед присваиванием X: 11
После присваивания X: 12
Перед присваиванием X: 20
После присваивания X: 21
Перед присваиванием X: 12
После присваивания X: 13
NIL

Обратите внимание, начальное значение *x* равно 11: предыдущий вызов foo действительно изменил глобальное значение. Первый вызов foo из bar увеличивает глобальную привязку до 12. Средний вызов не видит глобальную привязку из-за LET. А затем последний вызов снова может видеть глобальную привязку и увеличивает ее с 12 до 13.

Так как это работает? Как LET знает, когда связывает *x*, что подразумевается создание динамической привязки вместо обычной лексической? Она знает, так как имя было объявлено специальным12). Имя каждой переменной, определенной с помощью DEFVAR и DEFPARAMETER автоматически глобально объявляется специальным. Это означает, что когда бы вы не использовали это имя в связывающей форме (в форме LET, или как параметр функции, или в любой другой конструкции, которая создает новую привязку переменной, вновь создаваемая привязка будет динамической. Вот почему *соглашение* *по* *именованию* так важно: будет не очень хорошо, если вы используете имя, о котором вы думаете как о лексической переменной, а эта переменная окажется глобальной специальной. С одной стороны, код, который вы вызываете, сможет изменить значение этой связи; с другой, вы сами можете скрыть связь, установленную кодом, находящимся выше по стеку. Если вы всегда будете именовать глобальные переменные, используя соглашение по именованию *, вы никогда случайно не воспользуетесь динамической связью, желая создать лексическую.

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

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

Константы

Еще одним видом переменных, вообще не упомянутых ранее, являются оксюморонические "константные переменные". Все константы являются глобальными и определяются с помощью DEFCONSTANT. Базовая форма DEFCONSTANT подобна DEFPARAMETER.

(defconstant name initial-value-form [ documentation-string ])

Как и в случае с DEFPARAMETER, DEFCONSTANT оказывает глобальный эффект на используемое имя: после этого, имя может быть использовано только для обращения к константе; оно не может быть использовано как параметр функции или быть пересвязано с помощью любой другой связывающей формы. Поэтому многие программисты на Lisp следуют соглашению по именованию и используют для констант имена начинающиеся и заканчивающиеся знаком +. Этому соглашению следуют немного в меньшей степени, чем соглашению для глобальных динамических имен, но оно является хорошей идеей по сходным причинам14).

Ещё один важный момент: несмотря на то что язык позволяет вам переопределять константы путем перевычисления DEFCONSTANT с другой формой начального значения, не определено то, что именно произойдет после такого переопределения. На практике, большинство реализаций требуют, чтобы вы перевычислили любой код, ссылающийся на константу, чтобы изменение вступило в силу, так как старое значение могло быть встроено (inlined). Следовательно, правильным будет использовать DEFCONSTANT для определения только тех вещей, которые действительно являются константами, такие как значение NIL. Для вещей, которые вам может когда-нибудь понадобиться изменить, следует использовать DEFPARAMETER.

Присваивание

После создания привязки вы можете совершать с ней два действия: получить текущее значение и установить ей новое значение. Как вы видели в главе 4, символ вычисляется в значение переменной, которую он именует, поэтому вы можете получить текущее значение просто обратившись к переменной. Для присваивания нового значения привязке используйте макрос SETF, являющийся в Common Lisp оператором присваивания общего назначения. Базовая форма SETF следующая:

(setf place value)

Так как SETF является макросом, он может оценить форму "места", которому он осуществляет присваивание и расшириться (expand) в соответствующие низкоуровневые операции, осуществляющие необходимые действия. Когда "место" является переменной, этот макрос расширяется в вызов специального оператора SETQ, который, как специальный оператор, имеет доступ и к лексическим, и к динамическим привязкам15). Например, для присваивания значения 10 переменной x вы можете написать это:

(setf x 10)

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

(defun foo (x) (setf x 10))

не окажет никакого влияния на любое значение вне foo. Привязка, которая создается при вызове foo, устанавливается в 10, незамедлительно заменяя то значение, что было передано в качестве аргумента. В частности, следующая форма:

(let ((y 20))
  (foo y)
  (print y)
)

напечатает 20, а не 10, так как именно оно является значением y, которое передается foo, где уже является значением переменной x перед тем, как SETF дает x новое значение.

SETF также может осуществить последовательное присваивание множеству "мест". Например, вместо следующего:

(setf x 1)
(setf y 2)

вы можете записать следующее:

(setf x 1 y 2)

SETF возвращает присвоенное значение, поэтому вы можете вкладывать вызовы SETF как в следующем примере, который присваивает и x, и y одинаковое случайное значение:

(setf x (setf y (random 10)))

Обобщенное присваивание

Привязки переменных, конечно, не являются единственными "местами", которые могут содержать значения. Common Lisp поддерживает составные структуры данных, такие как массивы, хэш-таблицы, списки, а также определенные пользователем структуры данных — такие структуры состоят из множества "мест", способных содержать значения.

Я опишу эти структуры данных в последующих главах, но так как мы рассматриваем присваивание, вы должны знать, что SETF может присвоить значение любому "месту". Когда я буду описывать различные составные структуры данных, я буду указывать, какие функции могут использоваться как "места, обрабатываемые SETF" ("SETFable places"). Кратко же можно сказать, что если вам нужно присвоить значение "месту", почти наверняка следует использовать SETF. Возможно даже расширить SETF для того, чтобы он мог осуществлять присваивание определенным пользователем "местам", хотя я не описываю такие возможности16).

В этом отношении SETF не отличается от оператора присваивания = языков, произошедших от C. В этих языках оператор = присваивает новые значения переменным, элементам массивов, полям классов. В языках, таких как Perl и Python, которые поддерживают хэш-таблицы как встроенные типы данных, = может также устанавливать значения элементов хэш-таблицы. Таблица 6-1 резюмирует различные способы, которыми используется = в этих языках.

Таблица 6-1. Присваивание с помощью = в других языках программирования

Присваивание ... Java, C, C++ Perl Python
... переменной x = 10; $x = 10; x = 10
... элементу массива a[0] = 10; $a[0] = 10; a[0] = 10
... элементу хэш-таблицы $hash{'key'} = 10; hash['key'] = 10
... полю объекта o.field = 10; $o->{'field'} = 10; o.field = 10

SETF работает сходным образом: первый "аргумент" SETF является "местом" для хранения значения, а второй предоставляет само значения. Как и с оператором = в этих языках, вы используете одинаковую форму и для выражения "места", и для получения значения17). Таким образом, эквиваленты вышеприведенных в таблице 6-1 присваиваний для Lisp следующие (AREF — функция доступа к массиву, GETHASH осуществляет операцию поиска в хэш-таблице, а field может быть функцией, которая обращается к слоту под именем field определенного пользователем объекта):

Простая переменная:    (setf x 10) 
Массив: (setf (aref a 0) 10)
Хэш-таблица: (setf (gethash 'key hash) 10)
Слот с именем 'field': (setf (field o) 10)

Обратите внимание, что присваиваение с помощью SETF "месту", которое является частью большего объекта, имеет ту же семантику, что и присваивание переменной: "место" модифицируется без оказания какого-либо влияния на объект, который хранился там до этого. И вновь, это подобно тому, как ведет себя = в Java, Perl и Python18).

Другие способы изменения "мест"

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

(setf x (+ x 1))

или уменьшить его так:

(setf x (- x 1))

Но это слегка утомительно по сравнению с стилем C: ++x и

--

x. Вместо этого вы можете использовать макросы INCF и DECF, которые увеличивают и уменьшают "место" на определенную величину, по умолчанию 1.

(incf x)    === (setf x (+ x 1))
(decf x)    === (setf x (- x 1))
(incf x 10) === (setf x (+ x 10))

INCF и DECF являются примерами определенного вида макросов, называемых модифицирующими макросами (modify macros). Модифицирующие макросы являются макросами, построенными поверх SETF, которые модифицируют "места" путем присваивания нового значения, основанного на их текущем значении. Главным преимуществом таких макросов является то, что они более краткие, чем аналогичные операции, записанные с помощью SETF. Вдобавок, модифицирующие макросы определены таким образом, чтобы быть безопасными при использовании с "местами", когда выражение "места" должно быть вычислено лишь единожды. Несколько надуманным примером является следующее выражение, которое увеличивает значение произвольного элемента массива:

(incf (aref *array* (random (length *array*))))

Наивный перевод этого примера в выражение, использующее SETF, может выглядить следующим образом:

(setf (aref *array* (random (length *array*)))
      (1+ (aref *array* (random (length *array*))))
)

Однако, это не работает, так как два последовательных вызова RANDOM не обязательно вернут одинаковое значение: это выражение вероятно получит значение одного элемента массива, увеличит его, а затем сохранит его как новое значение другого элемента массива. Однако, выражение INCF сделает все правильно, так как знает, как правильно разобрать это выражение:

(aref *array* (random (length *array*)))

чтобы извлечь те части, которые возможно могут иметь побочные эффекты, и гарантировать, что они будут вычисляться лишь один раз. В этом случае, выражение INCF вероятно расширится в нечто более или менее подобное этому:

(let ((tmp (random (length *array*))))
  (setf (aref *array* tmp) (1+ (aref *array* tmp)))
)

Вообще, модифицирующие макросы гарантируют однократное вычисление слева направо своих аргументов, а также подформ формы места (place form).

Макрос PUSH, который вы использовали в примере с базой данных для добавления элементов в переменную *db*, является еще одним модифицирующим макросом. Более подробно о его работе и работе POP и PUSHNEW будет сказано в главе 12, где я буду говорить о том, как представляются (represented) списки в Lisp.

И наконец, два слегка эзотерических, но полезных модифицирующих макроса – ROTATEF и SHIFTF. ROTATEF циклически сдвигает значение между "местами". Например, если вы имеете две переменные, a и b, этот вызов:

(rotatef a b)

обменяет значения двух переменных и вернет NIL. Так как a и b являются переменными и вам не нужно беспокоиться о побочных эффектах, предыдущее выражение ROTATEF эквивалентно следующему:

(let ((tmp a)) (setf a b b tmp) nil)

С другими видами "мест" эквивалентное выражение с использованием SETF может быть более сложным.

SHIFTF подобен ROTATEF за исключением того, что вместо циклического сдвига значений, он просто сдвигает их влево: последний аргумент предоставляет значение, которое перемещается в предпоследний аргумент и так далее. Исходное значение первого аргумента просто возвращается. Таким образом, следующее:

(shiftf a b 10)

эквивалентно (и снова, так как вам не нужно беспокоиться о побочных эффектах) следующему:

(let ((tmp a)) (setf a b b 10) tmp)

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

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

1)Динамические переменные также иногда называют специальными переменными по причинам, рассматриваемым далее в этой главе. Очень важно чтобы вы знали об этом синониме, так как некоторые люди (и реализации Lisp) использует один термин, а другие — другой.
2)Ранние реализации Lisp использовали динамические переменные в качестве локальных, по крайней мере при интерпретации. Elisp, диалект Lisp, используемый в Emacs, является в некоторой мере устаревшим в этом отношении, продолжая поддерживать только динамические переменные. Некоторые другие языки повторили этот путь от динамических к лексическим переменным: например, локальные переменные в Perl являются динамическим, в то время как my-переменные, введенные в Perl 5 — лексические. Python никогда не имел настоящих динамических переменных, но ввел настоящую лексическую область видимости только с версии 2.2. (Лексические переменные Python все еще ограничены в сравнении с Lisp из-за объединения присваивания (assignment) и связывания (binding) в синтаксисе языка.)
3)В действительности не совсем корректно говорить о том, что все ошибки типов всегда будут обнаружены: существует возможность использования опциональных деклараций для указания компилятору того, что определенные переменные всегда будут содержать объекты определенного типа, и для отключения проверок типов во время выполнения определенных участков кода.
4)В целях оптимизации определенные виды объектов, такие как целые числа, меньшие определенного размера, и знаки могут быть представлены непосредственно в памяти, другие же объекты будут представляться указателями на действительные объекты. Однако, поскольку целые числа и знаки являются неизменяемыми значениями, не имеет значения тот факт, что может существовать несколько копий "одного и того же" объекта в различных переменных. Это и является корнем различия между EQ и EQL, описанного в главе 4.
5)Переменные в формах LET и параметры функций создаются с помощью одного и того же механизма. На самом деле в некоторых диалектах Lisp (но не в Common Lisp) LET является просто макросом, который расширяется (expands) в вызов анонимной функции. Таким образом, в таких диалектах следующее:
(let ((x 10)) (format t "~a" x))
является формой макроса, которая расширяется в следующее:
((lambda (x) (format t "~a" x)) 10)
6)Java маскирует глобальные переменные как публичные статические поля, C использует внешние (extern) переменные, а переменные уровня модулей в Python и переменные уровня пакетов в Perl аналогично могут быть доступны отовсюду.
7)фабрика — стандартный шаблон проектирования —прим. переводчика
8)Если вам специально нужно сбросить переменную, созданную с помощью DEFVAR, вы можете установить ее напрямую с помощью SETF или сделать ее несвязанной с помощью MAKUNBOUND и перевычислить форму DEFVAR.
9)Метод временного переназначения *standard-output* также плох если система многонитевая (multithreaded): если несколько нитей (threads) управления попытаются напечатать в различные потоки (streams) вывода в одно и то же время, они все попытаются назначить глобальной переменной поток, который они хотят использовать, чем помешают друг другу. Вы можете использовать блокировку для контроля доступа к глобальной переменной, но тогда вы не получите пользы от распараллеливания, так как какая-нибудь нить при печати блокирует остальные нити до завершения этой операции даже если эти нити хотят печатать в другой поток.
10)Технический термин, обозначающий интервал, в течение которого к привязке можно обратиться, — протяженность (extent) привязки. Таким образом, область видимости и протяженность являются взаимодополняющими понятиями: область видимости относится к пространству, а протяженность — ко времени. Лексические переменные имеют лексическую область видимости и неопределенную протяженность, имея ввиду то, что они существуют неопределенное время, определяемое тем, как долго они нужны кому-либо. Динамические переменные, наоборот, имеют неопределенную область видимости, так как они доступны отовсюду, но динамическую протяженность. Для того, чтобы запутать вас еще больше, стоит упомянуть, что комбинацию неопределенной области видимости и динамической протяженности часто неверно называют динамической областью видимости.
11)Хотя стандарт и не указывает, как должна быть реализована многонитевость (multithreading) в Common Lisp, реализации, которые предоставляют ее, следуют практике, установленной на Lisp-машинах, и создают динамические привязки для каждой нити. Обращение к глобальной переменной найдет привязку, либо установленную последней в текущей нити, либо глобальную привяку.
12)Вот почему динамические переменные также иногда называют специальными перменными (special variables)
13)Если вы хотите знать об этом, вы можете взглянуть на DECLARE, SPECIAL и LOCALLY в HyperSpec.
14)Несколько ключевых констант, определенных самим языком, не следуют этому соглашению: в первую очередь T и NIL. Это иногда раздражает, например, когда кто-то хочет использовать t как имя глобальной переменной. Другим примером является PI, которая содержит наилучшую апроксимацию математической константы пи в виде числа с плавающей точкой.
15)Некоторые программисты на Lisp старой школы предпочитают использовать SETQ с переменными, но современный стиль склоняется к использованию SETF для всех операций присваивания.
16)Взгляните на DEFSETF, DEFINE-SETF-EXPANDER для получения дополнительной информации.
17)Широкая распространенность Algol-подобного синтаксиса для присваивания с "местом" слева от = и новым значением справа от него породило термины lvalue, сокращенно от "left value", что означает нечто, чему можно присваивать, и rvalue, означающее нечто, предоставляющее значение. Хакеры компилятора обычно говорят: "SETF рассматривает свой первый аргумент как lvalue".
18)Программисты на C могут хотеть думать о переменных и других "местах" как о содержащих указатель на действительный объект; присваивание переменной просто изменяет то, на какой объект она указывает, а присваивание значения части составного объекта подобно косвенному обращению по указателю к действительному объекту. Программисты на C++ должны обратить внимание, что поведение оператора = в C++ при обращении с объектами (а именно memberwise FIXME копирование) абсолютно отличается.
Предыдущая Оглавление Следующая
@2009-2013 lisper.ru