Software Development
C# attributes and annotations in Java’s upcoming JDK 1.5—both hereafter called tags—provide the basic hook for adopting a range of useful meta-programming techniques. Tags are new to these C-syntax languages, and we’re starting to see their adoption by programmers, tools and APIs. Lisp has had technology like this for years, so it’s worth looking back to see what 30-plus years of experience can teach us.
The Lisp family of languages has always had the upper hand on macros. Lisp’s macro technology is powerful and principled; thus, Lisp users have developed a rich practice of when and how to use macros to good advantage.
Reading this, you may be thinking “Macros—aargh! Macros are terrible!” C’s text-based macros are so fragile that for many people, the term has developed very negative connotations.
The first lesson is that in Lisp, the term macros has never meant what it
does in C. Lisp macros are syntactic: They operate on the program abstract
syntax tree (AST) after parsing. This gives the macros a higher—and
more principled—level from which to operate. By their nature, syntactic
macros produce well-formed programs, which makes them inherently easier to
understand and debug than text-based macros.
C# attributes and Java annotations are not themselves a macro facility, but
because they’re parsable, they inherently support syntactic macros: They
have already adopted Lisp’s first lesson.
After years of experience with syntactic macros, a further enhancement called hygienic macros emerged. Hygienic macros gave macro writers tools that automatically dealt with such issues as preventing name capture, importing definitions from other packages and the like. These issues are also critically important for C# and Java, and should find their way into the macro- and bytecode-editing toolkits.
Two Positions for Tags
The tags that support macros can potentially appear in evaluated and nonevaluated positions. An evaluated position means that the tag appears somewhere you’d expect it to be evaluated (compiled first, of course). A non-evaluated position means that, by default, you’d expect the annotation to be just an attribute of the declaration.
In Lisp, almost every position is evaluated, so, for a number of years, Lisp macros used only evaluated-position tags. But in the 1980s, the declare syntax was added, providing a nonevaluated position for tags.
C# attributes and Java annotations can appear in a number of positions (the
two mechanisms differ slightly on this), but for now, just consider that they
can appear before any declaration.
“Macro and Attribute Syntax”
compares tag appearance in Lisp and C#, listing three Lisp implementations
of a procedure that updates a record, along with two C# implementations. All
five examples wrap the core update functionality in a transaction. In the left
column, the transaction code appears explicitly in the procedure. In the top
center, in Lisp, an evaluated-position macro is used to wrap the transaction
code. In the third column, an annotation declares that the body should be in
a transaction. The corresponding Java code looks very similar to the C#, but
with a tag syntax of @Transactional.
Metadata
In Lisp, the declare tag was commonly used for simple metadata tagging; say, telling the editor how to indent code, or to provide different kinds of documentation. Similarly, in C# and Java, attributes have been used at their basic level for metadata that simply labels or decorates a program; typical examples include deployment descriptions, documentation and cross-referencing code with UML tools. These tags can also be used to name anonymous procedures or classes—in Lisp, the named-lambda macro was used for this. (Next month, we’ll see a powerful synergy between this use of tags and AOP.)
Simple Semantic Extensions
By default, tags have no semantic effect—in this way, they differ from Lisp macros—but this doesn’t mean that attributes can’t have semantic effect. A preprocessor can read the source code and make additions or changes as directed by the attributes. Alternatively, the compiler can preserve the tags, and then a postprocessor can read the bytecode and manipulate it as directed by the tags. Both of these have the essential properties of syntactic macros.
A simple example of a semantic extension is a tag that says something like “Generate getter and setter methods for a field,” or “Generate value-checking code for a field or parameter” (such as [Positive] to ensure an integer is positive). There are already products that provide this kind of functionality for C# and Visual Studio, and doubtless Java will soon see similar tools.
Beware Macro Overload
Lisp history also teaches us the important lesson that a little goes a long way when using semantic extensions—in the Lisp world, it was common for macro tyros to define a slew of them. It was great fun—it made their code shorter, and, to them, clearer. I was as guilty of this as anyone; for about three months, I tormented colleagues with my extreme use of macrolet, which allowed a kind of nested macro.
The problem with an abundance of semantic extensions is exactly the same as the feature—it leaves lots of little languages floating around. Lisp’s history in this respect has taught us that people found it unduly difficult to read each other’s code: Imagine reading C# code with lists of attributes longer than the method bodies, or just with as many different attributes as the underlying API.
New languages should be defined sparingly, only when they make a big difference to code clarity, and when it’s likely that any future reader of the code will easily understand it. Extensions to define getters and setters are probably reasonable. Extensions to deal with a focused and well-defined element of domain knowledge may be reasonable. Widespread use of semantic extensions for things that look almost as good when done with ordinary OOP (or AOP) is probably not.
So have fun with attributes and the metaprogramming tools they enable. Used properly, they can help with what I’ve always thought was our ultimate goal: making the code look more like the design the programmer’s thinking about. But remember, a little goes a long way.
Next month: Now that I’ve explained attributes and annotations, next month’s column will focus on using them together with aspects.
Gregor Kiczales led the Xerox PARC teams that developed aspect-oriented programming and AspectJ, and is a leading AOP evangelist based in Vancouver.