В статье рассматриваются способы представления переменных и констант в языке Лисп.
Автор: В.Водолазкий
Написал: artish   Дата: 2008-09-08 03:51
Комментарии: (0)   Рейтинг:
Пользовательская оценка (от 1 до 10): Пока не оценено   
Проголосовавших: 0

Common Lisp - переменные и константы

Поскольку часть объектов Лиспа используются для представления программ, нам не всегда предоставляется возможность поместить константу в программу путем простого объявления нового Лисп-объекта --- ведь вводимый объект может быть как константой, так и фрагментом самой программы. Для разрешения этой неоднозначности используется специальная форма quote. Все переменные Common Lisp могут быть разделены на два больших класса --- простые переменные и имена функций. Между ними есть определенное сходство, а в ряде случаев для их определения используются даже сходные средства (например, boundp и fboundp). Однако в большинстве случаев эти два вида переменных используются для решения совершенно разных задач: одни предназначены для обращения к определенным в программе функциям, макросам и специальным формам, то есть объектам, в которых хранится исполняемый код Лисп-программы, а вторые - для обозначения (и обращения) объектов, в которых хранятся данные.

В соответствии с современными представлениями, с каждой функцией ассоциируется ее имя (некоторая конструкция function-name), которая может представлять собой либо символ, либо список из двух элементов, первым элементом которого является символ setf, а второй представляет собой собственно символ, обозначающий ссылку на определение функции. Необходимость введения такого метода именования функции состоит в желании предоставить возможность гибкого расширения общих функций объектно-ориентированного расширения CLOS  методами определенными пользователями. В частности, этот подход позволяет использовать для переопределения методов простую и привычную для начинающих функцию setf. Во многих случаях при разработке программ на Common Lisp, где необходимо передавать в качестве аргумента имя функции вы можете столкнуться с тем, что вместо символа от вас потребуют описанный выше двухэлементный список; например, определение defun в ряде реализаций системы изменено так, что вы можете записать (defun (setf foo) ...), и специальная форма function будет изменена таким образом, чтобы принимать любой список вида function-name.
А в соответствии с традициями, любая функция вызванная, как (setf  f) , должна вернуть одно единственное значение, которое равно ее первому аргументу, чтобы сохранить в силе спецификацию setf, в соответствии с которой эта функция возвращает новое значение установленной переменной.

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

Ссылки

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


Специальная форма -  (quote object)

Вызов (quote  x) всегда возвращает  x. Вычисление (оценка) значения object не производится, а поэтому он может представлять собой произвольный Лисп-объект, что позволяет ввести в программу на Лиспе константу, содержащую любую конструкцию. Например:

(setq a 43)
(list a (cons a 3))                  ; (43 (43 . 3)) 

(list (quote a) (quote (cons a 3))   ; (a (cons a 3))

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

(setq x '(the magic quote hack)) 

интерпретируется модулем чтения read как,

(setq x (quote (the magic quote hack))) 

Специальная форма - ( function fn)

Значение вызова function всегда представляет собой интерпретацию  fn как функции; иначе говоря  fn рассматривается так, как если бы было помещено на месте функционала в вызове функции. Это называется функциональной блокировкой. В частности, если fn представляет собой символ, возвращается ассоциируемое с ним определение функции. Если  fn представляет собой lambda-выражение, возвращается сообщение о лексическом замыкании (``lexical closure''), которое означает, что при вызове этой функции тело lambda-выражения будет выполняться таким образом, как если бы значения всех свободных переменных, используемых в этой функции, остались теми же, что и на момент ее определения.

Результат вызова специальной формы function всегда имеет тип function. Это означает, что форма (function fn) может быть интерпретирована как (the (function  fn)). При попытке применить function к символу, который не обозначает (в локальном или глобальном) контексте функцию, генерируется сообщение об ошибке. Например, не допускается использовать function по отношению к символам, обозначающим макросы или специальные формы. В ряде реализаций Common Lisp сообщение об ошибке в этих случаях не выдается (В сопроводительной документации это объясняется ``for performance reasons...''), но сам вызов все равно не обрабатывается. Иначе говоря, отсутствие сообщения об ошибке не означает, что вы получите ожидаемый результат.

В тех версиях Common Lisp, где имя функции может быть представлено двухэлементным списком  форма function может принимать любое имя функции, как в виде обычного символа, так и в виде такого списка. Поэтому допустимо использование (function (setf cadr)) для обращения к расширению функции setf для cadr. В GNU Common Lisp это не работает

Вот несколько примеров:

 (defun adder (x) (function (lambda (y) (+ x y)))) 

Результатом вызова (adder 3) будет функция, которая будет добавлять число 3 к своему аргументу:

(setq add3 (adder 3))
(funcall add3 5)                 ;  8


Этот фрагмент оказывается работоспособным, поскольку function создает замыкание внутреннего lambda-выражения, что позволяет сослаться на значение 3 переменной x даже после того как работа функции adder будет завершена.

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

(defun two-funs (x) 
  (list (function (lambda () x)) 
     (function (lambda (y) (setq x y))))) 
     (setq funs (two-funs 6)) 
     (funcall (car funs))                   ; 6 
     (funcall (cadr funs) 43)              ; 43 
     (funcall (car funs))                  ; 43

Функция two-funs возвращает список, содержащий две функции, каждая из которых сслыается на связь переменной x, созданной при обращении к two-funs, когда она была вызвана с аргументом 6. Эта связь имеет вначале значение, равное 6, однако setq может эту связь изменить. Лексическое замыкание, созданное для первого lambda-выражения не выполняет ``фиксирование'' значения 6 переменной x при создании замыкания. Вторая функция может изменить связь (в нашем случае, на 43 ) и это измененное значение становится доступным и первой функции тоже!

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

Например,

(let ((x 5) (funs '())) 
  (dotimes (j 10) 
    (push \#'(lambda (z) 
             (if (null z) (setq x 0) (+ x z))) 
           funs)) 
  funs)

Результатом приведенного выше фрагмента станет список, содержащий десять замыканий. Каждое из них использует только связь с x. В каждом случае это одна и та же связь, поэтому все десять замыканий могут быть идентичными (по eq) объектами. Но с другой стороны, результатом выражения:

(let ((funs '())) 
  (dotimes (j 10)
     (let ((x 5)) 
           (push (function (lambda (z) 
                     (if (null z) (setq x 0) (+ x z)))) 
              funs))) 
  funs)

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

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

(let ((funs '())) 
  (dotimes (j 10) 
         (let ((x 5))
              (push (function (lambda (z) (+ x z)))
                     funs))) 
          funs)

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


(let ((funs '()))
(dotimes (j 10)
(push (function (lambda (z) (+ 5 z)))
funs))
funs)

откуда явно следует использование одного и того же замыкания. Поэтому общее правило состоит в том, что Common Lisp вправе выбирать два различных механизма оценки для одной и той же формы \cd{function}. Первый, оптимизирующий механизм, сформирует идентичные в смысле  eq замыкания, если сумеет доказать, что концептуально различные замыкания на самом деле идентичны с точки зрения их поведения. Второй, строгий механизм, предусматривает создание в каждом случае нового замыкания не равного в смысле eq ни какому другому.Довольно часто компилятор оказывается в состоянии доказать, что замыкание вовсе не нуждается в закрытии связей с какими-либо переменными. Например:

(mapcar (function (lambda (x) (+ x 2))) y)

функция (lambda (x) (+ x 2)) не содержит ссылок на какие-либо внешние объекты. В этом случае одно и то же замыкание может с успехом использоваться как значение для всех вызовов специальной формы function. В самом деле, это значение просто не имеет смысла оформлять как лексическое замыкание --- оно может представлять собой простую скомпилированную функцию, которая не содержит (поскольку не нуждается) никакой информации о вычислительном (лексическом) контексте.

Поскольку формы function используются достаточно часто, для них придумано стандартное сокращение: любая форма  f перед которой следуют знаки #' (# за которым следует апостроф) интерпретируется как аргумент (function ), то есть конструкция (function  f) . Например:


(remove-if  #'numberp '(1 a b 3))

обычно воспринимается модулем чтения, как

(remove-if (function numberp) '(1 a b 3))

Функция - (symbol-value symbol)

Функция symbol-value возвращает текущее значение динамической (специальной) переменной, обозначаемой с помощью

символа symbol. Если переданный функции символ не имеет значения, то Лисп генерирует сообщение об ошибке.

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

Функция symbol-value не обеспечивает доступа к значениям лексических переменных. Функция оказывается особенно полезной при реализации интерпретаторов языков, встроенных в Лисп-систему.  В части функций присваивания ей соответствует вызов set; впрочем, symbol-value может использоваться и с setf.

Функция - (symbol-function symbol)

Функция symbol-function возвращает текущее глобальное определение функции, ассоциируемой с символом symbol. Если символ не содержит определения функции, Лисп генерирует сообщение об ошибке. Стоит иметь в виду, что определение может быть как функцией, так и объектом, представляющим специальную форму или макроопределение. Однако, в последнем случае попытка вызвать объект как функцию приведет к ошибке. Если при разработке программы, например, при написании интерпретатора, возникает необходимость единообразно обрабатывать макросы, специальные формы и функции, то целесообразно вначале проверить тип символа с помощью macro-function и special-form-p, а уже затем вызывать его функциональное значение, но только в том случае если оба перечисленных выше теста закончились неудачно (то есть вернули false).

Функция особенно полезна при реализации интерпретаторов языков, встроенных в Лисп-систему.

Функция symbol-function не обеспечивает доступа к значениям лексических имен функций, генерируемых с помощью

flet или labels; она может работать только с глобальными значениями (определениями) функций.

Эти глобальные определения могут быть изменены с помощью применения setf к результату symbol-function. Выполнение этой операции приводит к тому, что символу соответствует  единственное глобальное определение функции; все предыдущие определения, независимо от того, были ли они макросами или функциями, теряются. При этом не допускается переопределение специальных форм.

Поведение symbol-function в целом определяется типом функции, которую содержит ее аргумент.

Функция symbol-function может вызываться с использованием в качестве аргумента любого символа, для которого предикат fboundp возвращает true. При этом стоит помнить, что fboundp возвращает true для символов, ссылающихся на макросы или на специальные формы.

Если fboundp возвращает true, но ее аргумент представляет собой макрос или специальную форму, то значение функции symbol-function является неопределенным, однако symbol-function не генерирует сообщение об ошибке.

В тех случаях, когда symbol-function используется совместно с setf, новое значение должно иметь тип function. Не допускается присваивать результат применения  symbol-function символу, списку или значению, возвращенному symbol-function после применения к макросу или специальной форме.

Предикат - (boundp symbol)

Предикат boundp возвращает true, если динамическая (специальная) переменная  symbol имеет значение; ы противном

случае возвращается false.

Предикат - (fboundp symbol)

Предикат fboundp возвращает true, если переданный ему аргумент имеет глобальное определение функции. При этом fboundp возвращает true и в тех случаях, когда переданный ему символ ссылается на макрос или на специальную форму. Для выявления (различения) этих случаев могут быть использованы тесты macro-function и special-form-p.

Предикат - (special-form-p symbol)

Аргументом предиката special-form-p является символ. Если этот символ содержит определение специальной формы, то возвращается значение отличное от false, в противном случае возвращается false.

Эта оговорка весьма существенна --- обычно, отличное от nil значение представляет собой функцию, которая может быть использована для интерпретации (оценки значения) специальной формы, однако в целом возвращаемое значение зависит от версии Common Lisp.

При этом допускается, что для некоторого символа как  special-form-p , так и macro-function вернут true. Так происходит в тех случаях, когда макрос, для повышения производительности системы реализуется также и как макрос. С другой стороны, макроопределение должно быть доступно для использования программами, которые воспринимают только стандартные специальные формы,

Присвоение значений

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

Специальная форма - (setq var form)

Специальная форма (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 var form)

Форма 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 позволяет измеенить значение динамической (специальной) переменной и приводит к тому, что переменная, адресуемая с помощью symbol назначается в качестве значения  value. При этом значение может представлять собой произвольный объект Лиспа.

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

(set (if (eq a b) 'c 'd) 'foo)

приведет либо к установке c в foo, либо к установке d в foo, в зависимости от результатов проверки (eq a b).

set возвращает в качестве результата   value.

set не может изменить значение локальной (лексически связанной) переменной. Для изменения значений переменных в программах обычно используется специальная форма setq. Функция set особенно полезна при разработке интерпретаторов языков, встраиваемых в Лисп.

Функция - (makunbound symbol)

Функция 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.

Макрос - (setf place newvalue)


Макрос (setf  place  newvalue) использует форму  place, которая позволяет прочитать значение некоторого объекта данных, для синтеза формы, позволяющей установить этому объекту новое значение и выполняет вновь сгенерированную форму с передачей ей newvalue. Для простоты можно считать, что вызов setf расширяется в форму обновления данных, которая сохраняет результат оценки формы newvalue в месте, на которое ссылается форма  place.

В случае, если при вызове указано больше одной пары place-newvalue, эти пары обрабатываются последовательно.

И с целью обеспечения полноты области определения макроса,допускается использование (setf), который просто возвращает nil.

В макросе setf прилагается немало усилий к тому, чтобы сохранить традиционный порядок оценки субформ ``слева-направо''. С другой стороны, конкретный вид расширения некоторой формы ничем не гарантируется, и даже может зависеть от реализации Лиспа; единственное, что можно утверждать, это то, что расширение формы setf будет представлять собой форму обновления данных, которая будет работать в данной реализации системы, и при этом будет сохранен порядок оценки субформ ``слева-направо''.

Главный результат оценки формы setf состоит в получении значения  newvalue. Поэтому (setf (car x) y) не расширяется в конструкцию (rplaca x y), но представляет собой что-нибудь вроде

(let ((G1 x) (G2 y)) (rplaca G1 G2) G2)

Конечно, точная реализация расширения зависит от реализации системы. Вы всегда можете пощупать, что происходит на самом деле с помощью macroexpand . Кроме того, пользователь может определить новые расширения setf с помощью defsetf.

К числу дополнительных средств, обеспечивающих работу с обобщенными переменными относятся макросы

getf, remf, incf, decf, push, pop assert, ctypecase и ccase.

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

В некоторых макросах, использующих ссылки на обобщенные переменные, таких как shiftf, incf, push, и setf обобщенная переменная используется как для чтения, так и для записи. Поэтому сохранение принятого в исходной программе порядка оценки значения и количества этих оценок оказывается исключительно важным для обеспечения правильной работы разрабатываемого программного кода.

В качестве примера использования этих семантических правил, укажем, что в ссылке на обобщенную переменную (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, которая принимает в качестве аргумента ссылку на обобщенную переменную, должен выполнять три основных операции:

 

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

Формы, используемые для указания адресов переменных, такие как  ldb , mask-field и getf допускают использование в качестве аргументов других идентификаторов place. Поэтому при выполнении макрорасширения setf в этих формах, необходимо обязательно вызывать get-setf-method , чтобы определить, как должна быть интерпретирована вложенная обобщенная переменная.

В формах вида


(setf (ldb byte-spec  place-form) newvalue-form)


адрес переменной извлекается из place-form. При этом функция обновления переменной также задается формой place-form, а не объектом типа integer. Поэтому вызов setf должен генерировать код, который выполняет следующие операции:

Оценка значения byte-spec и его связь с временной переменной;
Временное связывание для place-form;
Оценка значения newvalue-form и связывание его со внутренней переменной;
Осуществление доступа к place-form;
Выполнение сохранения нового значения в place-form , где заданное битовое поле считанного целого числа заменяется значением сохраненной переменной.

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

Встроенные макросы, такие как setf и push, которые самостоятельно соблюдают правила семантики.
Макрос define-modify-macro, который позволяет заметно упростить процедуру создания новых макросов, обслуживающих
обобщенные переменные. При этом автоматически обеспечивается соблюдение всех семантических правил.
Макрос defsetf, который позволяет легко объявлять новые типы ссылок на обобщенные переменные. При его использовании все семантические правила также соблюдаются автоматически.
Макрос define-setf-method и функция get-setf-method , которые осуществляют доступ ко внутренним механизмам Лиспа, в тех случаях, когда необходимо определить новый сложный тип обобщенной переменной или ориентированный на работу с такой переменной макрос.

          

Макрос - (define-modify-macro name lambda-list function [doc-string] )

Этот макрос определяет макрос вида ``чтение-модификация-запись'' с названием name. Примером такого макроса может служить хорошо известный incf. Первая субформа сгенерированного макроса будет представлять собой ссылку на обобщенную переменную. Функция function представляет собой полное описание функции, которое должно быть применено к старому содержимому обобщенной переменной для получения нового значения; этот аргумент, по вполне понятным причинам не оценивается.

Список 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)

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



Онлайн :

0 пользователь(ей), 37 гость(ей) :




Реклама на сайте: