Clojure: Избегайте в Сите Эратосфена?

Здесь моя реализация Сита Эратосфена в Clojure (на основе урока SICP по потокам):

(defn nats-from [n]
  (iterate inc n))

(defn divide? [p q]
  (zero? (rem q p)))

(defn sieve [stream]
  (lazy-seq (cons (first stream)
            (sieve (remove #(divide? (first stream) %) 
               (rest stream))))))

(def primes (sieve (nats-from 2)))

Теперь все ОК, когда я беру первые 100 простых чисел:

(take 100 primes)

Но, если я пытаюсь принять первые 1000 простых чисел, программа прерывается из-за. Мне интересно, возможно ли изменить как-то сито функции, чтобы стать хвостовым рекурсивным и, тем не менее, сохранить "потоки" алгоритма?

Любая помощь???

Ответ 1

Во-первых, это не сито Эратосфена... см. мой комментарий для деталей.

Во-вторых, извинения за закрытое голосование, так как ваш вопрос не является фактическим дубликатом того, на который я указал... Мой плохой.

Объяснение того, что происходит

Разница заключается, конечно, в том, что вы пытаетесь построить инкрементное сито, где диапазон, над которым работает вызов remove, бесконечен и, следовательно, невозможно просто обернуть вокруг него doall. Решение состоит в том, чтобы реализовать одно из "реальных" инкрементальных SoEs из бумаги, с которой я, похоже, довольно часто ссылаюсь в наши дни - Мелисса Э. О'Нил Подлинное сито Эратосфена.

Особенно красивая реализация этого сита Clojure была написана Кристофом Грандом и доступна здесь для восхищения всех, кто может быть интересно. Очень рекомендуемое чтение.

Что касается источника проблемы, то вопросы, которые я изначально считал вашими, были дубликатом содержательных объяснений, которые должны быть полезны для вас: см. здесь и . Еще раз, извините за сырое голосование, чтобы закрыть.

Почему рекурсия хвоста не поможет

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

Это довольно важный момент, чтобы иметь в виду и тот, который выталкивает многих неопытных программистов Clojure (или Haskell). Причина в том, что хвостовая рекурсивная функция по необходимости возвращает свое значение только после того, как она "готова" - в самом конце вычисления. (Итеративный процесс может в конце любой конкретной итерации либо возвращать значение, либо продолжать переход к следующей итерации.) В режиме constrast функция, которая генерирует ленивую последовательность, должна немедленно возвращать ленивый объект последовательности, который инкапсулирует биты кода, которые может потребоваться произвести головку или хвост последовательности всякий раз, когда это необходимо.

Таким образом, ответ на проблему укладки ленивых преобразований заключается не в том, чтобы сделать что-либо хвостом рекурсивным, а для слияния преобразований. В этом конкретном случае наилучшую производительность можно получить, используя специальную схему для слияния операций фильтрации на основе очередей приоритетов или карт (см. вышеупомянутая статья для деталей).