从嵌套结构中选择与Clojure中的条件匹配的元素

时间:2016-07-26 19:17:08

标签: clojure specter

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

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

(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 comprehensionlooping以及使用consconj来实施select-walker。在所有这些情况下 返回值是嵌套列表,而不是元素的平面向量。

然而,我的实现似乎不是惯用的Clojure,并且时间和空间复杂性很差。

(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. 可以用更惯用的Clojure写出select-walker吗?
  3. 使select-walker执行得更快的任何提示?

1 个答案:

答案 0 :(得分:2)

  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)))))
    

    在repl中:

    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"
    

    (简单的选择步行者为我工作约200ms)

    第二个(虽然更慢,更适合更困难的任务)是使用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"
    

    这个非常慢,因为拉链在更复杂的结构上运行。它的强大功能为这项简单的任务带来了不必要的开销。

  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"
    
    关于这个变体还有一点需要注意的是,它不是尾递归的,所以在真正深度嵌套的结构的情况下它会失败(也许我错了,但我想这是几千级嵌套),仍然适合大多数情况。