Clojure:减少与应用


126

我了解reduce和之间的概念差异apply

(reduce + (list 1 2 3 4 5))
; translates to: (+ (+ (+ (+ 1 2) 3) 4) 5)

(apply + (list 1 2 3 4 5))
; translates to: (+ 1 2 3 4 5)

但是,哪个是惯用的Clojure?一种或另一种方式有什么不同吗?从我的(有限的)性能测试来看,它似乎reduce要快一些。

Answers:


125

reduce并且apply对于需要在可变对数情况下查看其所有参数的关联函数,当然仅等效(就返回的最终结果而言)。当它们在结果上是等效的时,我会说这apply总是完全惯用的,而reduce在等效情况下,它是等效的,并且在许多常见情况下可能会眨眼之间。接下来是我相信这一点的理由。

+它本身是reduce针对可变变量情况(超过2个参数)实现的。的确,对于任何可变参数关联功能,这似乎都是一种非常明智的“默认”方式:reduce可能会进行一些优化以加快处理速度-可能是通过诸如internal-reduce最近在master中禁用的1.2新奇功能之类的方法实现的,但是希望在将来重新引入-在每个函数中复制可能很愚蠢,在vararg情况下可能会从中受益。在这种常见情况下,apply只会增加一点开销。(请注意,不必担心。)

另一方面,一个复杂的函数可能会利用一些优化机会,而这些优化机会通常不够内置reduce。然后apply让您利用这些机会,而reduce这实际上可能会降低您的速度。一个实际情况下后一种情况的很好的例子提供了str:它在StringBuilder内部使用,将从中使用apply而不是从中受益reduce

所以,apply如果有疑问,我会说使用。如果您碰巧知道它并没有给您买完任何东西reduce(而且这种情况不太可能很快改变),请随意使用它reduce来减少不必要的少量开销。


好答案。附带说明一下,为什么不包括sumhaskell这样的内置函数?似乎很普通的操作。
dbyrne

17
谢谢,很高兴听到这个消息!关于:sum,我想说Clojure具有此功能,可以调用+它,并且可以与一起使用apply。:-)认真地说,我认为在Lisp中,一般来说,如果提供了可变函数,通常不会伴随对集合进行操作的包装程序-这就是您使用的apply功能(或者reduce,如果您知道更有意义的话)。
米哈尔Marczyk

6
有趣的是,我的建议却相反:reduce当有疑问时,apply当您确定知道有优化时。reduce的合同更精确,因此更容易进行一般优化。apply更模糊,因此只能根据具体情况进行优化。strconcat是两个普遍的例外。
cgrand

1
@cgrand对我的基本原理的改写大概是对于那些在结果上reduceapply在结果上等效的函数,我希望有问题的函数的作者知道如何最好地优化其可变参数重载,并仅在reduceif 方面实现它。确实,这才是最有意义的选择(这样做的选择当然总是可用的,并且是非常明智的默认选择)。不过,我确实知道您来自哪里,这reduce绝对是Clojure的绩效故事的核心(而且越来越重要),而且经过高度优化和明确说明。
2013年

51

对于初学者,请
注意,他们是不同的:

(apply hash-map [:a 5 :b 6])
;= {:a 5, :b 6}
(reduce hash-map [:a 5 :b 6])
;= {{{:a 5} :b} 6}

21

意见不一-在更大的Lisp世界中,reduce绝对被认为是惯用语。首先,已经讨论了各种各样的问题。而且,某些Common Lisp编译器在apply处理很长的列表时实际上会因其处理参数列表的方式而失败。

不过,在我圈子中的Clojurist专家中,apply在这种情况下使用似乎更为普遍。我发现更容易gr嘴,也更喜欢它。


19

在这种情况下没有什么区别,因为+是一种特殊情况,可以应用于任意数量的参数。Reduce是一种将期望固定数量参数(2)的函数应用于任意长参数列表的方法。


9

我通常会发现自己比较喜欢在任何类型的集合上执行reduce的操作-它表现良好,并且通常是一个非常有用的功能。

我会使用apply的主要原因是,如果参数在不同位置表示不同的意思,或者您有几个初始参数但想从集合中获取其余参数,例如

(apply + 1 2 other-number-list)

9

在这种情况下,我更喜欢,reduce因为它更具可读性:当我阅读时

(reduce + some-numbers)

我立即知道您正在将序列转换为值。

随着apply我要考虑的是被应用于其功能:“啊,它的+功能,所以我得到...单号”。不太直接。


7

使用+等简单函数时,实际上使用哪一个都没有关系。

通常,此想法reduce是一个累加操作。您向累加函数提供当前累加值和一个新值。该函数的结果是下一次迭代的累加值。因此,您的迭代看起来像:

cum-val[i+1] = F( cum-val[i], input-val[i] )    ; please forgive the java-like syntax!

对于apply,这个想法是您试图调用一个期望有多个标量参数的函数,但是它们目前在一个集合中,需要被拉出。因此,与其说:

vals = [ val1 val2 val3 ]
(some-fn (vals 0) (vals 1) (vals 2))

我们可以说:

(apply some-fn vals)

并转换为等效于:

(some-fn val1 val2 val3)

因此,使用“ apply”就像在序列周围“删除括号”。


4

在这个话题上有点晚了,但是在阅读了这个例子之后我做了一个简单的实验。这是我的代表的结果,我只是无法从响应中推断出任何内容,但是在reduce和apply之间似乎存在某种缓存作用。

user=> (time (reduce + (range 1e3)))
"Elapsed time: 5.543 msecs"
499500
user=> (time (apply + (range 1e3))) 
"Elapsed time: 5.263 msecs"
499500
user=> (time (apply + (range 1e4)))
"Elapsed time: 19.721 msecs"
49995000
user=> (time (reduce + (range 1e4)))
"Elapsed time: 1.409 msecs"
49995000
user=> (time (reduce + (range 1e5)))
"Elapsed time: 17.524 msecs"
4999950000
user=> (time (apply + (range 1e5)))
"Elapsed time: 11.548 msecs"
4999950000

查看clojure的源代码可以通过内部缩减来减少其相当干净的递归,尽管在apply的实现中未发现任何问题。+的Clojure实现适用于内部调用reduce,由repl缓存,这似乎解释了第4个调用。有人可以澄清这里到底发生了什么吗?


我知道我会
尽量

2
您不应该将range呼叫放在time表单内。将其放在外面以消除序列构建的干扰。就我而言,reduce始终表现出色apply
Davyzhu 2015年

3

apply的美丽之处在于它的功能(在这种情况下为+)可以应用于通过在中间插入带有结束集合的自变量而形成的自变量列表。Reduce是处理为每个集合项应用功能的集合项的抽象,不适用于可变args大小写。

(apply + 1 2 3 [3 4])
=> 13
(reduce + 1 2 3 [3 4])
ArityException Wrong number of args (5) passed to: core/reduce  clojure.lang.AFn.throwArity (AFn.java:429)
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.