CS 1321X - Lecture 25 - November 13, 2003

CS 1321X - Lecture 25

The Thing That's Beyond the Scope of this Course


I. Macros

If you've read any gothic horror literature, you may have come across
an author by the name of H.P. Lovecraft.  It's odd stuff, to be sure,
and Lovecraft has a penchant for begging the question of describing
just how horrible the horrors are in his stories by resorting to
things like "it was the horror that could not be named" or 
"the thing that could not be described" and so on.  It gets a little
tedious after awhile.

How does that relate to 1321X?  It's simple...every so often some
issue will come up and I'll say something like "well, that's not
really a function, and it doesn't evaluate its arguments when
it's called, and how to write something like that is BEYOND
THE SCOPE OF THIS COURSE."  It struck me that my answer was sort
of Lovecraftian in its own way, so I've decided to put how to
write something like that within the scope of this course.  
You'll thank me for it later.  Sure.

We told you way back when that in Scheme, procedures and data were for 
all intents and purposes the same thing, and that there were ways
in which procedures could create other procedures.  One of the mechanisms 
available to you for creating procedures at run time is something called 
the "macro".  A macro works like a function, except that when Scheme sees a 
call to a macro in some code that is being executed, the definition of the 
macro is inserted in place of the macro call. It's nothing more than a text 
substitution for the most part. This is in contrast to a "normal" function 
call which results in a copy of the current environment being saved on the 
program stack, followed by the execution of the function definition, which 
in turn is followed by the "popping" of the program stack. Another way to 
think of the difference between functions and macros is this: A function 
produces results, while a macro produces expressions which, when evaluated, 
produce results.


II. Macros and their arguments

One big important feature of macros is that they allow you to create 
constructs which do not evaluate their arguments. The importance of this may 
not be immediately obvious. An example will help you see the light. Say that 
you had a version of Scheme in which there was no "if" form, and you wanted 
to create one. How would you do it?

Your first thought might be to create an ordinary function:

(define (my-if test then-part else-part)
  (cond (test then-part)
        (else else-part)))

That looks reasonable, no?  Let's test it:


> (my-if #t 2 3)
2
> (my-if #f 2 3)
3

Perfect. Exactly what we want. Let's test it just a little bit more:

> (my-if #t (display "yes") (display "no"))
yesno


OOPS!! What happened here? Why does "my-if" do that? The problem is that 
"my-if" evaluates its arguments when called, and these particular arguments 
have side-effects when evaluated. So, even before the "my-if" definition is 
applied to the arguments, the two "print" expressions are evaluated, 
resulting in both "yes" and "no" being printed on the terminal. But that's 
not what "if" does, is it?

How then does "if" work? It's actually something called a special form, 
hard-coded into the Scheme compiler. You can't write a special form; they're 
written only by the folks who created the compiler. But, "if" could be 
implemented as a macro, and that's something you could do yourself. How? By 
using another special form called "define-macro":


 (define-macro my-if
   (lambda (test then-part else-part)
     (list 'cond (list test then-part)
           (list 'else else-part))))

When Scheme then sees:


> (my-if #t (display "yes") (display "no"))

it first "expands the macro", replacing the macro call with:


(cond (#t (display "yes"))
      (else (display "no")))


In other words, anything that's quoted is used literally, and anything that's 
not quoted is replaced by the value it's bound to at the time the macro call 
is encountered.

Next we'll look at a different syntax for creating macros that's easier on 
the eyes, but first some cautionary notes. The approach to macros that we're 
looking at here is not part of the Scheme language standard. The 
"define-macro" feature is specific to our Dr. Scheme system, so it's not 
very portable to other Scheme systems, and that's generally a bad thing. 
To get this stuff to work on your system, you'll need to pull down
Dr. Scheme's "language" menu, click "choose language", and then under
"Professional Languages" you'll click on "PLT" and choose "Pretty Big"
Scheme.  I hadn't chosen "Pretty Big" Scheme on my laptop, and that's
why my example didn't work in class today.

On the other hand, it's a fairly simple macro capability, and that makes it 
a good thing for you, the relatively new programmer. But in the long run, 
keep in mind that macros are useful features, they exist in languages besides 
Scheme, and the principles of use are generally the same regardless of which 
language, or even which dialect of a given language.


III. Backquote-comma syntax

We could write our macros they way we did above, but the macros that result 
don't look much like the code they're going to expand into. This makes macros 
hard to read and debug. So a syntax for macro creation has been invented, 
which when used makes these things look more like templates for the code to 
be generated.

The backquote-comma syntax introduces two new operators: the backquote (`) 
and the comma (,). What did you expect? The backquote tells Scheme to quote 
everything in the list that follows (so in that sense it works just like 
quote), except for those items immediately preceded by a comma. The comma 
works only within the scope of a backquote. It says "turn off the quoting 
for now". When it appears in front of a list element, it cancels out the 
quote that would have been in effect otherwise. So instead of the literal 
symbol being put into that template, the value of that symbol at the time 
of the macroexpansion is put there.

In other words, backquote makes a template while comma makes a slot in 
the template.

Here's "my-if" again, using backquote-comma syntax:


(define-macro my-if
  (lambda (test then-part else-part)
    `(cond (,test ,then-part)
           (else ,else-part))))


This expands exactly the same way as our previous macro definition, but it's 
much easier to read because it looks much more like the code that it will 
turn into, no?


IV. How to define simple macros (taken from "On LISP" by Paul Graham)

Let's walk through another macro example in gory detail and, along the way, 
we'll outline a step-by-step process for defining macros.

Let's say you're a side-effect-happy programmer who longs for the old days, 
and you'd like to be able to increment some numeric variable x by 1 just by 
typing in

(++ x)

instead of

(set! x (+ x 1))

Would that be neat or what? Can it be a function?:

(define (++ x)
  (set! x (+ x 1))

Let's test it:

>(define test 3)

>(++ test)
>test
3

No, that won't work, because of lexical scoping.  But if we had just written
(set! x (+ x 1)) in place of the function call to (++ test), we'd get what 
we want.  A macro will allow us to do what we want here.  And that would be 
an easy macro to write. Graham tells us that the first step in macro 
definition is to begin with a typical call to the macro that we want to 
define, and write it on a piece of paper. Below that, we should write the 
expression that we want the macro call to expand into:

call:        (++ x)

expansion:   (set! x (+ x 1))


Then, we begin the macro definition like this:


(define-macro ++


And we construct the argument list for the macro definition from the 
parameters in the macro call. To avoid confusion, we invent new names for 
each of these arguments:


(define-macro ++
  (lambda (var)


Now we go back to the call and expansion that we wrote down. For each 
argument in the macro call, we draw a line connecting it with the place 
it appears in the expansion:



call:        (++ x)
                  \ \
                   \  \
                    \   \
expansion:    (set! x (+ x 1))


Now we look at the expansion, and while reading the expansion, we start 
constructing the macro body. We begin with a backquote:


(define-macro ++
  (lambda (var)
    `

Then, whenever we see a parenthesis in the expansion that isn't part of an 
argument in the macro call, we put that same parenthesis in the macro 
definition:


(define-macro ++
  (lambda (var)
    `(


And then, for each expression in the expansion, we do the following:


1.  If there is no line connecting it with the macro call, we write down 
    the expression itself, as is.

2.  If there is a connection to one of the arguments in the macro call, we 
    write down the symbol which occurs in the corresponding position in 
    the macro parameter list, immediately preceded by a comma (i.e., we 
    don't want this quoted...instead, we want to pass a value through here).

Since there's no connecting line to the first element, "set!", we write it 
down as is:


(define-macro ++
  (lambda (var)
    `(set!


On the other hand, "x" does have a connecting line, so in the macro body we 
insert the corresponding parameter, preceded by a comma:


(define-macro ++
  (lambda (var)
    `(set! ,var


And we do this stuff until we're done:


(define-macro ++
  (lambda (var)
    `(set! ,var (+ ,var 1))))


That was easy, wasn't it?


V.  Advantages and disadvantages of macros

There are two key advantages to using macros:


1.  The arguments to macros are not evaluated. Thus, new syntactic 
    constructs can be defined with macros. This is essential to defining 
    new languages on top of Scheme, especially languages that aren't 
    constrained in ways that functional programming languages often are.

2.  Macros produce faster (but more) code than functions because they avoid 
    the overhead of function calls.

But there are also disadvantages:


1.  If you compile macros, you can't detect them with your usual debugging 
    tools.  Things like the macro name goes away once the text replacement
    is done.

2.  Since macros aren't functions, they can't be used with things that 
    expect functions, like "apply".

3.  Again, if you compile macros, when you redefine a macro, you must 
    recompile every function whose source contains a call to the macro. If 
    you don't, your source code may contain the results of expanding old 
    macro definitions instead of the new ones. You may think the new macro 
    has been expanded where the macro call occurs, but it won't be. This can 
    be especially aggravating if the old macro definition resulted in code 
    which didn't blow up. So your program does something, it just doesn't do 
    what your new macro definition says it should do. You can spend lots of 
    hours spinning wheels trying to debug that kind of thing.  It's not
    a big deal for you to worry about now, but it's something that might
    bite you in the years to come if you're working with macros.

4.  Macros produce more (but faster) code than functions because additional 
    code is added to your program every time a macro call is expanded.

There are lots of other useful things to know about macros, issues like 
"variable capture", but those sorts of things are, um, beyond the scope
of this course.



Copyright (c) 2003 by Kurt Eiselt.  All rights reserved, with 
the exception of stuff that belongs to somebody else.

Last revised: November 13, 2003