Select 个嵌套结构中与 Clojure 中的条件匹配的元素

Select elements from a nested structure that match a condition in Clojure

我最近发现了 Specter 库,它提供数据结构导航和转换功能,是用 Clojure 编写的。

将它的一些 API 作为学习练习来实施似乎是个好主意。 Spectre 实现了一个 API 将一个函数和一个嵌套结构作为参数,并且 return 是一个来自满足如下函数的嵌套结构的元素向量:

(select (walker number?) [1 :a {:b 2}]) => [1 2]

下面是我尝试实现一个类似 API:

的函数
(defn select-walker [afn ds]
  (vec (if (and (coll? ds) (not-empty ds))
         (concat (select-walker afn (first ds)) 
                 (select-walker afn (rest ds)))
         (if (afn ds) [ds]))))

(select-walker number? [1 :a {:b 2}]) => [1 2]

我尝试使用 list comprehension, looping, and using cons and conj 实现 select-walker。在所有这些情况下 return 值是嵌套列表而不是元素的平面向量。

然而我的实现看起来不像惯用的 Clojure,而且时间和 space 复杂度都很低。

(time (dotimes [_ 1000] (select (walker number?) (range 100))))
"Elapsed time: 19.445396 msecs"

(time (dotimes [_ 1000] (select-walker number? (range 100))))
"Elapsed time: 237.000334 msecs"

请注意,我的实现比 Spectre 的实现慢了大约 12 倍。

我对 select-walker 的实现有三个问题。

  1. select-walker 的尾递归实现是否可行?
  2. select-walker 可以用更地道的 Clojure 编写吗?
  3. 有什么提示可以使 select-walker 执行得更快?
  1. 至少有两种可能性让它成为尾递归。第一个是像这样循环处理数据:

    (defn select-walker-rec [afn ds]
      (loop [res [] ds ds]
        (cond (empty? ds) res
              (coll? (first ds)) (recur res 
                                        (doall (concat (first ds) 
                                                       (rest ds))))
              (afn (first ds)) (recur (conj res (first ds)) (rest ds))
              :else (recur res (rest ds)))))
    

    回复:

    user> (select-walker-rec number? [1 :a {:b 2}])
    [1 2]
    
    user> user> (time (dotimes [_ 1000] (select-walker-rec number? (range 100))))
    "Elapsed time: 19.428887 msecs"
    

    (简单的 select-walker 对我来说工作大约 200 毫秒)

    第二个(虽然速度较慢,但​​更适合更困难的任务)是使用 zippers:

    (require '[clojure.zip :as z])
    
    (defn select-walker-z [afn ds]
      (loop [res [] curr (z/zipper coll? seq nil ds)]
        (cond (z/end? curr) res
              (z/branch? curr) (recur res (z/next curr))
              (afn (z/node curr)) (recur (conj res (z/node curr))
                                         (z/next curr))
              :else (recur res (z/next curr)))))
    
    user> (time (dotimes [_ 1000] (select-walker-z number? (range 100))))
    "Elapsed time: 219.015153 msecs"
    

    这个真的很慢,因为 zipper 在更复杂的结构上运行。它的强大功能给这个简单的任务带来了不必要的开销。

  2. 我想最惯用的方法是使用 tree-seq:

    (defn select-walker-t [afn ds]
      (filter #(and (not (coll? %)) (afn %))
              (tree-seq coll? seq ds)))
    
    user> (time (dotimes [_ 1000] (select-walker-t number? (range 100))))
    "Elapsed time: 1.320209 msecs"
    

    它的速度快得令人难以置信,因为它会产生一系列惰性结果。事实上你应该了解它的公平测试数据:

    user> (time (dotimes [_ 1000] (doall (select-walker-t number? (range 100)))))
    "Elapsed time: 53.641014 msecs"
    

    关于这个变体还有一点需要注意,它不是尾递归的,所以在嵌套非常深的结构的情况下它会失败(也许我错了,但我猜它大约有几千层嵌套), 仍然适用于大多数情况。