Подтвердить что ты не робот

Почему сокращение дает StackOverflowError в Clojure?

Я пытаюсь объединить Seq Seqs.

Я могу сделать это с помощью apply concat.

user=> (count (apply concat (repeat 3000 (repeat 3000 true))))
9000000

Однако из моих ограниченных знаний я бы предположил, что использование apply заставляет ленивый Seq быть реализованным, и это не кажется правильным для очень больших входов. Я бы предпочел сделать это лениво, если смогу.

Поэтому я подумал, что использование reduce будет выполнять эту работу.

user=> (count (reduce concat (repeat 3000 (repeat 3000 true))))

Но это приводит к

StackOverflowError   clojure.lang.RT.seq (RT.java:484)

Я удивлен, потому что я бы подумал, что семантика reduce будет означать, что это был вызов хвоста рекурсивным.

Два вопроса:

  • Лучший способ сделать это apply?
  • Является ли reduce неприемлемым для больших входов?
4b9b3361

Ответ 1

Используйте apply. Когда аргумент функции ленив, то есть apply.

Позвольте проверить, что с побочным эффектом подсчета на нижележащие подпоследовательности:

(def counter (atom 0))

(def ss (repeatedly 3000 
          (fn [] (repeatedly 3000 
            (fn [] (do (swap! counter inc) true))))))


(def foo (apply concat ss))

so.core=> @counter
0

so.core=> (dorun (take 1 foo))
nil

so.core=> @counter
1

so.core=> (dorun (take 3001 foo))
nil

so.core=> @counter
3001

reduce с большим количеством переполнений concat из-за состава thunk

Ленивые последовательности, такие как созданные concat, реализуются с помощью thunks, отложенных вызовов функций. Когда вы concat результат concat, вы вложили thunk в другой thunk. В вашей функции вложенность занимает 3000 глубин, и, таким образом, стек переполняется, как только запрашивается первый элемент, а 3000 вложенных громкостей разматываются.

so.core=>  (def bar (reduce concat (repeat 3000 (repeat 3000 true))))
#'so.core/bar

so.core=> (first bar)
StackOverflowError   clojure.lang.LazySeq.seq (LazySeq.java:49)

реализация ленивых последовательностей в общем случае развяжет вложенные трюки батут-стиля, когда seq ed и не ударит стек:

so.core=> (loop [lz [1], n 0] 
            (if (< n 3000) (recur (lazy-seq lz) (inc n)) lz))
(1)

Однако, если вы вызываете seq в ленивой последовательности на нереализованной части, реализуя ее...

so.core=> (loop [lz [1], n 0] 
            (if (< n 3000) (recur (lazy-seq (seq lz)) (inc n)) lz))
StackOverflowError   so.core/eval1405/fn--1406 (form-init584039696026177116.clj:1)

so.core=> (pst 3000)
StackOverflowError
        so.core/eval1619/fn--1620 (form-init584039696026177116.clj:2)
        clojure.lang.LazySeq.sval (LazySeq.java:40)
        clojure.lang.LazySeq.seq (LazySeq.java:49)
        clojure.lang.RT.seq (RT.java:484)
        clojure.core/seq (core.clj:133)
        so.core/eval1619/fn--1620 (form-init584039696026177116.clj:2)
        clojure.lang.LazySeq.sval (LazySeq.java:40)
        clojure.lang.LazySeq.seq (LazySeq.java:49)
        clojure.lang.RT.seq (RT.java:484)
        clojure.core/seq (core.clj:133)
        ... (repeatedly)

Затем вы завершаете создание фреймов стека seq. Реализация concat такова. Изучите трассировку стека для вашего StackOverflowError с помощью concat, и вы увидите подобное.

Ответ 2

Я могу предложить способ избежать этой проблемы. Функция reduce здесь не проблема; concat есть.

Взгляните на: https://stuartsierra.com/2015/04/26/clojure-donts-concat

Вместо использования concat используйте into

(count (reduce into (repeat 3000 (repeat 3000 true))))
9000000