Теория языков программирования

Макросы

Каждый раз при написании #[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 медленным?

Связанные уроки

  • comp-01-intro
Макросы

0

1

Войти