Статьи

Жизненный цикл макроса в Clojure

Если вам все еще трудно понять, что такое макросы в Clojure и почему они так полезны, я приведу вам другой пример сегодня. Мы узнаем, когда макросы распознаются, оцениваются, расширяются и выполняются. Я считаю, что наиболее важной концепцией является их сходство с обычными функциями. Как я описывал в прошлый раз , макросы являются обычными функциями, но выполняются во время компиляции и принимают в качестве аргументов код, а не значения. Второе отличие является слегка искусственным, поскольку код Clojure является значением в том смысле, что его можно передавать. Итак, давайте сосредоточимся на том, когда макросы действительно расширяются и выполняются. Мы начнем с тривиальной реализации GCD в Clojure как обычной функции:

1
2
3
4
(defn gcd [a b]
    (if (zero? b)
        a
        (recur b (mod a b))))

Вызов этой функции приведет к выполнению хвостового рекурсивного цикла
во время выполнения каждый раз, когда это встречается:

1
2
3
4
5
6
user=> (gcd 18 12)
6
user=> (gcd 9 2)
1
user=> (gcd 9 (inc 2))
3

Не очень интересно Но что если мы поместим ссылку на gcd внутри макроса?

1
2
     
(defmacro runtime-gcd [a b] (list 'gcd a b))

Или более краткий синтаксис:

1
(defmacro runtime-gcd-quote [a b] `(gcd ~a ~b))

Теперь посмотрите на объявление runtime-gcd но замените defmacro на defn , как если бы это была обычная функция:

1
(defn runtime-gcd-fun [a b] (list 'gcd a b))

Каждый раз, когда вы вызываете runtime-gcd-fun в своем коде Clojure, он заменяется следующим списком: (gcd 12 8) . Как видите, это в основном вызов функции gcd . Он заключен в кавычки, поэтому остается списком, а не вызывает фактическую функцию. Вы можете оценить эту структуру данных, запустив (eval) :

1
2
3
4
5
6
user=> (eval '(gcd 12 8))
4
user=> (eval (list 'gcd 12 8))
4
user=> (eval (runtime-gcd-fun 12 8))
4

Как вы можете видеть, runtime-gcd-fun — это функция, которая создает структуру данных ( list ), которая является допустимым кодом Clojure! runtime-gcd-fun не вызывает (gcd ab) , он возвращает код (выражение), который вызывает gcd . Хорошо, но какое это имеет отношение к макросам? Давайте вернемся к нашему оригинальному макросу runtime-gcd :

1
2
3
4
5
6
user=>     (defmacro runtime-gcd [a b] (list 'gcd a b))
#'user/runtime-gcd
user=> (runtime-gcd 12 8)
4
user=> (runtime-gcd 12 (inc 7))
4

Ооочень … в чем разница? Пока нигде. (defmacro) выполняется ( раскрывается ) во время компиляции. Это в основном функция, вызываемая во время компиляции. Так же, как вызов нормальной функции заменяется ее значением во время выполнения, значение, возвращаемое из макроса, заменяет каждое вхождение этого макроса в коде. Прежде чем он будет скомпилирован в байт-код. Поэтому, если встречается runtime-gcd , компилятор вызывает его и заменяет его результатом, то есть: (gcd ab) . Это означает, что мы можем просто заменить, например, (runtime-gcd 12 8) на (gcd 12 8) — это то, что компилятор делает для нас в любом случае.

Что в этом такого? Пока что макросы — это просто необычные функции, выполняемые во время компиляции. Но что, если мы пропустим цитирование и определим
compile-time-gcd следующим образом?

1
2
3
4
user=> (defmacro compile-time-gcd [a b] (gcd a b))
#'user/compile-time-gcd
user=> (compile-time-gcd 12 8)
4

Оставайтесь со мной, вы так близки к просветлению. Обратите внимание, что мы больше не цитируем вызов gcd . Это имеет огромные последствия. На этот раз, когда компилятор встречает макрос compile-time-gcd он выполняет свое тело ( расширяет его ). В то время как тело runtime-gcd функцию list (таким образом, возвращая список), тело compile time-gcd gcd немедленно вызывает gcd — и помните, что это происходит во время компиляции! (gcd 12 8) выполняется компилятором, и его значение ( 4 ) возвращается как результат расширения макроса. Это означает, что целое (compile-time-gcd 12 8) заменяется во время компиляции номером 4 . Другими словами, вычисление было сделано во время компиляции, а издержки gcd отсутствуют во время выполнения. Проверьте вывод macroexpand который показывает, какой макрос возвращается, не оценивая его:

1
2
3
4
5
user=> (macroexpand '(runtime-gcd 12 8))
(gcd 12 8)
  
user=> (macroexpand '(compile-time-gcd 12 8))
4

Это то, о чем вы должны подумать. Макросы — это не просто расширенные средства поиска и замены, встроенные в компилятор. Это «настоящие» функции Clojure, которые могут иметь логику и условия. Разница лишь в том, что они работают во время компиляции и работают с кодом, а не со значениями. Так почему бы не использовать макросы все время, если они могут запускать программу во время компиляции и избегать вычислений во время выполнения? Помните, что макросы живут только в компиляторе, они ничего не знают о вашей среде выполнения:

1
2
3
user=> (compile-time-gcd 12 (inc 7))
ClassCastException clojure.lang.PersistentList cannot be cast to java.lang.Number
    clojure.lang.Numbers.isZero (Numbers.java:90)

Эта ошибка на самом деле всплывает во время компиляции, а не во время выполнения! Компилятор пытается запустить (gcd 12 '(inc 7)) . Цитируемый '(inc 7) список не равен числу 8. Это list ! И когда компилятор выполняет условие (zero? '(inc 7)) ClassCastException знакомое ClassCastException . Не путайте это с внешне похожим (zero? (inc 7)) — приращение 7 не заключено в кавычки и, следовательно, равно 8 .

Вы все еще в замешательстве? Давайте сделаем это еще более явным:

1
2
3
(defmacro printer [s]
    (println "Compile time:" s)
    (list 'println "Runtime:" s))

Этот макрос является функцией с двумя выражениями. Теперь скомпилируйте следующий файл Clojure:

1
2
(printer "buzz")
(printer (str "foo" "bar"))

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

1
2
Compile time: buzz
Compile time: (str foo bar)

Это доказывает, что макросы раскрываются и выполняются во время компиляции. Но что случилось со второй строкой? Ну, значение последнего выражения любой функции (макросы здесь не исключение) становится значением этой функции. Таким образом, каждое вхождение макроса (println "Runtime:" s) (printer s) заменяется списком (println "Runtime:" s) — и этот фрагмент кода будет скомпилирован так, как если бы он был println с самого начала.

Чтобы убедиться, что вы действительно хорошо понимаете макросы, переключите операторы в макросе printer и попытайтесь выяснить, что будет делать этот макрос, как во время компиляции, так и во время выполнения (подсказка: значение println равно nil ):

1
2
3
(defmacro broken-printer [s]
    (list 'println "Runtime:" s)
    (println "Compile time:" s))

Мы даже не приблизились к объяснению всех аспектов макросов в Clojure. Мы не рассматривали различные причуды цитирования, gensym , сплайсинг и т. Д. Но я надеюсь, что эта статья (вместе с макросами Clojure для начинающих ) даст вам базовую идею, почему макросы так важны в семействе языков Lisp.

Ссылка: жизненный цикл макроса в Clojure от нашего партнера по JCG Томаша Нуркевича из блога Java и соседей .