在Clojure中通过集合递归的惯用方法

时间:2012-02-09 21:49:47

标签: data-structures recursion clojure

我试图理解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))检查原子

2 个答案:

答案 0 :(得分:10)

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

(if (seq coll)
  ...
  )

nil?是不必要的,因为来自nil的非seq返回值保证为seq,因此nilfalse都不是因此真的。

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

关于first / rest - 记住restnext的严格变体是有用的,使用它比包裹{{更惯用'更惯用1}}在rest

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

答案 1 :(得分:8)

我个人喜欢以下方法来递归集合:

(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和empty collection case
  • 您可以使用解构来命名您在函数体中使用的第一个和下一个元素

请注意,一般情况下,如果可能的话,最好使用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