【问题标题】:Perform multiple reductions in a single pass in Clojure在 Clojure 中一次执行多次归约
【发布时间】:2016-07-16 04:10:49
【问题描述】:

在 Clojure 中,我想找到多次归约的结果,同时只使用一次序列。在 Java 中,我会执行以下操作:

double min = Double.MIN_VALUE;
double max = Double.MAX_VALUE;
for (Item item : items) {
    double price = item.getPrice();
    if (price > min) {
        min = price;
    }

    if (price < max) {
        max = price;
    }
}

在 Clojure 中,我可以通过使用循环和递归来做很多相同的事情,但它不是很可组合 - 我想做一些事情,让您可以根据需要添加其他聚合函数。

我为此编写了以下函数:

(defn reduce-multi
  "Given a sequence of fns and a coll, returns a vector of the result of each fn
  when reduced over the coll."
  [fns coll]
  (let [n (count fns)
        r (rest coll)
        initial-v (transient (into [] (repeat n (first coll))))
        fns (into [] fns)
        reduction-fn
        (fn [v x]
          (loop [v-current v, i 0]
            (let [y (nth v-current i)
                  f (nth fns i)
                  v-new (assoc! v-current i (f y x))]
              (if (= i (- n 1))
                v-new
                (recur v-new (inc i))))))]
    (persistent! (reduce reduction-fn initial-v r))))

这可以通过以下方式使用:

(reduce-multi [max min] [4 3 6 7 0 1 8 2 5 9])
=> [9 0]

我很欣赏它没有以最惯用的方式实现,但主要问题是它的速度大约是一次减少一个的 10 倍。这对于在 seq 执行大量 IO 的情况下执行大量归约可能很有用,但肯定会更好。

现有的 Clojure 库中有什么东西可以满足我的需求吗?如果没有,我的功能哪里出错了?

【问题讨论】:

  • 你读过传感器吗?如果是这样,他们会感到困惑吗?如果没有,建议使用。
  • @AWebb juxt 看起来很完美......但不是必需的,对吧?我的理解是,传感器允许在不创建中间序列的情况下进行一系列单独的reduce 调用,这就是我的目标。
  • @Mars 在系列中,就像在构图中一样,是的。我并没有立即看到并列,就像并列一样,干净,但遗憾的是,我还没有在编写自己的传感器方面玩太多。
  • @Frank C. 不,现在才开始研究它们。我之前听说过它们,但我认为它们更多是为了有效地将这些操作链接在一起,而不是让你做一些完全不同的事情。正如我在 cmets 对 leetwinski 的回答中所指出的那样,也许性能提升对于实现优雅同时仍然以更幼稚的方法保持同等速度是必要的。

标签: clojure functional-programming reduce


【解决方案1】:

reduce 的代码对于 reducible 集合来说很快。所以值得尝试将multi-reduce 建立在核心reduce 上。为此,我们必须能够构造正确形状的归约函数。这样做的辅助功能是......

(defn juxt-reducer [f g]
  (fn [[fa ga] x] [(f fa x) (g ga x)]))

现在我们可以定义你想要的函数,将juxtreduce组合成...

(defn juxt-reduce
  ([[f g] coll]
   (if-let [[x & xs] (seq coll)]
     (juxt-reduce (list f g) [x x] xs)
     [(f) (g)]))
  ([[f g] init coll]
   (reduce (juxt-reducer f g) init coll)))

例如,

(juxt-reduce [max min] [4 3 6 7 0 1 8 2 5 9]) ;=> [9 0]

以上遵循核心reduce的形状。它显然可以扩展为处理两个以上的功能。而且我希望它比您的可还原集合更快。

【讨论】:

  • 什么是可约集合?或者问一个 non-reducible 集合是什么样子会更好?
  • 一个 reducible 集合是一个知道如何减少自身的集合 - clojure.lang.IReduce 的一个实例,如 the source code for reduce 所示。我认为这就是@AWebb 在他对@Leetwinski's answer 的评论中所指的内容。 repl 告诉我向量和范围是可约的,但列表、集合和映射不是。
  • 太好了,谢谢。那么性能损失从何而来?在我正在使用的示例中,我使用的范围应该很快。在我看来,问题在于我和@Leetwinski 使用的归约函数有点慢。
【解决方案2】:

这就是我要做的:只需将此任务委托给核心 reduce 函数,如下所示:

(defn multi-reduce
  ([fs accs xs] (reduce (fn [accs x] (doall (map #(%1 %2 x) fs accs)))
                        accs xs))
  ([fs xs] (when (seq xs)
             (multi-reduce fs (repeat (count fs) (first xs))
                           (rest xs)))))

在回复中:

user> (multi-reduce [+ * min max] (range 1 10))
(45 362880 1 9)

user> (multi-reduce [+ * min max] [10])
(10 10 10 10)

user> (multi-reduce [+ * min max] [])
nil

user> (multi-reduce [+ * min max] [1 1 1000 0] [])
[1 1 1000 0]

user> (multi-reduce [+ * min max] [1 1 1000 0] [1])
(2 1 1 1)

user> (multi-reduce [+ * min max] [1 1 1000 0] (range 1 10))
(46 362880 1 9)

user> (multi-reduce [max min] (range 1000000))
(999999 0)

【讨论】:

  • 对于足够大的序列,这会导致堆栈溢出错误,例如(multi-reduce [max min] (range 10000)) StackOverflowError clojure.lang.LazySeq.sval (LazySeq.java:40) 然而这绝对是在正确的轨道上——它是接受多个集合的 map 版本有很大帮助.我完全忘记了这一点,这在我自己的解决方案中造成了很大的复杂性。
  • 哦,这个堆栈溢出很容易解决。我的错。那是因为map应该完全实现。
  • 也可以只替换map -> mapv 而不是doall
  • 啊,谢谢。麻烦的是,虽然它比我的解决方案优雅得多,但它的速度大约是我的解决方案的两倍; (time (multi-reduce [max min] (range 1000000))) 在我的机器上大约是 700 毫秒,我的 reduce-multi 大约是 350 米,两者都落后于简单地分别减少两次,大约需要 20 毫秒。在大多数情况下,这可能无关紧要,但如果必须以这种代价来获得这种优雅,那就太可惜了。也许这就是传感器的用武之地。
  • @RobertJohnson 这可以通过使用reducers eager版本来加快速度,例如地图和瞬态如在 OP 中。但是,范围非常有效地减少了自身——编译成一个紧凑的小循环——因此在你的基准测试中减少两次与容器类上的操作相比,一次迭代的每次迭代几乎没有开销。
【解决方案3】:

我会这样做:

(ns clj.core
  (:require [clojure.string :as str] )
  (:use tupelo.core))

(def data   (flatten [ (range 5 10) (range 5) ] ))
(spyx data)

(def result (reduce   (fn [cum-result curr-val]                         ; reducing (accumulator) fn
                        (it-> cum-result 
                              (update it :min-val min curr-val)
                              (update it :max-val max curr-val)))
                      { :min-val (first data) :max-val (first data) }   ; inital value
                      data))                                            ; seq to reduce
(spyx result)
(defn -main [] )

;=> data => (5 6 7 8 9 0 1 2 3 4)
;=> result => {:min-val 0, :max-val 9}

因此归约函数 (fn ...) 通过序列的每个元素携带像 {:min-val xxx :max-val yyy} 这样的映射,并在每个步骤中根据需要更新最小值和最大值。

虽然这确实只传递了一次数据,但它会做大量额外的工作,每个元素调用两次 update。除非您的序列非常不寻常,否则两次(非常有效)通过数据可能更有效,例如:

(def min-val (apply min data))
(def max-val (apply max data))
(spyx min-val)
(spyx max-val)
;=> min-val => 0
;=> max-val => 9

【讨论】:

    猜你喜欢
    • 2021-07-28
    • 1970-01-01
    • 1970-01-01
    • 2021-04-19
    • 2012-02-28
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多