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

Clojure: Цитирование

Начнем с небольшой задачи, допустим, у нас есть определенный список названий в пространстве имен, которые мы хотим оставить нетронутыми в этом пространстве. Иначе говоря, мы хотим позволить именовать (определять) функции только разрешенными именами.

Сначала проанализируем, какие данные будут переданы в наш макрос. По сути, они будут такими же, как и в форме defn, то есть name, args и body.

Классическое объявление функции это список - (defn name args body). То есть если мы хотим определить функцию через макрос, мы должны вернуть список, как при определении функции. Единственное отличие, наш макрос special-defn должен проверить, находится ли name в запрещенном списке имен, перед тем, как объявить функцию в пространстве имен.

Теперь попробуем решить эту задачу:

; сохраним в множество строку, которую сконвертирум в символ (потому что в defn название тоже передается символом)
(def forbidden-list #{(symbol "clojure") (symbol "is") (symbol "bad")})

(defmacro special-defn [name args body]
  (if-not (contains? forbidden-list name)
    (list defn name args body)
    "you can't define this function"))

Syntax error compiling
Can't take value of a macro: #'clojure.core/defn

Ошибка возникает из-за того, что Clojure пытается вычислить у символа defn его значение. Но так получить значение макроса нельзя, возникает ошибка. Но нам и не нужно значение defn. Нам нужно получить список вида: (defn name args body). Познакомимся с оператором цитирования (quote).

Оператор ' (quote operator), является первым из инструментов, о которых узнает программист, когда сталкивается с макросами. Этот оператор сообщает Clojure пропустить исполнение переданного символа. Рассмотрим пример:

; попробуем создать функцию с неопределенным до этого символом
(defn get-foo [] foo)
Syntax error compiling at (REPL:1:1).
Unable to resolve symbol: foo in this context

; логично, мы попытались вычислить foo и получили ошибку
; теперь воспользуемся оператором '
(defn get-foo [] 'foo)

; ошибки не возникло, вызовем нашу функцию
(get-foo)
foo

Несмотря на то, что символ foo не был связан со значением, функция get-foo вернула этот символ (не его значение!), однако при попытке вычислить его значение, все равно возникнет ошибка.

Получается, если нужно сослаться на символ, не вычисляя его значения, нужно использовать оператор '.

Если расположить оператор ' перед выражением, то этот оператор рекурсивно применится к выражению и его подвыражениям. Рассмотрим несколько примеров:

'(foo bar baz)
(foo bar baz)

'(foo (bar tavern pub) baz)
(foo (bar tavern pub) baz)

Теперь попробуем применить полученные знания к нашей изначальной задаче:

(def forbidden-list #{(symbol "clojure") (symbol "is") (symbol "bad")})

(defmacro special-defn [name args body]
  (if-not (contains? forbidden-list name)
    '(defn name args body)
    "you can't define this function"))

#'user/special-defn
; ура, наш макрос удачно скомпилировался, попробуем теперь его в деле

(special-defn clojure [a] a)
"you can't define this function"
; этот пример работает

(special-defn my-fn [a] a)
Syntax error macroexpanding clojure.core/defn
args - failed: vector? at: [:fn-tail :arity-1 :params] spec: :clojure.core.specs.alpha/param-list
args - failed: (or (nil? %) (sequential? %)) at: [:fn-tail :arity-n :bodies] spec: :clojure.core.specs.alpha/params+body

; а этот пример не работает :(

Мы раньше использовали macroexpand, как и macroexpand-1. Это очень полезные функции, чтобы выяснить, какой список возвращает макрос. Поэтому воспользуемся ими в очередной раз и разберемся, в чем проблема:

(macroexpand '(special-defn my-fn [a] a))
Syntax error macroexpanding clojure.core/defn at ...
args - failed: vector? at: [:fn-tail :arity-1 :params] spec: :clojure.core.specs.alpha/param-list
args - failed: (or (nil? %) (sequential? %)) at: [:fn-tail :arity-n :bodies] spec: :clojure.core.specs.alpha/params+body

Не очень информативный вывод, тогда воспользуемся macroexpand-1:

(macroexpand-1 '(special-defn my-fn [a] a))
(defn name args body)

defn выглядит нормально! Но что за name, args и body? Разве они не должны были замениться my-fn, [a] и a?

Список, который мы ожидали должен был быть (defn my-fn [a] a), но макрос вернул символы name, args, и body вместо их значений.

Перед тем, как мы исправим наш макрос, проверьте себя, почему macroexpand-1 сработал, а macroexpand нет? Причина в том, что macroexpand рекурсивно вызывает macroexpand-1, пока не вернется валидная Clojure форма. Так как мы возвращаем defn, а defn является макросом, то macroexpand пытается развернуть его и вызывает ошибку.

Задание

Вернемся к исправлению макроса special-defn. Вспомним, что мы хотим вернуть список, содержащий символ defn и значения name, args и body (подумайте о порядке "цитирования" в итоговом списке).

Затем создайте две функции через полученный макрос:

  • my-sum, в которую передаются два числа и суммируются;
  • my-diff, в которую передаются два числа, а затем из первого вычитается второе число.
Упражнение не проходит проверку — что делать? 😶

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

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

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

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

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

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

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

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

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

Полезное


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