Programming Language Theory
Macros
Every time you write #[derive(Serialize, Deserialize)] or #[tokio::main], procedural macros generate hundreds of lines of code automatically. Not magic. AST transformations.
- **serde**: #[derive(Serialize, Deserialize)] produces a full serialization implementation. 100M+ downloads/month on crates.io, the most used Rust library.
- **tokio::main**: a single attribute turns an ordinary main into an async runtime. Without this proc-macro, you would write dozens of lines of boilerplate.
- **SQLAlchemy**: Python metaclasses and decorators are a form of compile-time code generation. ORM models defined via __tablename__ and Column() are metaprogramming.
C Preprocessor Macros
The C preprocessor is textual substitution that runs before compilation. #define MAX(a,b) ((a)>(b)?(a):(b)) is not a function, just a text template. Problems: no types, no scope, double evaluation of arguments, hard to debug.
The C preprocessor is not part of the C language. It processes text before the parser. It is the source of 30% of the hard-to-debug bugs in C codebases. `#pragma once` versus include guards is another example of preprocessor pain.
Why does MAX(x++, 3) with a C macro increment x twice?
Lisp Macros
Lisp macros operate on the AST (S-expressions), not on text. Macro expansion happens after parsing. Lisp homoiconicity makes this trivial: code is data (lists), and a macro transforms a list.
Why are Lisp macros safer than the C preprocessor?
Hygienic Macros
A hygienic macro does not capture names from the calling context, and its own names do not clash with outside names. Scheme (syntax-rules) and Rust macro_rules! are hygienic systems. Common Lisp gensym is a manual solution.
What does 'hygienic macro' mean?
Rust Procedural Macros
Rust proc-macros are macros written as Rust functions that take a TokenStream and return a TokenStream. Three flavors: derive (for structs and enums), attribute (arbitrary annotations), and function-like (macro!()). You get the full power of Rust for code generation.
Macros are always confusing and tricky. Better avoided
#[derive(Debug, Clone, Serialize)] uses macros and is applied everywhere. They handle tedious boilerplate automatically.
The problem is not macros, but badly written ones. serde, tokio, sqlx, clap all use proc-macros. Knowing how they work is part of the Rust ecosystem.
What is the core difference between Rust proc-macros and macro_rules!?
Summary
- **C preprocessor**: textual substitution. No types, double evaluation, no scope. Legacy, avoid.
- **Lisp macros**: AST transformations via homoiconicity. when, unless, loop are all user-defined. No double evaluation.
- **Hygiene**: names inside the macro do not clash with outside names. Rust macro_rules! handles this automatically. Common Lisp uses gensym manually.
- **Rust proc-macros**: full Rust for generating TokenStream. serde, tokio, sqlx form an ecosystem built on proc-macros.
Related topics
Macros are tightly tied to metaprogramming:
- Metaprogramming — Macros are one of the tools of metaprogramming.
- AST and compilers — proc-macros work on TokenStream, the token stream before AST parsing.
Вопросы для размышления
- Lisp homoiconicity (code = data = S-expressions) makes macros trivial. Why do modern languages (Rust, Scala) choose complex macro systems instead of the Lisp approach?
- Rust proc-macros are compiled separately and run during compilation of the host code. What are the security implications? A proc-macro can read files and make HTTP requests.
- C++ template metaprogramming is macros without explicit macro syntax. Why are templates Turing-complete, and why does that make C++ compilation slow?