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!"Полезное
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!"Полезное
Ваше упражнение проверяется по этим тестам
(ns splicing-test
(:require [test-helper :refer [assert-output]]
[index :refer [strange-print]]))
(assert-output "oof\nFOO\nfoo\n!zab!\n!BAZ!\n!baz!\nERUJolc\nCLOJURE\nclojure")Решение учителя откроется через:
20:00
