Match Clojure's lazy seq realization model

jolt's seq layer realized one element ahead of Clojure, so a side-effecting
lazy seq ran its producer too eagerly. Four changes bring it in line:

- rest is Clojure's more(): it returns the tail without realizing it. An
  unforced tail (vector / string / lazy-seq cell) comes back as a deferred
  seq, so (rest (iterate f x)) does not call f. next still realizes one.
- iterate applies f lazily, inside the tail thunk, so (first (iterate f x))
  is x with no call to f (clojure.lang.Iterate parity).
- take realizes exactly n: the last element terminates without touching the
  rest, instead of forcing one more element of the source.
- an empty realized lazy seq is still a sequence value, printing "()" not
  "nil" (a JVM LazySeq is never nil).

Also: the map transducer's step fn now takes multiple inputs
([result input & inputs]) so a multi-collection transduce applies f across
all of them. Fixes medley's join/window/sequence-padded laziness and
multi-input transducer tests (now 293/293). The rest change also fixed a
latent overrun in distinct/dedupe over a map's empty tail.

iterate is a seed source, re-minted.
This commit is contained in:
Yogthos 2026-06-26 19:41:02 -04:00
parent 331a41ee26
commit 448611a5df
6 changed files with 57 additions and 12 deletions

View file

@ -96,8 +96,12 @@
([n x] (take n (repeat x))))
;; --- iterate ---
;; f is applied lazily, inside the tail thunk — (first (iterate f x)) is x with no
;; call to f, matching clojure.lang.Iterate. Wrapping the whole body in lazy-seq
;; instead would force (f x) the moment the head realizes (it is an eager argument
;; to cons), realizing one step ahead.
(defn iterate [f x]
(lazy-seq (cons x (iterate f (f x)))))
(cons x (lazy-seq (iterate f (f x)))))
;; --- partition-all --- (transducer + [n coll] + [n step coll])