[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
Re: Macros Make Me Mad
At 10:22 AM -0500 11/18/02, Jake Donham wrote:
>"Todd" == Todd Proebsting <toddpro@microsoft.com> says:
>
> Todd> Defining a macro is often an act of defining a language
> Todd> construct and implementing it. Reasonably or not, I hold
> Todd> language constructs to a higher standard than, say, a mere
> Todd> function definition.
>
>This idea gets to the heart of the disagreement. I think people who
>are pro-macro don't see a strong distinction here. In fact they see
>even a "mere" function definition as creating a new language
>construct.
>
[I haven't read my ll1-discuss mail for a while, due to schedule
deadlines. I hope I am not repeating the obvious.]
This person who is pro-macro agrees with Todd that holding
macro definitions to a higher standard then "*a* mere function
definition" is desirable.
However, writing non-trivial programs isn't a case of writing
one "mere function", or even an act of writing dozens or hundreds
of "mere function[s]". It's an act of writing suites of classes
and functions which operate together in a carefully prescribed
manner. JTree in Swing is a good example of this, where there
are numerous helper classes and dozens of functions, all of
which must be used in specific, coordinated ways. In effect,
JTree is a language, embedded in Swing, which is used to
manipulate graphical trees. Unfortunately, the design of this
language was not held to nearly a high enough standard, IMO.
A simpler example is any sort of "transaction", which has the
following protocol:
- bind a 'success' flag to false
- open a transaction
- execute the transacted body
- set the success flag to true
- in a protected block...
- if the success flag is true, then commit
- if the success flag is false, rollback
- close the transaction
Any "transaction" that does not follow this is in error. So,
would you rather do this:
let success = #f;
let transaction = db.get-transaction();
block ()
transaction.do-this();
transaction.do-that();
success := #t;
finally
if (success) transaction.commit()
else transaction.rollback();
transaction.close;
end;
or this:
with-transaction (transaction)
transaction.do-this();
transaction.do-that();
end;
OK, now you have a system with a few hundred or a few thousand
places in the code where there are transactions. Now you spot
the bug that I left in this protocol. Which would you rather
do, fix the few hundred/thousand call sites, or fix the macro
in one place to "protect" the 'transaction' variable and then
simply recompile the code? If you were a company supplying a
database product to your customers, which would you rather tell
them to do: fix your bug in all their code, or recompile against
a new release of the library? Which would you rather document?
This in a nutshell is why I think that the abstraction provided
by macros outweighs the risk of abuse. This is a simple example,
too. More complex ones come up in practice all the time. For
example (not to pick on him), look at all the little conventions
and kludges that Jeremy Hylton had to resort to in order to
declare the properties of "slots" in Python classes in order to
implement persistence. Wouldn't it have been nice if he had
been able to "shadow" the class-defining syntax and simply add
"adjectives" called something like 'volatile' and 'persistent'?
Wouldn't that have made all the client code more abstract and
clearer? That's a complex ad hoc protocol he had to define,
and once done, it's pretty much set in stone. What will Jeremy
do if he discovers some bug or shortcoming in this design?