|
|
В.Водолазкий
|
Функция symbol-function может вызываться с использованием в качестве аргумента любого символа, для которого предикат fboundp возвращает true. При этом стоит помнить, что fboundp возвращает true для символов, ссылающихся на макросы или на специальные формы. | |
Если fboundp возвращает true, но ее аргумент представляет собой макрос или специальную форму, то значение функции symbol-function является неопределенным, однако symbol-function не генерирует сообщение об ошибке. | |
В тех случаях, когда symbol-function используется совместно с setf, новое значение должно иметь тип function. Не допускается присваивать результат применения symbol-function символу, списку или значению, возвращенному symbol-function после применения к макросу или специальной форме. |
Предикат boundp возвращает true, если динамическая (специальная) переменная symbol имеет значение; ы противном
случае возвращается false.
Предикат fboundp возвращает true, если переданный ему аргумент имеет глобальное определение функции. При этом fboundp возвращает true и в тех случаях, когда переданный ему символ ссылается на макрос или на специальную форму. Для выявления (различения) этих случаев могут быть использованы тесты macro-function и special-form-p.
Аргументом предиката special-form-p является символ. Если этот символ содержит определение специальной формы, то возвращается значение отличное от false, в противном случае возвращается false.
Эта оговорка весьма существенна --- обычно, отличное от nil значение представляет собой функцию, которая может быть использована для интерпретации (оценки значения) специальной формы, однако в целом возвращаемое значение зависит от версии Common Lisp.
При этом допускается, что для некоторого символа как special-form-p , так и macro-function вернут true. Так происходит в тех случаях, когда макрос, для повышения производительности системы реализуется также и как макрос. С другой стороны, макроопределение должно быть доступно для использования программами, которые воспринимают только стандартные специальные формы,
Рассматриваемые ниже средства позволяют установить переменной новое
значение, или говоря, более точно, изменить значение, ассоциируемое с текущей
связью некоторого символа).
Специальная форма (setq var1 form1 var2 form2 ...) представляет собой ``простой оператор присвоения значений переменным''. При его выполнении прежде всего оценивается значение form1, и затем результат сохраняется в переменной var1, затем оценивается form2 и результате ее выполнения сохраняется в var2, и так далее... Переменные представляются как символы и интерпретируются как обращения к статическим или динамическим экземплярам, в соответствии с обычными правилами. Это позволяет использовать setq для присвоения значений не только обычным, но также лексическим и специальным переменным.
Функция setq возвращает последнее присвоенное значение, то есть результат оценки последнего аргумента. Ну и в самом крайнем случае, форма (setq) также допустима и возвращает false. Функция должна иметь четное количество аргументов. Например, в фрагменте
(setq x (+ 3 2 1) y (cons x nil))
x устанавливается равным 6, y --- также равным (6),
и само значение setq равно (6). Обратите внимание, что первое присвоение
выполняетося до того, как начнет выполняться второе, что позволяет использовать
уже вычисленные значения.
См. также описание setf , которая считается ``главным оператором присвоения значений'' в Common Lisp, и способна присваивать значения переменным, элементам массивов и т.д.
Форма psetq в целом аналогична форме setq, за исключением того, что все привоения осуществляются одновременно (параллельно). Вначале осуществляется оценка значений всех форм, а уже затем проводится назначение этих значений переменным. Сама форма psetq возвращает false. Например:
(setq a 1)
(setq b 2)
(psetq a b b a)
a ; 2
b ; 1
В этом примере значения a и b в результате параллельного присвоения меняются друг с другом. (В случае, елси одновременно нужно назначить несколько значений переменных, имеет смысл использовать конструкцию do ).
Функция set позволяет измеенить значение динамической (специальной) переменной и приводит к тому, что переменная, адресуемая с помощью symbol назначается в качестве значения value. При этом значение может представлять собой произвольный объект Лиспа.
Изменению подлежит только текущая динамическая связь. Если до этого не было установлено ни одной свзяи, изменется глобальное значение. Например:
(set (if (eq a b) 'c 'd) 'foo)
приведет либо к установке c в foo, либо к установке d в foo, в зависимости от результатов проверки (eq a b).
set возвращает в качестве результата value.
set не может изменить значение локальной (лексически связанной) переменной. Для изменения значений переменных в программах обычно используется специальная форма setq. Функция set особенно полезна при разработке интерпретаторов языков, встраиваемых в Лисп.
Функция makunbound отторгает значение, связываемое с переменной symbol, в результате чего оно приобретает статус ``unbound'' - несвязанной переменной, которая не имеет никакого значения. Функция fmakunbound осуществляет аналогичную операцию по отношению к глобальному определению функции, ассоциируемой с символом symbol.
Например:
(setq a 1)
a ; 1
(makunbound 'a)
a ; Ошибка!
(defun foo (x) (+ x 1))
(foo 4) ; 5
(fmakunbound 'foo)
(foo 4) ; Ошибка!
Обе функции возвращают в результате работы {\it symbol}.
В языке Лисп любая переменная может содержать ссылку на один единственный фрагмент данных, который представляет собой объект Лиспа. Основные операции, которые могут выполняться над переменными, состоят в извлечении этого объекта и присвоении в качестве значения переменной нового объекта взамен старого; эти действия часто называются операциями доступа и обновления . Однако, сама концепция переменных, доступ к которым осуществляется через символы, может быть расширена (обобщена) таким образом, чтобы обеспечить доступ к любым областям (ячейкам) памяти, которые могут использоваться для хранения определенных фрагментов данных, вне зависимости от того, как эта область именована. Примерами таких областей могут служить car и cdr части cons-ячеек, элементы массивов, и компоненты структур.
Для каждого из типов обобщенных переменных обычно обычно реализованы две функции, предназначенные для выполнения операций чтения и записи их значений. Так для переменной обычное упоминание ее имени означает выполнение операции чтения ее значения, а специальная форма setq, примененная к ней, может быть использована для обновления значения. Функция car позволяет прочитать car-часть cons-ячейки, а функция rplaca присваивает ей новое значение. Аналогичным образом, symbol-value осуществляет доступ (чтение) к динамическому значению переменной, связываемой с некоторым символом, а вызов функции set позволяет это значение изменить.
С практической точки зрения удобнее отказаться от концепции двух различных, независимых функций --- вместо этого мы будем рассматривать вызов функции доступа с заданными аргументами просто как обращение к имени name , представляющему собой идентификатор местоположения переменной в массиве памяти, доступном Лисп-системе. В результате запись x мы можем рассматривать как имя переменной (адрес в памяти), а (car x) --- как имя car -части некоей cons-ячейки, которая, в свою очередь, может быть идентифицирована с помощью x. Теперь, вместо того, чтобы утруждать мозги запоминанием двух функций для каждого типа обобщенных переменных, мы будем использовать единообразный армейский подход как в части доступа к значениям переменных (по имени), так и в части присвоения им новых значений --- с помощью макроопределения setf. По большому счету это аналогично тому подходу, который используется при применении специальной формы setq для преобразования имени переменной в форму, которая присваивает этой переменной новое значение. Доказать простоту и универсальность этого подхода нам поможет следующая таблица:
Функция доступа |
Функция обновления |
Обновление с помощью setf |
x |
(setq x datum) |
(setf x datum) |
(car x) |
(rplaca x datum) |
(setf (car x) datum) |
(symbol-value x) |
(set x datum) |
(setf (symbol-value x) datum) |
Как уже упоминалось выше, setf представляет собой макрокоманду, которая проверяет тип формы доступа к переменной, определяет, исходя из этого тип самой переменой, и затем осуществляет вызов соответствующей функции обновления значений.
После введения в Common Lisp макроса setf, использование конструкций setq, rplaca и set уже не является обязательным --- некоторые экстремисты даже рассматривают их как избыточные. Однако эти функции сохранены в Common Lisp не только ``в целях поддержания исторических традиций''. Во-первых, они необходимы для упрощения переносимости кода с других версий Лиспа. Во-вторых, в огромном числе случаев они выполняются быстрее, чем аналогичный вызов setf. И наконец, ``универсальный присваиватель'' setf сам использует эти функции внутри своего определения! С другой стороны, значительная часть других функций присваивания, таких как, например, putprop, представляющая собой функцию для записи свойств, полученных посредством get, исключена из Common Lisp, поскольку вместо нее теперь используется setf.В случае, если при вызове указано больше одной пары place-newvalue, эти пары обрабатываются последовательно.
И с целью обеспечения полноты области определения макроса,допускается использование (setf), который просто возвращает nil.
В макросе setf прилагается немало усилий к тому, чтобы сохранить традиционный порядок оценки субформ ``слева-направо''. С другой стороны, конкретный вид расширения некоторой формы ничем не гарантируется, и даже может зависеть от реализации Лиспа; единственное, что можно утверждать, это то, что расширение формы setf будет представлять собой форму обновления данных, которая будет работать в данной реализации системы, и при этом будет сохранен порядок оценки субформ ``слева-направо''.К числу дополнительных средств, обеспечивающих работу с обобщенными переменными относятся макросы
getf, remf, incf, decf, push, pop assert, ctypecase и ccase.
При этом все эти макросы должны гарантировать
соблюдение ими правил ``очевидной''
семантики: субформы ссылок на обобщенные переменные оцениваются ровно столько
раз, сколько они встречаются в исходных текстах программы, и оценка производится
точно в том же порядке, в котором они появляются в программе.
В качестве примера использования этих семантических правил, укажем, что в ссылке на обобщенную переменную (setf reference value) форма value будет оцениваться после всех субформ в reference , поскольку появляется справа (то есть позже) от нее.Расширение этих макросов должно содержать код, который следует описанным выше правилам. Это достигается путем применения вполне очевидного решения --- введения временных переменных, связанных с субформами, адресующими используемые обобщенные переменные. По мере расширения макроса, когда в дело вступает (если он используется) оптимизатор, эти временные переменные могут быть исключены, если оптимизатору удастся доказать, что их отсутствие не оказывает влияние на генерируемый программный код. Например, нет никакого смысла помещать во временную переменную значение константы. Точно так же, переменная или любая форма, которая не имеет никаких побочных эффектов, так же не нуждается в помещении во временную переменную, если можно доказать, что ее значение не изменяется при изменении значений обобщенной переменной.
В большинстве версий Common Lisp реализованы встроенные средства, осуществляющие
такого рода упрощения генерируемого кода. поскольку эти средства самостоятельно
следят за соблюдением описанных выше семантических правил, пользователь может
не беспокоиться о соблюдении этих правил, что заметно облегчает жизнь при
разработке сложных программ.
Примером такой оптимизации может служить следующий фрагмент:
(incf (ldb byte-field variable))
Этот вызов макроса будет расширен (в зависимости от версии) в конструкцию
вида:
(setq variable
(dpb (1+ (ldb byte-field variable))
byte-field
variable))
В этом примере мы проигнорирем все сложности связанные с возвращением
корректного значения, которое представляет собой инкрементированный на единичку
байт, а вовсе не новое значение variable. Заметьте, что переменная
byte-field оценивается дважды, а на переменную variable
вообще ссылаться приходиться целых три раза!
Теперь рассмотрим фрагмент:
(incf (ldb (aref byte-fields (incf
i))
(aref (determine-words-array) i)))
который должен расшириться в конструкцию:
(let ((temp1 (aref byte-fields (incf i))) (temp2 (determine-words-array))) (setf (aref temp2 i) (dpb (1+ (ldb temp1 (aref temp2 i))) temp1 (aref temp2 i))))вновь мы игнорируем сложности, связанные с возвращением корректного значения. Что нам действительно важно, так это то, что выражения (incf i) и (determine-words-array) не должны дублироваться, поскольку оба могут иметь побочный эффект, либо на них могут воздействовать побочные эффекты от применения других функций.
В целом, код, полученный в результате макрорасширения формы setf, которая принимает в качестве аргумента ссылку на обобщенную переменную, должен выполнять три основных операции:
Осуществляется оценка субформ, которые формируют значения в направлении слева-направо, а затем полученные значения присваиваются временным переменным; эта операция называется временным связыванием. | |
Производится считывание значения обобщенной переменной, с использованием переданной макросу формы доступа, и получается текущее (старое) значение этой переменной; эта операция называется выполнением доступа . Надо иметь в виду, что эта операция выполняется после того, как завершены все оценки, производящиеся на предыдущем шаге, включая все возможные побочные эффекты, к которым это могло привести. | |
Осуществляется связывание сохраненной переменной с новым значением, а затем присвоение этого значения обобщенной переменной с использованием переданной макросу формы обновления. Эта операция называется записью переменной. |
Оценка значения byte-spec и его связь с временной переменной; | |
Временное связывание для place-form; | |
Оценка значения newvalue-form и связывание его со внутренней переменной; | |
Осуществление доступа к place-form; | |
Выполнение сохранения нового значения в place-form , где заданное битовое поле считанного целого числа заменяется значением сохраненной переменной. |
Из этого примера следует, что сами по себе семантические правила работы с обобщенными переменными, вещь хотя и понятная, но достаточно утомительная при ее самостоятельной реализации. Кроме того, существует еще несколько тонкостей, которые заметно усложняют жизнь
Встроенные макросы, такие как setf и push, которые самостоятельно соблюдают правила семантики. | |
Макрос define-modify-macro, который позволяет заметно упростить процедуру создания новых макросов, обслуживающих | |
обобщенные переменные. При этом автоматически обеспечивается соблюдение всех семантических правил. | |
Макрос defsetf, который позволяет легко объявлять новые типы ссылок на обобщенные переменные. При его использовании все семантические правила также соблюдаются автоматически. | |
Макрос define-setf-method и функция get-setf-method , которые осуществляют доступ ко внутренним механизмам Лиспа, в тех случаях, когда необходимо определить новый сложный тип обобщенной переменной или ориентированный на работу с такой переменной макрос. |
Список lambda-list описывает остальные переменные функции function ; эти аргументы будут передаваться функции из оставшихся субформ макро, следующих после обращения к обобщенной переменной. Список lambda-list может содержать маркеры &optional and &rest. Однако, использование маркера &key не допускается, для целей, преследуемых при использовании вызова define-modify-macro вполне достаточно маркера &rest . И последним, необязательным аргументом doc-string является строка документации, описывающая работу с макросом name.
С помощью этого макроса мы можем определить макрос incf как:
(define-modify-macro incf (&optional
(delta 1)) +)
Или, вот пример полезного макроса, который не входит в число входящих в стандартную
библиотеку Common Lisp:
(define-modify-macro unionf (other-set
&rest keywords) union)
Надеюсь, что я не слишком уморил читателя обсуждением тонкостей
установки значений переменных в Лиспе. Но увы, без подобной теоретической
подготовки не обойтись никак. Ведь примеры постепенно становятся все сложнее,
а отвлеккаться каждый раз на обсуждение методов доступа или извлечению свойств
очень не хочется. Поэтому я полагаю, что этот материал будент воспринят вами
благосклонно. В следующий раз мы обсудим,
как же на самом деле в Лиспе производится
вызов функции и как можно "предсказать", читая
текст программы, какое же
из значений, формируемых в ходе вычислений, будет возвращено в результате
вычислений.
Послать письмо voldemarus@narod.ru
|