簡體   English   中英

clojure - (另一個)StackOverflow with loop / recur

[英]clojure - (Another one) StackOverflow with loop/recur

我知道這是一個反復出現的問題( 這里這里 ,以及更多),我知道這個問題與創建延遲序列有關,但我不明白它為什么會失敗。

問題:我編寫了一個(不是很好的)quicksort算法來排序使用loop / recur的字符串。 但是應用於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解決堆棧溢出問題之前, concat和添加doall問題。 但為什么?

這是concat的兩個版本的代碼的一部分。

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

請注意,它使用了另外兩個函數, lazy-seqseq lazy-seq有點像lambda,它包裝了一些代碼而沒有執行它。 lazy-seq塊內的代碼必須產生某種序列值。 當你在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)

Arne有一個很好的答案(事實上,我之前從未注意過cat !)。 如果您想要一個更簡單的解決方案,可以使用Tupelo庫中glue函數:


像集合一樣粘在一起

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了eager版本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

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM