AnnouncementsIntroducing Arkoren

    Menu

    Lazy Collections

    Defer collection work until traversal and choose safe stopping, buffering, and replay behavior for your source.

    What's on this page
    1. Consume only the needed prefix and recognize operations that read or buffer the full source.
    2. Supply iterators with explicit stop, cleanup, cancellation, and ownership rules.
    3. Interpret callback indexes and windows without assuming cached or replayable results.
    On this page

    If a packing station needs only the next two ready parcels, transforming every parcel first does unnecessary work. A LazySlice is an iterator: streaming steps such as Filter and Map run as you consume their result, not when you build the chain. LazyMap applies the same model to keyed entries.

    Use eager collections when you want an immediately available result. Use lazy collections when deferring callbacks or stopping before the end matters. A lazy chain does not cache values, add concurrency, or make a source replayable.

    Reading A Prefix

    Import arkoren.dev/foundation/collection. These parcels are already in memory; Lazy() changes when later operations run, without copying the source:

    type Parcel struct {
    	ID    string
    	Ready bool
    }
    
    parcels := collection.NewSlice([]Parcel{
    	{ID: "pkg-41", Ready: false},
    	{ID: "pkg-42", Ready: true},
    	{ID: "pkg-43", Ready: true},
    	{ID: "pkg-44", Ready: true},
    })

    Observe the source before filtering to distinguish records read from records selected:

    read := 0
    next := parcels.Lazy().TapEach(func(_ int, _ Parcel) {
    	read++
    }).Filter(func(_ int, parcel Parcel) bool {
    	return parcel.Ready
    }).Map(func(_ int, parcel Parcel) string {
    	return parcel.ID
    }).Take(2)

    read is still zero. TapEach observes each item without changing it; on a lazy collection it runs only during traversal. Materialize the selected IDs when the station is ready to use them:

    ids := next.Items()

    ids is []string{"pkg-42", "pkg-43"} and read is 3. Take(2) stops after two matches; it does not read pkg-44. Eager() instead returns an eager collection.Slice[string] with methods. Both materializers collect new outer storage and copy elements shallowly. A slice with no yielded values is nil; an empty materialized map is allocated.

    You can also range over a lazy collection directly, or pass Iter() to a standard-library iterator consumer. Breaking the loop stops traversal, provided the source honors the iterator’s stop signal.

    Full Input Operations

    Not every operation can produce its first result from a prefix:

    Operation Source Consumption
    Filter, Map, Take, Skip During traversal, as downstream asks for values.
    First, FirstWhere, Contains, IsEmpty At the call, stopping when an answer is known.
    Items, Eager, Len, Last, Reduce, Each At the call, through the end unless the operation can stop.
    Sort, Reverse The whole source before yielding, on every traversal.
    GroupBy, KeyBy, CountBy, Partition At the method call, buffering their results.

    Keep full-input operations off infinite sources. Sorting, reversing, grouping, and partitioning also retain data in memory; laziness does not remove that cost. In particular, source.Sort(compare).Take(2) still reads and sorts every item before it yields two. Unique streams but retains a set of seen keys, so its memory use can grow with the number of distinct keys.

    Supplying An Iterator

    NewLazySlice accepts an iterator yielding (index, item) pairs. For example, an application can expose an existing ordered set of bin labels without first building a collection result:

    bins := collection.NewLazySlice(func(yield func(int, string) bool) {
    	for index, label := range []string{"A-01", "A-02", "B-01"} {
    		if !yield(index, label) {
    			return
    		}
    	}
    })

    The constructor retains the function and its captures without invoking them. bins.Take(1).Items() returns only A-01. The source must return when yield returns false; continuing to yield violates the Go iterator protocol. NewLazyMap accepts an iter.Seq2[K, V], such as maps.All(existingMap) from the standard-library maps package. A Go-map source retains unspecified order.

    Supply a non-nil function. Lazy zero values and constructors given nil are nil functions, not iterable empty collections; traversing them panics. For an empty source, use collection.NewSlice[Parcel](nil).Lazy() or collection.NewMap[string, int](nil).Lazy().

    Source Ownership

    Wrapping an iterator does not acquire or close its resources. If your source opens a file or cursor inside its iterator, release it on every return, including early stop and failure. If the source captures an already open resource, its existing owner still arranges cleanup. For I/O that can fail, use Fallible Collections rather than hiding read errors in an infallible iterator.

    Collection operations run synchronously in the consuming goroutine. They start no goroutines and supply no context, timeout, or cancellation signal. A blocking source must capture and observe the caller’s context itself. Stopping downstream cannot interrupt work blocked before the next yield.

    The source and callbacks also determine concurrency safety. Do not mutate a captured slice or map while another goroutine traverses it without synchronization. A lazy wrapper is not a snapshot of its captured data.

    Callback Indexes

    A callback on LazySlice receives the index yielded by its immediate input. Custom sources can supply arbitrary indexes. Transformations yield dense output indexes starting at zero, so an index is not a stable record identifier:

    labels := parcels.Lazy().Filter(func(_ int, parcel Parcel) bool {
    	return parcel.Ready
    }).Map(func(index int, parcel Parcel) string {
    	return fmt.Sprintf("%d: %s", index, parcel.ID)
    }).Take(2).Items()

    With the standard-library fmt package imported, labels contains 0: pkg-42 and 1: pkg-43. The rejected parcel no longer occupies a position in Map’s input. Carry the parcel ID in the value when later steps need its identity. Map callbacks receive keys instead of indexes.

    Sampling And Windows

    Use Nth to sample positions, or Sliding to inspect complete neighboring groups. With the parcels above:

    sample := parcels.Lazy().Nth(2, 1).Items()
    windows := parcels.Lazy().Sliding(2, 3)

    sample contains pkg-42 and pkg-44: offset 1 is zero-based. Nth defaults to offset zero and clamps negative offsets to zero. Selection counts sequence positions, not the indexes a custom source supplies.

    windows is an iter.Seq2[int, []Parcel]. It yields only the first two-parcel window here; the next start has no complete window. Sliding defaults its step to one and omits incomplete final windows. Use Chunk for nonoverlapping groups that include a final short group. Lazy chunks and windows own independent outer slice storage, unlike eager windows; nested references remain shared.

    Validate sizes and steps before calling these methods: Chunk and Sliding panic on nonpositive sizes, and Nth and Sliding panic on nonpositive steps. Positive steps through math.MaxInt are valid without wrapping. A lazy source still reads intervening items to reach the next selection; a large step does not skip I/O. Stopping after a yielded selection or window avoids reading the following gap.

    Repeating Traversal

    Every traversal invokes the retained iterator again. Reusing next.Items() from the prefix example repeats its callbacks and increments read again. A slice-backed source can be traversed again, but sees shared element data, not an isolated snapshot.

    An already open file, channel, or network response may yield only its unread remainder on a second pass. A source that opens a fresh request can repeat I/O, including billed model work. Treat a source as single-use unless its contract says otherwise. If you need to inspect the same result twice, materialize it once and reuse that eager value rather than traversing again.

    For exact declarations, see the collection API reference.

    Open a bulk documentation archive?

    This file contains every guide in its scope, not API declarations. Loading it all into an agent can exhaust its context window and increase processing time and cost. For coding tasks, start with the index and read only the relevant pages.

    Use the full archive for offline indexing, bulk processing, or deliberate large-context use.

    Browse the index instead
    Open archive