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
的实现有三个问题。
select-walker
的尾递归实现是否可行?
select-walker
可以用更地道的 Clojure 编写吗?
- 有什么提示可以使
select-walker
执行得更快?
至少有两种可能性让它成为尾递归。第一个是像这样循环处理数据:
(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 在更复杂的结构上运行。它的强大功能给这个简单的任务带来了不必要的开销。
我想最惯用的方法是使用 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"
关于这个变体还有一点需要注意,它不是尾递归的,所以在嵌套非常深的结构的情况下它会失败(也许我错了,但我猜它大约有几千层嵌套), 仍然适用于大多数情况。
我最近发现了 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
的实现有三个问题。
select-walker
的尾递归实现是否可行?select-walker
可以用更地道的 Clojure 编写吗?- 有什么提示可以使
select-walker
执行得更快?
至少有两种可能性让它成为尾递归。第一个是像这样循环处理数据:
(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 在更复杂的结构上运行。它的强大功能给这个简单的任务带来了不必要的开销。
我想最惯用的方法是使用
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"
关于这个变体还有一点需要注意,它不是尾递归的,所以在嵌套非常深的结构的情况下它会失败(也许我错了,但我猜它大约有几千层嵌套), 仍然适用于大多数情况。