我知道这是一个重复的问题(here,here等等),我知道这个问题与创建延迟序列有关,但我可以不知道它失败的原因。
问题:我编写了一个(不是很好的)快速排序算法来排序使用循环/重复的字符串。但是应用于10000个元素,我得到一个StackOverflowError:
(defn qsort [list]
(loop [[current & todo :as all] [list] sorted []]
(cond
(nil? current) sorted
(or (nil? (seq current)) (= (count current) 1)) (recur todo (concat sorted current))
:else (let [[pivot & rest] current
pred #(> (compare pivot %) 0)
lt (filter pred rest)
gte (remove pred rest)
work (list* lt [pivot] gte todo)]
(recur work sorted)))))
我用这种方式:
(defn tlfnum [] (str/join (repeatedly 10 #(rand-int 10))))
(defn tlfbook [n] (repeatedly n #(tlfnum)))
(time (count (qsort (tlfbook 10000))))
这是堆栈跟踪的一部分:
[clojure.lang.LazySeq seq "LazySeq.java" 49]
[clojure.lang.RT seq "RT.java" 521]
[clojure.core$seq__4357 invokeStatic "core.clj" 137]
[clojure.core$concat$fn__4446 invoke "core.clj" 706]
[clojure.lang.LazySeq sval "LazySeq.java" 40]
[clojure.lang.LazySeq seq "LazySeq.java" 49]
[clojure.lang.RT seq "RT.java" 521]
[clojure.core$seq__4357 invokeStatic "core.clj" 137]]}
据我所知,loop / recur执行尾调用优化,因此不使用堆栈(实际上是使用递归语法编写的迭代过程)。
阅读其他答案,并且由于堆栈跟踪,我发现concat
存在问题,并在doall
之前添加concat
解决了堆栈溢出问题。但是......为什么?
答案 0 :(得分:13)
以下是concat的两个版本的代码的一部分。
(defn concat [x y]
(lazy-seq
(let [s (seq x)]
,,,))
)
请注意,它使用了另外两个函数lazy-seq
和seq
。 lazy-seq
有点像lambda,它包装了一些代码而没有执行它。 lazy-seq
块内的代码必须产生某种序列值。当你在lazy-seq
上调用任何序列操作时,它将首先评估代码("实现"懒惰的seq),然后对结果执行操作。
(def lz (lazy-seq
(println "Realizing!")
'(1 2 3)))
(first lz)
;; prints "realizing"
;; => 1
现在试试这个:
(defn lazy-conj [xs x]
(lazy-seq
(println "Realizing" x)
(conj (seq xs) x)))
请注意,它与concat
类似,在第一个参数上调用seq
,并返回lazy-seq
(def up-to-hundred
(reduce lazy-conj () (range 100)))
(first up-to-hundred)
;; prints "Realizing 99"
;; prints "Realizing 98"
;; prints "Realizing 97"
;; ...
;; => 99
即使您只询问了第一个元素,它仍然最终实现了整个序列。这是因为实现了外层""导致在下一个"层"上调用seq
,这实现了另一个lazy-seq,它再次调用seq等。所以它是一个实现一切的连锁反应,每一步消耗堆栈框架。
(def up-to-ten-thousand
(reduce lazy-conj () (range 10000)))
(first up-to-ten-thousand)
;;=> java.lang.StackOverflowError
堆叠concat
来电时遇到同样的问题。这就是为什么例如(reduce concat ,,,)
总是气味,而你可以使用(apply concat ,,,)
或(into () cat ,,,)
。
filter
和map
等其他懒惰运算符可能会出现完全相同的问题。如果您确实在序列上有很多转换步骤,请考虑使用传感器。
;; without transducers: many intermediate lazy seqs and deep call stacks
(->> my-seq
(map foo)
(filter bar)
(map baz)
,,,)
;; with transducers: seq processed in a single pass
(sequence (comp
(map foo)
(filter bar)
(map baz))
my-seq)
答案 1 :(得分:-1)
cat
!)。如果您想要更简单的解决方案,可以使用glue
函数from the Tupelo library:
concat函数有时会产生相当令人惊讶的结果:
(concat {:a 1} {:b 2} {:c 3} )
;=> ( [:a 1] [:b 2] [:c 3] )
在此示例中,用户可能打算将3个地图合并为一个。相反,这三个地图被神秘地转换为长度为2的向量,然后嵌套在另一个序列中。
conj功能也可以让用户感到惊讶:
(conj [1 2] [3 4] )
;=> [1 2 [3 4] ]
此处用户可能希望返回[1 2 3 4]
,但错误地获得了嵌套向量。
我们不需要想知道要合并的项目是否会被合并,嵌套或转换为另一种数据类型,我们提供粘合函数来始终将类似的集合组合到一个相同类型的结果集合中:
; Glue together like collections:
(is (= (glue [ 1 2] '(3 4) [ 5 6] ) [ 1 2 3 4 5 6 ] )) ; all sequential (vectors & lists)
(is (= (glue {:a 1} {:b 2} {:c 3} ) {:a 1 :c 3 :b 2} )) ; all maps
(is (= (glue #{1 2} #{3 4} #{6 5} ) #{ 1 2 6 5 3 4 } )) ; all sets
(is (= (glue "I" " like " \a " nap!" ) "I like a nap!" )) ; all text (strings & chars)
; If you want to convert to a sorted set or map, just put an empty one first:
(is (= (glue (sorted-map) {:a 1} {:b 2} {:c 3}) {:a 1 :b 2 :c 3} ))
(is (= (glue (sorted-set) #{1 2} #{3 4} #{6 5}) #{ 1 2 3 4 5 6 } ))
如果要“粘合”的集合不是全部相同类型,则会抛出异常。允许的输入类型是:
我将glue
放入您的代码而不是concat
,但仍然有一个StackOverflowError。因此,我还将懒惰的filter
和remove
替换为热切版本keep-if
和drop-if
以获得此结果:
(defn qsort [list]
(loop [[current & todo :as all] [list] sorted []]
(cond
(nil? current) sorted
(or (nil? (seq current)) (= (count current) 1))
(recur todo (glue sorted current))
:else (let [[pivot & rest] current
pred #(> (compare pivot %) 0)
lt (keep-if pred rest)
gte (drop-if pred rest)
work (list* lt [pivot] gte todo)]
(recur work sorted)))))
(defn tlfnum [] (str/join (repeatedly 10 #(rand-int 10))))
(defn tlfbook [n] (repeatedly n #(tlfnum)))
(def result
(time (count (qsort (tlfbook 10000)))))
-------------------------------------
Clojure 1.8.0 Java 1.8.0_111
-------------------------------------
"Elapsed time: 1377.321118 msecs"
result => 10000