Permalink
Switch branches/tags
Nothing to show
Find file
Fetching contributors…
Cannot retrieve contributors at this time
961 lines (799 sloc) 66.9 KB
\chapter{Практика: библиотека для генерации HTML -- компилятор}
\label{ch:31}
\thispagestyle{empty}
Теперь вы готовы к тому, чтобы увидеть, как работает компилятор FOO. Основное различие
между компилятором и интерпретатором заключается в том, что интерпретатор обрабатывает
программу и сразу производит какие-то действия~-- генерирует HTML в случае интерпретатора
FOO, а компилятор обрабатывает ту же самую прогрумму и генерирует код на каком-то другом
языке, который будет производить те же самые действия. В~языке FOO компилятор является
макросом Common Lisp, который транслирует инструкции FOO в код на Common Lisp, так что он
может быть встроен в программы на Common Lisp. В~общем, компиляторы имеют преимущества над
интерпретаторами, поскольку компиляция происходит заранее, так что они могут потратить
некоторое количество времени, оптимизуя генерируемый код, делая его более эффективным. Это
же делает компилятор FOO, объединяя текстовые строки, насколько это возможно, чтобы
выдавать точно такой же HTML с меньшим количеством операций записи по сравнению с
интерпретатором. Когда компилятор является макросом Common Lisp, вы также имеете
преимущества, поскольку компилятор сможет обрабатывать вставленные куски кода на Common
Lisp~-- компилятору нужно лишь распознать их и вставить в правильное место генерируемого
кода. Компилятор FOO получит некоторые преимущества от использования этой возможности.
\section{Компилятор}
Базовая архитектура компилятора состоит из трёх уровней. Сначала вы реализуете класс
\lstinline{html-compiler}, который имеет один слот, который содержит расширяемый вектор, который
используется для накопления кодов операций (ops), представляющих вызовы, сделанные к
обобщённым функциям реализации при выполнении \lstinline{process}.
Затем вы реализуете методы для обобщённых функций интерфейса реализации, которые будут
сохранять последовательность действий в векторе. Каждый код операции представлен списком,
состоящим из ключевого слова, именующего операцию, и аргументов, переданных функции,
которая сгенерировала этот код операции. Функция \lstinline{sexp->ops} реализует первую стадию
компиляции~-- преобразование списка выражений FOO путём вызова \lstinline{process} для каждого
выражения с объектом \lstinline{html-compiler}.
Этот вектор с кодами операций, созданный компилятором, затем передаётся функции, которая
оптимизирует его, объединяя последовательные операции \lstinline{raw-string} в одну операцию,
которая выдаст объединённую строку за один вызов. Функция оптимизации также может, не
обязательно, отбросить операции, которые необходимы только для выдачи хорошо
отформатированного кода, что является достаточно важным, поскольку это позволит объединить
большее количество операций \lstinline{raw-string}.
И в заключение оптимизированный вектор с кодами операций передаётся третьей функции,
\lstinline{generate-code}, которая возвращает список выражений Common Lisp, выполнение которых
приведёт к выводу HTML. Когда переменная \lstinline{*pretty*} имеет истинное значение, то
\lstinline{generate-code} генерирует код, который использует методы, специализированные для
\lstinline{html-pretty-printer}, для того чтобы вывести хорошо отформатированный HTML. Когда
\lstinline{*pretty*} равна \lstinline{NIL}, то эта функция генерирует код, который будет выводить
данные напрямую в поток \lstinline{*html-output*}.
Макрос \lstinline{html} в действительности генерирует тело выражения, которое содержит два
раскрытия кода~-- одно для случая, когда \lstinline{*pretty*} равно \lstinline{T}, и второе для
случая, когда \lstinline{*pretty*} равно \lstinline{NIL}. То, какое выражение будет использоваться,
определяется во время выполнения в зависимости от значения переменной \lstinline{*pretty*}.
Таким образом, любая функция, которая содержит вызов \lstinline{html}, будет иметь код для
генерации компактного и хорошо оформленного вывода.
Другим важным отличием между компилятором и интерпретатором является то, что компилятор
может внедрять выражения на Lisp в генерируемый код. Чтобы воспользоваться этим
преимуществом, вам необходимо изменить функцию \lstinline{process} таким образом, чтобы
она вызывала функции \lstinline{embed-code} и \lstinline{embed-value} в тех случаях, когда
её просят обработать выражение, которое не является выражением FOO. Поскольку все
самовычисляющиеся объекты являются допустимыми выражениями FOO, единственными выражениями,
которые не будут переданы \lstinline{process-sexp-html}, являются списки, которые не
соответствуют синтаксису выражений-ячеек (cons forms) FOO и не-именованным символам~--
единственным атомам, которые не вычисляются сами в себя. Вы можете предположить, что
любой список, не относящийся к FOO, является кодом, который необходимо выполнять, а все
символы являются переменными, чьи значения вы должны вставить в генерируемый код.
\begin{myverb}
(defun process (processor form)
(cond
((sexp-html-p form) (process-sexp-html processor form))
((consp form) (embed-code processor form))
(t (embed-value processor form))))
\end{myverb}
Теперь давайте взглянем на код компилятора. Во-первых, вы должны определить две функции,
которые абстрагируют вектор, который вы будете использовать для хранения кодов операций в
первых двух стадиях компиляции.
\begin{myverb}
(defun make-op-buffer () (make-array 10 :adjustable t :fill-pointer 0))
(defun push-op (op ops-buffer) (vector-push-extend op ops-buffer))
\end{myverb}
Затем вы можете определить класс \lstinline{html-compiler} и методы, специализированные для
него, реализующие интерфейс.
\begin{myverb}
(defclass html-compiler ()
((ops :accessor ops :initform (make-op-buffer))))
(defmethod raw-string ((compiler html-compiler) string &optional newlines-p)
(push-op `(:raw-string ,string ,newlines-p) (ops compiler)))
(defmethod newline ((compiler html-compiler))
(push-op '(:newline) (ops compiler)))
(defmethod freshline ((compiler html-compiler))
(push-op '(:freshline) (ops compiler)))
(defmethod indent ((compiler html-compiler))
(push-op `(:indent) (ops compiler)))
(defmethod unindent ((compiler html-compiler))
(push-op `(:unindent) (ops compiler)))
(defmethod toggle-indenting ((compiler html-compiler))
(push-op `(:toggle-indenting) (ops compiler)))
(defmethod embed-value ((compiler html-compiler) value)
(push-op `(:embed-value ,value ,*escapes*) (ops compiler)))
(defmethod embed-code ((compiler html-compiler) code)
(push-op `(:embed-code ,code) (ops compiler)))
\end{myverb}
После определения этих методов вы можете реализовать первую стадию компиляции~--
\lstinline{sexp->ops}.
\begin{myverb}
(defun sexp->ops (body)
(loop with compiler = (make-instance 'html-compiler)
for form in body do (process compiler form)
finally (return (ops compiler))))
\end{myverb}
Во время этой фазы вам нет необходимости учитывать значение переменной \lstinline{*pretty*}:
просто записывайте все функции, вызванные функцией \lstinline{process}. Вот что
\lstinline{sexp->ops} сделает из простого выражения FOO:
\begin{myverb}
HTML> (sexp->ops '((:p "Foo")))
#((:FRESHLINE) (:RAW-STRING "<p" NIL) (:RAW-STRING ">" NIL)
(:RAW-STRING "Foo" T) (:RAW-STRING "</p>" NIL) (:FRESHLINE))
\end{myverb}
На следующей фазе функция \lstinline{optimize-static-output} принимает вектор кодов операций
и возвращает новый вектор, содержащий оптимизированную версию. Алгоритм очень прост:
для каждой операции \lstinline{:raw-string} функция записывает строку во временный строковый
буфер. Таким образом, последовательные вызовы \lstinline{:raw-string} приведут к построению
одной строки, содержащей объединение всех строк, которые должны быть выведены. Когда вы
встречаете код операции, отличный от кода \lstinline{:raw-string}, то вы преобразуете созданную
строку в последовательность операций \lstinline{:raw-string} и \lstinline{:newline}, используя
вспомогательную функцию \lstinline{compile-buffer}, и затем добавляете новый код операции. В
этой функции вы также отбрасываете <<красивое>> форматирование, если значением
\lstinline{*pretty*} является \lstinline{NIL}.
\begin{myverb}
(defun optimize-static-output (ops)
(let ((new-ops (make-op-buffer)))
(with-output-to-string (buf)
(flet ((add-op (op)
(compile-buffer buf new-ops)
(push-op op new-ops)))
(loop for op across ops do
(ecase (first op)
(:raw-string (write-sequence (second op) buf))
((:newline :embed-value :embed-code) (add-op op))
((:indent :unindent :freshline :toggle-indenting)
(when *pretty* (add-op op)))))
(compile-buffer buf new-ops)))
new-ops))
(defun compile-buffer (buf ops)
(loop with str = (get-output-stream-string buf)
for start = 0 then (1+ pos)
for pos = (position #\bslash{}Newline str :start start)
when (< start (length str))
do (push-op `(:raw-string ,(subseq str start pos) nil) ops)
when pos do (push-op '(:newline) ops)
while pos))
\end{myverb}
Последним шагом является преобразование кодов операций в соответствующий код Common
Lisp. Эта фаза также учитывает значение переменной \lstinline{*pretty*}. Когда \lstinline{*pretty*}
имеет истинное значение, то функция генерирует код, который вызывает функции, используя
переменную \lstinline{*html-pretty-printer*}, которая содержит экземпляр класса
\lstinline{html-pretty-printer}. А когда значение \lstinline{*pretty*} равно \lstinline{NIL}, то функция
генерирует код, который выводит данные прямо в поток, указанный переменной
\lstinline{*html-output*}.
Реализация функции \lstinline{generate-code} крайне проста.
\begin{myverb}
(defun generate-code (ops)
(loop for op across ops collect (apply #'op->code op)))
\end{myverb}
Вся работа выполняется методами обобщённой функции \lstinline{op->code}, специализированной для
аргумента \lstinline{op} со специализатором \lstinline{EQL} для имени операции.
\begin{myverb}
(defgeneric op->code (op &rest operands))
(defmethod op->code ((op (eql :raw-string)) &rest operands)
(destructuring-bind (string check-for-newlines) operands
(if *pretty*
`(raw-string *html-pretty-printer* ,string ,check-for-newlines)
`(write-sequence ,string *html-output*))))
(defmethod op->code ((op (eql :newline)) &rest operands)
(if *pretty*
`(newline *html-pretty-printer*)
`(write-char #\bslash{}Newline *html-output*)))
(defmethod op->code ((op (eql :freshline)) &rest operands)
(if *pretty*
`(freshline *html-pretty-printer*)
(error "Bad op when not pretty-printing: ~a" op)))
(defmethod op->code ((op (eql :indent)) &rest operands)
(if *pretty*
`(indent *html-pretty-printer*)
(error "Bad op when not pretty-printing: ~a" op)))
(defmethod op->code ((op (eql :unindent)) &rest operands)
(if *pretty*
`(unindent *html-pretty-printer*)
(error "Bad op when not pretty-printing: ~a" op)))
(defmethod op->code ((op (eql :toggle-indenting)) &rest operands)
(if *pretty*
`(toggle-indenting *html-pretty-printer*)
(error "Bad op when not pretty-printing: ~a" op)))
\end{myverb}
Два наиболее интересных метода \lstinline{op->code}~-- это те, которые генерируют код для
операций \lstinline{:embed-value} и \lstinline{:embed-code}. В~методе \lstinline{:embed-value} вы
можете генерировать немного отличающийся код в зависимости от значения аргумента
\lstinline{escapes}, поскольку если \lstinline{escapes} равен \lstinline{NIL}, то вам нет необходимости
генерировать вызов \lstinline{escape}. И когда и \lstinline{*pretty*}, и \lstinline{escapes} равны
\lstinline{NIL}, то вы можете сгенерировать код, который будет использовать функцию
\lstinline{PRINC} для вывода значения напрямую в поток.
\begin{myverb}
(defmethod op->code ((op (eql :embed-value)) &rest operands)
(destructuring-bind (value escapes) operands
(if *pretty*
(if escapes
`(raw-string *html-pretty-printer* (escape (princ-to-string ,value) ,escapes) t)
`(raw-string *html-pretty-printer* (princ-to-string ,value) t))
(if escapes
`(write-sequence (escape (princ-to-string ,value) ,escapes) *html-output*)
`(princ ,value *html-output*)))))
\end{myverb}
Так что что-то, подобное вот такому коду:
\begin{myverb}
HTML> (let ((x 10)) (html (:p x)))
<p>10</p>
NIL
\end{myverb}
\noindent{}будет работать, поскольку \lstinline{html} преобразует \lstinline{(:p x)} в
что-то наподобие вот этого:
\begin{myverb}
(progn
(write-sequence "<p>" *html-output*)
(write-sequence (escape (princ-to-string x) "<>&") *html-output*)
(write-sequence "</p>" *html-output*))
\end{myverb}
И когда будет сгенерирован код, заменяющий вызов \lstinline{html} в контексте
\lstinline{LET}, то вы получите следующий результат:
\begin{myverb}
(let ((x 10))
(progn
(write-sequence "<p>" *html-output*)
(write-sequence (escape (princ-to-string x) "<>&") *html-output*)
(write-sequence "</p>" *html-output*)))
\end{myverb}
\noindent{}и ссылки на \lstinline{x} в сгенерированном коде превратятся в ссылки на лексическую переменную
из выражения \lstinline{LET}, окружающего выражение \lstinline{html}.
С другой стороны, метод \lstinline{:embed-code} интересен, поскольку она крайне примитивен.
Поскольку функция \lstinline{process} передала выражение функции \lstinline{embed-code}, которая
сохранила его в операции \lstinline{:embed-code}, то все, что вам нужно сделать,~-- извлечь и
вернуть это выражение.
\begin{myverb}
(defmethod op->code ((op (eql :embed-code)) &rest operands)
(first operands))
\end{myverb}
Это позволяет использовать, например, вот такой код:
\begin{myverb}
HTML> (html (:ul (dolist (x '(foo bar baz)) (html (:li x)))))
<ul>
<li>FOO</li>
<li>BAR</li>
<li>BAZ</li>
</ul>
NIL
\end{myverb}
Внешний вызов \lstinline{html} раскрывается в код, который делает что-то, подобное следующему коду:
\begin{myverb}
(progn
(write-sequence "<ul>" *html-output*)
(dolist (x '(foo bar baz)) (html (:li x)))
(write-sequence "</ul>" *html-output*))))
\end{myverb}
И затем если вы раскроете вызов \lstinline{html} в теле \lstinline{DOLIST}, то вы получите
что-то, подобное следующему коду:
\begin{myverb}
(progn
(write-sequence "<ul>" *html-output*)
(dolist (x '(foo bar baz))
(progn
(write-sequence "<li>" *html-output*)
(write-sequence (escape (princ-to-string x) "<>&") *html-output*)
(write-sequence "</li>" *html-output*)))
(write-sequence "</ul>" *html-output*))
\end{myverb}
Этот код будет генерировать результат, который вы уже видели выше.
\section{Специальные операторы FOO}
Вы можете остановиться на этом~-- язык FOO достаточно выразителен для генерации
практически любого HTML, какой вы можете придумать. Однако вы можете добавить две новые
возможности к языку с помощью небольшого количества кода, который сделает язык более
мощным: специальные операторы и макросы.
Специальные операторы FOO аналогичны специальным операторам в Common Lisp. Специальные
операторы обеспечивают возможность выражения тех вещей, которые не могут быть выражены на
языке, поддерживающем только базовые правила вычисления выражений. Или, если посмотреть с
другой стороны, специальные операторы предоставляют доступ к примитивам, используемым
ядром языка, вычисляющим выражения\pclfootnote{Аналогия между специальными операторами и
макросами FOO, которые я буду обсуждать в следующем разделе, и этими же вещами в Lisp
является достаточно логичной. В~действительности понимание того, как работают
специальные операторы и макросы FOO, может дать вам некоторое представление о том,
почему Common Lisp объединил их именно таким образом.}.
В~качестве простого примера в компиляторе FOO ядро языка использует функцию
\lstinline{embed-value} для генерации кода, который будет вставлять значение переменной в
генерируемый HTML. Однако поскольку \lstinline{embed-value} передаются только символы, то не
существует способа (в том языке, который я описывал) включить значение произвольного
выражения Common Lisp; в этом случае функция \lstinline{process} передаёт пары значений функции
\lstinline{embed-code}, а не \lstinline{embed-value}, так что возвращаемые значения игнорируются.
Обычно это то, что нам надо, поскольку главной причиной вставки кода на Lisp в программу
на FOO является возможность использования управляющих конструкций Lisp. Однако иногда
вы захотите вставить значение вычисленного выражения в сгенерированный HTML. Например, вы
можете захотеть, чтобы программа на FOO генерировала параграф, содержащий случайное число:
\begin{myverb}
(:p (random 10))
\end{myverb}
Но это не будет работать, поскольку код будет вычислен и его значение будет отброшено.
\begin{myverb}
HTML> (html (:p (random 10)))
<p></p>
NIL
\end{myverb}
В~реализованном нами языке вы можете обойти это путём вычисления значения вне вызова
\lstinline{html} и затем вставки его при помощи переменной.
\begin{myverb}
HTML> (let ((x (random 10))) (html (:p x)))
<p>1</p>
NIL
\end{myverb}
Но это будет раздражать, особенно когда вы считаете, что если бы вы могли передать
выражение \lstinline{(random 10)} функции \lstinline{embed-value} вместо \lstinline{embed-code}, то это
было бы то, что надо. Так что вы можете определить специальный оператор \lstinline{:print},
который будет обрабатываться ядром языка FOO с использованием правила, отличного от правил
для обычных выражений FOO. А именно вместо генерации элемента \lstinline{<print>} он будет
передавать выражение, заданное в его теле, функции \lstinline{embed-value}. Так что вы сможете
вывести параграф, содержащий случайное число с помощью вот такого кода:
\begin{myverb}
HTML> (html (:p (:print (random 10))))
<p>9</p>
NIL
\end{myverb}
Понятно, что этот специальный оператор полезен только в скомпилированном коде на FOO,
поскольку \lstinline{embed-value} не работает в режиме интерпретации. Ещё одним специальным
оператором, который может быть использован и в режиме компиляции, и в режиме
интерпретации, является оператор \lstinline{:format}, который позволяет вам генерировать вывод,
используя функцию \lstinline{FORMAT}. Аргументами специального оператора \lstinline{:format}
являются строка, управляющая форматом вывода данных, и за ней любые аргументы. Когда все
аргументы \lstinline{:format} являются самовычисляемыми объектами, то
строка генерируется путём передачи аргументов функции \lstinline{FORMAT}, и полученная строка
затем выводится так же, как и любая другая строка. Это позволяет использовать выражения
\lstinline{:format} в выражениях FOO, переданных функции \lstinline{emit-html}. В~скомпилированном
коде FOO аргументами \lstinline{:format} могут быть любые выражения Lisp.
Другие специальные операторы обеспечивают контроль за тем, какие символы будут
автоматически преобразовываться, а также использоваться для вывода символов новой строки:
специальный оператор \lstinline{:noescape} приводит к вычислению всех выражений, но при этом
переменная \lstinline{*escapes*} получает значение \lstinline{NIL}, в то время как
\lstinline{:attribute} вычисляет все выражения с \lstinline{*escapes*}, равным
\lstinline{*attribute-escapes*}. А оператор \lstinline{:newline} преобразуется в код, который
выдаёт явный перевод строки.
Так что, как вы будете определять специальные операторы? Существуют два аспекта обработки
специальных операторов: как обработчик языка распознаёт формы, которые используются для
представления специальных операторов и как он будет знать, какой код выполнять для
обработки каждого из специальных операторов?
Вы можете изменить функцию \lstinline{process-sexp-html}, чтобы она распозновала каждый из
специальных операторов и обрабатывала их соответствующим образом~-- с логической точки
зрения, специальные операторы являются частью реализации языка, и их не будет очень
много. Однако было бы удобно иметь модульный способ добавления новых специальных
операторов~-- не потому, что пользователи FOO будут иметь возможность их добавления, а
просто для нашего собственного удобства.
Определим специальное выражение как список, чьим значением \lstinline{CAR} является
символ, представляющий имя специального оператора. Вы можете пометить имена спе\-циаль\-ных
операторов путём добавления не \lstinline{NIL}-значения к списку свойств символов,
принадлежащему пункту \lstinline{html-special-operator}. Так что вы можете определить
функцию, которая проверяет, является ли данное выражение специальным оператором, примерно
вот так:
\begin{myverb}
(defun special-form-p (form)
(and (consp form) (symbolp (car form)) (get (car form) 'html-special-operator)))
\end{myverb}
Код, реализующий каждый из специальных операторов, также ответствен за обработку
оставшейся части списка и выполнение того, чего требует семантика специального оператора.
Предполагая, что вы также определили функцию \lstinline{process-special-form}, которая
принимает в качестве аргументов обработчик язык и выражение со специальным оператором и
выполняет соответствующий код для генерации последовательности вызовов для объекта
\lstinline{processor}, вы можете расширить функцию \lstinline{process} обработкой
специальных операторов следующим образом:
\begin{myverb}
(defun process (processor form)
(cond
((special-form-p form) (process-special-form processor form))
((sexp-html-p form) (process-sexp-html processor form))
((consp form) (embed-code processor form))
(t (embed-value processor form))))
\end{myverb}
Вы должны в начале добавить вызов \lstinline{special-form-p}, поскольку спе\-циаль\-ные операторы
могут выглядеть так же как обычные выражения FOO, точно так же как специальные операторы
Common Lisp выглядят так же как вызовы обычных функций.
Теперь вам нужно лишь реализовать \lstinline{process-special-form}. Вместо того чтобы
определять одну монолитную функцию, которая реализует все специальные операторы, вы должны
определить макрос, который позволит вам определять специальные операторы практически
так же, как обычные функции, и который возьмёт на себя заботу о добавлении записи
\lstinline{html-special-operator} в список свойств имён специальных операторов. В
действительности значением, которое вы сохраняете в списке свойств, может быть функция,
которая реализует специальный оператор. Вот определение соответствующего макроса:
\begin{myverb}
(defmacro define-html-special-operator (name (processor &rest other-parameters) &body body)
`(eval-when (:compile-toplevel :load-toplevel :execute)
(setf (get ',name 'html-special-operator)
(lambda (,processor ,@other-parameters) ,@body))))
\end{myverb}
Это достаточно сложный вид макроса, но если вы будете изучать по одной строке за раз, то
вы не найдёте ничего сложного. Для того чтобы увидеть, как он работает, рассмотрим
простое использование этого макроса~-- определение специального оператора
\lstinline{:noescape} и посмотрим на раскрытие этого макроса. Если вы напишите вот так:
\begin{myverb}
(define-html-special-operator :noescape (processor &rest body)
(let ((*escapes* nil))
(loop for exp in body do (process processor exp))))
\end{myverb}
\noindent{}то это приведёт к получению следующего кода:
\begin{myverb}
(eval-when (:compile-toplevel :load-toplevel :execute)
(setf (get ':noescape 'html-special-operator)
(lambda (processor &rest body)
(let ((*escapes* nil))
(loop for exp in body do (process processor exp))))))
\end{myverb}
Специальный оператор \lstinline{EVAL-WHEN}, как обсуждалось в главе~\ref{ch:20}, используется
для того, чтобы быть уверенными, что данный код будет виден во время компиляции с помощью
функции \lstinline{COMPILE-FILE}. Это нужно, если вы захотите определить
\lstinline{define-html-special-operator} в файле и затем использовать только что определённый
специальный оператор в том же самом файле.
Затем выражение \lstinline{SETF} устанавливает значение для свойства
\lstinline{html-special-operator} символа \lstinline{:noescape}, чтобы оно содержало
анонимную функцию, с тем же списком параметров, как это было определено в
\lstinline{define-html-special-operator}. За счёт того, что для
\lstinline{define-html-special-operator} параметры разбиваются на две части~--
\lstinline{processor} и все остальное,~-- вы будете уверены в том, что все специальные
аргументы будут принимать как минимум один аргумент.
Тело анонимной функции является выражением, передаваемым
\lstinline{define-html-special-operator}. Задачей анонимной функции является реализация
действия специального оператора путём вызова соответствующих функций интерфейса для
генерации корректного HTML или кода, который будет генерировать этот HTML. Она также
использует \lstinline{process} для вычисления выражения как выражения FOO.
Специальный оператор \lstinline{:noescape} является достаточно простым: все, что он делает,~--
это передача выражения в функцию \lstinline{process} с переменной \lstinline{*escapes*},
установленной в \lstinline{NIL}. Другими словами, этот специальный оператор запрещает
стандартное маскирование символов, выполняемое \lstinline{process-sexp-html}.
При использовании специальных операторов, определённых таким образом, все, что нужно делать
\lstinline{process-special-form},~-- всего лишь найти анонимную функцию в списке свойств символа
с именем оператора и применить её (с помощью \lstinline{APPLY}) к списку из обработчика и
оставшейся части выражения.
\begin{myverb}
(defun process-special-form (processor form)
(apply (get (car form) 'html-special-operator) processor (rest form)))
\end{myverb}
Теперь вы готовы к тому, чтобы определить пять оставшихся специальных операторов FOO.
Похожим на \lstinline{:noescape} является \lstinline{:attribute}, который вычисляет заданные
выражения с переменной \lstinline{*escapes*}, равной \lstinline{*attribute-escapes*}. Этот
специальный оператор полезен, если вы хотите написать вспомогательную функцию, которая
будет выдавать значения атрибутов. Если вы напишите вот такую функцию:
\begin{myverb}
(defun foo-value (something)
(html (:print (frob something))))
\end{myverb}
\noindent{}то макрос \lstinline{html} сгенерирует код, который выполнит маскирование символов, указанных в
\lstinline{*element-escapes*}. Но если вы планируете использовать \lstinline{foo-value} следующим
образом:
\begin{myverb}
(html (:p :style (foo-value 42) "Foo"))
\end{myverb}
\noindent{}то вы захотите, чтобы генерировался код, который бы использовал данные из переменной uses
\lstinline{*attribute-escapes*}. Так что вместо этого вы можете написать нечто
подобное\footnote{\lstinline{:noescape} и \lstinline{:attribute} должны быть определены как
специальные операторы, поскольку FOO определяет список маскируемых символов во время
компиляции, а не во время выполнения. Это позволяет FOO выполнять маскирование строк во
время компиляции, что более эффективно, по сравнению с проверкой всего вывода во время
выполнения.}:
\begin{myverb}
(defun foo-value (something)
(html (:attribute (:print (frob something)))))
\end{myverb}
Определение \lstinline{:attribute} выглядит следующим образом:
\begin{myverb}
(define-html-special-operator :attribute (processor &rest body)
(let ((*escapes* *attribute-escapes*))
(loop for exp in body do (process processor exp))))
\end{myverb}
Два других специальных оператора~-- \lstinline{:print} и \lstinline{:format}~-- используются для
вывода значений. Специальный оператор \lstinline{:print}, как обсуждалось ранее, используется
в скомпилированных программах на FOO для вставки значения произвольного выражения Lisp.
Специальный оператор \lstinline{:format} соответствует операции генерации строки с по\-мощью
выражения \lstinline{(format nil ...)} и последующей вставки этой строки в вывод. Основной
причиной определения \lstinline{:format} как специального оператора является удобство. Так,
\begin{myverb}
(:format "Foo: ~d" x)
\end{myverb}
\noindent{}выглядит лучше, чем:
\begin{myverb}
(:print (format nil "Foo: ~d" x))
\end{myverb}
Есть также небольшое преимущество, если вы используете \lstinline{:format} с самовычисляемыми
аргументами, то FOO может вычислить \lstinline{:format} во время
компиляции, а не ждать выполнения программы. Определения для \lstinline{:print} и
\lstinline{:format} выглядят вот так:
\begin{myverb}
(define-html-special-operator :print (processor form)
(cond
((self-evaluating-p form)
(warn "Redundant :print of self-evaluating form ~s" form)
(process-sexp-html processor form))
(t
(embed-value processor form))))
(define-html-special-operator :format (processor &rest args)
(if (every #'self-evaluating-p args)
(process-sexp-html processor (apply #'format nil args))
(embed-value processor `(format nil ,@args))))
\end{myverb}
Специальный оператор \lstinline{:newline} приводит к выводу знака новой строки, что иногда удобно.
\begin{myverb}
(define-html-special-operator :newline (processor)
(newline processor))
\end{myverb}
В~заключение специальный оператор \lstinline{:progn} аналогичен специальному оператору
\lstinline{PROGN} в Common Lisp. Он просто последовательно обрабатывает выражения внутри
свое\-го тела.
\begin{myverb}
(define-html-special-operator :progn (processor &rest body)
(loop for exp in body do (process processor exp)))
\end{myverb}
Другими словами, следующий код:
\begin{myverb}
(html (:p (:progn "Foo " (:i "bar") " baz")))
\end{myverb}
\noindent{}сгенерирует тот же код, что и:
\begin{myverb}
(html (:p "Foo " (:i "bar") " baz"))
\end{myverb}
Это может быть показаться странным, поскольку обычное выражение FOO может иметь любое
количество выражений внутри своего тела. Однако специальный оператор удобен в одной
ситуации~-- при написании макросов FOO, что приводит нас к последней возможности языка,
которую нам надо реализовать.
\section{Макросы FOO}
Макросы FOO аналогичны по духу макросам Common Lisp. Макрос FOO является отрывком кода,
который принимает в качестве аргумента выражение на FOO, и возвращает в качестве
результата новое выражение на FOO, которое затем вычисляется в соответствии со
стандартными правилами вычисления выражений на FOO. Реализация очень похожа на реализацию
специальных операторов.
Так же как и для специальных операторов вы можете определить функцию-предикат, которая
будет проверять, является ли заданное выражение макросом.
\begin{myverb}
(defun macro-form-p (form)
(cons-form-p form #'(lambda (x) (and (symbolp x) (get x 'html-macro)))))
\end{myverb}
Тут мы используем функцию \lstinline{cons-form-p}, определённую выше, поскольку мы хотим
позволить использовать любой синтаксис FOO-выражений. Однако вам нужно передать другую
функцию-предикат, которая будет проверять, является ли имя выражения символом с
не \lstinline{NIL}-свойством \lstinline{html-macro}. Так же как и при реализации специальных
операторов, мы определим макрос для определения макросов FOO, который будет отвечать за
сохранение функции в списке свойств символа с именем макроса (имя свойства будет равно
\lstinline{html-macro}). Однако определение макроса немного более сложное, поскольку FOO
поддерживает использование двух видов макросов. Некоторые из макросов, которые вы будете
определять, будут вести себя как обычные элементы HTML, и вам может понадобиться упрощённый
доступ к списку аттрибутов. Другие макросы будут требовать упрощённого доступа к
элементам их тела.
Вы можете сделать различие между двумя видами макросов неявным: когда вы определяете
макрос FOO, то список параметров может включать параметр \lstinline!&attributes!. Если он
будет указан, то макровыражение будет рассматриваться как обычное выражение-ячейка и
макрофункция будет получать два значения~-- список свойств-аттрибутов и список выражений,
из которых состоит тело выражения. Макровыражение без параметра \lstinline!&attributes!
будет разбираться как не имеющее аттрибутов, и макрофункция будет принимать один
параметр~-- список, содержащий выражения, составляющие тело макроса. Первый вид полезен
для шаблонов HTML. Например:
\begin{myverb}
(define-html-macro :mytag (&attributes attrs &body body)
`((:div :class "mytag" ,@attrs) ,@body))
\end{myverb}
\begin{myverb}
HTML> (html (:mytag "Foo"))
<div class='mytag'>Foo</div>
NIL
HTML> (html (:mytag :id "bar" "Foo"))
<div class='mytag' id='bar'>Foo</div>
NIL
HTML> (html ((:mytag :id "bar") "Foo"))
<div class='mytag' id='bar'>Foo</div>
NIL
\end{myverb}
Последний вид макросов более полезен для написания макросов, которые манипулируют
выражениями, составляющими их тело. Этот тип макросов может работать как управляющие
конструкции HTML. В~качестве простого примера рассмотрим следующий макрос, который
реализует конструкцию \lstinline{:if}:
\begin{myverb}
(define-html-macro :if (test then else)
`(if ,test (html ,then) (html ,else)))
\end{myverb}
Этот макрос позволит вам писать так:
\begin{myverb}
(:p (:if (zerop (random 2)) "Heads" "Tails"))
\end{myverb}
\noindent{}вместо такой более многословной версии:
\begin{myverb}
(:p (if (zerop (random 2)) (html "Heads") (html "Tails")))
\end{myverb}
Для того чтобы определить, какой тип макроса вы должны генерировать, вам необходима
функция, которая выполнит разбор списка параметров, переданных \lstinline{define-html-macro}.
Эта функция возвращает два значения: имя параметра \lstinline!&attributes!, или
\lstinline{NIL}, если он не указан, и список всех элементов \lstinline{args}, оставшихся после
удаления маркера \lstinline!&attributes! и последующих элементов
списка\footnote{Заметьте, что \lstinline!&attributes!~-- это лишь обычный символ, нет ничего
специального в символах, чьи имена начинаются с \lstinline!&!.}\hspace{\footnotenegspace}.
\begin{myverb}
(defun parse-html-macro-lambda-list (args)
(let ((attr-cons (member '&attributes args)))
(values
(cadr attr-cons)
(nconc (ldiff args attr-cons) (cddr attr-cons)))))
\end{myverb}
\begin{myverb}
HTML> (parse-html-macro-lambda-list '(a b c))
NIL
(A B C)
HTML> (parse-html-macro-lambda-list '(&attributes attrs a b c))
ATTRS
(A B C)
HTML> (parse-html-macro-lambda-list '(a b c &attributes attrs))
ATTRS
(A B C)
\end{myverb}
Элемент, следующий за \lstinline!&attributes! в списке параметров, также может быть
списком параметров.
\begin{myverb}
HTML> (parse-html-macro-lambda-list '(&attributes (&key x y) a b c))
(&KEY X Y)
(A B C)
\end{myverb}
Теперь у вас все готово для написания \lstinline{define-html-macro}. В~зависимости от
того, были ли указан параметр \lstinline!&attributes!, вам нужно сгенерировать один или
другой из видов макросов HTML, так что главный макрос просто определяет что он должен
генерировать, и затем вызывает вспомогательную функцию, которая будет генерировать нужный
код.
\begin{myverb}
(defmacro define-html-macro (name (&rest args) &body body)
(multiple-value-bind (attribute-var args)
(parse-html-macro-lambda-list args)
(if attribute-var
(generate-macro-with-attributes name attribute-var args body)
(generate-macro-no-attributes name args body))))
\end{myverb}
Функции, которые генерируют соответствующий код, выглядят так:
\begin{myverb}
(defun generate-macro-with-attributes (name attribute-args args body)
(with-gensyms (attributes form-body)
(if (symbolp attribute-args) (setf attribute-args `(&rest ,attribute-args)))
`(eval-when (:compile-toplevel :load-toplevel :execute)
(setf (get ',name 'html-macro-wants-attributes) t)
(setf (get ',name 'html-macro)
(lambda (,attributes ,form-body)
(destructuring-bind (,@attribute-args) ,attributes
(destructuring-bind (,@args) ,form-body
,@body)))))))
(defun generate-macro-no-attributes (name args body)
(with-gensyms (form-body)
`(eval-when (:compile-toplevel :load-toplevel :execute)
(setf (get ',name 'html-macro-wants-attributes) nil)
(setf (get ',name 'html-macro)
(lambda (,form-body)
(destructuring-bind (,@args) ,form-body ,@body)))))
\end{myverb}
Функции, которые вы определите, принимают либо один, либо два аргумента и затем
используют \lstinline{DESTRUCTURING-BIND} для их разделения и связывания их с
параметрами, определёнными в вызове к \lstinline{define-html-macro}. В~обоих раскрытиях
выражений вам необходимо сохранить макрофункции в списке свойств символа, используя имя
свойства, равное \lstinline{html-macro}, а также логическое значение, указывающее на то,
принимает ли макрос параметр \lstinline!&attributes!, в свойстве
\lstinline{html-macro-wants-attributes}. Вы используете это свойство в следующей функции,
\lstinline{expand-macro-form}, для того чтобы определить, как макрофункция должна быть
запущена:
\begin{myverb}
(defun expand-macro-form (form)
(if (or (consp (first form))
(get (first form) 'html-macro-wants-attributes))
(multiple-value-bind (tag attributes body) (parse-cons-form form)
(funcall (get tag 'html-macro) attributes body))
(destructuring-bind (tag &body body) form
(funcall (get tag 'html-macro) body))))
\end{myverb}
Нам надо сделать последний шаг для интеграции макросов в наш язык путём добавления
соответствующей ветки в \lstinline{COND} в функцию \lstinline{process}.
\begin{myverb}
(defun process (processor form)
(cond
((special-form-p form) (process-special-form processor form))
((macro-form-p form) (process processor (expand-macro-form form)))
((sexp-html-p form) (process-sexp-html processor form))
((consp form) (embed-code processor form))
(t (embed-value processor form))))
\end{myverb}
Это окончательная версия \lstinline{process}.
\section{Публичный интерфейс разработчика (API)}
Теперь вы готовы к реализации макроса \lstinline{html}~-- основной точке входа компилятора
FOO. Другими частями публичного интерфейса разработчика являются \lstinline{emit-html} и
\lstinline{with-html-output}, которые мы обсуждали в предыдущей главе, и
\lstinline{define-html-macro}, которую мы обсуждали в предыдущем разделе. Макрос
\lstinline{define-html-macro} должен быть частью интерфейса разработчика, поскольку
пользователи FOO захотят писать свои собственные макросы. С другой стороны,
\lstinline{define-html-special-operator} не является частью интерфейса, поскольку он требует
слишком глубокого знания внутреннего устройства FOO для определения нового специального
оператора. И должно быть очень мало вещей, которые не смогут быть сделаны при наличии
существующих возможностей языка и специальных операторов\pclfootnote{Один из элементов,
который в настоящее время не доступен через специальный оператор,~-- это расстановка
отступов. Если вы захотите сделать FOO более гибким, хотя и ценой того, что его
интерфейс разработчика будет более сложным, вы можете добавить специальный оператор,
который будет управлять расстановкой отступов. Но кажется, что цена того, что
потребуется объяснять наличие дополнительных операторов, будет перевешивать относительно
небольшое преимущество в выразительности.}.
Последним элементом публичного интерфейса, который мы рассмотрим до \lstinline{html}, является
ещё один макрос~-- \lstinline{in-html-style}. Этот макрос контролирует то, должен ли FOO
генерировать XHTML или простой HTML путём установки переменной \lstinline{*xhtml*}. Причиной
того, что вам нужен макрос, является то, что вы можете захотеть обернуть код, который
устанавливает \lstinline{*xhtml*} в \lstinline{EVAL-WHEN}, так что вы можете установить его в файл,
и это будет влиять на поведение макроса \lstinline{html}, находящегося в том же файле.
\begin{myverb}
(defmacro in-html-style (syntax)
(eval-when (:compile-toplevel :load-toplevel :execute)
(case syntax
(:html (setf *xhtml* nil))
(:xhtml (setf *xhtml* t)))))
\end{myverb}
И в заключение давайте рассмотрим \lstinline{html}. Единственная нестандартность в реализации
\lstinline{html} возникает из необходимости генерировать код, который будет использоваться для
генерации и компактного, и <<красивого>> (pretty) вывода, в зависимости от значения
переменной \lstinline{*pretty*} во время выполнения. Таким образом, в \lstinline{html} требуется
генерировать раскрытие, которое будет содержать выражение \lstinline{IF} и две версии кода~--
одну скомпилированную с \lstinline{*pretty*}, равным истине, и одну~-- для значения переменной,
равной \lstinline{NIL}. Также составляет сложность то, что достаточно часто один вызов
\lstinline{html} содержит вложенные вызовы \lstinline{html}, например вот так:
\begin{myverb}
(html (:ul (dolist (item stuff)) (html (:li item))))
\end{myverb}
Если внешний вызов \lstinline{html} раскрывается в выражение \lstinline{IF} с двумя версиями кода,
одним для случая, когда переменная \lstinline{*pretty*} имеет истинное значение, и вторым,
ког\-да она имеет ложное, то будет глупо, если вложенные выражения \lstinline{html} также будут
раскрываться в две версии. В~действительности это будет вести к экспоненциальному росту
кода, поскольку вложенные \lstinline{html} уже будут раскрыты дважды~-- один раз для ветви
\lstinline{*pretty*-is-true} и один раз для ветви \lstinline{*pretty*-is-false}. Если каждое из
раскрытий сгенерирует две версии, то вы будете иметь 4 версии кода. А если вложенное
выражение \lstinline{html} содержит ещё одно вложенное выражение \lstinline{html}, то вы получите
восемь версий. Если компилятор достаточно умен, то он может распознать, что большая часть
кода не будет использована, и удалит её, но распознавание таких ситуаций займёт достаточно
большое время, замедляя компиляцию любой функции, которая использует вложенные вызовы
\lstinline{html}.
К счастью, вы можете легко избежать этого разрастания ненужного кода путём генерации
раскрытия, которое локально переопределяет макрос \lstinline{html}, используя \lstinline{MACROLET},
для того чтобы генерировать только нужный вид кода. Сначала вы определяете
вспомогательную функцию, которая получает вектор кодов операций, возвращаемый
\lstinline{sexp->ops}, и прогоняет его через функции \lstinline{optimize-static-output} и
\lstinline{generate-code} (две стадии, на которые влияет значение переменной \lstinline{*pretty*}) с
переменной \lstinline{*pretty*}, установленной в нужное значение, и затем собирает
результирующий код в \lstinline{PROGN}. (\lstinline{PROGN} возвращает \lstinline{NIL} лишь для унификации
результатов.)
\begin{myverb}
(defun codegen-html (ops pretty)
(let ((*pretty* pretty))
`(progn ,@(generate-code (optimize-static-output ops)) nil)))
\end{myverb}
Используя эту функцию, вы можете определить \lstinline{html} следующим образом:
\begin{myverb}
(defmacro html (&whole whole &body body)
(declare (ignore body))
`(if *pretty*
(macrolet ((html (&body body) (codegen-html (sexp->ops body) t)))
(let ((*html-pretty-printer* (get-pretty-printer))) ,whole))
(macrolet ((html (&body body) (codegen-html (sexp->ops body) nil)))
,whole)))
\end{myverb}
Параметр \lstinline!&whole! представляет оригинальное выражение \lstinline{html}, и поскольку
он интерполируется в раскрытие в теле двух \lstinline{MACROLET}, то он будет обрабатываться с
каждым из определений \lstinline{html}~-- для кода, выдающего <<красивый>> и обычный результат.
Заметьте, что переменная \lstinline{*pretty*} используется и при раскрытии макроса, и при
выполнении сгенерированного кода. Она используется при раскрытии макроса в
\lstinline{codegen-html} для того, чтобы заставить \lstinline{generate-code} генерировать нужный вид
кода. И она используется во время выполнения в выражении \lstinline{IF}, сгенерированном
макросом \lstinline{html} самого верхнего уровня, для того, чтобы определить, какая из
ветвей~-- \lstinline{pretty-printing} и \lstinline{non-pretty-printing}~-- будет выполнена.
\section{Завершение работы}
Как обычно, вы можете продолжить работу над этим кодом для расширения его
функциональности. Одной из интересных задач может быть использование системы генерации
вывода для создания других видов выходных данных. В~реализации FOO, которую вы можете
скачать с сайта, посвящённого книге, вы можете найти код, который реализует вывод CSS,
который может быть интегрирован в HTML и в компиляторе, и в интерпретаторе. Это
интересный случай, поскольку синтаксис CSS не может быть так же просто отображён в
s-выражения, как это можно сделать для HTML. Однако если вы посмотрите в этот код, то
увидите, что все равно можно определить синтаксис, основанный на s-выражениях, для
представления разных конструкций, доступных в CSS.
Более амбициозной задачей будет добавление поддержки генерации JavaScript. При правильном
подходе добавление поддержки JavaScript в FOO может привести к двум большим победам.
Во-первых, если вы определите синтаксис, основанный на s-выражениях, так что вы сможете
отобразить его на синтаксис JavaScript, то вы сможете начать писать макросы (на Common
Lisp) для добавления новых конструкций к языку, который вы используете для написания кода,
исполняемого на стороне пользователя, который затем будет компилироваться в JavaScript.
Во-вторых, при переводе s-выражений FOO с поддержкой JavaScript в обычный JavaScript вы
можете столкнуться с небольшими, но раздражающими различиями в реализации JavaScript в
разных браузерах. Так что код JavaScript, генерируемый FOO, либо может содержать
соответствующие условия для выполнения одних операций в одном браузере и других в другом
браузере, либо может генерировать разный код в зависимости то того, какой браузер вы
хотите поддерживать. Так что если вы используете FOO в динамически генерируемых
страницах, то вы можете использовать информацию из заголовка \lstinline{User-Agent},
заставляя функцию \lstinline{request} генерировать правильный код JavaScript для
конкретного браузера.
Но если это вас интересует, то вы можете это реализовать сами, поскольку это конец
последней практической главы данной книги. В~следующей главе я подведу итоги, сделаю
короткий обзор некоторых тем, которых я не затрагивал в этой книге, например как искать
библиотеки, как оптимизировать код на Common Lisp и как распространять приложения на Lisp.
%%% Local Variables:
%%% mode: latex
%%% TeX-master: "pcl-ru"
%%% TeX-open-quote: "<<"
%%% TeX-close-quote: ">>"
%%% End: