通过 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))
来检查空集合。最后我们使用first
和rest
将当前树解构到左分支和树的其余部分。
总结一下,以下是 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-not
或seq
改为empty?
;后者被实现为seq
和not
的组合(这就是为什么写(not (empty? xs))
不是惯用的,参见empty?
的文档字符串)。
至于first
/rest
——记住rest
、next
的严格变体很有用,其使用比将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)
,而recur
ing 与rest
不要在下一次迭代中对返回值调用seq
。对于您的 length
函数,我可能仍会使用 next
尽可能清楚地表明该函数是严格的,但我想说它并没有太大的区别。以上是关于通过 Clojure 中的集合进行递归的惯用方式的主要内容,如果未能解决你的问题,请参考以下文章