通过 Clojure 中的集合进行递归的惯用方式

Posted

技术标签:

【中文标题】通过 Clojure 中的集合进行递归的惯用方式【英文标题】:Idiomatic way to recurse through collections in Clojure 【发布时间】:2012-03-02 10:10:37 【问题描述】:

我试图了解 Clojure 中递归遍历由 Clojure 列表(或其他集合类型)表示的树或列表的惯用方式是什么。

我可以编写以下代码来计算平面集合中的元素(忽略它不是尾递归的事实):

(defn length
  ([xs]
     (if (nil? (seq xs))
       0
       (+ 1 (length (rest xs))))))

现在在 Scheme 或 CL 中,所有示例只对列表执行此操作,因此这些语言的惯用基本案例测试将是 (nil? xs)。在 Clojure 中,我们希望这个函数适用于所有集合类型,所以惯用的测试 (nil? (seq xs)),或者可能是 (empty? xs),或者完全不同的东西?

我想考虑的另一种情况是树遍历,即遍历表示树的列表或向量,例如[1 2 [3 4].

例如,计算树中的节点数:

(defn node-count [tree]
  (cond (not (coll? tree)) 1
        (nil? (seq tree)) 0
        :else (+ (node-count (first tree)) (node-count (rest tree)))))

这里我们使用(not (coll? tree)) 来检查原子,而在Scheme/CL 中我们使用atom?。我们还使用(nil? (seq tree)) 来检查空集合。最后我们使用firstrest 将当前树解构到左分支和树的其余部分。

总结一下,以下是 Clojure 中惯用的形式:

(nil? (seq xs)) 测试空集合 (first xs)(rest xs) 挖掘收藏 (not (coll? xs)) 检查原子

【问题讨论】:

【参考方案1】:

我个人喜欢以下通过集合进行递归的方法:

(defn length
  "Calculate the length of a collection or sequence"
  ([coll]
     (if-let [[x & xs] (seq coll)]
       (+ 1 (length xs))
       0)))

特点:

(seq coll) 用于测试集合是否为空(根据 Michal 的最佳答案) if-let with (seq coll) 自动处理 nil 和空集合情况 您可以根据需要使用解构来命名第一个和下一个元素,以便在函数体中使用

请注意,一般情况下,如果可能,最好使用recur 编写递归函数,这样您就可以获得尾递归的好处并且不会冒着炸毁堆栈的风险。所以考虑到这一点,我实际上可能会写这个特定的函数如下:

(defn length
  "Calculate the length of a collection or sequence"
  ([coll]
    (length coll 0))
  ([coll accumulator]
    (if-let [[x & xs] (seq coll)]
      (recur xs (inc accumulator))
      accumulator)))

(length (range 1000000))
=> 1000000

【讨论】:

不错!我想专注于集合递归习语而不涉及尾调用,所以我故意不使用recur @mikera 这对惰性无限序列有效吗? (出于明显的原因,以 map 为例,而不是 length )。我的理解是向量并不懒惰,所以(if-let [[x & xs] (seq coll)] 会爆炸,对吗? (如果是这样,解决方法是什么)? 该技术适用于惰性无限序列,但前提是您不抓住头部。如果您保留对序列开头的引用,则垃圾收集器将无法删除任何内容,并且您迟早会耗尽内存。【参考方案2】:

非空 seqable 的惯用测试是 (seq coll):

(if (seq coll)
  ...
  )

nil? 是不必要的,因为来自seq 的非nil 返回值保证是一个序列,因此既不是nil 也不是false,因此是真实的。

如果要先处理nil的情况,可以将if改为if-notseq改为empty?;后者被实现为seqnot 的组合(这就是为什么写(not (empty? xs)) 不是惯用的,参见empty? 的文档字符串)。

至于first/rest——记住restnext的严格变体很有用,其使用比将rest包装在seq中更惯用。

最后,coll? 检查它的参数是否是 Clojure 持久集合(clojure.lang.IPersistentCollection 的一个实例)。这是否是对“非原子”的适当检查取决于代码是否需要将 Java 数据结构作为非原子处理(通过互操作):例如(coll? (java.util.HashSet.))false(coll? (into-array [])) 也是如此,但您可以同时调用 seq。在新的模块化 contrib 中的 core.incubator 中有一个名为 seqable? 的函数,该函数承诺确定 (seq x) 是否会在给定的 x 上成功。

【讨论】:

感谢您的回答。关于rest/next,所以你说我应该在递归调用中使用(length (next xs)),因为无论如何我都要在集合上调用seq?至于coll?,此时我只对原生Clojure 集合类型感兴趣,所以coll? 应该可以满足我的需求。 不客气。我的主要意思是在rest 的返回值上直接调用seq(例如(if-let [new-xs (seq (rest xs))] ...)),其中的成语肯定是(next xs),而recuring 与rest不要在下一次迭代中对返回值调用seq。对于您的 length 函数,我可能仍会使用 next 尽可能清楚地表明该函数是严格的,但我想说它并没有太大的区别。

以上是关于通过 Clojure 中的集合进行递归的惯用方式的主要内容,如果未能解决你的问题,请参考以下文章

clojure 中的惯用配置管理?

惯用的 Clojure 函数别名

生成和管理后台线程的惯用 Clojure 方式

Clojure 中惯用的字符串旋转

使用传递值运行嵌套循环的惯用方式

用于在随机加权选择之间进行选择的惯用 Clojure