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

Clojure cons vs conj with lazy-seq

Почему cons работает в этом контексте с lazy-seq, но conj не?

Это работает:

(defn compound-interest [p i]
   (cons p (lazy-seq (compound-interest (* p (+ 1 i)) i))))

Это не делает (это исключает переполнение стека [1]):

(defn compound-interest2 [p i]
   (conj (lazy-seq (compound-interest2 (* p (+ 1 i)) i)) p))

[1] О, я! Задание вопроса о переполнении стека в stackoverflow.

4b9b3361

Ответ 1

(conj collection item) добавляет item в collection. Для этого нужно реализовать collection. (Я объясню, почему ниже.) Таким образом, рекурсивный вызов происходит немедленно, а не откладывается.

(cons item collection) создает последовательность, начинающуюся с item, а затем все в collection. Примечательно, что ему не нужно понимать collection. Таким образом, рекурсивный вызов будет отложен (из-за использования lazy-seq), пока кто-то не попытается получить хвост результирующей последовательности.

Я объясню, как это работает внутри:

cons фактически возвращает объект clojure.lang.Cons, из которого сделаны ленивые последовательности. conj возвращает тот же тип коллекции, который вы передаете (будь то список, вектор или что-то еще). conj делает это с использованием полиморфного вызова метода Java в самой коллекции. (См. строка 524 из clojure/src/jvm/clojure/lang/RT.java.)

Что происходит, когда вызов метода Java происходит в объекте clojure.lang.LazySeq, который возвращается lazy-seq? (Как cons и LazySeq объекты работают вместе, чтобы сформировать ленивые последовательности станут яснее ниже.) Посмотрите строка 98 из clojure/src/jvm/clojure/lang/LazySeq.java. Обратите внимание, что он вызывает метод с именем seq. Это то, что реализует значение LazySeq (переход к ISeq.

Обратите внимание, что cons объекты в Clojure отличаются от "cons cells" в других Lisps - в большинстве Lisps "cons" - это просто объект, который содержит 2 указателя на другие произвольные объекты. Таким образом, вы можете использовать cons-клетки для создания деревьев и т.д. A Clojure cons принимает произвольную Object как head, а ISeq - как хвост. Поскольку cons сам реализует ISeq, вы можете создавать последовательности из cons объектов, но они могут точно также указывать на векторы или списки и т.д. (Обратите внимание, что "список" в Clojure является специальным type (PersistentList) и не построен из объектов cons.) clojure.lang.LazySeq также реализует ISeq, поэтому его можно использовать как хвост ( "cdr" в Lisps) cons. A LazySeq содержит ссылку на некоторый код, который оценивается как ISeq какого-либо типа, но фактически не оценивает этот код до тех пор, пока он не понадобится, и после того, как он оценит код, он кэширует возвращенный ISeq и делегирует к нему.

... это все начинает иметь смысл? Вы понимаете, как работают ленивые последовательности? В основном, вы начинаете с LazySeq. Когда реализуется LazySeq, он вычисляет a cons, который указывает на другой LazySeq. Когда этот реализуется... вы получаете идею. Таким образом, вы получаете цепочку из LazySeq объектов, каждая из которых удерживает (и делегирует) a cons.

О разнице между "conses" и "lists" в Clojure, "lists" (PersistentList objects) содержит кэшированное поле "length", поэтому они могут отвечать на count в O (1) раз, Это не будет работать в других Lisp, потому что в большинстве Lisps "списки" изменяемы. Но в Clojure они неизменяемы, поэтому кэширование длины работает.

cons объекты в Clojure не имеют кешированной длины - если да, то как их можно использовать для реализации ленивых (и даже бесконечных) последовательностей? Если вы попытаетесь взять count для cons, он просто вызывает count на своем хвосте и затем увеличивает результат на 1.