【问题标题】:Difference between calling function and macro inside macro?宏内部调用函数和宏的区别?
【发布时间】:2014-01-01 03:23:44
【问题描述】:

我的困惑是下面的例子:

(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 表达式。我想知道它们是否可以互换使用,除非它们在不同阶段进行评估。另一个例子:

(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)]

【问题讨论】:

    标签: clojure macros


    【解决方案1】:

    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,因此生成了映射 {: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 编译,但这最终会发出abc-name 作为正在定义的Var 的名称,并在生成的代码中包含三次向量文字[a b c] 的代码。然而,我们可以调用它:

    `(emit-abc ~abc-name ~abc-vals)
    

    这按预期工作。

    【讨论】:

    • 谢谢。所以我必须使用: (defmacro macro2 [x] `(do (macro1 ~x) ~(func1 x)))
    • @M.Tong - 这将导致在编译时打印与x 关联的任何代码片段,并在代码实际运行时打印x 的值。尝试将它包装在一个函数中,就像我在回答中建议的那样进行测试。我赞成这个答案,因为它完全正确,但我希望我的答案中的额外解释也有帮助(尽管我承认我不觉得我解释得像我希望的那样清楚)。
    • 谢谢你们两个。我编辑了我的问题并提供了一个例子。我的问题。
    • 我更新了我的答案以回应您的编辑。我认为@DaoWen 提出了一个很好的建议,即使用函数来测试你的宏以确定何时会发生各种副作用;您甚至可以添加一些额外的调试打印输出((println "expanding macro1") 等)以使事情更清晰。
    • @MichałMarczyk 非常有帮助。谢谢。因此,通常调用辅助函数比发出另一个宏更可取? (比如说我只想分解宏的工作)。
    【解决方案2】:

    我认为您对宏和函数之间的区别感到困惑。

    • 宏在编译时进行评估,它们将code作为输入并给出code作为输出。
    • 函数在运行时评估其结果,将运行时作为输入并返回运行时作为输出。李>

    宏的结果几乎总是一个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 中的语法引用使其返回代码,而不是调用返回nilprintln,那么一切都会改变:

    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 现在实际上返回源代码,它实际上像 println 一样工作。

    请注意,testing-macro-2 不打印任何内容,因为macro2 丢弃了中间计算的结果`(macro1 ~x) 并简单地返回nilprintln 的结果)。换句话说,使用(macro2 foo) 与在代码中添加nil 文字相同,只是编译器在评估宏时会打印foo 作为副作用。

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

    【讨论】:

      【解决方案3】:

      到目前为止,我对答案并不满意,所以让我试一试......

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

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

      1. `(macro1 ~x) 是引用的语法,因此它的计算结果为(macro1 hello),并且由于语法引用而进一步评估。结果,这条线实际上什么也没做。
      2. (func1 x) 在宏内部执行,打印字符串,并返回结果 nil
      3. 调用(macro2 hello) 的结果是nil,因此没有进一步发生。

      稍后,您拨打macro3 会发生以下步骤

      1. (func1 x) 执行,打印 hello,返回 nil。但由于宏尚未完成,因此 nil 什么也不做,并计算下一个表达式。
      2. `(macro1 ~x) 评估(和以前一样)为(macro1 hello) 并作为代码返回(它没有进一步评估,因为它是引用的语法)。这是调用macro3 的返回值,因为它是defmacro 的隐式do 中的最后一个表达式。
      3. 现在评估从上一步调用macro3 的返回值。它评估为 (println hello),但尚未评估。
      4. 最后评估前面步骤的结果并打印第二个hello

      我认为要理解的重点是宏返回要执行的代码,并且只返回最后一个表达式。或许还请记住,语法引号 ` 会阻止对其后的表达式进行求值,而是在适当的位置创建一个不会进一步求值的列表(除非采取其他一些操作来求值该表达式)。

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

      【讨论】:

        猜你喜欢
        • 2015-09-26
        • 2020-03-05
        • 2011-08-18
        • 2018-08-12
        • 1970-01-01
        • 2019-09-10
        • 2018-07-24
        • 2022-01-10
        • 2017-07-28
        相关资源
        最近更新 更多