September 04, 2007

Macros: what are they good for?

Paul Graham wrote an entire book about the uses of Common Lisp macros. In it, he lists the basic things you can do with macros (pp 107-109). Here's my condensed version:
  1. Transformation
    • treat argument as a syntax tree data structure rather than code to be evaluated
    • argument can be transformed before being compiled (example)
  2. Binding
    • if you want to pass an argument to a function that doesn't evaluate its argument, you can't evaluate your argument either
  3. Conditional evaluation
    • don't evaluate any argument whose value isn't needed
  4. Multiple evaluation
    • do as many evaluations of an argument as necessary
  5. Using the calling environment
    • equivalent to referring to global variables
    • so much for lexical scope
    • this is almost always a mistake (you should pass it in as a parameter instead)
  6. Wrapping a new environment
    • macros can redefine variables that appear in their arguments before the argument is evaluated
    • this is useful, but creates nasty bugs when it happens by accident
  7. Saving function calls
    • Do as much work at compile time as possible
    • Similar to inline functions, constant folding
5 and 6 are not allowed in Scheme's hygienic macros. These evaluate free variables using lexical scope (eliminating 5), and automatically gensym new variables in macros (preventing 6). I agree with eliminating 5, but 6 can be extremely useful. For example, 4 is really a special case of 6, since evaluating an argument will give you the same answer every time unless you change the environment in which it's evaluated.

A lot of this has to do with strict vs lazy functions, that is, strict functions, which evaluate all their arguments exactly once, at the time when they are called, vs lazy functions, which only evaluate an argument at the time(s) (if any) the value is required. Various languages allow this, e.g., Scala, which is strict by default but allows lazy functions, or Haskell, which is lazy by default but allows strict behaviour. However, many programmers use languages that are completely strict (C, C++, Java, etc.) or restrict themselves to subsets of a language which are entirely strict (Lisp without macros, Perl and Python without certain libraries).

On Lisp also lists some good and bad points about macros (again, my condensed version):

The Good:
  • Computation at compile time (as much as possible)
    • a well-made compiler should be doing this automatically
  • Integration (e.g., write a compiler that translates a mini-language into the base language)
    • This is basically another way of saying "compute at compile time"
  • Saving function calls
    • again, a well-made compiler should be doing this automatically
The Bad:
  • Macros aren't first class
    • they should be, just like functions
  • Lack of clarity
    • code that rewrites code can be disconcerting at first
  • Lack of debugger support
    • debuggers are typically tuned to help you debug stuff that happens at run-time, not compile-time
  • Macro recursion
    • is even more confusing than normal recursion, but you managed that, right?
Haskell is of interest, because it was designed from the ground up with laziness in mind. Practically everything in Haskell is defined in terms of a transformation into simpler constructs, eventually terminating with a small subset of Haskell (the Haskell kernel). In short, everything in Haskell except the kernel is already a macro. The lazy functions are first class, don't look strange (because they're everywhere), and the debugger supports them (duh!). All you have to do is wrap your head around the idea of lazy evaluation, and you have half the power of macros without the disadvantages.

What you can ignore with Haskell:
2. not a problem; lazy evaluation by default
3. lazy evaluation by default
5. lexical scope by default
7. compiler does it automatically

So in Haskell, all you need to worry about are 4 & 6 (which are the same thing), and the big 1, which is hard to equal without Lisp's syntax tree data structures. I haven't learned enough Haskell to be sure, but some things I've read indicate Haskell allows these things too, in a nice way.