Раздел 31 · Janet на практике

Макросы: функция вычисляет, макрос нет

middle~25 мин

открытый урокЭтот раздел читается без входа. Войди, чтобы отмечать прогресс, вести заметки и решать задачи в редакторе. войти

Макросы: функция вычисляет, макрос нет

Всё различие умещается в одну фразу, и с неё начнём. Дальше пишем первый рабочий макрос и учимся видеть его раскрытие глазами.

Одна фраза, из которой следует всё

Когда ты пишешь (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 или сохранить в переменную, это не значение;
  • он усложняет чтение, читатель должен знать, во что раскрывается вызов;
  • ошибки в раскрытом коде указывают на места, которых нет в исходнике;
  • он работает во время компиляции, поэтому его сложнее тестировать.

Правило выбора. Макрос оправдан ровно в двух случаях.

  1. Нужно отложить или отменить вычисление аргумента, как делают if, and, when.
  2. Нужен доступ к исходному коду аргумента, а не к его значению. Например, чтобы вывести само выражение в сообщении об ошибке.

Всё остальное (“хочу покороче”, “хочу свой синтаксис”) почти всегда лучше решается функцией. Функцию проще тестировать и проще складывать с другими.

Классический пример уместного макроса второго типа это assert: он печатает не только факт провала, но и само выражение, которое не выполнилось. Функция этого не смогла бы, она получила бы только false без всякого представления, откуда он взялся. Своя версия пишется в три строки:

Приглядись к ',form: это цитата поверх подстановки. В раскрытие попадает не результат вычисления, а сам исходный текст выражения, поэтому сообщение об ошибке показывает (= 1 2), а не false. Приём пригодится в упражнении e8.2. К assert мы ещё вернёмся в уроке про тестирование.

Что запомнить

  • Функция получает значения аргументов, макрос получает их исходные формы.
  • Макрос работает во время компиляции и возвращает код, а не значение.
  • Скелет макроса: ~ для шаблона, , для подстановки формы, ,; для тела из нескольких форм, do чтобы склеить их в одно выражение.
  • macex1 показывает раскрытие. Смотри на него первым делом.
  • Макрос обязан быть определён выше по файлу, чем его вызов.
  • Сначала пиши целевой код руками, потом обобщай в шаблон.
  • Макрос оправдан, если надо отложить вычисление или получить доступ к тексту аргумента. Иначе бери функцию.

Упражнения

Дальше

Первый макрос написан, и он почти наверняка содержит одну из трёх классических ошибок. Следующий урок целиком про них: двойное вычисление, захват переменной и рекурсивная ссылка на себя.

домашка

Домашка