Теория языков программирования
Метапрограммирование
Хорошие программисты пишут код. Великие программисты пишут код, который пишет код. Каждый раз при использовании @Autowired, @derive(Debug) или protobuf - метапрограммирование уже работает за разработчика.
- **NestJS / Spring**: IoC-контейнеры используют рефлексию для Dependency Injection - миллионы строк enterprise кода существуют благодаря этому паттерну
- **Rust derive macros**: #[derive(Serialize, Deserialize)] генерирует весь serde код - без этого экосистема Rust была бы намного сложнее
- **Protocol Buffers / gRPC**: Google генерирует клиент-серверный код для 50+ языков из одной .proto схемы - основа внутренней инфраструктуры Google
- **Webpack / Babel**: AST-трансформации превращают современный JS в совместимый - это макросы на уровне инфраструктуры фронтенда
Макросы
Макросы трансформируют синтаксическое дерево (AST) во время компиляции. Это позволяет расширять язык без изменения компилятора. Lisp-макросы работают с homoiconicity - код и данные одинаковы (списки). Rust proc-macros работают с TokenStream.
Почему unless нельзя реализовать как обычную функцию в большинстве языков?
Рефлексия
Рефлексия - способность программы исследовать и модифицировать собственную структуру в runtime. Introspection (читать) vs Intercession (изменять). Используется в IoC-контейнерах, сериализации, ORM, тестовых фреймворках.
В чём основной недостаток рефлексии по сравнению с compile-time метапрограммированием?
Кодогенерация
Кодогенерация - создание исходного кода или байткода программой. Бывает внешней (protobuf генерирует .java/.py из .proto) и внутренней (JVM JIT генерирует машинный код из байткода). Template metaprogramming в C++ - compile-time кодогенерация.
Какое главное преимущество кодогенерации из схемы (как protobuf) перед ручным написанием serialization кода?
Multi-stage Programming
Staging - разделение вычислений на фазы. Статически известные значения вычисляются на ранней стадии, динамические - на поздней. Это позволяет специализировать общий код под конкретные входные данные, получая производительность ad-hoc кода с гибкостью общего.
Метапрограммирование всегда делает код сложнее и хуже читаемым
Хорошее метапрограммирование устраняет boilerplate и делает код декларативнее. Плохое - магия без понимания
#[derive(Serialize)] в Rust читается лучше, чем 50 строк ручного impl. Проблема не в метапрограммировании, а в его злоупотреблении
Какова ключевая идея multi-stage programming?
Ключевые идеи
- **Макросы** трансформируют AST до выполнения. Lisp homoiconicity делает код = данные. Rust proc-macros - типобезопасные AST трансформации
- **Рефлексия** - runtime introspection. Удобно для IoC/DI, но ценой compile-time безопасности и производительности
- **Кодогенерация** - единый источник истины. Protobuf: одна схема, любое количество языков, гарантированная совместимость
- **Staging** - специализация кода по статически известным данным. power(x, 8) -> unrolled multiplication без runtime overhead
Связанные темы
Метапрограммирование пронизывает компиляцию и DSL:
- Макросы подробно — Детальное рассмотрение C-макросов, Lisp-макросов, гигиены и proc-macros
- DSL компиляторы — Staging и кодогенерация - основа создания DSL компиляторов
Вопросы для размышления
- Homoiconicity в Lisp означает, что код - это данные. Как это принципиально меняет возможности метапрограммирования по сравнению с Rust proc-macros?
- Reflection обходит систему типов. В Spring Boot это позволяет DI без boilerplate. Как найти баланс между удобством и type safety?
- Template Haskell позволяет проверять SQL запросы во время компиляции Haskell. Почему эта возможность не стала мейнстримом, несмотря на очевидную пользу?