Я знаю, что ленивые последовательности оценивают только элементы в запрашиваемой последовательности, как это происходит?
Я думаю, что ранее опубликованные ответы уже помогли объяснить эту часть. Я только добавлю, что «форсирование» ленивой последовательности неявно - без паренов! :-) - вызов функции; возможно, такой образ мыслей прояснит некоторые вещи. Также обратите внимание, что принудительное выполнение ленивой последовательности включает в себя скрытую мутацию - принудительный поток должен создать значение, сохранить его в кэше (мутация!) И выбросить свой исполняемый код, который больше не потребуется (мутация снова!) .
Я знаю, что ленивые последовательности оценивают только те элементы в запрашиваемой последовательности, как это происходит?
Что делает ленивые последовательности настолько эффективными, что они не потребляют много стека?
Какие ресурсы потребляют ленивые последовательности, чтобы делать то, что делает?
Они не потребляют стек, потому что вместо этого они потребляют кучу. Ленивая последовательность - это структура данных, находящаяся в куче, которая содержит небольшой кусочек исполняемого кода, который можно вызывать для создания большей структуры данных, если / когда это требуется.
Почему вы можете обернуть рекурсивные вызовы в ленивую последовательность и больше не получать переполнение стека для больших вычислений?
Во-первых, как упомянул dbyrne, вы можете очень хорошо получить SO при работе с ленивыми последовательностями, если самим thunks нужно выполнить код с очень глубоко вложенной структурой вызовов.
Однако, в определенном смысле вы можете использовать ленивые seqs вместо хвостовой рекурсии, и в той степени, в которой это работает для вас, вы можете сказать, что они помогают избежать SO. Фактически, довольно важно, что функции, создающие ленивые последовательности , не должны быть хвостовой рекурсивной; сохранение стекового пространства у ленивых производителей seq происходит из вышеупомянутой передачи стека -> кучи, и любые попытки записать их в хвостовой рекурсивной манере только сломают вещи.
Ключевое понимание заключается в том, что ленивая последовательность - это объект, который при первом создании не содержит никаких элементов (как всегда делает строгая последовательность); когда функция возвращает ленивую последовательность, только этот «ленивый объект последовательности» возвращается вызывающей стороне, прежде чем произойдет какое-либо форсирование. Таким образом, кадр стека, используемый вызовом, который возвратил ленивую последовательность, выталкивается до того, как произойдет любое форсирование. Давайте посмотрим на пример функции производителя:
(defn foo-producer [] ; not tail recursive...
(lazy-seq
(cons :foo ; because it returns the value of the cons call...
(foo-producer)))) ; which wraps a non-tail self-call
Это работает, потому что lazy-seq
возвращает немедленно , таким образом, (cons :foo (foo-producer))
также немедленно возвращается, и кадр стека, используемый внешним вызовом foo-producer
, немедленно извлекается. Внутренний вызов foo-producer
скрыт в части rest
последовательности, которая является thunk; если / когда этот thunk форсирован, он ненадолго израсходует свой собственный кадр в стеке, но затем немедленно вернется, как описано выше и т. д.
Chunking (упомянутый dbyrne) очень немного меняет эту картину, потому что на каждом шаге создается большее количество элементов, но принцип остается тем же: каждый шаг израсходовал некоторый стек, когда соответствующие элементы lazy seq выполняются производится, затем этот стек восстанавливается до того, как произойдет больше форсирования.
В каких случаях ленивые последовательности неэффективны?
В каких случаях ленивые последовательности наиболее эффективны?
Нет смысла быть ленивым, если вам все равно нужно держать все это сразу. Ленивая последовательность делает выделение кучи на каждом шаге, когда он не разделен на части, или на каждом фрагменте - один раз каждые 32 шага - когда разделен на части; избегая этого, вы можете получить прирост производительности в некоторых ситуациях.
Однако ленивые последовательности включают конвейерный режим обработки данных:
(->> (lazy-seq-producer) ; possibly (->> (range)
(a-lazy-seq-transformer-function) ; (filter even?)
(another-transformer-function)) ; (map inc))
Строгое выполнение этого в любом случае выделит кучу кучи, поскольку вам нужно будет сохранять промежуточные результаты, чтобы передать их на следующий этап обработки. Более того, вам нужно сохранить все это, что на самом деле невозможно в случае (range)
- бесконечная последовательность! - и когда это возможно, это обычно неэффективно.