Clojure: 对嵌套序列进行半扁平化

47

我有一个嵌套向量列表的列表,看起来像这样:

(([1 2]) ([3 4] [5 6]) ([7 8]))

我知道这不是理想的工作方式。我想把它展平成 ([1 2] [3 4] [5 6] [7 8])

flatten 不起作用:它给了我 (1 2 3 4 5 6 7 8)

我该怎么办?我想我需要根据每个列表项的内容而不是项创建一个新的列表,并且从文档中找不到如何执行这一步骤。

5个回答

63

如果你只想把它展开一层,你可以使用concat

(apply concat '(([1 2]) ([3 4] [5 6]) ([7 8])))
=> ([1 2] [3 4] [5 6] [7 8])

28
将一个列表中的多个子列表转化成一个包含所有元素的单一列表,你可以使用nickik所建议的apply concat
不过,通常会有更好的解决方案:在一开始就不要生成列表。例如,假设你有一个名为get-names-for的函数,它接收一个符号并返回一个包含可用于称呼该符号的所有酷炫名称的列表:
(get-names-for '+) => (plus add cross junction)

如果你想获取一些符号列表的所有名称,你可以尝试使用

(map get-names-for '[+ /]) 
=> ((plus add cross junction) (slash divide stroke))

但这会导致你之前遇到的问题。你可以用 apply concat 将它们粘合在一起,但更好的方法是一开始就使用 mapcat 而不是 map

(mapcat get-names-for '[+ /]) 
=> (plus add cross junction slash divide stroke)

1
赞一个 mapcat,它带来了一种比我原先想到的更加优雅的解决方案。非常好知道,谢谢! - jm0
我正在使用 map-indexed,但是没有 mapcat-indexed - Post Self
@PostSelf map-indexed 只是为 map 添加一个参数的简写。不要使用 (apply concat (map-indexed f xs)),而是尝试使用 (mapcat f (range) xs) - amalloy
@amalloy 太棒了!我意识到我根据每对的第二个元素进行filter,所以除非有一种方法可以将其合并,否则我必须保持原样。 - Post Self
@PostSelf 当然有!mapcat 是一种“基本”的列表操作,许多其他操作都是基于它构建的。你可以使用 (mapcat (fn [i x] (let [x' (f x)] (when (pred x') [i x']))) (range) xs) 代替 (apply concat (filter (comp pred second) (map-indexed f xs))) - amalloy
mapcat 是最有价值的球员。 - Alper

9
flatten 的代码非常简短:
(defn flatten
  [x]
  (filter (complement sequential?)
    (rest (tree-seq sequential? seq x))))

它使用tree-seq来遍历数据结构并返回原子序列。由于我们想要所有底层序列,因此我们可以像这样进行修改:

(defn almost-flatten
  [x]
  (filter #(and (sequential? %) (not-any? sequential? %))
    (rest (tree-seq #(and (sequential? %) (some sequential? %)) seq x))))

因此,我们返回不包含序列的所有序列。


(def balls '((:a 1) ((:b 1) (:c 1))))不完全正确: (apply concat balls) => (:a 1 (:b 1) (:c 1))完美的: (almost-flatten balls) => ((:a 1) (:b 1) (:c 1)) - Siraj K

4

你可能会发现这个通用的一级展平函数很有用,我在clojuremvc上找到了它:

(defn flatten-1 
  "Flattens only the first level of a given sequence, e.g. [[1 2][3]] becomes
   [1 2 3], but [[1 [2]] [3]] becomes [1 [2] 3]."
  [seq]
  (if (or (not (seqable? seq)) (nil? seq))
    seq ; if seq is nil or not a sequence, don't do anything
    (loop [acc [] [elt & others] seq]
      (if (nil? elt) acc
        (recur
          (if (seqable? elt)
            (apply conj acc elt) ; if elt is a sequence, add each element of elt
            (conj acc elt))      ; if elt is not a sequence, add elt itself 
       others)))))

例子:

(flatten-1 (([1 2]) ([3 4] [5 6]) ([7 8])))
=>[[1 2] [3 4] [5 6] [7 8]]

concat 的示例肯定可以为您完成任务,但是这个 flatten-1 也允许集合中包含非序列元素:

(flatten-1 '(1 2 ([3 4] [5 6]) ([7 8])))
=>[1 2 [3 4] [5 6] [7 8]]
;whereas 
(apply concat '(1 2 ([3 4] [5 6]) ([7 8])))
=> java.lang.IllegalArgumentException: 
   Don't know how to create ISeq from: java.lang.Integer

3
这里有一个函数,可以将不均匀嵌套的内容压缩到序列级别:
(fn flt [s] (mapcat #(if (every? coll? %) (flt %) (list %)) s))

所以,如果你的原始序列是:

'(([1 2]) (([3 4]) ((([5 6])))) ([7 8]))

您仍将获得相同的结果:

([1 2] [3 4] [5 6] [7 8])

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接