【问题标题】:multiple arity in defmacro of clojureclojure 的 defmacro 中的多重参数
【发布时间】:2014-10-23 08:09:12
【问题描述】:

我在 Clojure 中遇到了一个与 defmacro 相关的奇怪问题,我有类似的代码

(defmacro ttt
  ([] (ttt 1))
  ([a] (ttt a 2))
  ([a b] (ttt a b 3))
  ([a b c] `(println ~a ~b ~c)))

我用(ttt) 运行,它应该变成(println 1 2 3),并打印“1 2 3”,但我得到的是

ArityException Wrong number of args (-1) passed to: t1$ttt clojure.lang.Compiler.macroexpand1 (Compiler.java:6473)

经过一番调查,我明白我应该写

(defmacro ttt
  ([] `(ttt 1))
  ([a] `(ttt ~a 2))
  ([a b] `(ttt ~a ~b 3))
  ([a b c] `(println ~a ~b ~c)))

但是为什么第一个版本失败了?而args太奇怪了,看不懂,-1是哪里来的?

【问题讨论】:

    标签: macros clojure


    【解决方案1】:

    当Clojure 处理ttt 宏的定义时,它尚未创建,不能用于宏定义内的源代码转换。对于编译器,您的宏类似于(嗯,不是真的,但它是一个很好的例子):

    (defmacro ttt0 []       (ttt1 1))
    (defmacro ttt1 [a]      (ttt2 a 2))
    (defmacro ttt2 [a b]    (ttt3 a b 3))
    (defmacro ttt3 [a b c] `(println ~a ~b ~c))
    

    尝试评估ttt0的定义,你会得到:

    CompilerException java.lang.RuntimeException: Unable to resolve symbol: ttt1 in this context

    因此,当 Clojure 处理宏的定义时,它必须在定义的未引用部分扩展宏,就像代码的任何其他部分一样。它以ttt1 失败,并且 must 在您的情况下失败。我的猜测是它就像一个错误。很难说你为什么会得到-1,我认为这与语言实现的内部机制有关。

    在这里我们可以看到宏和函数之间的区别:宏作用于任何输入代码以立即对其进行转换,而函数必须被调用并且一切都始终被定义并准备就绪:

    user> (defn ttt
            ([] (ttt 1))
            ([a] (ttt a 2))
            ([a b] (ttt a b 3))
            ([a b c] :works!))
    ;; => #'user/ttt
    user> (ttt)
    ;; => :works!
    

    这里对ttt的调用只是指令,它们会在ttt被调用时执行。

    【讨论】:

    • 恐怕这个答案的要点是错误的。将宏视为在编译时运行的简单函数会很有帮助。在大多数情况下,它们的工作方式相同,只是在不同的时间。实际上,您可能有一个递归宏,甚至是一个多元宏,尽管很少见。这个多参数示例失败了,因为 Clojure 为内部调用期间未提供的宏插入了两个隐藏参数。
    【解决方案2】:

    宏有两个隐藏参数

    宏有两个隐藏的参数&form&env,它们提供了有关调用和绑定的附加信息,这些信息是导致此处的arity 异常的原因。要在同一宏中引用其他 arity 版本,请使用准引号扩展。

    (defmacro baz
      ([] `(baz 1))
      ([a] `(baz ~a 2))
      ([a b] `(baz ~a ~b 3))
      ([a b c] `(println ~a ~b ~c)))
    
    user=> (macroexpand-1 '(baz))
    (clojure.core/println 1 2 3)
    
    user=> (baz)
    1 2 3
    nil
    

    Arity 异常消息从计数中减去隐藏参数

    您得到 (-1) arity 异常的原因是编译器在为一般宏用法生成错误消息时减去了这两个隐藏参数。对于您的第一个版本的 ttt,此处的真实信息将是“参数数量错误 (1)”,因为您提供了一个参数 a,但自调用未提供另外两个隐藏参数。

    在野外不常见的多元宏

    在实践中,我建议完全避免使用多元宏。相反,考虑一个辅助函数来代表宏完成大部分工作。事实上,这通常也是其他宏的好习惯。

    (defn- bar
      ([] (bar 1))
      ([a] (bar a 2))
      ([a b] (bar a b 3))
      ([a b c] `(println ~a ~b ~c)))
    
    
    (defmacro foo [& args] (apply bar args))
    
    user=> (macroexpand-1 '(foo))
    (clojure.core/println 1 2 3)
    
    user=> (foo)
    1 2 3
    nil
    

    宏扩展是递归的

    由于宏扩展的递归性质,您的第二个 ttt 版本也可以正常工作

    user=> (macroexpand-1 '(ttt))
    (user/ttt 1)
    user=> (macroexpand-1 *1)
    (user/ttt 1 2)
    user=> (macroexpand-1 *1)
    (usr/ttt 1 2 3)
    user=> (macroexpand-1 *1)
    (clojure.core/println 1 2 3)
    

    所以,

    user=> (macroexpand '(ttt))
    (clojure.core/println 1 2 3)
    

    【讨论】:

    • 好吧,那么。什么文档描述了两个隐藏的参数?它们包含什么信息?
    • 关于“宏扩展是递归的”的吹毛求疵:macroexpand 通过重复调用macroexpand-1 来计算一个固定点,但macroexpandmacroexpand-1 都不会递归调用自己(除了如果你的宏有)。另见stackoverflow.com/q/39943529/124319
    • 我不理解您所传达的微妙之处,但macroexpand 确实 确实以递归方式自称。也许您是说macroexpand 会下降到子表单中?这是真的,但是编译器当然会执行,然后会根据需要触发对macroexpand 的新调用。
    • 显式传递隐藏的参数是可行的,但你不应该这样做,因为它依赖于实现细节。准报价方式是唯一的方式。 (source)
    猜你喜欢
    • 2011-12-06
    • 1970-01-01
    • 2018-08-31
    • 1970-01-01
    • 2023-04-02
    • 1970-01-01
    • 2018-08-28
    • 1970-01-01
    相关资源
    最近更新 更多