I was having a conversation with a colleague recently and tried telling him about the beauty of (Common) Lisp. I tried to explain macros somehow, since I consider macros one of the killer features of Lisp, but I failed rather miserably -- I couldn't find a good example which would be short, concise and understandable by a "mere mortal" programmer (decad开发者_高级运维e of Java experience, a bright guy altogether, but very little experience with "higher-order" languages).
How would you explain Lisp macros by example if you had to?
From my experience, macros make the best impression on people when they see how it helps to produce code, that cannot be made by the procedures or other constructs. Very often such things may be described as:
<common code>
<specific code>
<other common code>
where <common code>
is always the same. Here are some examples of such schema:
1. The time
macro. Code in a language without macros will look something like this:
int startTime = getCurrentTime();
<actual code>
int endTime = getCurrentTime();
int runningTime = endTime - startTime;
You cannot put all common code to procedure, since it wraps actual code around. (OK, you can make a procedure and pass actual code in lambda function, if the language supports it, but it is not always convenient).
And, as you most probably know, in Lisp you just create time
macro and pass actual code to it:
(time
<actual code>)
2. Transactions. Ask Java-programmer to write method for simple SELECT
with JDBC - it will take 14-17 lines and include code to open connection and transaction, to close them, several nested try-catch-finally
statements and only 1 or 2 lines of unique code.
In Lisp you just write with-connection
macro and reduce code to 2-3 lines.
3. Synchronization. OK, Java, C# and most of the modern languages already have statements for it, but what to do if your language doesn't have such a construct? Or if you want to introduce new kind of synchronization like STM-based transactions? Again, you should write separate class for this task and work with it manually, i.e. put common code around each statement you want to synchronize.
That was only few examples. You can mention "not-to-forget" macros like with-open
series, that clean-up environment and protect you from recourses leaks, new constructs macros like cond
instead of multiple if
s, and, of course, don't forget about lazy constructs like if
, or
and and
, that do not evaluate their arguments (in opposite to procedure application).
Some programmers may advocate, that their language has a technology to treat this or that case (ORM, AOP, etc), but ask them, would all these technologies be needed, if macros existed?
So, taking it altogether and answering original question about how to explain macros. Take any widely-used code in Java (C#, C++, etc), transform it to Lisp and then rewrite it as a macro.
New WHILE statement
Your language designer forgot a WHILE statement. You've mailed him several times. No success. You've waited from language version 2.5, 2.6 to 3.0. Nothing happened...
In Lisp:
(defmacro while ... insert your while implementation here ...)
Done.
The trivial implementation using LOOP takes a minute.
Code generation from specifications
Then you may want to parse call detail records (CDRs). You have record names with field descriptions. Now I can write classes and methods for each of those. I could also invent some configuration format, parse a configuration file and create the classes. In Lisp I would write a macro that generates the code from a compact description.
See Domain Specific Languages in Lisp, a screencast showing a typical development cycle from a working sketch to a simple macro based generalization.
Code rewriting
Imagine that you have to access slots of objects using getter functions. Now imagine that you need to access some objects multiple times in some code region. For some reason using temporary variables is no solution.
...
... (database-last-user database) ...
...
Now you could write a macro WITH-GETTER which introduces a symbol for the getter expression.
(with-getters (database (last-user database-last-user))
...
... last-user
...)
The macro would rewrite the source inside the enclosed block and replace all specified symbols with the getter expression.
Since concrete examples can get bogged down in the details of the language you're writing them in, consider a non-concrete but relatable statement:
"You know all that boilerplate code that you sometimes have to write? You never have to write boilerplate in lisp, since you can always write a code-generator to do it for you."
By 'boilerplate', I'm thinking of one-off interface implementations in Java, overriding implicit constructors in c++, writing get()-set() pairs, etc. I think this rhetorical strategy might work better than trying to explain macros directly in too much detail, since he's probably all too familiar with various forms of boilerplate, whereas he's never seen a macro.
I don't know CL well enough, but will Scheme macros do? Here's a while loop in Scheme:
(define-syntax while
(syntax-rules ()
((while pred body ...)
(let loop ()
(if pred (begin body ... (loop)))))))
In this case, the example demonstrates that you can easily write your own control structures using macros. foof-loop
is a collection of even more useful looping constructs (probably nothing new given CL's ones, but still good for a demonstration).
Another use case: picking values out of associative lists. Say users pass in an alist as options to your function. You can easily pick values out by using this macro:
(define-syntax let-assq
(syntax-rules ()
((let-assq alist (key) body ...)
(let ((key (assq-ref alist 'key)))
body ...))
((let-assq alist (key rest ...) body ...)
(let ((key (assq-ref alist 'key)))
(let-assq alist (rest ...) body ...)))))
;; Guile built-in
(define (assq-ref alist key)
(cond ((assq key alist) => cdr)
(else #f)))
Example usage:
(define (binary-search tree needle (lt? <))
(let loop ((node tree))
(and node
(let-assq node (value left right)
(cond ((lt? needle value) (loop left))
((lt? value needle) (loop right))
(else value))))))
Notice how the let-assq
macro allows picking out the value
, left
, and right
keys from the "node" without having to write a much longer let
form.
I view macros as an abstraction similar (or dual) to functions, except you may choose when and how to evaluate the arguments. This underlines why macros are useful - just like functions are, to prevent code duplication and to ease maintenance.
My favorite example are anaphoric macros. Like aif, awhile or aand:
(defmacro aif (test-form then-form &optional else-form)
`(let ((it ,test-form))
(if it ,then-form ,else-form)))
(defmacro awhile (expr &body body)
`(do ((it ,expr ,expr)) ((not it))
,@body))
(defmacro aand (&rest args)
(cond
((null args) t)
((null (cdr args)) (car args))
(t `(aif ,(car args) (aand ,@(cdr args))))))
These are very simple and can save a lot of typing.
It's not something you can explain in a short amount of time, well it is, the concept of macro can be explained in one sentence and an example such as a while is fairly easy to understand, the problem is that that person will not really understand why macros are a nice thing to have with only such trivial examples.
精彩评论