【问题标题】:clojure - (Another one) StackOverflow with loop/recurclojure - (另一个)带有循环/递归的 StackOverflow
【发布时间】:2017-05-27 10:05:23
【问题描述】:

我知道这是一个反复出现的问题(herehere 等),我知道这个问题与创建惰性序列有关,但我不明白为什么它失败了。

问题:我编写了一个(不是很好的)快速排序算法来对使用循环/递归的字符串进行排序。但应用于 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]]}

据我所知,循环/递归执行尾调用优化,因此不使用堆栈(实际上是使用递归语法编写的迭代过程)。

阅读其他答案,并且由于堆栈跟踪,我发现concat 存在问题,并在concat 解决堆栈溢出问题之前添加doall。但是……为什么?

【问题讨论】:

    标签: recursion clojure


    【解决方案1】:

    这是 concat 的二元版本的部分代码。

    (defn concat [x y]
      (lazy-seq
       (let [s (seq x)]
         ,,,))
      )
    

    请注意,它使用了另外两个函数,lazy-seqseqlazy-seq 有点像 lambda,它封装了一些代码但尚未执行。 lazy-seq 块内的代码必须产生某种序列值。当你在lazy-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 ,,,)

    filtermap 等其他惰性运算符可能会出现完全相同的问题。如果您对一个序列确实有很多转换步骤,请考虑使用转换器。

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

    【讨论】:

    【解决方案2】:

    Arne 有一个很好的答案(事实上,我以前从未注意到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。所以,我也将懒惰的filterremove 替换为渴望的版本keep-ifdrop-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
    

    【讨论】:

    • 谢谢艾伦!事实上,tupelo 看起来像一个有趣的库。但我的问题更多是关于“为什么”而不是“如何”
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2011-04-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-06-08
    • 2019-04-28
    相关资源
    最近更新 更多