0:00
, In this segment, I want to continue looking at how Macros interact with the
treatment of variables in our programming language. It turns out that semantics of
Macros is different than the naive sort of macro expansion I've shown you so far,
that's a good thing. It's better in situations where variables may shadow
things that are in a macro. And that better semantics goes by the name of
hygienic macros. That's what I'm going to show you here. So, to do this, I'm going
to use a very simple example, which is a macro that doubles its argument. So, I
want to emphasize first, it says, poor style. If you need something that doubles
its argument, use one of these two functions you see at the top of the slide.
They're both perfectly good and they are equivalent to each other. And no client
can tell how you've implemented it, you just doubling and that's wonderful. But,
for the sake of a short example that will fit on a slide, here are two macro
versions, where the first macro replaces double of some expression x with plus x,
x. The second one replaces double of some expression x with star 2 x. Now, these two
macros are not equivalent to each other. We saw this sort of thing in the previous
segment. If I use the first macro with this argument, something that prints hi
and then returns 42, I do get the result 84. But I also print out hi twice, because
I would replace double of this thing with plus of this thing and this thing and so
each of them will print. Whereas, the second version will only do the printing
once, because the replacement only uses this expression once. So, they're not
equivalent, and we want to study that sort of thing in this segment, and even more
subtle things. So, if you wanted to do something like the first one, you wanted
to implement doubling as adding twice. And you wanted to use a macro and you didn't
want it to print twice. Then, you should do something like this third version. What
you should do is, say, well, when I have double of some expression, let's expand
tha t to a let expression. Let's introduce a local variable y to hold x, so x is some
expression, maybe it prints But then, y will jsut be the result of that
expression, because that's how let expressions work. And then, we'll add y
and y. And this is, these y's, are jsut looking up a variable that holds the
result of a previous computation. So now, this macro seems to work much more like
the version at star 2 x, because it only prints once. Fine, the general technique
here is to use a let expression to make sure that your macro arguments are
evaluated exactly once. Now, that's not always what you want. The my delay macro
we did earlier we didn't necessary want to evaluate it once, we wanted to put it
inside a thunk. Other times, you may want to have some of your macro arguments
purposely be evaluated multiple times because that's the entire purpose of the
macro. In the next segment, I'll show you something with a for loop, where there
actually is some expression we want to evaluate some number of times and then,
you shouldn't do this let expression either. But the technique is to use a let
expression to control the number of times something is evaluated. Another thing you
might think about is the order that your arguments are evaluated. So, here's an
example here on the bottom that does something that you might not want. So,
this defines a macro, take e1 from e2, so this is a subtraction, but in the opposite
order. It says the result should be, the result of evaluating e1 and e2 and then
subtracting from e2, the amount e1. Maybe this is more logical ordering of the
arguments to you. But the way this is being macro expanded, we end up with minus
e2, e1. And that's right for producing the right answer. But if the order of
evaluating your subexpressions matters, this will evaluate e2 before e1, because
we do the expansion, the result of the expansion is a function call and in
Racket, function arguements are evaluated in order from left to right. That might,
can the user of your macro, who doe s not know that this is the macro expansion,
they just read the documentation of use this macro to take e1 from e2, might be
very surprised when things are evaluated in the opposite order that they wrote them
down and you can once again fix this, not shown here, with a let expression to
evalute e1 and then e2 and then use those results in the subtraction.
Okay. So, I'm recommending, for issues like you
see on this slide, putting local variable in your macros. Those of you that have,
perhaps, done a lot of programming with C and C++ macros might be really upset that
I'm doing that. Because in those languages, it's usually very unwise to put
local variables inside of your macros and if you ever need to do it, people end up
using variable names that probably no one else would possibly use anywhere, Like,
__strange_name34. So, for all of you, let me explain why,
people in impoverished macro systems end up doing this and how Racket has a
semantics that makes this unnecessary, you can use local variables in your macros,
and everything works out fine. So, here is an example. Here is yet
another version of double, it's very silly for the purpose of demonstration. So, what
this does is it takes in some expression x and then it does multiply 2 and x. So,
that was the easy version. But then, it also multiplies by y, where y is some
local variable that holds 1. So, it seems clear that this will do the right thing,
it will double its argument, it just has this unnecessary argument y. But now,
here's a very strange use of the macro. It's not strange to the user, makes
perfect sense that I might have some local variable y, that's 7 and then use the
double macro with y. So, if I'm the user of the double macro, I'd expect this to
return 14. The fact that there's some other y up in the macro definition, should
be an implementation detail. But under the naive semantics, I've been kind of
assuming throughout this section, that would not be what happens. If you take
this double y and you replace it, as the rules up here would suggest, you would end
up with let y be 1 in star 2 y, y. All I did was take the body of the macro. But
wherever there is an x, I put the actual argument to the macro, which is y. This is
a problem. If you evaluate this entire result, which is what you would get from
that naive macro expansion, you will not get 14, you'll get 2. Somehow, the y here
at the macro used got captured, got put underneath the y in the macro definition.
This is the sort of thing that can happen in C or C++, this is not what happens in
Racket. Racket gets this right if you have this use, exactly as you see on the slide,
when you run this, you will actually get 14. This is part of what it means for your
macros to be hygienic. They are clean, they keep separate the local variables in
the macro from the any local variables that might be in scope where you're using
the macro. The way Racket does this is it just changes the names of variables in
macro definitions as necessary and we're not going to go into the details of how it
does it. Just rest assured that you get 14 in Racket, you do not get the naive
expansion. So, it turns out that's one of the problems that can come up. There's a
second one, where you also get the wrong answer and that Racket also fixes. So, let
me now show you that one. Here, I have this perfectly reasonable macro. There's
nothing wrong with this macro except that you should define a function and it just
takes double of sum expression x and produces star 2 x. But what if we use that
somewhere, where the function star, and star is just a function, I could have
shown you an example of a function named foo instead of star. What if we used this
macro somewhere where star referred to something else because the built-in
standard library star was shadowed? Here is such a use right here. If I call the
double macro with 42 in an environment where multiplication has been shadowed by
addition, then the naive expansion would be star 2 of 42 in a situation where star
has actually been rebound to plus and I would get 44 instead of 84. And again,
Racket does the right thing. If you have variables like star in a macro definition
that are defined outside the macro definition, you look them up in the
environment where the macro was defined, not in the environment where the macro was
used. This is our old friend lexical scope and its applies to macro definitions in
Racket, the same way it applies function definitions, then this is again, something
that most macro systems including the pre-processor for C and C++ simply do not
do. This is sometimes abused. This sort of dynamic scoping in those languages as a
feature instead of a bug. But I would say that experience suggests that the vast
majority of the time, you would rather have Racket's lexical scope and hygienic
semantics, than what you get in these other systems. Okay.
So, a hygienic macrosystem basically gets these two issues correct. The way it does
it is for the first issue, when you have a macro definition, it secretly replaces all
the local variables with other local variables and uses them consistently so
they can't interfere with anything that might be used in the use of the macro. And
for the second issue it basically uses lexical scope for macros the same way it
does for functions. This is not what you get by naive
expansion so it's actually quite a bit harder to implement Racket's module system
than you would naively think. But the Racket designers and implementers were
able to do that, and were able to enjoy the benefits. I would just remark in
passing, these hygenic rules are not always what you want. They're just what
you want the vast majority of the time. So, there's much more to Racket's module
system. You could read about in the Racket reference if you're implement, interested.
And they even have specific ways to indicate specific places where you don't
want these hygenic rules, and they'll give you a different semantics when that's what
you ask for.