NAME

Sidef::Object::Lazy - Lazy evaluation wrapper that computes values only when needed.

DESCRIPTION

This class implements lazy evaluation for iterable objects in Sidef. A Lazy object wraps another iterable (anything with an iter method -- ranges, arrays, file handles, and so on) together with a pipeline of pending operations. Nothing is computed until a terminal method is called; values are then pulled one at a time from the wrapped iterable and pushed through the whole pipeline before the next one is pulled, without ever materializing an intermediate array in memory.

Methods split into two kinds:

  • Intermediate methods ("grep", "select", "map", "collect") don't run anything -- they return a new Lazy object with one more step appended to the pipeline, so they can be chained freely.

  • Terminal methods (everything else: "each", "to_a", "first", "nth", "sum", "reduce", and so on) actually drive the iteration and produce a concrete result.

At each step of the pipeline, only a single value is in flight -- blocks passed to "grep" and "map" each receive (and, for map, must return) one value at a time, not a list.

SYNOPSIS

# Create a lazy iterator from a range
say((^Inf).lazy.grep { .is_prime }.first(10))     # first 10 primes

# Lazy processing of file lines
File("data.txt").open_r.lazy.grep { .match(/pattern/) }.map { .uc }.first(5)

# Lazy array processing
say([1, 2, 3, 4, 5].lazy.grep { .is_even }.map { _**2 }.to_a)

INHERITS

Inherits methods from Sidef::Object::Object.

CONSTRUCTION

new

Lazy.new(obj: iterable)

Creates a new Lazy object wrapping iterable (anything with an iter method), with an empty pipeline. This is typically not called directly; instead, call .lazy on the object you want to iterate lazily (see "lazy" in Sidef::Object::Object), or use "lazy" below to get an existing Lazy object back unchanged.

ITERATION

each

self.each(block)

Drives the pipeline to completion, calling block once per resulting element (for side effects), and returns self.

(1..10).lazy.grep { .is_prime }.each {|p| say(p) }
# Prints: 2, 3, 5, 7

iter

self.iter

Returns a Block (an iterator) that, each time it's called, pulls the next raw value from the wrapped iterable, runs it through the pipeline (skipping over any values filtered out by a grep step), and returns the result -- or nil once the underlying iterable is exhausted.

var it = (1..5).lazy.map {|n| n**2 }.iter
say(it.run)    #=> 1
say(it.run)    #=> 4
say(it.run)    #=> 9

to_a

self.to_a

Drives the pipeline to completion and collects every resulting element into an Array. Use with caution on an infinite or very large underlying sequence, since this method never stops on its own.

say((1..5).lazy.map {|n| n**2 }.to_a)    #=> [1, 4, 9, 16, 25]

lazy

self.lazy

Returns self unchanged. This method is idempotent and exists so that .lazy can always be chained safely, even on a value that's already a Lazy object.

var lz = (1..10).lazy.lazy    # same as (1..10).lazy

FILTERING AND TRANSFORMATION

grep

self.grep(block)

Returns a new Lazy object with a filtering step appended to the pipeline: only elements for which block returns a truthy value are kept. Nothing is evaluated until a terminal method is called on the result.

say((1..100).lazy.grep {|n| n.is_even }.first(5))    #=> [2, 4, 6, 8, 10]

Aliases: select

map

self.map(block)

Returns a new Lazy object with a transformation step appended to the pipeline: each element is replaced with the result of calling block on it. Nothing is evaluated until a terminal method is called on the result.

say((1..5).lazy.map {|n| n**2 }.to_a)    #=> [1, 4, 9, 16, 25]

Aliases: collect

while

self.while(block)

Unlike "grep" and "map", this is a terminal method: it immediately drives the pipeline, collecting elements into an Array for as long as block returns a truthy value, and stops (without including the element that failed) as soon as block returns a falsy value.

say((^Inf).lazy.map {|n| n**2 }.while {|n| n < 100 })
#=> [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

SEARCHING

first

self.first
self.first(n)
self.first(block)

With no argument, drives the pipeline just far enough to produce a single value and returns it directly (not wrapped in an array), or nil if the sequence is empty. With a number n, returns an Array of the first n resulting elements (an empty Array immediately if n <= 0, without touching the iterator at all). With a block, delegates entirely to "first_by".

say((^Inf).lazy.grep {|n| n.is_prime }.first(5))    #=> [2, 3, 5, 7, 11]
say((^Inf).lazy.grep {|n| n.is_prime }.first)       #=> 2
say((1..10).lazy.first(0))                          #=> []

first_by

self.first_by(block)

Returns the first element for which block returns a truthy value, or nil if no such element exists. Implemented as self.grep(block).first.

say((1..100).lazy.first_by {|n| n.is_prime && n > 50 })    #=> 53

nth

self.nth(n)

Returns the n-th (1-indexed) resulting element, or nil if n is less than or equal to 0. This is a positional lookup on the pipeline's output, not a predicate search -- chain a "grep" first if you need the n-th element matching some condition.

say((^Inf).lazy.grep {|n| n.is_prime }.nth(5))    #=> 11

REDUCTION AND AGGREGATION

reduce

self.reduce(op_name)
self.reduce(op_name, initial)
self.reduce(block)
self.reduce(block, initial)

Drives the pipeline to completion, combining all resulting elements into a single value. If given a block, delegates entirely to "reduce_by". If given a string naming an operator (such as '+' or '*'), repeatedly calls that operator method on an accumulator, starting from initial (or the first element, if initial is omitted).

say((1..5).lazy.reduce('+'))       #=> 15
say((1..5).lazy.reduce('*', 1))    #=> 120

reduce_by

self.reduce_by(block, initial=nil)

Drives the pipeline to completion, repeatedly calling block(accumulator, element) and using the result as the new accumulator. Starts from initial if given, or from the first element otherwise.

say((1..5).lazy.reduce_by {|acc, v| acc + v })            #=> 15
say((1..5).lazy.reduce_by({|acc, v| acc + v }, 10))       #=> 25

sum

self.sum
self.sum(block)

Drives the pipeline to completion and returns the sum of the resulting elements. With a block, sums block applied to each element instead. Processes elements in internal batches, so it doesn't need to hold the whole sequence in memory at once.

say((1..5).lazy.sum)                #=> 15
say((1..5).lazy.sum {|n| n**2 })    #=> 55

Aliases: sum_by

prod

self.prod
self.prod(block)

Drives the pipeline to completion and returns the product of the resulting elements. With a block, multiplies block applied to each element instead. Processes elements in internal batches, so it doesn't need to hold the whole sequence in memory at once.

say((1..5).lazy.prod)                #=> 120
say((1..5).lazy.prod {|n| n**2 })    #=> 14400

Aliases: prod_by

sum_kv

self.sum_kv(block)

Drives the pipeline to completion and returns the sum of calling block(index, element) for each resulting element, where index is a 0-based position in the resulting sequence (not the original, pre-pipeline one).

say((1..3).lazy.sum_kv {|k, v| k + v })    #=> 9  # (0+1) + (1+2) + (2+3)

prod_kv

self.prod_kv(block)

Drives the pipeline to completion and returns the product of calling block(index, element) for each resulting element, where index is a 0-based position in the resulting sequence (not the original, pre-pipeline one).

say((1..3).lazy.prod_kv {|k, v| k + v })    #=> 15  # (0+1) * (1+2) * (2+3)

SEE ALSO

Sidef::Object::Object