Clojure:减少与应用

Posted

技术标签:

【中文标题】Clojure:减少与应用【英文标题】:Clojure: reduce vs. apply 【发布时间】:2011-03-10 08:45:27 【问题描述】:

我了解reduceapply 之间的概念区别:

(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 似乎要快一些。

【问题讨论】:

除非你有理由关心性能,否则基本上没关系。我更喜欢 +,因为它显示了 + 函数没有延迟,并且可以像你期望的那样接受任意数量的参数。 另外,在便携性方面考虑this answer到this question 【参考方案1】:

当使用像 + 这样的简单函数时,使用哪一个并不重要。

一般来说,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)

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

【讨论】:

【参考方案2】:

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)

【讨论】:

【参考方案3】:

这个话题有点晚了,但我在阅读了这个例子后做了一个简单的实验。这是我的repl的结果,我无法从响应中推断出任何东西,但似乎在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 的源代码使用 internal-reduce 减少其非常干净的递归,但没有发现任何关于 apply 的实现。 + for apply的Clojure实现内部调用reduce,由repl缓存,这似乎解释了第4次调用。有人可以澄清这里到底发生了什么吗?

【讨论】:

我知道我会更喜欢尽可能减少 :) 您不应该将range 调用放在time 表单中。放在外面,以消除序列构造的干扰。就我而言,reduce 的表现始终优于apply【参考方案4】:

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

(reduce + some-numbers)

我马上就知道你正在将一个序列变成一个值。

对于apply,我必须考虑应用哪个函数:“啊,这是+ 函数,所以我得到...一个数字”。稍微不那么直截了当。

【讨论】:

【参考方案5】:

意见不一——在更大的 Lisp 世界中,reduce 绝对被认为更惯用。首先,有已经讨论过的可变参数问题。此外,当 apply 应用于非常长的列表时,一些 Common Lisp 编译器实际上会失败,因为它们处理参数列表的方式。

不过,在我圈子里的 Clojurists 中,在这种情况下使用 apply 似乎更常见。我发现它更容易摸索,也更喜欢它。

【讨论】:

【参考方案6】:

对于查看此答案的新手, 小心,它们不一样:

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

【讨论】:

【参考方案7】:

在对任何类型的集合进行操作时,我通常发现自己更喜欢 reduce - 它执行得很好,而且总的来说是一个非常有用的函数。

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

(apply + 1 2 other-number-list)

【讨论】:

【参考方案8】:

reduceapply 当然对于需要在可变参数情况下查看所有参数的关联函数来说是等价的(就返回的最终结果而言)。当它们在结果方面等效时,我会说 apply 始终是完全惯用的,而 reduce 在许多常见情况下是等效的 - 并且可能眨眼间就减少了一小部分.以下是我相信这一点的理由。

+ 本身是根据 reduce 实现的,用于可变参数情况(超过 2 个参数)。事实上,对于任何可变参数、关联函数来说,这似乎是一种非常明智的“默认”方式:reduce 有可能执行一些优化以加快速度——也许通过类似internal-reduce 之类的东西,1.2 新奇最近在 master 中禁用,但希望将来重新引入 - 在 vararg 情况下可能从它们中受益的每个函数中复制它是愚蠢的。在这种常见情况下,apply 只会增加一点开销。 (请注意,这没什么好担心的。)

另一方面,一个复杂的函数可能会利用一些不够通用的优化机会,这些机会不足以内置到reduce中;那么apply 会让你利用这些优势,而reduce 实际上可能会减慢你的速度。 str 提供了在实践中发生的后一种情况的一个很好的例子:它在内部使用StringBuilder,并且将大大受益于使用apply 而不是reduce

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

【讨论】:

很好的答案。顺便说一句,为什么不包括一个内置的 sum 函数,就像在 haskell 中一样?似乎是一个很常见的操作。 谢谢,很高兴听到这个消息!回复:sum,我想说 Clojure 有这个功能,它叫做+,你可以和apply 一起使用。 :-) 说真的,我认为在 Lisp 中,一般来说,如果提供了可变参数函数,它通常不会伴随着对集合进行操作的包装器——这就是你使用 apply 的方式(或 reduce,如果你知道的话这更有意义)。 有趣的是,我的建议恰恰相反:reduce 有疑问时,apply 当你确定有优化时。 reduce 的合约更精确,因此更容易进行一般优化。 apply 更加模糊,因此只能根据具体情况进行优化。 strconcat 是两个普遍的例外。 @cgrand 对我的基本原理的重新表述可能大致是对于 reduceapply 在结果方面等效的函数,我希望相关函数的作者知道如何最好优化它们的可变参数重载并仅根据reduce 来实现它,如果这确实是最有意义的(这样做的选项肯定总是可用的,并且是一个非常明智的默认值)。不过,我确实知道您来自哪里,reduce 绝对是 Clojure 性能故事的核心(而且越来越重要),非常高度优化并且非常明确。 @dbyrne:Clojure 没有提供 sum 函数来给语言律师一些(其他)争论。【参考方案9】:

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

【讨论】:

以上是关于Clojure:减少与应用的主要内容,如果未能解决你的问题,请参考以下文章

在 Clojure 中将嵌套向量减少为另一个向量

Clojure:减少大型懒惰收集会占用内存

包含混合向量和双精度的向量的 Clojure 减少函数

变压器与减速器的区别是什么? - Clojure

Clojure 在函数中减少溢出,但在直接传递给 REPL 时不会溢出

Clojure 与 Numpy 中的矩阵乘法