Metaprogramming
Good programmers write code. Great programmers write code that writes code. Every time you use @Autowired, @derive(Debug), or protobuf, metaprogramming is doing the work for you.
- **NestJS / Spring**: IoC containers use reflection for Dependency Injection. Millions of lines of enterprise code exist thanks to this pattern
- **Rust derive macros**: #[derive(Serialize, Deserialize)] generates all the serde code. Without it the Rust ecosystem would be much more painful
- **Protocol Buffers / gRPC**: Google generates client and server code for 50+ languages from a single .proto schema. This is core to Google's internal infrastructure
- **Webpack / Babel**: AST transformations turn modern JS into compatible JS. These are macros at the level of front-end infrastructure
Macros
Macros transform the syntax tree (AST) at compile time. They let you extend the language without modifying the compiler. Lisp macros exploit homoiconicity: code and data share the same representation (lists). Rust proc-macros operate on TokenStream.
Why can't unless be implemented as an ordinary function in most languages?
Reflection
Reflection is the ability of a program to inspect and modify its own structure at runtime. Introspection (reading) vs intercession (modifying). It powers IoC containers, serialization, ORMs, and test frameworks.
What is the main drawback of reflection compared with compile-time metaprogramming?
Code Generation
Code generation is the production of source code or bytecode by a program. It can be external (protobuf generates .java/.py from .proto) or internal (the JVM JIT generates machine code from bytecode). Template metaprogramming in C++ is compile-time code generation.
What is the main advantage of schema-driven code generation (like protobuf) over hand-written serialization code?
Multi-stage Programming
Staging splits computation into phases. Statically known values are computed at an earlier stage, dynamic ones at a later stage. This lets you specialize general-purpose code for concrete inputs, getting the performance of hand-written code with the flexibility of generic code.
Metaprogramming always makes code more complex and less readable
Good metaprogramming eliminates boilerplate and makes code more declarative. Bad metaprogramming is magic with no clarity
#[derive(Serialize)] in Rust reads better than 50 lines of hand-rolled impl. The problem is not metaprogramming itself but its misuse
What is the core idea of multi-stage programming?
Key Ideas
- **Macros** transform the AST before execution. Lisp homoiconicity makes code = data. Rust proc-macros are type-safe AST transformations
- **Reflection** is runtime introspection. Handy for IoC/DI, but you pay in compile-time safety and performance
- **Code generation** gives you a single source of truth. Protobuf: one schema, any number of languages, guaranteed compatibility
- **Staging** specializes code for statically known data. power(x, 8) becomes unrolled multiplication with no runtime overhead
Related Topics
Metaprogramming runs through compilation and DSLs:
- Macros in depth — A deeper look at C macros, Lisp macros, hygiene, and proc-macros
- DSL compilers — Staging and code generation are the foundation of DSL compilers
Вопросы для размышления
- Homoiconicity in Lisp means code is data. How does that fundamentally change what is possible with metaprogramming compared with Rust proc-macros?
- Reflection circumvents the type system. In Spring Boot that enables DI with no boilerplate. Where do you draw the line between convenience and type safety?
- Template Haskell lets you validate SQL queries at compile time. Why has this capability not become mainstream, despite the obvious benefit?