clojure.core apply 函数的宏形式

Shm*_*ger 3 macros performance clojure apply

其中clojure.core有一个apply函数,以多重数量风格编写(大概是为了效率)。

但是我问是否有这样的实现:

(defmacro apply [f coll] (conj (seq coll) f))

不会更好。

我知道宏的效率低于函数,但它真的有这么大的区别吗?

或者,是否不可能将此宏编写为函数(我尝试过但失败了,但这并不能证明,因为我没有编写宏的经验)?

ama*_*loy 8

使用函数而不是宏的原因apply是它必须如此。您定义的宏版本是无用的:它不起作用。

但为什么不呢?这似乎是一个合理的想法。让我们尝试一下,但调用它mapply以便我们仍然可以引用现有的apply

=> (defmacro mapply [f coll] (conj (seq coll) f))
Run Code Online (Sandbox Code Playgroud)

我打赌你做到了这一点,而且我打赌你甚至测试过它:你可以在绝对最简单的情况下使用mapply,如下所示:

=> (macroexpand '(mapply + [1 2 3]))
(+ 1 2 3) 
=> (mapply + [1 2 3])
6
Run Code Online (Sandbox Code Playgroud)

问题是这没有用:你写了(mapply + [1 2 3]),但写起来也同样容易(实际上更容易)(+ 1 2 3)。所以这个用例并不是mapply有用的证据。还有什么时候apply-like 宏会有用?

的真正价值apply在于,您可以将其应用于在运行时确定的列表:当参数列表在编译时固定时,您不需要,apply因为您可以直接编写(f x y)而不是(apply f [x y]).

因此,让我们在运行时计算的列表上尝试您的版本:

=> (apply + (range 4))
6
=> (macroexpand '(mapply + (range 4)))
(+ range 4)
=> (mapply + (range 4))
Execution error (ClassCastException) at user/eval2051 (REPL:1).
clojure.core$range cannot be cast to java.lang.Number
Run Code Online (Sandbox Code Playgroud)

发生了什么?你mapply看到那(range 4)是一个集合,所以它放在+它的前面。您想要的是评估(range 4),生成一个列表,然后以某种方式添加其元素。但因为mapply在编译时工作,所以它看到的列表是(range 4):一个包含元素range和 4 的二元素列表。

这不仅仅是mapply定义中的一些简单错误:不可能定义mapply为宏(除非是一个在编译时不起作用并apply在运行时委托给的微不足道的宏)。对于一个更明显不可能的示例,请考虑mapply从函数内部调用您的:

=> (defn sum [xs] (apply + xs))
=> (sum (range 4))
6
=> (defn msum [xs] (mapply + xs))
Syntax error macroexpanding mapply at (REPL:1:17).
Don't know how to create ISeq from: clojure.lang.Symbol
Run Code Online (Sandbox Code Playgroud)

既然mapply是一个宏,它当然会在编译时运行,即在我们定义时运行msum。它只有一次扩展的机会,不知道xs我们将来会通过它做什么。因此,当它查看 时xs,它只看到符号xs,而不是某个列表。结果,seq调用不可避免地失败。再说一遍,没有其他实现可以工作,因为您在编译时根本不知道列表有多少元素,并且无法扩展到正确的调用。

作为最后的对待,考虑另一个apply不能是宏的原因:您可以将函数应用于无限的参数列表,但如果您尝试生成无限长的宏扩展,编译器将度过一段非常悲伤的时光。

=> (defn apply-to-nats [f] (apply f (range)))
=> (apply-to-nats (fn [& args] (first args)))
0
=> (defmacro mapply-to-nats [f] (cons f (range)))
=> (mapply-to-nats (fn [& args] (first args)))
Syntax error (OutOfMemoryError) compiling at (REPL:1:1).
GC overhead limit exceeded
Run Code Online (Sandbox Code Playgroud)

发生了什么?编译器循环运行,直到我的系统内存不足,尝试编译

((fn [& args] (first args)) 0 1 2 3 4 ...)
Run Code Online (Sandbox Code Playgroud)

  • @ShmuelGreenberger,如果我的意思是“实际上你完全可以用这个简单的技巧来做到这一点”,我就不会在我的回答中多次说“不,这是不可能的,这就是原因”。 (3认同)