关于分区:按过滤器进行Clojure分区

Clojure partition by filter

在Scala中,partition方法将一个序列分为两个单独的序列-谓词为true的序列和为false的序列:

1
2
scala> List(1, 5, 2, 4, 6, 3, 7, 9, 0, 8).partition(_ % 2 == 0)
res1: (List[Int], List[Int]) = (List(2, 4, 6, 0, 8),List(1, 5, 3, 7, 9))

注意,Scala实现仅遍历该序列一次。

在Clojure中,partition-by函数将序列分为多个子序列,每个子序列满足或不满足谓词的最长子集:

1
2
user=> (partition-by #(= 0 (rem % 2)) [1, 5, 2, 4, 6, 3, 7, 9, 0, 8])
((1 5) (2 4 6) (3 7 9) (0 8))

split-by产生:

1
2
user=> (split-with #(= 0 (rem % 2)) [1, 5, 2, 4, 6, 3, 7, 9, 0, 8])
[() (1 5 2 4 6 3 7 9 0 8)]

是否有内置的Clojure函数与Scala partition方法具有相同的功能?


我相信您正在寻找的功能是clojure.core / group-by。它返回键映射到原始序列中的项目列表,分组功能为其返回该键。如果您使用产生真/假的谓词,则会得到您要查找的拆分。

1
2
user=> (group-by even? [1, 5, 2, 4, 6, 3, 7, 9, 0, 8])
{false [1 5 3 7 9], true [2 4 6 0 8]}

如果看一下实现,它将满足您的要求,即仅使用一次通过。另外,它使用引擎盖下的瞬变信号,因此它应该比到目前为止发布的其他解决方案更快。一个警告是,您应该确定分组功能正在生成的键。如果生成的是nil而不是false,则您的地图将在nil键下列出失败的项目。如果您的分组函数产生非零值而不是true,则可能在多个键下列出了传递的值。这不是一个大问题,只需注意您需要为分组函数使用一个true / false产生谓词。

关于group-by的好处是,它比将序列分为通过和失败的项目更为通用。您可以轻松地使用此功能将序列分为所需的多个类别。非常有用且灵活。这可能就是为什么group-by位于clojure.core中而不是separate中的原因。


clojure.contrib.seq-utils的一部分:

1
2
3
4
user> (use '[clojure.contrib.seq-utils :only [separate]])
nil                                                                                                                                                        
user> (separate even? [1, 5, 2, 4, 6, 3, 7, 9, 0, 8])
[(2 4 6 0 8) (1 5 3 7 9)]


请注意,Jürgen,Adrian和Mikera的答案都遍历了输入序列两次。

1
2
3
4
5
6
7
8
(defn single-pass-separate
  [pred coll]
  (reduce (fn [[yes no] item]
            (if (pred item)
              [(conj yes item) no]
              [yes (conj no item)]))
          [[] []]
          coll))

单次通过只能是渴望。懒惰必须经过两遍,再加上微弱地抓住头部。

编辑:lazy-single-pass-separate是可能的,但很难理解。实际上,我认为这要比简单的第二遍慢。但是我还没有检查。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
(defn lazy-single-pass-separate
  [pred coll]
  (let [coll       (atom coll)
        yes        (atom clojure.lang.PersistentQueue/EMPTY)
        no         (atom clojure.lang.PersistentQueue/EMPTY)
        fill-queue (fn [q]
                     (while (zero? (count @q))
                       (locking coll
                         (when (zero? (count @q))
                           (when-let [s (seq @coll)]
                             (let [fst (first s)]
                               (if (pred fst)
                                 (swap! yes conj fst)
                                 (swap! no conj fst))
                               (swap! coll rest)))))))
        queue      (fn queue [q]
                     (lazy-seq
                       (fill-queue q)
                       (when (pos? (count @q))
                         (let [item (peek @q)]
                           (swap! q pop)
                           (cons item (queue q))))))]
    [(queue yes) (queue no)]))

这很懒,您可以获得:

1
2
3
4
5
6
7
8
9
10
11
12
user=> (let [[y n] (lazy-single-pass-separate even? (report-seq))] (def yes y) (def no n))
#'user/no
user=> (first yes)
">0<"
0
user=> (second no)
">1<"
">2<"
">3<"
3
user=> (second yes)
2

综上所述,我会说"渴望"或"两次通过"。


也许请参见https://github.com/amalloy/clojure-useful/blob/master/src/useful.clj#L50-是否两次遍历序列取决于您"遍历序列"的含义。

编辑:现在我不在手机上,我想链接而不是粘贴是愚蠢的:

1
2
3
4
5
6
7
(defn separate
  [pred coll]
  (let [coll (map (fn [x]
                    [x (pred x)])
                  coll)]
    (vec (map #(map first (% second coll))
              [filter remove]))))

编写一些有用的技巧并不难:

1
2
3
4
5
6
7
8
9
(defn partition-2 [pred coll]
  ((juxt
    (partial filter pred)
    (partial filter (complement pred)))
  coll))

(partition-2 even? (range 10))

=> [(0 2 4 6 8) (1 3 5 7 9)]