简体   繁体   English

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

[英]Idiomatic way to recurse through collections in Clojure

I'm trying to understand what is the idiomatic way in Clojure to recurse through a tree or a list represented by a Clojure list (or another collection type). 我试图理解在Clojure中通过树或由Clojure列表(或其他集合类型)表示的列表进行递归的惯用方法。

I could write the following to count the elements in a flat collection (ignore the fact that it's not tail-recursive): 我可以编写以下内容来计算平面集合中的元素(忽略它不是尾递归的事实):

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

Now in Scheme or CL all the examples only ever do this over lists, so the idiomatic base case test in those languages would be (nil? xs) . 现在在Scheme或CL中,所有示例都只在列表上执行此操作,因此这些语言中的惯用基本案例测试将是(nil? xs) In Clojure we'd like this function to work on all collection types, so is the idiomatic test (nil? (seq xs)) , or maybe (empty? xs) , or something completely different? 在Clojure中,我们希望这个函数适用于所有集合类型,惯用测试(nil? (seq xs)) ,或者可能(empty? xs) ,还是完全不同的东西?

The other case I'd like to consider is tree traversal, ie traversing through a list or vector that represents a tree, eg [1 2 [3 4] . 我要考虑的另一种情况是树遍历,即遍历表示树的列表或向量,例如[1 2 [3 4]

For example, counting the nodes in a tree: 例如,计算树中的节点:

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

Here we use (not (coll? tree)) to check for atoms, whereas in Scheme/CL we'd use atom? 这里我们使用(not (coll? tree))来检查原子,而在Scheme / CL中我们使用atom? . We also use (nil? (seq tree)) to check for an empty collection. 我们还使用(nil? (seq tree))来检查空集合。 And finally we use first and rest to destructure the current tree to the left branch and the rest of the tree. 最后我们first使用rest来将当前树解构为左分支和树的其余部分。

So to summarise, are the following forms idiomatic in Clojure: 总而言之,Clojure中的以下形式是惯用的:

  • (nil? (seq xs)) to test for the empty collection (nil? (seq xs))测试空集合
  • (first xs) and (rest xs) to dig into the collection (first xs)(rest xs)深入挖掘集合
  • (not (coll? xs)) to check for atoms (not (coll? xs))检查原子

The idiomatic test for a non-empty seqable is (seq coll) : 非空seqable的惯用测试是(seq coll)

(if (seq coll)
  ...
  )

The nil? nil? is unnecessary, since a non- nil return value from seq is guaranteed to be a seq and thus neither nil nor false and therefore truthy. 是不必要的,因为非nil从返回值seq保证是一个SEQ并且因此既不nil也不false因此和truthy。

If you want to deal with the nil case first, you can change the if to if-not or seq to empty? 如果你想先处理nil情况,你可以将if改为if-notseqempty? ; ; the latter is implemented as a composition of seq with not (which is why it is not idiomatic to write (not (empty? xs)) , cf. the docstring of empty? ). 后者被实现为组合物seqnot (这就是为什么它不是惯用写(not (empty? xs))参见的文档字符串empty? )。

As for first / rest -- it's useful to remember about the strict variant of rest , next , the use of which is more idiomatic than wrapping rest in a seq . 至于first / rest - 记住rest的严格变体是有用的, next ,使用它比在seq包裹rest更惯用。

Finally, coll? coll? checks if its argument is a Clojure persistent collection (an instance of clojure.lang.IPersistentCollection ). 检查它的参数是否是Clojure持久集合( clojure.lang.IPersistentCollection一个实例)。 Whether this is an appropriate check for "non-atoms" depends on whether the code needs to handle Java data structures as non-atoms (via interop): eg (coll? (java.util.HashSet.)) is false , as is (coll? (into-array [])) , but you can call seq on both. 这是否是对“非原子”的适当检查取决于代码是否需要将Java数据结构作为非原子处理(通过interop):例如(coll? (java.util.HashSet.))false ,因为(coll? (into-array [])) ,但你可以在两者上调用seq There is a function called seqable? 有一个叫做seqable?的函数seqable? in core.incubator in the new modular contrib which promises to determine whether (seq x) would succeed for a given x . 在新模块contrib中的core.incubator中,它承诺确定(seq x)是否会对给定的x成功。

I personally like the following approach to recurse through a collection: 我个人喜欢以下方法来通过集合进行递归:

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

Features: 特征:

  • (seq coll) is idiomatic for testing whether a collection is empty (as per Michal's great answer) (seq coll)是用于测试集合是否为空的惯用语(根据Michal的好答案)
  • if-let with (seq coll) automatically handles both the nil and empty collection case if-let with(seq coll)自动处理nil和empty collection case
  • You can use destructuring to name the first and next elements as you like for use in your function body 您可以使用解构来命名第一个和下一个元素,以便在函数体中使用

Note that in general it is better to write recursive functions using recur if possible, so that you get the benefits of tail recursion and don't risk blowing up the stack. 请注意,一般情况下,如果可能的话,最好使用recur编写递归函数,这样您就可以获得尾递归的好处,并且不会冒着炸毁堆栈的风险。 So with this in mind, I'd actually probably write this specific function as follows: 因此,考虑到这一点,我实际上可能会编写以下特定函数:

(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

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM