Книга: Practical Common Lisp

FOO Macros

FOO Macros

FOO macros are similar in spirit to Common Lisp's macros. A FOO macro is a bit of code that accepts a FOO expression as an argument and returns a new FOO expression as the result, which is then evaluated according to the normal FOO evaluation rules. The actual implementation is quite similar to the implementation of special operators.

As with special operators, you can define a predicate function to test whether a given form is a macro form.

(defun macro-form-p (form)
(cons-form-p form #'(lambda (x) (and (symbolp x) (get x 'html-macro)))))

You use the previously defined function cons-form-p because you want to allow macros to be used in either of the syntaxes of nonmacro FOO cons forms. However, you need to pass a different predicate function, one that tests whether the form name is a symbol with a non-NIL html-macro property. Also, as in the implementation of special operators, you'll define a macro for defining FOO macros, which is responsible for storing a function in the property list of the macro's name, under the key html-macro. However, defining a macro is a bit more complicated because FOO supports two flavors of macro. Some macros you'll define will behave much like normal HTML elements and may want to have easy access to a list of attributes. Other macros will simply want raw access to the elements of their body.

You can make the distinction between the two flavors of macros implicit: when you define a FOO macro, the parameter list can include an &attributes parameter. If it does, the macro form will be parsed like a regular cons form, and the macro function will be passed two values, a plist of attributes and a list of expressions that make up the body of the form. A macro form without an &attributes parameter won't be parsed for attributes, and the macro function will be invoked with a single argument, a list containing the body expressions. The former is useful for what are essentially HTML templates. For example:

(define-html-macro :mytag (&attributes attrs &body body)
`((:div :class "mytag" ,@attrs) ,@body))
HTML> (html (:mytag "Foo"))
<div>Foo</div>
NIL
HTML> (html (:mytag :id "bar" "Foo"))
<div>Foo</div>
NIL
HTML> (html ((:mytag :id "bar") "Foo"))
<div>Foo</div>
NIL

The latter kind of macro is more useful for writing macros that manipulate the forms in their body. This type of macro can function as a kind of HTML control construct. As a trivial example, consider the following macro that implements an :if construct:

(define-html-macro :if (test then else)
`(if ,test (html ,then) (html ,else)))

This macro allows you to write this:

(:p (:if (zerop (random 2)) "Heads" "Tails"))

instead of this slightly more verbose version:

(:p (if (zerop (random 2)) (html "Heads") (html "Tails")))

To determine which kind of macro you should generate, you need a function that can parse the parameter list given to define-html-macro. This function returns two values, the name of the &attributes parameter, or NIL if there was none, and a list containing all the elements of args after removing the &attributes marker and the subsequent list element.[320]

(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)))))
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)

The element following &attributes in the parameter list can also be a destructuring parameter list.

HTML> (parse-html-macro-lambda-list '(&attributes (&key x y) a b c))
(&KEY X Y)
(A B C)

Now you're ready to write define-html-macro. Depending on whether there was an &attributes parameter specified, you need to generate one form or the other of HTML macro so the main macro simply determines which kind of HTML macro it's defining and then calls out to a helper function to generate the right kind of code.

(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))))

The functions that actually generate the expansion look like this:

(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)))))

The macro functions you'll define accept either one or two arguments and then use DESTRUCTURING-BIND to take them apart and bind them to the parameters defined in the call to define-html-macro. In both expansions you need to save the macro function in the name's property list under html-macro and a boolean indicating whether the macro takes an &attributes parameter under the property html-macro-wants-attributes. You use that property in the following function, expand-macro-form, to determine how the macro function should be invoked:

(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))))

The last step is to integrate macros by adding a clause to the dispatching COND in the top-level process function.

(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))))

This is the final version of process.

Оглавление книги

Оглавление статьи/книги

Генерация: 1.200. Запросов К БД/Cache: 3 / 1
поделиться
Вверх Вниз