Макро композиция - PullRequest
       22

Макро композиция

2 голосов
/ 07 июня 2019

Я немного играю с макросами Clojure, особенно пробую макро-версию карты, которую я образно назвал "mmap":

(defmacro mmap [f coll]
    `(vector ~@(map #(list f %) coll)))

Пожалуйста, не останавливайтесь на причинах 1005 * почему я могу это делать.Это всего лишь минимальный воспроизводимый пример.

Он отлично работает, пока в него передается коллекция, но когда я пытаюсь скомпоновать его с другим макросом, mmap нарезает секцию с помощью самого макроса, а не егорасширение.

Я бы хотел, чтобы «mmap» работал так:

(mmap inc [1 2 3 4])            => [2 3 4 5]
(mmap dec (mmap inc [1 2 3 4])) => [1 2 3 4] NOPE. See the edit.

Итак, вопрос: есть ли способ заставить расширение макроса передать в качестве аргумента другомумакрос?В каком случае, как?

Спасибо за понимание!

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

(mmap inc (vector 1 2 3 4))

, который не имеет смысла в этом конкретном случае, но я все еще хочу знать, можно ли это как-то сделать

EDIT2 использованиемассива [1 2 3 4] вместо символа, например «arg» предназначался для передачи того, что все это делается во время компиляции, а не во время выполнения.

Ответы [ 2 ]

4 голосов
/ 07 июня 2019

Функции всегда оценивают свои аргументы перед , вызывая функцию, ведущие выражения должны быть оценены в привычном шаблоне "наизнанку":

(/ (inc 3) (dec 3))  ; orig
(/ 4 2)              ; args sent to `/` function
2                    ; final result

Цельмакроса , чтобы избежать вычисления аргументов перед вызовом функции, что приводит к тому, что выражения оцениваются "снаружи в".Это позволяет составлять макросы, как если бы каждый из них был новой языковой функцией:

(ns tst.demo.core
  (:use demo.core tupelo.core tupelo.test))

(defmacro infix
  [[l op r]]
  `(~op ~l ~r))

(defmacro snoop
  [& forms]
  `(let [result# ~@forms]
     (do
       (println "snoop => " result#)
       result#)))

(dotest
  (spyx (infix (2 + 3)))
  (spyx (infix (5 - 3)))
  (newline)

  (snoop (+ 1 2))
  (snoop (infix (4 + 5)))
  (newline)

  (println :last
    (infix ((snoop 4) + (snoop 5)))))

с результатом

(infix (2 + 3)) => 5
(infix (5 - 3)) => 2

snoop =>  3
snoop =>  9

snoop =>  4
snoop =>  5
:last 9

Так что в последнем примере есть промежуточный этап, который выглядит следующим образом:

(println :last
  (+ (snoop 4) (snoop 5)))

, где видно, что infix был оценен до 2 snoop вызовов.Это как раз сила макросов, и без них они были бы бесполезны.

Возможно, вы могли бы создать новый макрос, который работал бы так:

(defmacro mmap
  [fns coll]
  `(let [comp-fn# (comp ~@fns)]
     (mapv comp-fn# ~coll)))

(mmap [inc inc inc] [1 2 3]) => [4 5 6]

В то время как выше mmap работает, это действительно злоупотребление макросистемой и может потерпеть неудачу, если вы дадите ей что-то кроме констант времени компиляции.

Лучшей альтернативой для базовых функций является простое запоминание нормальной функции:

(def do-stuff
  (memoize ; will only execute on the first usage
    (fn
      [coll]
      (mapv #(* 2 %) (mapv inc coll)))))

(do-stuff [1 2 3 4]) => [4 6 8 10]

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

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


Относительно mapv

Я также обнаружил mapv случайно и только спустя много месяцев.

Обязательно добавьте в закладки Clojure CheatSheet и всегда держите вкладку браузера открытой для него.Регулярно изучайте его, пока не вспомните каждую из функций.:)

Также имейте в виду, что некоторые неясные предметы там не перечислены !


Обновление:

«Убийственная особенность» макросов заключается в добавлении новых языковых возможностей.В Java и др. Можно добавлять только новые библиотеки.В качестве примера, spyx выше - макрос, очень похожий на пример snoop.Для других примеров смотрите with-exception-default, vals-> map и it-> из в библиотеке Tupelo .Фактически, многие «основные» функции Clojure, такие как выражение for или логические операторы and и / или, на самом деле являются макросами, которые любая прикладная программа может написать (или изменить!).

1 голос
/ 07 июня 2019

Макроразложение работает из самой внешней формы и вызывается многократно, пока не будет достигнута точка фиксации. Это означает, что из вашего макроса вам дается нерасширенный код. Это может быть нелогичным, если вы ожидаете, что оно будет работать как функции, где аргументы оцениваются до вызова функции. С макросом ничего не раскрывается и ничего не оценивается, что позволяет вам интерпретировать базовый код по вашему желанию.

Но вы также можете сами вызывать macroexpand из своего макроса, что обеспечит расширение вложенных форм в первую очередь. В вашем случае это означает, что с литеральными значениями вы получите данные обратно, и вы можете обработать макрорасширенное значение, как если бы оно было передано непосредственно вашему макросу.

Но учтите, что это работает, только если внутреннему вызову вашего макроса дано буквальное значение и возвращено литеральное значение. В любом случае макрос не может знать, какое значение будет доступно во время выполнения. Обычно люди тогда пытаются вызвать eval непосредственно в формах, но это плохая идея, потому что код зависит от контекста времени выполнения, в общем нет смысла оценивать код во время макроразложения / компиляции, который должен оцениваться во время выполнения.

...