Готовый перевод Let Over Lambda - 50 Years of Lisp / Let Over Lambda - 50 лет Lisp-у: Глава 6 - Анафорические Макросы
6.1 Больше Фор?
Анафорические макросы (Anaphoric macros) - это одни из наиболее интересных макросов из книги Пола Грэма On Lisp. Анафорический макрос - это макрос осуществляющий преднамеренный захват переменной из форм, переданных в макрос. Благодаря наличию прозрачных спецификаций (transparent specifications) эти преднамеренно захваченные переменные порождают окна, позволяющие осуществлять контроль над расширением макроса. Через эти окна мы можем манипулировать расширением с помощью комбинаций (combinations).
Классическая анафора, такая как в On Lisp, названа также, как и дословное 1 анафор (anaphor) и во множественном числе, анафора (anaphora). Анафор обозначает захват свободного слова U-Языка (free U-Language word) для использования в последующем U-Языке. В терминах программирования, реализация классической анафоры означает нахождение места в вашем коде - или в коде, который вы хотите написать - где выражения могли бы извлечь выгоду, будучи способными сослаться на результаты предыдущих, связанных выражений. Настоятельно рекомендуется к изучению анафора Грэма и код, связанный с этой анафорой. Особенно обратите внимание на макрос defanaph, позволяющий использовать некоторые интересные типы автоматического анафора (automatic anaphor).
1 Цитирование U-Языка.
По истечении некоторого периода было обнаружено, что alambda - это наиболее полезный анафорический макрос в On Lisp. К тому же alambda - это один из наиболее простых и элегантных примеров, демонстрирующих анафорический макрос и преднамеренный захват переменной.
Листинг 6.1: ALAMBDA
;; alambda Грэма
(defmacro alambda (parms &body body)
`(labels ((self ,parms ,@body))
#'self))
С alambda мы захватываем имя self, таким образом, мы можем использовать его для построения очень анонимной функции. Другими словами рекурсия становится такой же простой как и вызов self. Например, следующая функция возвращает список 2 чисел с n до 1:
2 Если условие возвращает ложное значение, то отсутствующий третий элемент if формы вернёт nil, что в свою очередь является списком.
(alambda (n)
(if (> n 0)
(cons
n
(self (- n 1)))))
Alambda делает наш код более интуитивным и простым для чтения, позволяя нам думать об анонимных функциях как о функциях способных вызывать саму себя с той же лёгкостью, как и добавление единственной буквы 3. Поскольку alambda прозрачно специфицирован для self привязки - и тот факт что единственная причина использования alambda - это применение этой привязки - то нежелательный захват переменной уже не проблема.
3 Это другая причина не шарп-закавычивать лямбда формы. Изменение шарп-закавыченных лямбда форм в alambda формы также потребует удаления двух символов.
Другой удобный анафорический макрос из On Lisp - это aif, макрос, привязывающий результат проверки к it при положительном (вторичном) случае для последующего применения 4. Aif использует удобную COMMON LISP особенность: обобщённые булевы значения (generalised booleans). В COMMON LISP, все не-nil значения являются истинными значениями, поэтому COMMON LISP программисты обычно встраивают интересную информацию в истинные значения. Языки в которых зарезервированы истинные и ложные значения - в частности Scheme - используют явные булевы значения (explicit booleans), которые иногда заставляют вас выводить дополнительную информацию для удовлетворения конструкций с избыточным типом. В Scheme добавлен костыль (kludge) для того, чтобы if, cond, and, or и do работали с не-булевыми 5 значениями [R5RS-P25]. Конечно, COMMON LISP спроектирован правильно - всё является булевым значением.
4 Упражнение: Почему ложный (третичный или альтернативный) вариант никогда не использует эту анафору?
5 С помощью Scheme предиката boolean?.
Листинг 6.2: AIF
;; aif Грэма
(defmacro aif (test then &optional else)
`(let ((it ,test))
(if it ,then ,else)))
Также следует отметить что aif и alambda, как и все анафорические макросы, нарушают лексическую прозрачность. Или иначе, выражаясь модными словами, можно сказать что они не гигиеничные (unhygienic) макросы. Они, как и большое количество макросов в этой книге, вводят невидимые лексические привязки и поэтому не могут быть созданы в макро системах со строгим соблюдением гигиены. Даже большое количество Scheme систем, платформа в которой много экспериментировали с гигиеной, предоставляют макросы в негигиеничном defmacro стиле - возможно, по той причине, что даже разработчики Scheme не воспринимали всерьёз гигиену. Подобно обучению катания на велосипеде, гигиенические системы в своём большинстве игрушки, которые забрасывают после овладевания минимальным уровнем мастерства.
Да, существует много интересных вещей, которые мы можем делать с помощью преднамеренного захвата переменной. Существует множество фор (phors). Эта книга и книга Грэма On Lisp описывает лишь малую часть возможностей, заложенных в эту технику. Из грамотного применения анафорических макросов можно извлечь много новых потрясающих изобретений.
После краткой беседы об анафорах, продемонстрированной считывающими макросами, остаток этой главы описывает узкое приложение анафор к одной из центральных тем этой книги: лексические замыкания - let, окружающий lambda. Большая часть этой главы посвящена интересным анафорическим макросам, предназначенным для изменения, адаптации и расширении замыканий. И хотя данная тема очень удобна в практическом применение для написания кода, мы используем её как платформу для обсуждения возможностей и вариантов анафорических макросов. Применение макросов для расширения концепции замыканий - это, на данный момент, горячо исследуемая тема [FIRST-CLASS-EXTENTS] [OPENING-CLOSURES].
6.2 Шарп-Обратное Закавычивание
Хотя большинство анафор представлено в виде обычных макросов, у считывающих макросов тоже есть возможность использования кода, создающего для нас невидимые привязки. Такие считывающие макросы называются считывающими анафорами (read anaphora). Этот раздел ознакомит вас с таким макросом, который являясь весьма незатейливым, удивил даже меня и который стал одним из наиболее полезных макросов, используемых в этой книге. Я постарался как можно скорее перейти к этому макросу, поскольку он будет использоваться в оставшемся коде. Более того, несколько рассмотренных нами макросов уже используют его.
Листинг 6.3: SHARP-BACKQUOTE
(defun |#`-reader| (stream sub-char numarg)
(declare (ignore sub-char))
(unless numarg (setq numarg 1))
`(lambda ,(loop for i from 1 to numarg
collect (symb 'a i))
,(funcall
(get-macro-character #\`) stream nil)))
(set-dispatch-macro-character
#\# #\` #'|#`-reader|)
Шарп-обратное закавычивание - это считывающий макрос, выполняющий считывание как лямбда форма. По умолчанию, эта лямбда форма получает только один аргумент: a1. Затем считывающий макрос рекурсивно вызывает считывающую функцию для переданного потока. Ниже есть пример с остановленным вычислением (с помощью quote), и мы можем рассмотреть прозрачное введение считывающей анафоры 6:
6 Конечно, префикс захваченного символа, "a", расшифровывается анафорой.
* '#`((,a1))
(LAMBDA (A1)
`((,A1)))
Этот считывающий макрос формирует абстракцию общего шаблона макроса. Например, если у нас есть список переменных и мы хотим создать список let привязок, где каждая переменная привязана к символу, скажем, empty, то мы можем использовать mapcar следующим способом:
* (mapcar (lambda (a)
(list a ''empty))
'(var-a var-b var-c))
((VAR-A 'EMPTY)
(VAR-B 'EMPTY)
(VAR-C 'EMPTY))
Но особенно для сложных списковых структур, такой подход может оказаться довольно грязным, поэтому лисп программисты предпочитают использовать обратное закавычивание для поднятия на один уровень закавычивания:
* (mapcar (lambda (a)
`(,a 'empty))
'(var-a var-b var-c))
((VAR-A 'EMPTY)
(VAR-B 'EMPTY)
(VAR-C 'EMPTY))
Наш новый считывающий макрос вводит анафору и скрывает лямбда форму:
* (mapcar #`(,a1 'empty)
'(var-a var-b var-c))
((VAR-A 'EMPTY)
(VAR-B 'EMPTY)
(VAR-C 'EMPTY))
1 в символе a1 выше говорит о том, что пользователи считывающего макроса могут вводить переменное число анафор, зависящее от числа переданного в параметр numarg считывающего макроса:
* '#2`(,a1 ,a2)
(LAMBDA (A1 A2)
`(,A1 ,A2))
Таким образом мы можем одновременно заmapcar-ить шарп-обратно закавыченные выражения с несколькими списками:
* (let ((vars '(var-a var-b var-c)))
(mapcar #2`(,a1 ',a2)
vars
(loop for v in vars
collect (gensym
(symbol-name v)))))
((VAR-A '#:VAR-A1731)
(VAR-B '#:VAR-B1732)
(VAR-C '#:VAR-C1733))
Ещё один способ думать о шарп-обратном закавычивании: шарп-обратное закавычивание выполняет интерполяцию списка, также, как функция format выполняет интерполяцию строки. Format позволяет нам использовать шаблоны со слотами, заполняемыми значениями отдельных аргументов, аналогично шарп-обратное закавычивание позволяет нам отделить структуру интерполированного списка от значений, с которыми мы хотим их объединить. Благодаря ранее описанному дуализму синтаксиса между лямбда формами в позиции функции в списке и лямбда формами, использующими макрос lambda для расширения в функцию, мы также можем использовать шарп-обратное закавычивание в роли первого элемента при вызове функции:
* (#3`(((,a1)) ,@a2 (,a3))
(gensym)
'(a b c)
'hello)
(((#:G1734)) A B C (HELLO))
В отличие от format шарп-обратное закавычивание не использует последовательное позиционирование. Вместо этого используется числа для наших анафорических привязок. Вследствие этого порядок может перемешиваться и мы можем сращивать привязки более одного раза:
* (#3`(((,@a2)) ,a3 (,a1 ,a1))
(gensym)
'(a b c)
'hello)
(((A B C)) HELLO (#:G1735 #:G1735))
Упражнение: Ссылки на gensym #:G1735 выглядят так, как будто ссылаются на один и тот-же символ, но, конечно, вы никогда не можете утверждать это глядя на напечатанные имена. Являются ли эти символы eq-абельными? Обоснуйте ответ.
6.3 Alet и Машины с Конечным Состоянием.
С lambda и if возможна только одна полезная анафорическая конфигурация. Но большинство интересных типов анафорических макросов используют расширения самыми неожиданными способами. Этот раздел - как и большая часть этой главы - посвящена одному из таких макросов: alet. Какие дополнительные привязки могут быть полезны для форм внутри тела let формы? Let используется для того, чтобы создать привязки к переданным переменным. Однако, макро улучшение let даст возможность получения доступа ко всем переданным формам, даже если тела этих выражений должны вычисляться с новыми привязками. Так какая же часть тела наиболее полезна? В большинстве случаев это последняя форма тела, поскольку результаты этой формы будут возвращены из самого оператора let 7. Мы уже видели что когда мы возвращаем лямбда выражение, ссылающееся на привязки, созданные let-ом, то результатом является лексическое замыкание - объект, часто сохраняемый и используемый для последующего доступа к переменным в let операторе. Таким образом, расширение нашего аналога объекта-замыкания, макрос alet% работает также, как и специальная форма let, с тем отличием, что захватывает символ this из тела и привязывает его к последнему выражению в теле формы - единственное, что будет возвращено как замыкание 8.
7 Поскольку let предоставляет неявный progn.
8 Setq применяется потому что форма привязанная к this определена в лексической области видимости других аргументов заданных через letargs.
Листинг 6.4: ALET-1
(defmacro alet% (letargs &rest body)
`(let ((this) ,@letargs)
(setq this ,@(last body))
,@(butlast body)
this))
Alet% может быть полезен в случае когда у нас есть инициализирующий код в лямбда форме, который мы не хотим дублировать. Поскольку this привязан к возвращаемой лямбда форме, то у нас есть возможность выполнить его перед выходом из окружающего let. Следующий пример - это замыкание, конструкция которого показывает простой пример использования alet% с целью избежания дублирования инициализирующего и сбрасывающего кода:
* (alet% ((sum) (mul) (expt))
(funcall this :reset)
(dlambda
(:reset ()
(psetq sum 0
mul 1
expt 2))
(t (n)
(psetq sum (+ sum n)
mul (* mul n)
expt (expt expt n))
(list sum mul expt))))
#<Interpreted Function>
С помощью этого кода мы можем последовательно изменять значения sum, mul и expt:
* (loop for i from 1 to 5 collect (funcall * 2))
((2 2 4)
(4 4 16)
(6 8 256)
(8 16 65536)
(10 32 4294967296))
Мы можем сбросить замыкание вызвав метод :reset. Заметьте, что благодаря alet%, для сброса в базовые состояния (0 для sum, 1 для mul и 2 для expt) достаточно использовать :reset в одном месте:
* (funcall ** :reset)
NIL
Теперь, когда переменные замыкания были сброшены, мы можем увидеть новую последовательность:
* (loop for i from 1 to 5 collect (funcall *** 0.5))
((0.5 0.5 1.4142135)
(1.0 0.25 1.1892071)
(1.5 0.125 1.0905077)
(2.0 0.0625 1.0442737)
(2.5 0.03125 1.0218971))
Учтите, что alet% меняет порядок вычисления форм в теле let. Если вы посмотрите на расширение, вы увидите, что последняя форма тела вычислена первой, её результат привязан лексической привязке this, а после этого вычисляются предыдущие формы. И пока последний аргумент является константой, изменение порядка вычислений не играет никакой роли. Помните что лямбда выражение 9 является константой и, таким образом, идеально подходит для использования в alet%.
9 Dlambda расширяется в лямбда формы.
Листинг 6.5: ALET
(defmacro alet (letargs &rest body)
`(let ((this) ,@letargs)
(setq this ,@(last body))
,@(butlast body)
(lambda (&rest params)
(apply this params))))
Поскольку нам доступны множество уровней свободы, улучшение этого макроса, как и многие другие улучшения макросов, кажутся нелогичными. И хотя существуют множество возможностей, этот раздел посвящён рассмотрению одного такого специфического улучшения. Alet% можно модифицировать таким образом, чтобы он не возвращал последнюю форму своего тела - про которое мы уже заранее знаем что оно будет лямбда формой - взамен функции, находящей другую функцию внутри лексической области видимости let формы, и вызывающей эту функцию. Иногда это называется косвенностью (indirection), поскольку вместо возвращения функции, выполняющей что либо, мы возвращаем функцию, которая ищет функцию с помощью разыменовывания указателя и после этого использует эту функцию. Косвенность - это понятие, применимое ко всем языкам программирования. Оно позволяет нам менять объекты во время выполнения в то время когда без косвенности, эти объекты являются неизменными во время компиляции. Лисп позволяет нам использовать косвенность в более кратком и удобном стиле чем во многих других языках программирования. Alet - версия alet% с косвенностью, позволяет получить доступ к функции, возвращённой нами как замыкание или заместить эту функцию кодом внутри тела alet или, если мы используем dlambda, как это будет показано позже, даже кодом, находящимся за пределом тела alet.
Теперь, когда мы можем изменить функцию, которая запускается при вызове замыкания с нашим макросом alet, у нас есть возможность создания пары взаимно ссылающихся функций с помощью шаблона под названием alet, окружающий alambda. Если все состояния возвращаются в исходное состояние - вместо того, чтобы переходить в состояния друг в друга - то, alet, окружающий alambda - это удобный способ определения безымянных машин с состоянием.
Следующее типичное замыкание-счётчик получает аргумент n и может менять направление счётчика между инкрементированием и декрементированием на n при передаче символа invert в качестве аргумента взамен числа n:
* (alet ((acc 0))
(alambda (n)
(if (eq n 'invert)
(setq this
(lambda (n)
(if (eq n 'invert)
(setq this #'self)
(decf acc n))))
(incf acc n))))
#<Interpreted Function>
Сохраним это замыкание для того, чтобы мы могли использовать его так часто, насколько это нам будет нужно:
* (setf (symbol-function 'alet-test) *)
#<Interpreted Function>
При первом запуске счётчик идёт на увеличение:
* (alet-test 10)
10
Но, мы можем изменять функцию, на вызов внутреннего лямбда выражения в нашем определении через передачу символа invert замыканию:
* (alet-test 'invert)
#<Interpreted Function>
И теперь мы идём вниз:
* (alet-test 3)
7
И наконец благодаря привязке self предоставляемой alambda мы можем снова сменить вызываемую функцию передав символ invert:
* (alet-test 'invert)
#<Interpreted Function>
Мы вернулись к предыдущему направлению и идём вверх:
* (alet-test 5)
12
Это замыкание будет привязано к символу alet-test относящемуся к пространству функции . Но это замыкание слегка отличается от обычного замыкания. Оба этих замыкания (обычное замыкание и наше замыкание) являются ссылками к одной среде, которое может иметь любое количество указателей, это замыкание использует косвенность для изменения кода, который будет выполнен при вызове. И хотя установленным может быть любой код, получить доступ к лексическим привязкам анафоре this в лексической области alet может только один код. Но, нам по прежнему ничто не мешает установить новое замыкание, с его собственными лексическими привязками и, возможно, с изменённым поведением в косвенной среде, установленной с помощью alet. Большая часть этой главы посвящена полезным приёмам, которые мы можем выполнять с косвенной средой, созданной с помощью alet.
Общая техника работы с макросами неформально известна как выворачивание макроса наизнанку (turning a macro inside out). Когда вы выворачиваете макрос наизнанку вы получаете типичную форму, использующую макрос, подобный макросу который вы хотите создать, и расширяете её. Дальше вы используете расширение как шаблон для вашего желаемого макроса. Например, мы хотели бы получить более универсальный метод создания замыканий с множественными состояниями чем способ с вышеприведённым счётчиком, основанном на alet, окружающем alambda. Вот расширение изнанки в случае с инвертируемым alambda счётчиком:
* (macroexpand
'(alambda (n)
(if (eq n 'invert)
(setq this
(lambda (n)
(if (eq n 'invert)
(setq this #'self)
(decf acc n))))
(incf acc n))))
(LABELS ((SELF (N)
(IF (EQ N 'INVERT)
(SETQ THIS
(LAMBDA (N)
(IF (EQ N 'INVERT)
(SETQ THIS #'SELF)
(DECF ACC N))))
(INCF ACC N))))
#'SELF)
Если мы выполним маленький рефакторинг вышеприведённого расширения с учётом того, что labels позволяет нам создавать множественные привязки функций 10, то получится следующее:
10 Используется множественность labels.
(alet ((acc 0))
(labels ((going-up (n)
(if (eq n 'invert)
(setq this #'going-down)
(incf acc n)))
(going-down (n)
(if (eq n 'invert)
(setq this #'going-up)
(incf acc (- n)))))
#'going-up))
Листинг 6.6: ALET-FSM
(defmacro alet-fsm (&rest states)
`(macrolet ((state (s)
`(setq this #',s)))
(labels (,@states) #',(caar states))))
Здесь нужно учесть что alambda может сделать доступными все тела специальной формы labels для всех функций. И что ещё более важно, теперь у нас есть довольно неплохой шаблон для нашего событийного макроса.
Alet-fsm даёт нам возможность использовать удобный синтаксис для выражения нескольких возможных состояний (states) для нашего замыкания. Это очень тонкий слой сахара из макросов поверх labels, скомбинированный с проходом-по-коду macrolet трансформации, позволяющей нам сымитировать функцию state, изменяющую текущее состояние замыкания через анафору this предоставляемую alet-ом. В качестве примера приведена более чистая версия счётчика с изменяемым направлением роста:
(alet ((acc 0))
(alet-fsm
(going-up (n)
(if (eq n 'invert)
(state going-down)
(incf acc n)))
(going-down (n)
(if (eq n 'invert)
(state going-up)
(decf acc n)))))
Alet-fsm представляет экземпляр техники, которую мы ещё не видели: инъекция анафоры (anaphor injection). Использование этой анафоры нарушает лексическую прозрачность многими способами, что так или иначе, становится лексически невидимым (lexically invisible). Кроме невидимой привязки this alet-ом, также невидимым является использование this макросом alet-fsm. Alet-fsm инъецирует свободную переменную в наш лексический контекст так, что мы не можем её увидеть.
В данном случае стилистические проблемы не строго очерчены, поскольку программирование макросов это конечно не вопрос стиля. Это вопрос мощи. Иногда инъекция свободной переменной может создавать симбиоз между двумя макросами - вместе они смогут создать расширение с более лучшей программной конструкцией чем два изолированных расширения. Поскольку этот тип макро программирования сложен, то опять можно провести параллели с указателями C. Как изучение указателей C порождает сомнительные стилистические приёмы, также происходит и с инъекцией свободной переменной.
11 Как и все стилистические вопросы. Если вы что-либо поймёте в полном объёме, то вопросы стиля становятся неуместными. Инъекция свободной переменной понята не до конца.
Наиболее правдоподобная гипотеза объясняющая трудность понимания инъекции свободной переменной связана с вопросом отказобезопасного поведения 12. С анафорой, в случае когда код переданный пользователем не использует привязку, возможно дальнейшее функционирование кода. Но, есть вероятность, что такой код может отказать без каких-либо предупреждений, а это небезопасно. Однако, когда вы вставите свободную переменную и нет окружения в котором эта переменная может быть захвачена, то всё выражение будет свободным. При возникновении такой ситуации вам нужно решить что делать до того, как вы будете вычислять это выражение. Оно может быть отказобезопасным.
12 Здесь под понятием безопасности, в отличие от реального мира, понимается максимально быстрый и громкий отказ.
Отставим в сторону стиль. Иногда инъекция свободной переменной - это то, что нам нужно для того, чтобы создать коммуникацию между двумя связанными макросами. Инъекция выполняет то же, что и анафора, но в другом направлении. Поскольку вы открываете новый канал коммуникации между вашими макросами, то проблемы сложности масштабируются очень быстро. Рассмотрим нахождение в доме полном хрупкого стекла. Вы можете безопасно швырять предметы в людей снаружи дома, даже если они не озаботятся их ловлей, но, вам лучше убедиться что вы сможете поймать любой объект брошенный в вас.
6.4 Косвенные Цепи
Листинг 6.7: ICHAIN-BEFORE
(defmacro! ichain-before (&rest body)
`(let ((,g!indir-env this))
(setq this
(lambda (&rest ,g!temp-args)
,@body
(apply ,g!indir-env
,g!temp-args)))))
Существуют много способов с помощью которых мы можем извлечь пользу из анафоры this, предоставляемой макросом alet. Поскольку доступ к окружению осуществляется через замыкание-пустышку, перенаправляющее все вызовы к настоящему замыканию, на которое указывает this, то мы можем где угодно передавать ссылку на замыкание-пустышку копируя его так часто, сколько нам это будет нужно. Удобство подобной косвенности в том, что мы можем менять происходящие события при вызове замыкания-пустышки без изменения ссылок на само замыкание-пустышку.
Ichain-before предназначен для расширения в форме alet. Этот макрос добавляет исполнение нового кода до вызова главного замыкания. Вернёмся к нашему примеру со счётчиком, ichain-before позволяет нам добавить новое замыкание, которое печатает предыдущее значение переменной acc до того как это значение будет увеличено:
* (alet ((acc 0))
(ichain-before
(format t "Changing from ~a~%" acc))
(lambda (n)
(incf acc n)))
#<Interpreted Function>
Результат такой, какой и ожидался:
* (funcall * 2)
Changing from 0
2
* (funcall ** 2)
Changing from 2
4
Есть причина по которой в названии ichain-before содержится упоминание цепи (chain). Мы можем вставить сколько угодно исполняемых замыканий:
* (alet ((acc 0))
(ichain-before
(format t "A~%"))
(ichain-before
(format t "B~%"))
(ichain-before
(format t "C~%"))
(lambda (n)
(incf acc n)))
#<Interpreted Function>
Каждое новое звено будет добавляться в начало цепи, в результате появление звеньев цепи будет происходить в обратном порядке:
* (funcall * 2)
C
B
A
2
Иногда статическое добавление косвенных цепей оказывается полезным для того, чтобы избежать реструктуризацию макросов просто добавив вместо этого новый окружающий код. Но наиболее интересные возможности косвенных цепей возникают при их динамическом добавлении. Поскольку мы можем создавать новые функции во время исполнения, а также можем получать доступ ко внутренностям замыкания через анафору, то мы можем переписать работу функций во время исполнения. Ниже представлен простой пример в котором каждый вызов замыкания добавляет код, печатающий "Hello world" при запуске:
* (alet ((acc 0))
(lambda (n)
(ichain-before
(format t "Hello world~%"))
(incf acc n)))
#<Interpreted Function>
Каждый вызов добавляет новое замыкание к косвенной цепи:
* (loop for i from 1 to 4
do
(format t "~:r invocation:~%" i)
(funcall * i))
first invocation:
second invocation:
Hello world
third invocation:
Hello world
Hello world
fourth invocation:
Hello world
Hello world
Hello world
Макрос ichain-after подобен макросу ichain-before за исключением того, что он добавляет замыкания в другой конец исполняемой цепи: после вызова главного замыкания. Ichain-after использует prog1, который последовательно исполняет переданные формы и затем возвращает результат вычисления первой формы.
Ichain-before и ichain-after могут комбинироваться, пример:
* (alet ((acc 0))
(ichain-before
(format t "Changing from ~a~%" acc))
(ichain-after
(format t "Changed to ~a~%" acc))
(lambda (n)
(incf acc n)))
#<Interpreted Function>
* (funcall * 7)
Changing from 0
Changed to 7
7
Листинг 6.8: ICHAIN-AFTER
(defmacro! ichain-after (&rest body)
`(let ((,g!indir-env this))
(setq this
(lambda (&rest ,g!temp-args)
(prog1
(apply ,g!indir-env
,g!temp-args)
,@body)))))
Ichain-before и ichain-after - это макросы, которые инъецируют свободные переменные в свои расширения. Они инъецируют символ this, который предназначен для захвата расширением макроса alet. Такая разновидность инъекции символов может показаться плохим стилем программирования, который способен порождать ошибки, но на самом деле - это общая техника макросов. На деле почти все макросы инъецируют символы в расширение. К примеру: помимо this, макрос ichain-before также инъецируют такие символы как let, setq и lambda предназначенные для сращивания с окружением, в котором расширяется макрос. Разница между такими символами как this и такими предварительно определёнными символами как setq в том, что lambda всегда ссылается к единственному хорошо описанному ANSI макросу, а такие символы как this могут ссылаться к различным объектам в зависимости от сред, в которых они были расширены.
Листинг 6.9: ICHAIN-INTERCEPT-1
(defmacro! ichain-intercept% (&rest body)
`(let ((,g!indir-env this))
(setq this
(lambda (&rest ,g!temp-args)
(block intercept
(prog1
(apply ,g!indir-env
,g!temp-args)
,@body))))))
Ichain-before и ichain-after полезны для того, чтобы метить код, выполняемый до или после исполнения исходного перекрытого выражения. Но это ни в коем случае не единственная операция, которую мы можем выполнять с помощью анафоры this. Другой распространённой операцией является проверка данных замыкания после вызова замыкания.
Ichain-intercept% - это другой макрос, предназначенный для использования внутри формы alet. Идея заключается в том, что мы хотим перехватывать вызовы замыкания и проверять результаты действия замыкания с целью предотвращения некорректных состояний в замыкании.
Так мы можем добавить перехват в наше обычное замыкание-счётчик:
* (alet ((acc 0))
(ichain-intercept%
(when (< acc 0)
(format t "Acc went negative~%")
(setq acc 0)
(return-from intercept acc)))
(lambda (n)
(incf acc n)))
#<Interpreted Function>
Код, установленный при помощи ichain-intercept%, предупредит нас о падении значения счётчика ниже 0:
* (funcall * -8)
Acc went negative
0
Листинг 6.10: ICHAIN-INTERCEPT
(defmacro! ichain-intercept (&rest body)
`(let ((,g!indir-env this))
(setq this
(lambda (&rest ,g!temp-args)
(block ,g!intercept
(macrolet ((intercept (v)
`(return-from
,',g!intercept
,v)))
(prog1
(apply ,g!indir-env
,g!temp-args)
,@body)))))))
Счётчик будет сброшен в 0:
* (funcall ** 3)
3
Наиболее интересным моментом в ichain-intercept% является введение блокирующей анафоры (block anaphor) под названием intercept. Для использования этой анафоры мы применяем return-from. Блок будет возвращать это значение из вызова замыкания перехватывая исходное значение.
Вместо захвата блокирующей анафоры intercept, ichain-intercept создаёт локальный макрос, позволяющий коду внутри ichain-intercept использовать intercept для расширения в return-from где блок определён как gensym.
* (alet ((acc 0))
(ichain-intercept
(when (< acc 0)
(format t "Acc went negative~%")
(setq acc 0)
(intercept acc)))
(lambda (n)
(incf acc n)))
#<Interpreted Function>
Результат тот же, что и в ichain-intercept%:
* (funcall * -8)
Acc went negative
0
* (funcall ** 3)
3
Конечно, прозрачное введение всех этих замыканий в операции может сказаться на производительности во время выполнения. К счастью, современные лисп компиляторы очень хорошо оптимизируют замыкания. Если ваше приложение может разыменовывать несколько указателей - а часто оно это может - косвенные цепи могут быть лучшим способом для их структуризации. Смотрите раздел 7.4, Область Видимости Указателя, там описаны другой взгляд на косвенные цепи. Кроме того, рассмотрите CLOS-овскую функциональность до, после и вокруг.
6.5 Замыкания, Поддерживающие Горячую Замену
В этом разделе было запланировано три цели. Первая, было рассмотрено другое интересное использование анафоры this из alet. Второе, обсудили шаблон alet окружающий dlambda. И наконец, введена полезная макро техника под названием закрытие анафоры.
Для того, чтобы ясно проиллюстрировать закрытие анафоры мы не будем работать с макросом alet и переключимся на изнанку расширения. Alet-hotpatch% - это расширение alet со специальной лямбда формой. Эта лямбда форма проверяет первый аргумент 13 на соответствие с ключевым символом :hotpatch и при обнаружении этого символа заменяет косвенное замыкание на переданный аргумент.
13 Со сравнением указателя.
Листинг 6.11: ALET-HOTPATCH-1
(defmacro alet-hotpatch% (letargs &rest body)
`(let ((this) ,@letargs)
(setq this ,@(last body))
,@(butlast body)
(lambda (&rest args)
(if (eq (car args) ':hotpatch)
(setq this (cadr args))
(apply this args)))))
Возможность замены замыкания использованного в другом выполняющемся замыкании во время работы программы называется горячей заменой (hotpatching). Например, мы создали замыкание с поддержкой горячей замены и, для дальнейшего применения, сохранили его в ячейку символа-функции символа hotpatch-test:
* (setf (symbol-function 'hotpatch-test)
(alet-hotpatch% ((acc 0))
(lambda (n)
(incf acc n))))
#<Interpreted Function>
Возможное применение:
* (hotpatch-test 3)
3
* (hotpatch-test 4)
7
Мы можем заменить лямбда форму - вместе со связанной с ней средой - вызвав это замыкание с символом :hotpatch и заменяющей функцией или замыканием:
* (hotpatch-test
:hotpatch
(let ((acc 0))
(lambda (n)
(incf acc (* 2 n)))))
#<Interpreted Function>
Листинг 6.12: ALET-HOTPATCH
(defmacro alet-hotpatch (letargs &rest body)
`(let ((this) ,@letargs)
(setq this ,@(last body))
,@(butlast body)
(dlambda
(:hotpatch (closure)
(setq this closure))
(t (&rest args)
(apply this args)))))
Теперь замыкание обладает новым, исправленным на лету поведением:
* (hotpatch-test 2)
4
* (hotpatch-test 5)
14
Заметьте как значение счётчика сбросилось в 0 после горячей замены среды замыкания на новое значение для аккумулятора счётчика acc.
Кажется мы уже видели такую разновидность деструктуризации во время исполнения, основанную на символах ключевых слов? Да, на деле мы написали макрос выполняющий то же самое что и в разделе 5.7, Dlambda. Alet-hotpatch - это версия alet-hotpatch% основанный на dlambda. Иногда, даже не осознавая этого, при написании новых макросов мы создаём уже написанные макросы, то есть применяем комбинирование макросов (macro combination). У хорошо спроектированного макроса расширение полностью понятно и хотя возможны разнообразные нарушения лексической прозрачности, проблем с комбинированием не возникает, поскольку все компоненты подходят друг к другу.
Листинг 6.13: LET-HOTPATCH
(defmacro! let-hotpatch (letargs &rest body)
`(let ((,g!this) ,@letargs)
(setq ,g!this ,@(last body))
,@(butlast body)
(dlambda
(:hotpatch (closure)
(setq ,g!this closure))
(t (&rest args)
(apply ,g!this args)))))
Alet-hotpatch создаёт замыкание с поддержкой горячей замены, но в этом макросе есть один концептуальный недостаток. Поскольку alet-hotpatch используется для создания замыканий с горячей заменой, то мы можем забыть что этот макрос вводит анафору this в область видимости переданных форм. Если мы забудем о созданной анафоре, то мы рискуем столкнуться с проблемой нежелательного захвата переменной. Для того, чтобы избежать подобных проблем, мы должны применить технику известную как закрытие анафоры (anaphor closing). При закрытии анафоры мы не меняем способ функционирования анафорического макроса, а просто запрещаем некоторые способы комбинирования этого макроса.
Поскольку мы вывернули расширение alet наизнанку, то мы можем лексически увидеть создание анафоры this в определении alet-hotpatch. И поскольку alet-hotpatch также содержит код, использующий анафору this для реализации горячей замены, то мы можем закрыть анафору так, что символ this больше не будет захватываться макросом. Каким стандартным способом мы избегаем введения нежелательных привязок? Конечно мы именуем привязки с помощью gensym-ов.
Let-hotpatch - это пример закрытия анафоры this в более замкнутую версию - более безопасная версия после реализации всех необходимых нам горячих замен. "A", удалённое из названия макроса, обозначает что этот новый макрос больше не вносит анафору в переданный код. Конечно если нам необходимо по некоторым причинам, отличающимся от простой горячей замены, сослаться на this, то мы должны оставить анафору открытой.
Техника открытия и закрытия анафоры становится второй натурой программиста после того, как он напишет достаточное количество подобных макросов. Также, как мы можем писать макросы, инъецирующие свободные переменные в расширение не задумываясь о том как мы будем их захватывать пока мы пишем лексический контекст в котором они будут расширяться, мы иногда оставляем открытым анафору при разработке макроса для экспериментирования с комбинациями анафорических макросов и инъекции свободных переменных. После того, как будут найдены наиболее полезные комбинации, мы можем произвести слияние макросов и заменить все использованные во время разработки анафоры на gensym-ы. Также, как это делает let-hotpatch, эта техника может использовать defmacro! для перемещения области видимости анафоры с макро расширения в макро определение. Вместо лексического введения анафоры мы ввели другой тип анафоры - такой, который не может работать в целой лексической области видимости расширения, но работает только в другой, более ограниченной области видимости. Эта область видимости будет описываться в следующем разделе.
6.6 Суб-Лексическая Область Видимости
Наш макро-определяющий макрос defmacro! определённый в разделе 3.5, Нежелательный Захват ищет присутствие автоматических gensym-ов в переданном коде с помощью утилиты Грэма flatten. А теперь пришло время признаться в маленькой лжи. До сих пор, пока мы не рассмотрели инъекцию свободной переменной и анафору, мы считали что символьные G-bang имена в defmacro! определениях применимы в лексической области видимости макро определения. На самом деле это не правда - defmacro! предоставляет эти привязки под несколько отличающимся типом области видимости которая называется суб-лексической областью видимости (sub-lexical scope).
Помните, что область видимости обозначает корректность ссылок на переменную, а лексическая область видимости означает что имя применимо к коду в текстовом теле таких создающих привязки конструкций, как let. Важное различие между лексической областью видимости и суб-лексической областью видимости в том, что в лексическая область видимости включает в себя все макрорасширения кода в теле let. Таким образом, высказывание о том, что лексическая область видимости - это создание переменных, доступных только из кода в текстовом теле создающих привязки конструкций является ложным - макросы могут инъецировать ссылки на переменные. Такие переменные инъецируются извне текстового тела конструкций, создающих привязки.
Реализация уникальной текстовой области видимости с ограничением на возможные способы доступа к лексическим переменным равняется суб-лексической области видимости. Ссылки на переменные в суб-лексической области видимости действительны только если представляющие их символы присутствуют в сырых списках, переданных лиспу до макро-расширения.
Поскольку defmacro! производит предварительную обработку полученного кода и создаёт списки всех G-bang символов до расширения кода, то отсюда следует что G-bang символы суб-лексически привязаны. Мы не можем написать макросы, инъецирующие G-bang символы в defmacro! поскольку лексические привязки для G-bang символов никогда не создавались. Вот типичное применение суб-лексического G-bang символа:
* (defmacro! junk ()
`(let ((,g!var))
,g!var))
JUNK
Оба G-bang символа были созданы в суб-лексической области видимости defmacro! таким образом, расширение будет таким, каким мы его ожидали увидеть:
* (macroexpand '(junk))
(LET ()
(LET ((#:VAR1663))
#:VAR1663))
T
Однако, для того, чтобы изучить концепцию суб-лексической области видимости мы определим макрос, инъецирующий G-bang символ:
* (defmacro injector-for-g!var ()
''g!var)
INJECTOR-FOR-G!VAR
Теперь мы можем написать junk2. Junk2 идентичен junk с тем исключением, что мы заменили наши G-bang символы макросом, который расширяется в G-bang символ:
* (defmacro! junk2 ()
`(let ((,(injector-for-g!var)))
,(injector-for-g!var)))
JUNK2
Но поскольку G-band символы привязаны суб-лексически - и поэтому не ищутся в макро расширениях форм - то defmacro! не конвертирует символы в автоматические gensym-ы:
* (macroexpand '(junk2))
(LET ()
(LET ((G!VAR))
G!VAR))
T
И хотя вышеприведённый код по-прежнему функционален, указатели на переменные, находящиеся в суб-лексической области видимости могут вывести из строя выражения, в которых некоторые указатели ссылаются на переменные существующие в суб-лексической области видимости а другие - нет:
* (defmacro! junk3 ()
`(let ((,g!var))
,(injector-for-g!var)))
JUNK3
* (macroexpand '(junk3))
(LET ()
(LET ((#:VAR1672))
G!VAR))
T
Суб-лексическая область видимости удивительно часто появляется в сложных макросах. Кроме defmacro! можно привести по крайней мере один пример: макрос with-all-cxrs из раздела 5.6, Рекурсивные Решения суб-лексически привязывает функции получения элементов списка. В результате работы суб-лексических привязок оказывается что к подобным привязкам невозможно сослаться из макро расширения. Иногда подобное ограничение полезно, иногда нет. В случае с with-all-cxrs суб-лексичность может рассматриваться как нежелательный эффект. Когда наш метдод получения доступа к элементу списка находится в суб-лексической области видимости with-all-cxrs то проблем не возникает:
* (with-all-cxrs
(cadadadr nil))
NIL
Кроме этого мы можем даже написать макросы, расширяющиеся в эти методы получения доступа, поскольку макро определения находятся в суб-лексической области видимости with-all-cxrs:
* (with-all-cxrs
(macrolet ((accessor (l)
`(cadadadr ,l)))
(accessor nil)))
NIL
Но, помните, что что with-all-cxrs привязывает функции получения доступа суб-лексически, поэтому мы не можем инъецировать макрос, в функцию получения доступа:
* (macrolet ((accessor (l)
`(cadadadr ,l)))
(with-all-cxrs
(accessor nil)))
This function is undefined: CADADADR
Теперь, когда мы знакомы с понятием анафор и увидели большое количество примеров сложных макросов - включая и те, которые используют суб-лексическую область видимости - мы можем обсудить интересный теоретический макрос: sublet. Этот макрос создаёт суб-лексические привязки кода, используя синтаксис, подобный синтаксису обычной let формы. Обсуждение sublet мы начнём также, как и обсуждения многих лисп макросов - с утилиты.
Листинг 6.14: LET-BINDING-TRANSFORM
(defun let-binding-transform (bs)
(if bs
(cons
(cond ((symbolp (car bs))
(list (car bs)))
((consp (car bs))
(car bs))
(t
(error "Bad let bindings")))
(let-binding-transform (cdr bs)))))
Let-binding-transform - это простая утилита, обрабатывающая случай когда let форма привязывает единственный символ. В следующем примере a нормализуется до (a):
* (let-binding-transform
'(a (b) (c nil)))
((A) (B) (C NIL))
Кроме того, sublet использует утилиту tree-leaves определённую нами в разделе 5.3, Неявные Контексты. Напомним, что макрос tree-leaves получает три аргумента: произвольную списковую структуру, выражение, которое может использовать переменную x для определения изменяемого листа и другое выражение, которое может использовать другую x и определяющее на что должны быть изменены нужные листья.
Выбрав неявственность привязок x с одним и тем же именем мы получили преимущества дуализма синтаксиса. Если у нас не получается обычными способами выделить общий код в выражении, то иногда мы можем добиться сокращения кода воспользовавшись синтаксическим дуализмом. Определение sublet использует считывающий макрос ссылающийся сам на себя, эти макросы описаны в разделе 4.5, Циклические Выражения. Особенно для таких концепций как функции получения доступа к элементам списка, которые могут меняться много раз в процессе написания кода, считывающие макросы позволяют нам работать только с одним-единственным отображением оператора получения доступа к элементу списка. Благодаря использованию неявственности (implicitisation) с макросом tree-leaves, легко найти и понять дублирование кода.
Листинг 6.15: SUBLET
(defmacro sublet (bindings% &rest body)
(let ((bindings (let-binding-transform
bindings%)))
(setq bindings
(mapcar
(lambda (x)
(cons (gensym (symbol-name (car x))) x))
bindings))
`(let (,@(mapcar #'list
(mapcar #'car bindings)
(mapcar #'caddr bindings)))
,@(tree-leaves
body
#1=(member x bindings :key #'cadr)
(caar #1#)))))
Sublet получает форму, представляющую let привязки и применяет нашу утилиту let-binding-transform, генерирующую в процессе новую списковую структуру. Далее происходит присоединение 14 gensym-а к каждой привязке с печатью соответствующего имени для имени привязки. Sublet расширяется в let форму, привязывающую gensym символы к значениям, переданным в привязываемой форме, затем применяется tree-leaves для замещения всех вхождений имён символов привязок в переданном коде на их соответствующие gensym-ы. Sublet не раскрывает никаких макросов и не разбирает никаких специальных форм в теле на предмет поиска вхождений этих имён символов привязок поскольку sublet создаёт суб-лексические привязки. Например, если все ссылки на a являются суб-лексическими, то они будут замещены на gensym-ы:
14 Присоединяются, а не добавляются, поскольку в этом случае мы можем продолжать поддерживать привязки без значений по умолчанию, например (a).
* (macroexpand
'(sublet ((a 0))
(list a)))
(LET ((#:A1657 0))
(LIST #:A1657))
T
Однако, поскольку суб-лексическая область видимости не предполагает расширения макросов, и само собой разумеется отсутствие интерпретации таких специальных форм как quote, то экземпляры символа a не являющиеся ссылками на переменную также будут меняться:
* (macroexpand
'(sublet ((a 0))
(list 'a)))
(LET ((#:A1658 0))
(LIST '#:A1658))
T
Суб-лексическая область видимости вступает в силу до того как списковая структура будет интерпретирована как лисп код вашим системным проходчиком-по-коду. Это важное наблюдение - поскольку не все последствия этого наблюдения исследованы до конца. Интерпретация кода sublet-ом отличается от интерпретации проходчика-по-коду, осуществляемого COMMON LISP-ом.
Теперь мы стоим на одной из многих граней понимания макросов. Сколько разновидностей интересных типов областей видимости лежит между нераскрытой суб-лексической областью видимости и полностью раскрытой лексической областью видимости? За неимением лучшего имени мы будем называть эту бесконечно большую категорию областей видимости супер суб-лексическими областями видимости (super sub-lexical scopes) 15.
15 Я дал это глупое название по тому что ожидаю что по мере изучения этой концепции появятся более лучшие имена, глубже отображающие суть явления.
Весьма очевидно, что супер суб-лексическая область видимости использует sublet*. Этот макрос основан на sublet но изменяет каждую форму в теле осуществляя макрорасширение этих форм с помощью функции macroexpand-1. Теперь вместо появления в сырой списковой структуре ссылки на символы должны появляться после первого шага макро расширения. Этот тип супер суб-лексической области видимости позволяет макросам инъецировать или удалять ссылки из области видимости внутри тела let формы. Если макросы не выполняют эти операции - или если формы вовсе не являются макросами - то этот тип супер суб-лексической области видимости работает также, как и суб-лексическая область видимости:
Листинг 6.16: SUBLET*
(defmacro sublet* (bindings &rest body)
`(sublet ,bindings
,@(mapcar #'macroexpand-1 body)))
* (macroexpand
'(sublet* ((a 0))
(list a)))
(LET ((#:A1659 0))
(LIST #:A1659))
T
Но мы можем определить другой макрос - инъектор для проверки этой супер суб-лексической области видимости:
* (defmacro injector-for-a ()
'a)
INJECTOR-FOR-A
Sublet* раскроет этот макрос - инъектор:
* (macroexpand-1
'(sublet* ((a 0))
(injector-for-a)))
(SUBLET ((A 0))
A)
T
Что в свою очередь будет суб-лексически интерпретироваться sublet-ом, обозначая присутствие инъецированной переменной внутри супер суб-лексической области видимости предоставляемой sublet*-ом:
* (macroexpand-1 *)
(LET ((#:A1663 0))
#:A1663)
Но вложенные макросы в выражении не раскрываются через macroexpand-1 поэтому sublet* не вставляет их в суб-лексическую область видимости sublet:
* (macroexpand-1
'(sublet* ((a 0))
(list (injector-for-a))))
(SUBLET ((A 0))
(LIST (INJECTOR-FOR-A)))
T
Поэтому a не захватывается суб-лексически 16:
16 Walker:macroexpand-all - это компонент CMUCL и представляет собой завершённый проходчик-по-коду.
* (walker:macroexpand-all *)
(LET ((#:A1666 0))
(LIST A))
С помощью sublet и sublet* мы можем, используя суб-лексические или супер суб-лексические области видимости, произвольно контролировать на каком уровне макро расширения переменная a будет рассматриваться как корректная. Как замечено выше супер суб-лексическая область видимости это почти полностью не исследованный бесконечный класс областей видимости. Способов прохода по коду - большое множество, существует такое же количество множеств супер суб-лексических областей видимости. Этот класс областей видимости подводит нас к другой категории почти неисследованных макросов: макросы, изменяющие работу лисп макросов. Эти макросы способны определять когда следует раскрывать макрос, где ссылки на переменные являются корректными, как интерпретировать специальные формы и т.д. То есть, мы получаем макро-программируемый раскрыватель макросов.
6.7 Пандорические Макросы
Ящик Пандоры (Pandora's box) - это греческий миф о первой в мире женщине: Пандора. Пандора, символ U-Языка переводится с греческого как "всем одарённая". Снедаемая любопытством Пандора открыла маленький ящик и тем самым безвозвратно выпустила на весь мир всё человеческое зло и грехи. Макросы, описанные в этом разделе являются очень мощными и они должны научить вас такому способу программирования, который вы никогда не забудете, и будьте уверены, что получившийся результат будет куда более лучшим чем то, что получила бедная Пандора. Откроем ящик.
Для начала мы отойдём от нашего повествования и ознакомимся с другой знаменитой книгой, посвящённой лиспу: Lisp in Small Pieces [SMALL-PIECES] за авторством Кристиана Кеннека (Christian Queinnec). Кеннек - это широко известный и уважаемый лисп эксперт и он внёс большой вклад в понимание лиспа. Книга Кеннека рассказывает о реализации различной сложности компиляторов и интерпретаторов в и для языка программирования Scheme 17.
17 Иногда через него описываются другие лиспы и их особенности.
В Lisp in Small Pieces есть короткая, но очень интересная дискуссия о макросах. Большая часть посвящена описанию различных вариаций макро системы возможной благодаря неоднозначности макро спецификации Scheme 18, но, кроме того, есть несколько интересных заметок о причинах использования макросов и способах их использования. Если вы прочитали и поняли главу 3, Основы Макросов, то большинство макросов описанных в главе о макросах в Lisp in Small Pieces, для вас будут тривиальными, но, существует один интересный макрос, который нам стоит рассмотреть подробнее.
18 Спасибо, не надо.
Как и многие книги, посвящённые программированию, Lisp in Small Pieces переносит и оставляет нас на реализации системы объектно-ориентированного программирования. Обычно эти реализации служат примером подмножества CLOS, Объектной Системы COMMON LISP. Кеннек называет это подмножество MEROONET. Кеннек отмечает, что при определении методов для MEROONET класса было бы хорошо иметь возможность прямо ссылаться к полям определяемого объекта вместо использования получателей доступа. Слова Кеннека (переведено) [SMALL-PIECES-P340-341]:
Возьмём, к примеру, макрос with-slots из CLOS; мы адаптируем его к контексту MEROONET. Поля объекта - пусть это будут поля экземпляра Point - обрабатываются с помощью чтения и записи через такие функции, как Point-x или set-Point-y!. Будет гораздо проще взаимодействовать с ними по имени их полей, x или y, например в контексте определения метода.
Ниже представлен пример желаемого Кеннеком интерфейса (который он назвал define-handy-method), определяющий новый метод double:
(define-handy-method (double (o Point))
(set! x (* 2 x))
(set! y (* 2 y))
o )
Этот подход более удобен программистам нежели MEROONET синтаксис:
(define-method (double (o Point))
(set-Point-x! o (* 2 (Point-x o)))
(set-Point-y! o (* 2 (Point-y o)))
o )
Другими словами: было бы неплохо использовать макросы для получения доступа к сторонним привязкам - в данном случае слоты объекта - как если бы они были лексическими привязками. И хотя такой подход чрезвычайно удобен в целях сокращения кода, его главное следствие - это способность дать нам дуализм синтаксиса для уже существующих и будущих макросов.
Как замечает Кеннек, COMMON LISP реализует эту функциональность для CLOS с помощью макроса под названием with-slots. Это COMMON LISP-овский пример выполнения того, что было спроектировано: создание абстракций на базе чистой, стандартизированной макро системы. В то время, когда большинство языков спроектированы для того, чтобы быть элегантными, COMMON LISP спроектирован для того, чтобы быть мощным для программирования. Кеннек пришёл к выводу, что ограничения языка делают невозможным подобные ухищрения в Scheme, особенно там, где необходима переносимость:
Листинг 6.17: PANDORICLET
(defmacro pandoriclet (letargs &rest body)
(let ((letargs (cons
'(this)
(let-binding-transform
letargs))))
`(let (,@letargs)
(setq this ,@(last body))
,@(butlast body)
(dlambda
(:pandoric-get (sym)
,(pandoriclet-get letargs))
(:pandoric-set (sym val)
,(pandoriclet-set letargs))
(t (&rest args)
(apply this args))))))
Из-за отсутствия информации о языке и его реализациях мы не можем написать переносимый проходчик-по-коду в Scheme, поэтому нам пришлось отказаться от написания define-handy-method.
Хотя COMMON LISP предоставляет большое количество корректных способов реализации макро систем, он спроектирован с учётом поддержки универсальных инструментов мета-программирования, которые способны работать как в стандартном, так и в переносимом режимах. Существует две мощных макро особенности COMMON LISP-а, позволяющих реализовывать такую функциональность как CLOS-овское with-slots, - это обобщённые переменные (generalised variables) и символьные макросы (symbol macros). В этом разделе мы покажем вам слияние возможностей COMMON LISP-а и к какому замечательному результату это приведёт, кроме того, мы объединим воедино всё что мы уже узнали в отношении анафорических макросов и в процессе мы откроем интересный класс макросов под названием пандорические макросы (pandoric macros).
Идея лежащая в основе pandoriclet - это открытые замыкания, дающие возможность доступа извне к их перекрытым лексическим переменным. Также как и предыдущие макросы, такие как alet-hotpatch, pandoriclet компилируется в косвенную среду, которая по разному реагирует на различные переданные аргументы.
Мы опять начали с изнанки расширения alet, не забывая о присутствии анафоры под названием this. Pandoriclet похож на уже виденные нами остальные макросы. Как и все наши анафорические варианты let, мы предполагаем что финальной формой тела pandoriclet будет лямбда форма. Как и alet-hotpatch pandoriclet использует макрос dlambda для осуществления диспетчеризации между различными возможными участками исполняемого кода при возвращении замыкания из вызова pandoriclet. Pandoriclet использует утилиту let-binding-transform, представленную в предыдущем разделе для работы с null привязками - такими как (let (a) ...). Эта функция утилита необходима для pandoriclet по той же причине, по которой она была необходима для sublet: эти макросы осуществляют проход-по-коду переданных привязок let, в то время как предыдущие макросы слепо сращивались с привязками в другой let.
У нас присутствует два вызова списко-создающих функций утилит, которые ещё не были определены: pandoriclet-get и pandoriclet-set, каждая из которых получает список let привязок. Мы можем ссылаться на ещё не определённые функции до расширения макроса что, очевидно, не произойдёт до тех пор, пока мы не используем макрос. Использование вспомогательных функций, облегчающих определение макросов - это хорошая привычка и эту привычку можно взять на вооружение. Она может не только улучшить читабельность ваших определений, но также может помочь при тестировании компонентов макроса и доказать пригодность использования в будущих макросах. Наиболее примечательная черта такой разновидности абстракций в том, что при комбинировании макросов мы продолжаем поддерживать доступность лексического контекста для остальных используемых утилит.
Поэтому мы написали pandoriclet-get и pandoriclet-set с учётом лексического контекста. Pandoriclet-get мы написали с учётом того, что dlambda привязывает переменную sym к сращиванию нашего списка. Дальше case форма производит сравнение sym с символами, переданными в pandoriclet 19. Если мы найдём символ, то будет возвращено значение на которое ссылается привязка. Если нет, то будет сгенерирована ошибка. Pandoriclet-set похож на pandoriclet-get, с той разницой, что dlambda привязывает ещё один используемый символ: val. Pandoriclet-set использует setq для изменения привязок в зависимости от sym на val.
19 Напомним, что case с символами компилируется в сравнение с одним указателем для каждого case-случая.
Листинг 6.18: PANDORICLET-ACCESSORS
(defun pandoriclet-get (letargs)
`(case sym
,@(mapcar #`((,(car a1)) ,(car a1))
letargs)
(t (error
"Unknown pandoric get: ~a"
sym))))
(defun pandoriclet-set (letargs)
`(case sym
,@(mapcar #`((,(car a1))
(setq ,(car a1) val))
letargs)
(t (error
"Unknown pandoric set: ~a"
sym val))))
Pandoriclet предоставляет тот-же интерфейс, что и все наши анафорические реализации let, поэтому мы можем использовать его для создания простого замыкания - счётчика:
* (setf (symbol-function 'pantest)
(pandoriclet ((acc 0))
(lambda (n) (incf acc n))))
#<Interpreted Function>
Что работает так, как и задумывалось:
* (pantest 3)
3
* (pantest 5)
8
Однако, сейчас у нас есть прямой доступ к привязке под названием acc, с помощью которого было создано замыкание:
* (pantest :pandoric-get 'acc)
8
И мы можем также просто поменять значение этой привязки:
* (pantest :pandoric-set 'acc 100)
100
* (pantest 3)
103
Здесь доступно даже значение анафоры this, поскольку мы преднамеренно оставили анафору открытой и добавили символ this в список привязок letargs при раскрытии макроса:
* (pantest :pandoric-get 'this)
#<Interpreted Function>
Поэтому это замыкание, которое мы создали с помощью pandoriclet, - больше не является закрытым. Среда используемая этим замыканием - даже если все лексические символы значений были удалены компилятором - по прежнему доступны через нашу анонимную функцию, возвращённую от pandoriclet. Как это работает? В случае с пандорическими макросами дополнительный код компилируется так, чтобы предоставить способ получения доступа к замыканию снаружи. Но мощь пандорических макросов невозможно понять изучая низкоуровневую картину происходящего. То что мы создали называется протоколом взаимодействия замыканий (inter-closure protocol) или системой передачи сообщений, предназначенной для коммуникации между замыканиями.
Прежде чем мы продолжим изучение пандорических макросов, мы должны уделить внимание одной из наиболее важных примеров двойственности синтаксиса в COMMON LISP: обобщённым переменным. Детали обобщённых переменных сложны и я бы не хотел описывать их все здесь. Для этого я рекомендую книгу Грэма On Lisp - лучшую книгу, посвящённую разбору этой темы. В конечном счёте всё сводится к простой идее: доступ к обобщённой переменной синтаксически двойственен установлению её значения. У вас есть только одна, устанавливающая значения форма, setf, которая может устанавливать все типы переменных используя тот же синтаксис, который вы используете для получения доступа к ним.
Листинг 6.19: GET-PANDORIC
(declaim (inline get-pandoric))
(defun get-pandoric (box sym)
(funcall box :pandoric-get sym))
(defsetf get-pandoric (box sym) (val)
`(progn
(funcall ,box :pandoric-set ,sym ,val)
,val))
Например, при работе с обычной переменной вы традиционно получаете её значение через её символ, к примеру, x. Для присваивания какого либо значения вы можете использовать (setf x 5). Аналогично, для получения доступа к car слоту cons ячейки с названием, скажем, x вы используете (car x). Для присвоения значения вы можете использовать форму (setf (car x) 5). Такой способ скрывает тот факт, что правильный способ изменить данные cons ячейки - это использование функции rplaca. Реализовав подобную двойственность синтаксиса мы сократили более чем половину операторов получения доступа/присваивания значений и теперь нет нужды их всех запоминать, и что ещё более важно, у нас появляются новые способы использования макросов.
Функция get-pandoric - это обёртка вокруг получения синтаксиса протокола взаимодействия замыканий. Она объявлена встраиваемой для того, чтобы устранить любые влияния на производительность, которые может оказывать эта обёртка.
Defsetf - это интересный COMMON LISP макрос, в отличие от нашего defmacro!, который расширяется в defmacro и неявно привязывает gensym-ы вокруг переданной формы. Defsetf прекрасно работает для определения присваивающей стороны обобщённой переменной если присваиватель можно выразить как функцию или макрос, который вычисляет все свои аргументы только один раз. Get-pandoric можно было бы определить как макрос, но мы этого не сделали встраивания. Макросы не для встраивания, компиляторы для встраивания.
Вернёмся к нашему пандорическому счётчику, сохранённому в символ-функцию pantest, мы можем использовать эту новую функцию-получатель для получения текущего значения pantest-овской привязки acc:
* (get-pandoric #'pantest 'acc)
103
Теперь благодаря обобщённым переменным и defsetf мы можем использовать синтаксический дуализм и поменять значение переменной:
* (setf (get-pandoric #'pantest 'acc) -10)
-10
* (pantest 3)
-7
Среды над функциями - вот что подразумевает let в let, окружающий lambda, - начинают выглядеть как просто обобщённые переменные с несложным доступом, такие как cons ячейка или элемент хэш таблицы. Замыкания стали ещё более первоклассными структурами данных чем в начале. Привязки которые были в начале закрыты для внешнего кода теперь широко доступны для разнообразных операций, даже если эти привязки были скомпилированы для чего-то другого и имеют забытые символы доступа.
Но, любая дискуссия о обобщённой переменной будет неполной без упоминания её ближайшего родственника: символьного макроса (symbol macros). Symbol-macrolet, как следует из названия, позволяет нам расширять символы в общие лисп формы. Поскольку интуитивным и наиболее гибким использованием формы, представляющей макро трансформации 20, является рассмотрение формы как функции, то symbol-macrolet главным образом предназначен для одной цели: символьные макросы дают нам возможность скрывать обобщённые переменные таким образом, что пользователи макроса будут считать что получают доступ к обычным лексическим переменным.
20 Символьные макросы не получают аргументов, поэтому определение символьного макроса всегда расширяется в самого себя.
Введение символьных макросов привело к появлению одного из наиболее странных костылей (kludges) в языке COMMON LISP: обычно при присваивании значения переменной через обычный символ, например (setf x t), setf будет расширяться в форму setq, поскольку это именно то, для чего и был предназначен setq: присваивание значений лексическим и динамическим переменным (на которые всегда будут ссылаться по символам). Поскольку пециальная форма setq не может присваивать значения обобщённым переменным, но, при введении символьных макросов стало возможным выражать с помощью символов не только лексические/динамические привязки, но и любые обобщённые переменные, и тут появилась необходимость в конвертации setq форм обратно в setf формы. Как это ни странно, но это было правильным решением, поскольку такой подход позволяет макросам полностью скрывать присутствие обобщённых переменных от пользователя макроса, даже если он решит использовать setq. По настоящему правильным решением было бы удаление избыточной setq из языка в пользу более универсального setf, но, это не произойдёт во-первых, по очевидным причинам совместимости, а во-вторых, поскольку при создании макроса setq можно использовать в целях безопасности - setf плюс проверка на то что символ был сращен и не представляет списковую форму. При использовании setq помните что он полезен только для обеспечения безопасности сращивания; как мы уже видели, благодаря symbol-macrolet символ может ссылаться на любую обобщённую переменную.
Листинг 6.20: WITH-PANDORIC
(defmacro! with-pandoric (syms o!box &rest body)
`(symbol-macrolet
(,@(mapcar #`(,a1 (get-pandoric ,g!box ',a1))
syms))
,@body))
Макрос with-pandoric расширяется в symbol-macrolet который определяет символьные макросы для каждого символа содержащегося в syms. Каждый символьный макрос будет расширять ссылки на его символ в лексическую область видимости symbol-macrolet в ссылки обобщённой переменной используя get-pandoric для получения доступа к результатам вычисления второго аргумента макроса: o!box (сохраняется в g!box).
Таким образом with-pandoric позволяет нам заглянуть в привязку переменных, через закрывающее их замыкание:
* (with-pandoric (acc) #'pantest
(format t "Value of acc: ~a~%" acc))
Value of acc: -7
NIL
Поскольку в нашей архитектуре используются обобщённые переменные для формирования синтаксической двойственности, дающее нам возможность получения и присваивания значений, то мы можем считать что имеем дело с обычной лексической переменной и присваивать ей значения с помощью setq:
* (with-pandoric (acc) #'pantest
(setq acc 5))
5
* (pantest 1)
6
Мы только что познакомились с основными составными частями пандорических макросов. Первое, макрос, создающий замыкания: pandoriclet, захватывающий анафору this, ссылающуюся на функцию, использованную при вызове замыкания. Этот макрос компилируется в некоторый специальный код, который перехватывает определённые вызовы этого замыкания и получает доступ или модифицирует его перекрытые лексические переменные. Второе, единый синтаксис и для получения доступа и для присваивания нового значения реализованный с помощью get-pandoric и defsetf. И, наконец, макрос with-pandoric использующий symbol-macrolet для закладки обобщённых переменных выглядящих как новые лексические переменные с теми же именами что и перекрытые переменные. Эти переменные ссылаются на исходную среду, созданную с помощью pandoriclet, но из отдельных лексических контекстов.
В качестве примера мы используем эту способность открывать замыкания и сравним их с макросами горячей замены из раздела 6.5, Замыкания, Поддерживающие Горячую Замену. Напомним, что alet-hotpatch и его близкий анафорический двоюродный брат let-hotpatch создают замыкания с косвенной средой, таким образом функции, вызываемые при вызове замыкания могут изменяться на лету. Главное ограничение связанное с этими макросами заключается в том, что они принуждают вас выбрасывать все лексические привязки, перекрывающиеся предыдущей анонимной функцией при её горячей замене. Это было неизбежно поскольку в во время написания этих макросов замыкания были закрыты для нас.
Листинг 6.21: PANDORIC-HOTPATCH
(defun pandoric-hotpatch (box new)
(with-pandoric (this) box
(setq this new)))
При работе с alet-hotpatch и let-hotpatch мы должны были компилировать код специального назначения для каждого замыкания, которое было способно присвоить анафорической лексической привязке this новое значение. Но поскольку мы можем открыть замыкание определённое с pandoriclet и запустить внешний присваивающий код, то теперь возможно определить функцию горячей замены pandoric-hotpatch, которая способная работать с любыми пандорическими замыканиями.
Иногда абстракции лишь кажутся правильными и трудно сказать почему. Возможно, поскольку в большинстве случаев программирование - это не гармоничная комбинация несовместимых частей, и удивительно и приятно обнаружить абстракции, которые - как бы случайно - замечательно комбинируются вместе. Pandoric-hotpatch работает также, как и читается: открывает пандорический интерфейс, получает переменную this из лексической области замыкания box и затем использует setq для присваивания this нового заменяющего замыкания, new.
Мы можем применить pandoric-hotpatch на пандорических замыканиях которые мы создали ещё до того как задумались о горячей замене. Помните замыкание-счётчик с которым мы упражнялись на протяжении этого раздела? Это замыкание должно быть по прежнему привязано к symbol-function символа pantest. Счётчик был установлен в 6:
* (pantest 0)
6
Давайте внедрим новое замыкание - такое, которое имеет новую привязку к acc начинающуюся со 100 и является уменьшающимся:
* (pandoric-hotpatch #'pantest
(let ((acc 100))
(lambda (n) (decf acc n))))
#<Interpreted Function>
Листинг 6.22: PANDORIC-RECODE
(defmacro pandoric-recode (vars box new)
`(with-pandoric (this ,@vars) ,box
(setq this ,new)))
Для того, чтобы убедиться, что горячая замена прошла хорошо:
* (pantest 3)
97
Итак, наше замыкание-счётчик обладает новым значением, привязанным к this, которое используется для осуществления подсчёта. Однако, изменяет ли данная горячая замена пандорическое значение привязки acc?
* (with-pandoric (acc) #'pantest
acc)
6
Нет. Acc по прежнему равен предыдущему значению, 6, происходит это по той причине, что мы меняем только одну привязку this в пандорическом окружении, а this меняется только на новое замыкание в его собственной привязке acc.
Макрос pandoric-recode применяет несколько другой подход реализации горячей замены. Он сохраняет исходную лексическую среду кода и в то же время продолжает управлять изменением функции, исполняемой при вызове замыкания и компилируется извне. Звучит слишком хорошо, чтобы быть правдой? Вспомните, что текущее значение acc равняется 6 в исходной пандорической среде, и мы можем использовать pandoric-recode для размещения новой функции, использующей исходное значение и, скажем, уменьшать счётчик на половину от переданного значения n:
Листинг 6.23: PLAMBDA
(defmacro plambda (largs pargs &rest body)
(let ((pargs (mapcar #'list pargs)))
`(let (this self)
(setq
this (lambda ,largs ,@body)
self (dlambda
(:pandoric-get (sym)
,(pandoriclet-get pargs))
(:pandoric-set (sym val)
,(pandoriclet-set pargs))
(t (&rest args)
(apply this args)))))))
* (pandoric-recode (acc) #'pantest
(lambda (n)
(decf acc (/ n 2))))
#<Interpreted Function>
Убедимся в том, что у нас есть новое поведение, которое уменьшит acc на (* 1/2 2) с 6 до 5:
* (pantest 2)
5
И связано ли это значение с исходной пандорической привязкой?
* (with-pandoric (acc) #'pantest
acc)
5
Да. Как работает pandoric-recode? Этот макрос перекрывает переданную лямбда форму пандорически открытыми привязками исходного замыкания.
Макрос, который мы до сих пор использовали для создания пандорических замыканий называется pandoriclet. Plambda - это переписанная изнанка pandoriclet добавляющая несколько важных особенностей. Первое и самое главное, plambda не создаёт let среды используемые в наших пандорических методах доступа. Вместо этого plambda получает список символов ссылающихся на переменные, наличие которых подразумевается в лексической среде вызывателя макроса. Plambda может экспортировать любые переменные в вашу лексическую среду, делая их прозрачно доступными для других лексических областей видимости - даже если они были написаны и скомпилированы до или после формы plambda.
Это следующее улучшение нашей системы замыканий let, окружающий lambda, спроектированное на максимальное усиление синтаксической двойственности. Благодаря пандорическим макросам, наиболее важными из которых являются plambda и with-pandoric, мы можем при желании легко и просто выйти за границы лексической области видимости. Замыкания больше не закрыты; мы можем открыть замыкания также легко, как и переписать наши лямбда формы в plambda формы. Мы используем plambda для экспорта лексических переменных и with-pandoric для их импорта, в результате чего они становятся полностью эквивалентными лексическим переменным. На самом деле эти новые переменные настолько эквиваленты, что их уже нельзя рассматривать как новые переменные. Лучший способ думать о пандорических переменных - это просто считать их расширением исходной лексической области видимости. Простой пример использования plambda - это пандорический счётчик, экспортирующий переменные из двух потенциально различных лексических сред:
* (setf (symbol-function 'pantest)
(let ((a 0))
(let ((b 1))
(plambda (n) (a b)
(incf a n)
(setq b (* b n))))))
#<Interpreted Function>
Заметьте, насколько легко было экспортировать эти лексические указатели. Создание пандорического замыкания также легко как и добавление символа p перед lambda и добавление списка экспортируемых переменных после лямбда аргументов. Мы можем открыть это замыкание - и любое пандорическое замыкание, которое экспортирует символы a и b - используя with-pandoric:
Листинг 6.24: MAKE-STATS-COUNTER
(defun make-stats-counter
(&key (count 0)
(sum 0)
(sum-of-squares 0))
(plambda (n) (sum count sum-of-squares)
(incf sum-of-squares (expt n 2))
(incf sum n)
(incf count)))
* (defun pantest-peek ()
(with-pandoric (a b) #'pantest
(format t "a=~a, b=~a~%" a b)))
PANTEST-PEEK
* (pantest-peek)
a=0, b=1
NIL
Plambda является примером того, насколько полезным может быть исключение общих компонентов макро расширений. Помните когда мы писали pandoriclet мы решили переместить создание case условий для кода выполняющего получение доступа на функцию pandoriclet-get, а код, отвечающий за присваивание на pandoriclet-set? Plambda использует эти же функции. Даже если эти макросы сращивают результаты этих функций в абсолютно других лексических контекстах, но поскольку макросы написаны с соблюдением того же соглашения именования и протокола взаимодействия замыканий, то этот код пригоден к комбинированию с другим кодом.
Итак, пандорические макросы разрушают лексические границы. Они позволяют вам открывать замыкания по первому желанию и представляют прекрасное слияние различных особенностей языка COMMON LISP: анафорические макросы, обобщённые переменные и символьные макросы. Но какую пользу можно извлечь из них?
Пандорические макросы важны по той причине, что они дают нам все основные преимущества таких объектных систем как CLOS не требуя от нас отказа от более естественного стиля программирования, основанного на комбинациях let-lambda. В частности мы можем добавлять функциональность или методы для замыканий без необходимости создавать экземпляры экземпляров уже созданных объектов.
Листинг 6.25: DEFPAN
(defmacro defpan (name args &rest body)
`(defun ,name (self)
,(if args
`(with-pandoric ,args self
,@body)
`(progn ,@body))))
Make-stats-counter - это lambda, окружающая let, окружающий plambda, предназначенная для создания счётчиков, содержащих три единицы информации. В дополнение к сумме (sum), есть сумма квадратов (sum-of-squares) и число обработанных шагов. Если бы мы использовали lambda вместо plambda в определении make-stats-counter, то большинство этой информации не было бы нам доступно. Мы находились бы снаружи и эти переменные были бы закрыты для нас.
Как мы напишем пандорические методы? Мы можем просто получить доступ к переменным используя with-pandoric так, как это было показано выше, или, поскольку это лисп, спроектировать более узкоспециализированный интерфейс.
Defpan - это комбинация defun и макроса with-pandoric. Главная цель defpan - это реализация двойственности синтаксиса между функцией, написанной с помощью defun и доступом к сторонней лексической области видимости с помощью with-pandoric. И хотя мы передаём аргументы defpan в том же синтаксисе что и в лямбда формах - список символов - аргументы defpan обозначают нечто другое. Вместо создания новой лексической среды эти пандорические функции расширяют лексическую среду пандорических замыканий к которым они применяются. В случае с defun и обычными лямбда формами имя (символ) которое вы даёте переменной не играет важной роли. В случае с пандорическими функциями имя - это всё. Кроме того, в случае с пандорическими функциями порядок аргументов не имеет значения и количество используемых экспортируемых лексических переменных зависит от вашего желания.
Кроме того defpan предоставляет анафору под названием self, которая позволяет нам применять технику под названием анафорическая очередизация. Невидимо передавая значение self между пандорическими функциями мы можем управлять значением этой анафоры с помощью очередей из вызовов функций. Как и со всеми конструкциями очередей, убедитесь что очередь не заканчивается бесконечным циклом.
Листинг 6.26: STATS-COUNTER-METHODS
(defpan stats-counter-mean (sum count)
(/ sum count))
(defpan stats-counter-variance
(sum-of-squares sum count)
(if (< count 2)
0
(/ (- sum-of-squares
(* sum
(stats-counter-mean self)))
(- count 1))))
(defpan stats-counter-stddev ()
(sqrt (stats-counter-variance self)))
Мы написали три метода, которые могут быть использованы в наших замыканиях, созданных с помощью make-stats-counter или любом другом пандорическом замыкании, экспортирующем необходимые имена переменных. Stats-counter-mean просто возвращает среднее значение всех значений, переданных замыканию. Stats-counter-variance вычисляет дисперсию этих значений следуя ссылкам в очереди, а stats-counter-stddev применяется для того, чтобы вычислить стандартное отклонение. Обратите внимание что каждая ссылка в очереди нужна для того, чтобы передать анафору self для того, чтобы сослаться на полный лексический контекст замыкания. Мы видим, что отдельной пандорической функции нужно только сослаться на используемые переменные и на эти переменные можно ссылаться в том порядке, в каком нам заблагорассудится.
Итак, plambda создаёт другую анафору - self. В то время когда анафора this ссылается на замыкание, которое будет вызвано, self ссылается на косвенную среду, вызвавшую это замыкание. И хотя это прозвучит несколько странно, код внутри нашей plambda может использовать self для пандорического доступа к своей собственной лексической среде, вместо использования прямого доступа к ней. Пока что может показаться что такой трюк полезен для defpan методов, написанных для работы внутри нашей лексической области видимости.
Листинг 6.27: MAKE-NOISY-STATS-COUNTER
(defun make-noisy-stats-counter
(&key (count 0)
(sum 0)
(sum-of-squares 0))
(plambda (n) (sum count sum-of-squares)
(incf sum-of-squares (expt n 2))
(incf sum n)
(incf count)
(format t
"~&MEAN=~a~%VAR=~a~%STDDEV=~a~%"
(stats-counter-mean self)
(stats-counter-variance self)
(stats-counter-stddev self))))
Make-noisy-stats-counter идентичен make-stats-counter с тем исключением, что использует анафору self для вызова наших defpan функций stats-counter-mean, stats-counter-variance и stats-counter-stddev.
Plambda и with-pandoric могут перезаписать лексическую область видимости так, как мы этого захотим. Мы завершим эту главу таким примером. К одним из ограничений лексической области видимости иногда относят тот факт, что функция COMMON LISP eval отбрасывает нашу текущую лексическую среду при вычислении переданной ей формы. Другими словами, eval вычисляет форму в null лексической среде. В COMMON LISP иначе и не может быть: eval - это функция. Вот такая проблема:
* (let ((x 1))
(eval
'(+ x 1)))
Error: The variable X is unbound.
Иногда бывает желательно расширить вашу лексическую среду на eval. Но будьте осторожны. Весьма часто использование eval говорит о том, что вы, возможно, делаете что-то не так. Злоупотребление функцией eval может привести к замедлению программ поскольку eval может быть очень ресурсоёмкой операцией - в основном, это происходит из необходимости расширять макросы присутствующие в переданной форме. Если вы вдруг обнаружили необходимость в eval при программировании, спросите себя почему вы не сделали то, что вам нужно сделать гораздо раньше. Если ответом будет: "Раньше я не мог сделать это", скажем, поскольку вы только что считали форму, то, примите поздравления, вы обнаружили один из редких правомерных причин использования eval. Все другие ответы ведут к тому способу, который вы должны были применить в первую очередь: к макросу.
Листинг 6.28: PANDORIC-EVAL
(defvar pandoric-eval-tunnel)
(defmacro pandoric-eval (vars expr)
`(let ((pandoric-eval-tunnel
(plambda () ,vars t)))
(eval `(with-pandoric
,',vars pandoric-eval-tunnel
,,expr))))
Но, предположим, вам действительно нужно использовать eval, если бы только можно было взять с собой этот надоедливый лексический контекст. Макрос pandoric-eval - это пример интересного применения plambda и with-pandoric. Pandoric-eval использует специальную переменную под названием pandoric-eval-tunnel для того, чтобы сделать пандорическое замыкание доступным для функции eval через динамическую среду. Выбор лексических переменных предназначенных для туннелирования через динамическую среду осуществляется через передачу списка с их символами в качестве первого аргумента pandoric-eval. Вот вышеприведённый пример, но уже с использованием pandoric-eval:
* (let ((x 1))
(pandoric-eval (x)
'(+ 1 x)))
2
И выражение вычисленное с помощью pandoric-eval может модифицировать исходную лексическую среду; pandoric-eval - это двухсторонний туннель:
* (let ((x 1))
(pandoric-eval (x)
'(incf x))
x)
2
В этом разделе, хотя он был очень длинный, мы всего лишь вскользь прошлись о возможностях пандорических макросов и различных их вариациях. И я ожидаю большого количества различных интересных разработок, которые разовьются из них.
Упражнение: Может ли pandoric-eval вызываться вложенным? То есть, можно ли pandoric-eval применять для вычисления форм, вычисляющих pandoric-eval? Объясните ответ.
Упражнение: Хотя реализация пандорических макросов в этом разделе весьма эффективна, она может быть улучшена. Попробуйте заменить pandoriclet-get и pandoriclet-set на генерацию кода, использующего хэш-таблицу вместо case и замерьте производительность этих двух реализаций на малых и больших числах пандорических переменных. Рассмотрите вашу любимую реализацию CLOS, сымитируйте диспетчеризацию CLOS и замерьте производительность.
Переведено на Нотабеноиде
http://notabenoid.org/book/40014/199008
Переводчики: Charlz_Klug
Настройки
Готово:
