А теперь затронем еще одну важную тему в макросах, а точнее, их гигиеничность. Почему это важно? Рассмотрим для начала примеры:
(defmacro return-some-list [x]
(list 'let ['one 5]
['one x]))
(return-some-list 1)
[5 1]
Пока что все в порядке, но теперь добавим переменную one
в наше пространство имен:
(def one 1)
(return-some-list one)
[5 5]
Совсем не то, что ожидалось, не правда ли? Помните, что аргументы переданные в макрос не вычисляются и из-за того, что мы возвращаем символ one
в контексте вызова формы let
, значение one
перетирается 5.
(macroexpand-1 '(return-some-list one))
(let [one 5] [one one]) ; one здесь равняется 5
Этот эффект называется symbol capture или variable capture (захват/перекрытие символа или переменной, если переводить на русский), одна из частых ошибок, возникающих при создании макросов.
Можно попытаться процитировать объявление внутри макроса, однако это вызовет исключение. Попробуем:
(defmacro return-some-list [x]
`(let [one 5]
[one ~x]))
#'user/return-some-list
(return-some-list one)
Syntax error macroexpanding clojure.core/let .
user/one - failed: simple-symbol? at: [:bindings :form :local-symbol] spec: :clojure.core.specs.alpha/local-name
Это происходит из-за того, что let
принимает только простые символы, в то время как сложные символы - нет (на англ. fully-qualified symbols). Так как источник ошибки уходит глубоко во внутренности Clojure и потребует немало времени и сил, чтобы понять, что вообще происходит, создатели языка позаботились об этом понятным исключением failed: simple-symbol?
.
(simple-symbol? 'one)
true
(simple-symbol? 'user/one) ; в этом символе участвует еще и пространство имен
false
Однако, чтобы решить проблему с перекрытием объявления one
, мы бы могли использовать какое-нибудь уникальное название и Clojure представляет такую возможность благодаря gensym
!
(gensym)
G__2144 ; у вас скорее всего вывод будет отличаться
Проблема с перекрытием объявлений достаточно распространенная, поэтому в Clojure есть синтаксический сахар под это дело, называется он autogensym
или #
:
`(one#)
(one__2148__auto__)
(simple-symbol? `one#)
true
А теперь воспользуемся полученными знаниями для нашего макроса, в котором происходило перекрытие one
:
(defmacro return-some-list [x]
`(let [one# 5]
[one# ~x]))
(return-some-list one)
[5 1] ; работает!
Вот и все, это почти все инструменты, которые используются при написании макросов, однако нужно обсудить еще несколько важных вещей в следующем упражнении.
Создайте макрос auto-sum
, внутри которого объявляется символ my-var
со значением 10 и складывается с переданным в макрос числом (не забывайте про перекрытие объявлений!).
Если вы зашли в тупик, то самое время задать вопрос в «Обсуждениях». Как правильно задать вопрос:
Тесты устроены таким образом, что они проверяют решение разными способами и на разных данных. Часто решение работает с одними входными данными, но не работает с другими. Чтобы разобраться с этим моментом, изучите вкладку «Тесты» и внимательно посмотрите на вывод ошибок, в котором есть подсказки.
Это нормально 🙆, в программировании одну задачу можно выполнить множеством способов. Если ваш код прошел проверку, то он соответствует условиям задачи.
В редких случаях бывает, что решение подогнано под тесты, но это видно сразу.
Создавать обучающие материалы, понятные для всех без исключения, довольно сложно. Мы очень стараемся, но всегда есть что улучшать. Если вы встретили материал, который вам непонятен, опишите проблему в «Обсуждениях». Идеально, если вы сформулируете непонятные моменты в виде вопросов. Обычно нам нужно несколько дней для внесения правок.
Кстати, вы тоже можете участвовать в улучшении курсов: внизу есть ссылка на исходный код уроков, который можно править прямо из браузера.
Ваше упражнение проверяется по этим тестам
1(ns gensym-test
2 (:require [test-helper :refer [assert-solution]]
3 [index :refer [auto-sum]]))
4
5(assert-solution [[2] [12] [112]] [12 22 122] (fn [arg] (do (intern 'index 'my-var 0) (auto-sum arg))))
6
Решение учителя откроется через: