Макросы: код, который пишет код
Разбираемся, чем macro отличается от уже знакомого template и как макрос умеет собирать код почти как конструктор из деталей.
macro — конструкция Nim, которая во время компиляции получает код в виде дерева синтаксиса и может анализировать его и строить из него новый код.
Чем макрос принципиально отличается от шаблона
В прошлом уроке мы разобрали template — он подставляет своё тело как есть, почти как более умный copy-paste. Макрос устроен иначе и гораздо мощнее. Когда компилятор Nim читает исходный код, он сначала превращает его во внутреннее представление — дерево синтаксиса, то есть структуру, которая описывает код не как текст, а как набор связанных «узлов»: вот это вызов функции, у него такие-то аргументы, вот это условие, у него такая-то ветка и так далее. Обычно эта структура скрыта от программиста и существует только внутри компилятора. Макрос — это способ получить к ней прямой доступ ещё во время компиляции и написать код, который сам строит другой код, оперируя не текстом, а именно этим деревом.
Представьте разницу так: шаблон — это как если бы вы дали помощнику готовый бланк письма с пропусками и попросили вписать в них конкретные слова. Макрос — это как если бы вы дали помощнику полную свободу: он может прочитать черновик письма, понять его структуру (где приветствие, где основная часть, где подпись) и на основе этого понимания собрать совершенно новое письмо, возможно, в другом порядке или с дополнительными фразами. Такая свобода нужна не всегда — большинство задач прекрасно решаются шаблонами, — но когда требуется по-настоящему проанализировать код и сгенерировать что-то нетривиальное, без макроса не обойтись.
Простой макрос: quote do и генерация кода
Писать код дерева синтаксиса вручую, узел за узлом, неудобно и многословно. Поэтому в Nim есть специальный помощник — конструкция quote do:, которая позволяет написать обычный код Nim внутри макроса, и этот код автоматически превратится в нужное дерево синтаксиса. Посмотрим на простейший макрос, который оборачивает выражение в печать его же самого:
import std/macros
macro printTwice(expr: untyped): untyped =
result = quote do:
echo `expr`
echo `expr`
printTwice(2 + 2)Вывод:
4
4Разбираем построчно
Строка import std/macros подключает модуль стандартной библиотеки, без которого писать макросы нельзя — в нём определены сами инструменты для работы с деревом синтаксиса, включая quote do. Дальше macro printTwice(expr: untyped): untyped = объявляет макрос — обратите внимание, сигнатура внешне очень похожа на шаблон: тоже параметр untyped, тоже возвращаемый тип untyped. Но внутри макрос работает не с готовым кодом-текстом, а собирает код как результат — именно поэтому телу макроса нужно присвоить что-то переменной result, которая как раз и должна содержать собранное дерево синтаксиса.
Конструкция quote do: — это «режим написания обычного кода внутри необычного контекста»: всё, что внутри неё, выглядит как привычный Nim (echo дважды), но на самом деле компилятор превращает этот текст в дерево синтаксиса, которое и станет результатом макроса. Обратные кавычки вокруг `expr` — это специальный синтаксис «вставь сюда то дерево синтаксиса, что пришло в параметр expr», то есть подставь именно тот код, с которым вызвали макрос — в нашем случае 2 + 2. В результате макрос printTwice(2 + 2) раскрывается в код, эквивалентный echo 2 + 2; echo 2 + 2, что компилятор и выполняет — печатая 4 дважды.
Мини-пример: макрос debug, печатающий и код, и значение
Одна из самых практичных вещей, которую умеют макросы, а шаблоны — нет: заглянуть внутрь переданного выражения и достать из него не только значение, но и сам исходный текст, каким его написал программист. Для этого в Nim есть встроенная функция astToStr, которая на этапе компиляции превращает выражение в его же текстовое представление.
import std/macros
macro debug(expr: untyped): untyped =
let code = expr.astToStr
result = quote do:
echo `code`, " = ", `expr`
let vozrast = 16
debug(vozrast * 2)Вывод:
vozrast * 2 = 32Здесь expr.astToStr берёт дерево синтаксиса, пришедшее в параметр expr (в нашем случае — выражение vozrast * 2), и превращает его в обычную строку "vozrast * 2" ещё до того, как программа вообще начала выполняться. Дальше макрос собирает код, который печатает эту строку, знак равенства, а следом — фактическое вычисленное значение того же выражения. Такой макрос debug — это уже что-то похожее на инструмент, который в других языках называют «умным print» для отладки: вместо того чтобы вручную писать echo "vozrast * 2 = ", vozrast * 2 и дублировать выражение дважды, макрос делает это автоматически, читая исходный код один раз.
Частые ошибки новичков
Первая ошибка — пытаться сразу писать сложные макросы, вручную собирая дерево синтаксиса узел за узлом, вместо того чтобы начать с quote do. Ручная сборка дерева — мощный, но многословный и запутанный способ, к которому стоит переходить только тогда, когда quote do уже не хватает для нужной задачи. Для знакомства с макросами quote do — правильная отправная точка.
Вторая ошибка — путать макрос с шаблоном и ожидать, что раз внешне сигнатуры похожи, макрос можно писать так же просто. У макроса телу нужно вычислить и присвоить result — макрос не «разворачивается» автоматически как шаблон, а именно возвращает готовое дерево синтаксиса, которое компилятор потом вставит на место вызова.
Третья ошибка — недооценивать, насколько сложнее отлаживать макросы по сравнению с обычным кодом. Ошибка внутри макроса может проявиться странным сообщением компилятора про сгенерированный код, а не про то, что на самом деле написал программист. Начинающим стоит браться за макросы только тогда, когда задачу действительно нельзя решить процедурой или шаблоном — не потому что макросы «круче», а как последний инструмент, когда нужен настоящий анализ структуры кода.
Итоги
- Макрос работает не с текстом кода, а с деревом синтаксиса — внутренним представлением программы, которое обычно скрыто от программиста.
- Конструкция
quote do:позволяет писать обычный код Nim внутри макроса, автоматически превращая его в нужное дерево синтаксиса, а обратные кавычки вставляют туда переданные параметры. - Функция
astToStrдостаёт из выражения его исходный текст ещё на этапе компиляции — на этом основаны отладочные макросы вродеdebug. - Макросы — самый мощный, но и самый сложный инструмент метапрограммирования Nim: браться за них стоит тогда, когда не хватает возможностей обычной процедуры или шаблона.