Теория языков программирования
Макросы
Каждый раз при написании #[derive(Serialize, Deserialize)] или #[tokio::main] - процедурные макросы генерируют сотни строк кода автоматически. Это не магия - это AST трансформации.
- **serde**: #[derive(Serialize, Deserialize)] генерирует полную реализацию сериализации. 100M+ downloads/month в crates.io - самая используемая Rust библиотека
- **tokio::main**: один атрибут превращает обычный main в async runtime. Без этого proc-macro нужны были бы десятки строк boilerplate
- **SQLAlchemy**: Python метаклассы и декораторы - form of compile-time code generation. ORM модели через __tablename__ и Column() - это метапрограммирование
C Preprocessor Macros
C препроцессор - текстовая замена до компиляции. #define MAX(a,b) ((a)>(b)?(a):(b)) - не функция, а текстовый шаблон. Проблемы: нет типов, нет области видимости, двойное вычисление аргументов, сложная отладка.
C препроцессор - не часть языка C. Он обрабатывает текст до парсера. Это источник 30% сложных-для-отладки багов в C кодовых базах. `#pragma once` vs include guards - ещё один пример проблем препроцессора.
Почему MAX(x++, 3) с C макросом инкрементирует x дважды?
Lisp Макросы
Lisp макросы работают на AST уровне (S-expressions), а не на текстовом. Macro expansion происходит после парсинга. Homoiconicity Lisp делает это тривиальным: код - это данные (списки), макрос трансформирует список.
Почему Lisp макросы безопаснее C препроцессора?
Гигиеничные макросы
Гигиена макроса (hygienic macro) - макрос не захватывает имена из вызывающего контекста, его собственные имена не конфликтуют с именами снаружи. Scheme (syntax-rules), Rust macro_rules! - гигиеничные системы. Common Lisp gensym - ручное решение.
Что значит 'гигиеничный макрос'?
Rust Procedural Macros
Rust proc-macros - макросы как Rust функции, принимающие TokenStream и возвращающие TokenStream. Три вида: derive (для структур/перечислений), attribute (произвольные аннотации), function-like (macro!()). Полная мощь Rust при генерации кода.
Макросы - это всегда сложно и непонятно. Лучше избегать
#[derive(Debug, Clone, Serialize)] - это макросы которые применяются повсеместно. Они делают скучный boilerplate автоматически
Проблема не в макросах, а в плохо написанных макросах. serde, tokio, sqlx, clap - все используют proc-macros. Знать как они работают - важная часть Rust экосистемы
В чём принципиальное отличие Rust proc-macros от macro_rules!?
Итоги
- **C препроцессор**: текстовая замена - нет типов, двойное вычисление, нет области видимости. Legacy, избегать
- **Lisp макросы**: AST трансформация через homoiconicity. when, unless, loop - всё пользовательское. Нет двойного вычисления
- **Гигиена**: имена внутри макроса не конфликтуют с внешними. Rust macro_rules! - автоматически. Common Lisp - gensym вручную
- **Rust proc-macros**: полный Rust для генерации TokenStream. serde, tokio, sqlx - экосистема на proc-macros
Связанные темы
Макросы тесно связаны с метапрограммированием:
- Метапрограммирование — Макросы - один из инструментов метапрограммирования
- AST и компиляторы — proc-macros работают с TokenStream - потоком токенов до парсинга в AST
Вопросы для размышления
- Homoiconicity Lisp (код = данные = S-expressions) делает макросы тривиальными. Почему современные языки (Rust, Scala) выбирают сложные macro системы вместо Lisp-подхода?
- Rust proc-macros компилируются отдельно и запускаются во время компиляции host кода. Какие security implications это имеет - proc-macro может читать файлы и делать HTTP запросы!
- C++ template metaprogramming - это макросы без явного синтаксиса макросов. Чем шаблоны Turing-complete, и почему это делает C++ compilation медленным?