宏观组成

时间:2019-06-07 06:27:47

标签: clojure macros

我正在使用Clojure宏,特别是尝试使用map的宏版本,我将其想象性地命名为“ mmap”:

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

请不要研究原因,为什么我会这样做。这只是一个最小的可复制示例。

只要将集合传递给它,它就可以正常工作,但是当我尝试将其与另一个宏进行组合时,请使用mmap切片并用宏本身(而不是其扩展)将sexp切丁。

我希望“ 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 个答案:

答案 0 :(得分:3)

函数总是在调用函数之前 评估其参数,从而导致以熟悉的“由内而外”模式对表达式进行评估:

(/ (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在两次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,而且几个月后才发现。

请确保为the Clojure CheatSheet添加书签,并始终保持打开浏览器标签的状态。定期研究它,直到您可以记住每个功能。 :)

还请注意,这里还没有列出一些更晦涩的项目


更新

宏的“杀手级功能”是添加新的语言功能。在Java等人中,只能添加新的库。例如,上面的spyx是一个非常类似于snoop示例的宏。有关其他示例,请参见the Tupelo library中的with-exception-default,vals-> map和it->。实际上,许多“核心” Clojure功能(例如for表达式或and&或逻辑运算符)实际上是任何应用程序都可以编写(或修改!)的宏。

答案 1 :(得分:1)

宏扩展从最外面的形式开始工作,并被反复调用直到到达固定点。这意味着您可以从宏中获得未扩展的代码。如果您希望它能像函数一样工作,则在调用函数之前先对参数求值,这可能是反竞争的。使用宏,不会扩展任何内容,也不会评估任何内容,从而使您可以根据需要解释基础代码。

但是您也可以从自己的宏中自由调用macroexpand,这将确保嵌套表单首先被扩展。在您的情况下,这意味着使用文字值,您将获得数据,并且可以将宏扩展值当作直接提供给宏的方式进行处理。

但是请注意,只有在对宏的最内层调用被赋予文字值并产生文字值的情况下,此方法才起作用。在任何情况下,宏都无法知道哪个值将在运行时可用。通常,人们随后尝试直接在表单上调用eval,但这是一个不好的主意,因为代码取决于运行时上下文,因此在宏扩展/编译期间对代码进行评估通常是没有意义的,而应该在该宏扩展/编译过程中对代码进行评估。运行时。