Clojure: Splicing
Попробуем решить еще одну странную задачку! Создадим макрос, который принимает вектор чисел, выводит их на экран и затем возвращает вектор, попробуйте сделать такой макрос сами, перед тем как читать дальше.
А теперь рассмотрим макрос, который решает нашу задачу:
(defmacro print-els [coll]
`(do ~(map println coll)
~coll))
(print-els [1 2 3])
1
2
3
Syntax error (IllegalArgumentException)
Can't call nil, form: (nil nil nil)
Хммм, не работает, воспользуемся macroexpand-1
:
(macroexpand-1 '(print-els [1 2 3]))
1
2
3
(do (nil nil nil) [1 2 3])
Итак, вызов ~(map println coll)
вернул (nil nil nil)
, а так как nil нельзя вызвать, возникает ошибка. Однако, эта часть кода работает!
(do (map println [1 2 3]))
1
2
3
(nil nil nil)
; и этот код работает
(do nil nil nil [1 2 3])
[1 2 3]
По сути, мы хотим чтобы макрос и возвращал код выше.
Для решения этой проблемы воспользуемся новым оператором ~@
(unquote splicing). Этот оператор используется, если в макросе нужно получить содержание Clojure формы.
(defmacro print-els [coll]
`(do ~@(map println coll)
~coll))
(print-els [1 2 3])
1
2
3
[1 2 3]
(macroexpand-1 '(print-els [1 2 3]))
1
2
3
(do nil nil nil [1 2 3])
А может мы просто вернем форму и вычислим ее снаружи макроса? Попробуем так:
(defmacro print-els* [coll]
`(do (map println ~coll)
~coll))
Но что произойдет, если в пространстве имен, в котором мы определяем наш макрос, форма map
означает нечто иное? Допустим, map
будет определять хеш-мап?
; переопределим форму map
(def map #{:a 1})
; числа 1 2 3 не вывелись в консоль
(print-els* [1 2 3])
[1 2 3]
Помните третье правило макросов? Данные, возвращаемые макросом немедленно вычисляются и результат этого вычисления отдается наружу при вызове этого макроса.
Но как мы видим, контекст, в котором мы вызываем макрос, тоже важен! Поэтому немного дополним правило: Данные, возвращаемые макросом, немедленно вычисляются в контексте пространства имен, в котором макрос был вызван.
Задание
Создайте макрос strange-print
, который принимает строку, выводит в консоль строку сначала в обратном порядке, затем полностью в верхнем регистре, а затем полностью в нижнем и верните из макроса исходную строку.
А потом вызовите созданный макрос для строк: foo
, !baz!
, cloJURE
.
Пример:
(strange-print "foo")
oof
FOO
foo
"foo"
(strange-print "!baz!")
!zab!
!BAZ!
!baz!
"!baz!"
Упражнение не проходит проверку — что делать? 😶
Если вы зашли в тупик, то самое время задать вопрос в «Обсуждениях». Как правильно задать вопрос:
- Обязательно приложите вывод тестов, без него практически невозможно понять что не так, даже если вы покажете свой код. Программисты плохо исполняют код в голове, но по полученной ошибке почти всегда понятно, куда смотреть.
В моей среде код работает, а здесь нет 🤨
Тесты устроены таким образом, что они проверяют решение разными способами и на разных данных. Часто решение работает с одними входными данными, но не работает с другими. Чтобы разобраться с этим моментом, изучите вкладку «Тесты» и внимательно посмотрите на вывод ошибок, в котором есть подсказки.
Мой код отличается от решения учителя 🤔
Это нормально 🙆, в программировании одну задачу можно выполнить множеством способов. Если ваш код прошел проверку, то он соответствует условиям задачи.
В редких случаях бывает, что решение подогнано под тесты, но это видно сразу.
Прочитал урок — ничего не понятно 🙄
Создавать обучающие материалы, понятные для всех без исключения, довольно сложно. Мы очень стараемся, но всегда есть что улучшать. Если вы встретили материал, который вам непонятен, опишите проблему в «Обсуждениях». Идеально, если вы сформулируете непонятные моменты в виде вопросов. Обычно нам нужно несколько дней для внесения правок.
Кстати, вы тоже можете участвовать в улучшении курсов: внизу есть ссылка на исходный код уроков, который можно править прямо из браузера.