Макросы: функция вычисляет, макрос нет
открытый урокЭтот раздел читается без входа. Войди, чтобы отмечать прогресс, вести заметки и решать задачи в редакторе. войти
Макросы: функция вычисляет, макрос нет
Всё различие умещается в одну фразу, и с неё начнём. Дальше пишем первый рабочий макрос и учимся видеть его раскрытие глазами.
Одна фраза, из которой следует всё
Когда ты пишешь (f (expensive-operation)), функция f получает уже результат. Макрос получит саму форму (expensive-operation), как данные, нетронутыми, и решит, вычислять ли её вообще.
Поэтому if не может быть функцией: она вычислила бы обе ветки прежде, чем выбрать. И поэтому when, unless, ->, each это макросы, как мы видели в уроке про окружения.
Отсюда следует всё остальное. Макрос работает во время компиляции и возвращает не значение, а код, который встанет на место вызова.
Разверни несколько знакомых конструкций и посмотри, во что они превращаются:
Первый макрос
Напишем unless, то есть “выполнить тело, если условие ложно”:
(defmacro my-unless [condition & body]
~(if ,condition nil (do ,;body)))
Если держать в голове одну картинку, то квазицитата это бланк: ~ печатает заготовку целиком, , это поле, куда вписывают одну форму, ,; это поле, куда высыпают сразу пачку. Теперь разберём по частям:
~это квазицитата: всё дальше это шаблон кода, а не выполняемые выражения.,conditionсюда подставится форма, переданная первым аргументом.,;bodyэто splice: тело подставится как несколько форм, а не одним списком.(do ...)нужен, чтобы несколько форм тела стали одним выражением.
Проверяем:
macex1 показывает, во что раскрылся макрос. Это твой основной инструмент отладки, пользуйся им постоянно.
Макрос должен быть определён раньше вызова. Он работает во время компиляции, поэтому к моменту компиляции вызова обязан уже существовать. Функцию можно определить ниже по файлу, макрос нет.
Прокрути фазы на таймлайне: раскрытие макросов это отдельная фаза между чтением и компиляцией, и на ней у переменных ещё нет значений.
Порядок работы, который экономит часы
Сначала напиши руками тот код, который макрос должен породить. Убедись, что он работает. И только потом обобщай его в шаблон, заменяя конкретные части на ,argument.
Вот оба шага на примере my-unless. Шаг первый, целевой код руками, с конкретными формами вместо будущих аргументов:
(if (> x 10) nil (do (print "мало") (print x)))
Шаг второй: та же форма, но конкретные места заменены на подстановки. (> x 10) стал ,condition, две формы внутри do стали ,;body:
~(if ,condition nil (do ,;body))
Отлаживать шаблон вслепую заметно тяжелее: ошибка может быть и в логике, и в расстановке запятых, а сообщение компилятора укажет на раскрытый код, которого ты не писал.
Когда макрос не нужен
Макрос это не универсальный инструмент, а специализированный, и у него есть цена:
- его нельзя передать в
mapили сохранить в переменную, это не значение; - он усложняет чтение, читатель должен знать, во что раскрывается вызов;
- ошибки в раскрытом коде указывают на места, которых нет в исходнике;
- он работает во время компиляции, поэтому его сложнее тестировать.
Правило выбора. Макрос оправдан ровно в двух случаях.
- Нужно отложить или отменить вычисление аргумента, как делают
if,and,when. - Нужен доступ к исходному коду аргумента, а не к его значению. Например, чтобы вывести само выражение в сообщении об ошибке.
Всё остальное (“хочу покороче”, “хочу свой синтаксис”) почти всегда лучше решается функцией. Функцию проще тестировать и проще складывать с другими.
Классический пример уместного макроса второго типа это assert: он печатает не только факт провала, но и само выражение, которое не выполнилось. Функция этого не смогла бы, она получила бы только false без всякого представления, откуда он взялся. Своя версия пишется в три строки:
Приглядись к ',form: это цитата поверх подстановки. В раскрытие попадает не результат вычисления, а сам исходный текст выражения, поэтому сообщение об ошибке показывает (= 1 2), а не false. Приём пригодится в упражнении e8.2. К assert мы ещё вернёмся в уроке про тестирование.
Что запомнить
- Функция получает значения аргументов, макрос получает их исходные формы.
- Макрос работает во время компиляции и возвращает код, а не значение.
- Скелет макроса:
~для шаблона,,для подстановки формы,,;для тела из нескольких форм,doчтобы склеить их в одно выражение. macex1показывает раскрытие. Смотри на него первым делом.- Макрос обязан быть определён выше по файлу, чем его вызов.
- Сначала пиши целевой код руками, потом обобщай в шаблон.
- Макрос оправдан, если надо отложить вычисление или получить доступ к тексту аргумента. Иначе бери функцию.
Упражнения
Дальше
Первый макрос написан, и он почти наверняка содержит одну из трёх классических ошибок. Следующий урок целиком про них: двойное вычисление, захват переменной и рекурсивная ссылка на себя.
домашка