这几天试着用 PigPen 写一些脚本来处理一些数据。过程中遇到一个问题,感觉永别的语言写起来一定很简单,但是由于用 Clojure 写代码的经验少,一时不知道怎么用 Clojure 的方法来实现。
为了方便讲清楚问题,先对问题进行简化:
有一个数组 [1 2 3 7 8 9 13 13 14],我想计算相邻两个元素的差,如果大于3,就把之前的元素拿出来作为一个分组,最后返回一个包含所有分组的数组。如刚才那个数组返回的结果希望是 [[1 2 3] [7 8 9] [13 13 14]]。
如果用 Lua,实现很简单:
function my_partition(arr)
local grps, cur_grp, last_idx = {}, {}, 1
for idx,item in ipairs(arr) do
if item - arr[last_idx] > 3 then
table.insert(grps, cur_grp)
cur_grp = {}
table.insert(cur_grp, item)
else
table.insert(cur_grp, item)
end
last_idx = idx
end
table.insert(grps, cur_grp)
return grps
end
但是如果用 Clojure 来实现,感觉不太适合定义像 grps
,cur_grp
,last_idx
这样的状态变量,因为 Clojure 中很多数据结构都是 immutable 的,是尽量避免状态变量的。而且刚开始写 Clojure 就带着坏习惯,写惯了之后就很难改过来了。所有宁可刚开始慢一点,也尽量避免坏习惯。所以就向 cn-clojure邮件组 发了一封邮件问这个问题。
<!--more-->
最先回复的是 Dennis,给出了两个方案:
(let [prev (atom 0)]
(partition-by #(let [v @prev]
(reset! prev %)
(> (- % v) 3))
[1 2 3 7 8 9 13 13 14]))
这个方案中还是用到了一个状态变量 prev
,还有另一个方案:
(defn my-partition
([coll]
(my-partition [] coll))
([xs coll]
(lazy-seq
(when-let [s (seq coll)]
(let [fst (first s)
sec (second s)]
(if (or (nil? sec) (> (- sec fst) 3))
(cons (conj xs fst) (my-partition [] (next s)))
(my-partition (conj xs fst) (next s))))))))
这是个 lazy-seq
的方法,利用类似尾递归的方法返回 lazy 的分组,这种方法和 clojur.core 里的 partition-by
方法的实现类似,也是我看了 partition-by
的实现后想到的一种方法,但是自己没写下来。
还有其他一些同学的答案:
(reduce (fn [result i]
(let [prev (-> result last last)]
(if (and prev (<= (- i prev) 3))
(conj (pop result) (conj (peek result) i))
(conj result [i]))))
[]
[1 2 3 7 8 9 13 13 14])
还有:
(let [x [1 8 3 6 10 14 11 15]]
(->> (partition 2 1 x)
(map #(- (second %) (first %)))
(map (partial < 3))
(interleave x )
(apply vector)
(#(conj % (last x)))
(partition-by #(or (number? %) (false? %)))
(map #(filter number? %))
(filter seq)))
还有:
(defn f [coll]
(->> (conj coll (last coll))
(partition 2 1)
(keep-indexed #(if (> (- (second %2) (first %2)) 3) %1))
(cons -1)
(partition 2 1)
(map #(- (second %) (first %)))
(reduce #(concat (butlast %1) (split-at %2 (last %1))) [coll])))
每个看完后都感觉受益匪浅。