Clojure apply that does not realize the first four elements of a lazy sequence?

|▌冷眼眸甩不掉的悲伤 提交于 2019-12-24 00:46:46

问题


It appears that apply forces the realization of four elements given a lazy sequence.

(take 1
      (apply concat
             (repeatedly #(do
                            (println "called")
                            (range 1 10)))))

=> "called"
=> "called"
=> "called"
=> "called"

Is there a way to do an apply which does not behave this way?

Thank You


回答1:


Is there a way to do an apply which does not behave this way?

I think the short answer is: not without reimplementing some of Clojure's basic functionality. apply's implementation relies directly on Clojure's implementation of callable functions, and tries to discover the proper arity of the given function to .invoke by enumerating the input sequence of arguments.

It may be easier to factor your solution using functions over lazy, un-chunked sequences / reducers / transducers, rather than using variadic functions with apply. For example, here's your sample reimplemented with transducers and it only invokes the body function once (per length of range):

(sequence
  (comp
    (mapcat identity)
    (take 1))
  (repeatedly #(do
                 (println "called")
                 (range 1 10))))
;; called
;; => (1)

Digging into what's happening in your example with apply, concat, seq, LazySeq, etc.:

  • repeatedly returns a new LazySeq instance: (lazy-seq (cons (f) (repeatedly f))).
  • For the given 2-arity (apply concat <args>), apply calls RT.seq on its argument list, which for a LazySeq then invokes LazySeq.seq, which will invoke your function
  • apply then calls a Java impl. method applyToHelper which tries to get the length of the argument sequence. applyToHelper tries to determine the length of the argument list using RT.boundedLength, which internally calls next and in turn seq, so it can find the proper overload of IFn.invoke to call
  • concat itself adds another layer of lazy-seq behavior.

You can see the stack traces of these invocations like this:

(take 1
  (repeatedly #(do
                 (clojure.stacktrace/print-stack-trace (Exception.))
                 (range 1 10))))

The first trace descends from the apply's initial call to seq, and the subsequent traces from RT.boundedLength.




回答2:


in fact, your code doesn't realize any of the items from the concatenated collections (ranges in your case). So the resulting collection is truly lazy as far as elements are concerned. The prints you get are from the function calls, generating unrealized lazy seqs. This one could easily be checked this way:

(defn range-logged [a b]
  (lazy-seq
   (when (< a b)
     (println "realizing item" a)
     (cons a (range-logged (inc a) b)))))

user> (take 1
            (apply concat
                   (repeatedly #(do
                                  (println "called")
                                  (range-logged 1 10)))))

;;=> called
;;   called
;;   called
;;   called
;;   realizing item 1
(1)

user> (take 10
            (apply concat
                   (repeatedly #(do
                                  (println "called")
                                  (range-logged 1 10)))))
;; called
;; called
;; called
;; called
;; realizing item 1
;; realizing item 2
;; realizing item 3
;; realizing item 4
;; realizing item 5
;; realizing item 6
;; realizing item 7
;; realizing item 8
;; realizing item 9
;; realizing item 1
(1 2 3 4 5 6 7 8 9 1)

So my guess is that you have nothing to worry about, as long as the collection returned from repeatedly closure is lazy



来源:https://stackoverflow.com/questions/51959298/clojure-apply-that-does-not-realize-the-first-four-elements-of-a-lazy-sequence

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!