Бесплатный курс по Clojure. Зарегистрируйтесь для отслеживания прогресса →

Clojure: Правила макросов

Теперь посмотрим, как выполняется код внутри макросов:

(defmacro id-mac [x]
  "Hello, macro!"
  x)

(macroexpand-1 '(id-mac (println "str")))
; => (println "str")

Как видно из примера, строка, "Hello, macro!" нигде не появилась, следовательно, как и в обычной функции, возвращается последняя форма. Вспомним макрос из прошлого упражнения:

(defmacro identity-macro [x]
  (println "identity of x")
  x)

Как мы уже выяснили, когда мы вызываем identity-macro, форма (println "identity of x") вызывается до возвращения последней формы, что еще раз подтверждает, что тело макроса выполняется так же как и в обычной функции. Это будет вторым правилом макросов: Тело макросов выполняется в соответствии с обычными правилами Clojure.

Посмотрим еще несколько функций и макросов:

(defn triplet-fn [a b c]
  (list a b c))

(defmacro triplet-macro [a b c]
  (list a b c))

(triplet-fn 1 2 3)
; => (1 2 3)

(triplet-macro 1 2 3)
; => java.lang.Exception: Cannot call 1 as a function.

; Что-то пошло не так, посмотрим, во что макрос разворачивается

(macroexpand '(triplet-macro 1 2 3))
; => (1 2 3)

; Хм, кажется все начинает сходиться, попробуем вызвать макрос напрямую
(eval (macroexpand '(triplet-macro 1 2 3)))
; => java.lang.Exception: Cannot call 1 as a function.

; Ошибка такая же, как и в примере выше, Clojure пытается
; выполнить код (1 2 3), который не является валидной формой

Пора подводить итоги! Из примера выше можно сформировать еще одно, третье правило макросов: Данные, возвращаемые макросом немедленно вычисляются и результат этого вычисления отдается наружу при вызове этого макроса (третье правило не совсем корректно, но для нынешнего понимания этого пока что достаточно).

Полезно помнить, что возвращаемая форма выполняется дважды, когда вы пишете макрос и единожды, когда пишете обычную функцию. Теперь вспомним еще раз все правила, которые сформулировали:

  • Аргументы не выполняются перед тем, как были отправлены в тело макроса;
  • Тело макросов выполняется в соответствии с обычными правилами Clojure;
  • Данные, возвращаемые макросом немедленно вычисляются и результат этого вычисления отдается наружу при вызове этого макроса.

Задание

Исправьте triplet-macro, чтобы он работал так же, как и triplet-fn (не забывайте третье правило макросов!).

Упражнение не проходит проверку — что делать? 😶

Если вы зашли в тупик, то самое время задать вопрос в «Обсуждениях». Как правильно задать вопрос:

  • Обязательно приложите вывод тестов, без него практически невозможно понять что не так, даже если вы покажете свой код. Программисты плохо исполняют код в голове, но по полученной ошибке почти всегда понятно, куда смотреть.
В моей среде код работает, а здесь нет 🤨

Тесты устроены таким образом, что они проверяют решение разными способами и на разных данных. Часто решение работает с одними входными данными, но не работает с другими. Чтобы разобраться с этим моментом, изучите вкладку «Тесты» и внимательно посмотрите на вывод ошибок, в котором есть подсказки.

Мой код отличается от решения учителя 🤔

Это нормально 🙆, в программировании одну задачу можно выполнить множеством способов. Если ваш код прошел проверку, то он соответствует условиям задачи.

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

Прочитал урок — ничего не понятно 🙄

Создавать обучающие материалы, понятные для всех без исключения, довольно сложно. Мы очень стараемся, но всегда есть что улучшать. Если вы встретили материал, который вам непонятен, опишите проблему в «Обсуждениях». Идеально, если вы сформулируете непонятные моменты в виде вопросов. Обычно нам нужно несколько дней для внесения правок.

Кстати, вы тоже можете участвовать в улучшении курсов: внизу есть ссылка на исходный код уроков, который можно править прямо из браузера.

Нашли ошибку? Есть что добавить? Пулреквесты приветствуются https://github.com/hexlet-basics
Если вы столкнулись с трудностями и не знаете, что делать, задайте вопрос в нашем большом и дружном сообществе