调用函数和宏内部宏之间的区别?

时间:2014-01-01 03:23:45

标签: clojure macros

我的谜题是以下示例:

(defmacro macro1 [x]
  (println x))

(defn func1 [x]
  (println x))

(defmacro macro2 [x]
  `(macro1 ~x)
  (func1 x))

(defmacro macro3 [x]
  (func1 x)
  `(macro1 ~x))

(println "macro2")
(macro2 hello)

(println "macro3")
(macro3 hello)

令人惊讶的是,输出是:

macro2
hello
macro3
hello
hello

为什么macro2和macro3的输出不同?根据我的理解,宏内部宏的所有调用都可以用函数代替(除了重用的原因)。我的理解有什么不对吗?


感谢迈克尔的澄清。我的一般问题是如何在宏内部使用函数或宏来操作s-expression。我想知道它们是否可以交换使用,除了它们在不同的阶段被唤醒。另一个例子:

(defn manipulate-func [x]
  (list + x 1))

(defmacro manipulate-macro [x]
  (list + x 1))

(defmacro macro1 [x y]
  [(manipulate-func x) `(manipulate-macro ~y)])

(println (clojure.walk/macroexpand-all '(macro1 (+ 1 2) (+ 3 4))))
;; [(#<core$_PLUS_ clojure.core$_PLUS_@332b9f79> (+ 1 2) 1) (#<core$_PLUS_ clojure.core$_PLUS_@332b9f79> (+ 3 4) 1)]

3 个答案:

答案 0 :(得分:5)

macro2不会致电macro1。看看它的身体:

`(macro1 ~x)
(func1 x)

第一行是语法引用的;它的值是(user/macro1 x-value)形式的列表结构(假设在macro1命名空间中定义user; x-value这里是提供给macro2的文字参数)和它没有副作用。由于没有副作用且值被丢弃,因此该行无效。


回应编辑:

首先,区分调用宏内部的另一个宏与向另一个宏发出调用非常重要:

(defmacro some-macro []
  ...)

;; calls some-macro:
(defmacro example-1 []
  (some-macro))

;; emits a call to some-macro:
(defmacro example-2 []
  `(some-macro))

其次,在宏的体内调用函数和宏的情况下,必须记住运行时和编译时的相关概念是什么:

  • 宏调用的函数将在宏扩展器的运行时调用,从用户代码的角度来看是编译时间;

  • 编译宏体时,将扩展
  • 宏调用的宏。

如果宏发出对另一个宏的调用,则与发出的宏调用相关的运行时和编译时的概念将与原始宏调用相关的概念相同。如果一个宏调用另一个宏,它们会向后移动一步。

为了说明,让我们考虑一个将其所有工作委托给辅助函数的宏:

(defn emit-abc [abc-name [a b c]]
  `(def ~abc-name {:a ~a :b ~b :c ~c}))

(defmacro defabc [abc-name abc-vals]
  (emit-abc abc-name abc-vals))

来自REPL:

user> (defabc foo [1 2 3])
#'user/foo
user> foo
{:a 1, :c 3, :b 2}

如果emit-abc本身就是一个宏,那么defabc的上述定义甚至不会编译,因为emit-abc会尝试对文字符号abc-vals进行解构,抛出一个UnsupportedOperationException

这是另一个例子,可以更容易地解释发生了什么:

(let [[a b c] [1 2 3]]
  (defabc foo [a b c]))

defabc接收三个文字符号abc的向量作为第二个参数;它无权访问运行时值123。它将这个精确的符号向量传递给函数emit-abc,然后它可以到达此向量并提取符号以生成映射{:a a :b b :c c}。此地图成为defabc电话的扩展。在运行时abc结果将绑定到值12three,因此地图{ {1}}已生成。

假设我们尝试将{:a 1 :b 2 :c 3}编写为具有相同正文的宏(仅在其定义中将emit-abc更改为defn。然后我们无法从defmacro中有用地调用它,因为我们无法向它传达defabc的参数的实际值。我们可以写

defabc

使(emit-abc abc-name [(abc-vals 0) (abc-vals 1) (abc-vals 2)]) 编译,但这将最终发出defabc作为正在定义的Var的名称,并在生成的代码中包含向量文本abc-name的代码三次。然而,我们可以发出呼叫:

[a b c]

这可以按预期工作。

答案 1 :(得分:4)

我认为你对宏和功能之间的区别感到困惑。

  • 宏在编译时进行评估,并将代码作为输入,并将代码作为输出。
  • 函数在运行时评估其结果,将运行时作为输入,并将运行时作为输出返回。

宏的结果几乎总是s-expression表示应用宏的源代码。这就是宏通常使用语法引用功能的原因,因为它可以通过~~@转义轻松生成带有插入参数值的源代码。

定义一些函数可能会帮助您了解其工作原理。我们运行以下代码:

(defn testing-macro-2 [my-arg]
  (macro2 my-arg))

(testing-macro-2 "macro 2 test")

(defn testing-macro-3 [my-arg]
  (macro3 my-arg))

(testing-macro-3 "macro 3 test")

以下是我在REPL中的内容:

user=>
(defn testing-macro-2 [my-arg]
  (macro2 my-arg))
my-arg
#'user/testing-macro-2
user=> (testing-macro-2 "macro 2 test")
nil
user=>
(defn testing-macro-3 [my-arg]
  (macro3 my-arg))
my-arg
my-arg
#'user/testing-macro-3
user=> (testing-macro-3 "macro 3 test")
nil

如您所见,定义调用宏的函数时会打印my-arg,而不是在调用函数时。这是因为在Clojure编译器为函数生成代码时会对宏进行求值,因此在调用println时会发生这种情况。

但是,如果您使用macro1中的syntax-quote使其返回代码而不是调用返回println的{​​{1}},那么全部变化:

nil

由于在评估宏时调用user=> (defmacro macro1 [x] `(println ~x)) #'user/macro1 user=> (defn func1 [x] (println x)) #'user/func1 user=> (defmacro macro2 [x] `(macro1 ~x) (func1 x)) #'user/macro2 user=> (defmacro macro3 [x] (func1 x) `(macro1 ~x)) #'user/macro3 user=> (defn testing-macro-2 [my-arg] (macro2 my-arg)) my-arg #'user/testing-macro-2 user=> (testing-macro-2 "macro 2 test") nil (defn testing-macro-3 [my-arg] (macro3 my-arg)) my-arg #'user/testing-macro-3 user=> (testing-macro-3 "macro 3 test") macro 3 test nil user=> (macro2 hello) hello nil user=> (macro3 hello) hello CompilerException java.lang.RuntimeException: Unable to resolve symbol: hello in this context, compiling:(NO_SOURCE_PATH:107) ,每个宏仍会打印参数,但由于println现在实际上返回源代码,因此它实际上像macro3一样工作。 / p>

请注意println不打印任何内容,因为testing-macro-2会抛弃中间计算macro2的结果,只返回`(macro1 ~x)nil的结果)。换句话说,使用println与在代码中放置(macro2 foo)字面值相同,只是编译器在评估宏时会打印nil作为副作用。 / p>

调用foo会产生(macro3 hello)因为宏替换导致代码CompilerException,但未定义(println hello)。如果您执行hello之类的操作,那么您将不会收到错误,因为它会找到(def hello "Hello there!")的绑定。

答案 2 :(得分:0)

到目前为止,我对答案不满意,所以让我采取刺痛......

问题是defmacro返回的数据随后被用作代码。仅返回defmacro中的 last 表达式,然后在您的示例中将其评估为代码。

所以...在您致电macro2时,会发生以下步骤

  1. `(macro1 ~x)是引用的语法,因此它的评估结果为(macro1 hello),并且由于语法引用而未进一步评估 。这条线因此无效。
  2. (func1 x)在宏内执行,打印字符串,返回结果nil
  3. 调用(macro2 hello)的结果是nil,因此不会再发生任何事情。
  4. 稍后,您致电macro3并执行以下步骤

      执行
    1. (func1 x),打印hello,返回nil。但是由于宏没有完成,所以nil什么也不做,下一个表达式就会被评估。
    2. `(macro1 ~x)评估(就像之前一样)到(macro1 hello)并作为代码返回(它不会被进一步评估,因为它是语法引用的)。这是调用macro3的返回值,因为它是do的隐式defmacro中的最后一个表达式。
    3. 现在评估从上一步调用macro3的返回值。它评估为(println hello),但尚未评估。
    4. 最后评估前面的步骤结果并打印第二个hello
    5. 我相信需要理解的是,宏返回要执行的代码,只返回最后一个表达式。也许还要记住,语法引用`会阻止对其后面的表达式进行求值,而是创建一个未进一步求值的列表(除非采取其他一些操作来评估表达式)。

      是的,在评估代码之间存在运行时/编译时间差异,但对于这个问题,我不认为这是需要注意的关键事项。需要注意的关键是,使用macro2时,不会返回调用macro1的结果,但会在macro3中返回它,因此会进一步评估。