# Lazy Evaluation Pattern

## Problem

Eagerly computing all values upfront is wasteful when only a subset is needed, the dataset is very large (or infinite), or computing each element is expensive. Eager evaluation also forces the entire result into memory at once.

## Solution

**Lazy evaluation** defers computation until the value is actually needed. Values are produced on-demand using generators, thunks (deferred function calls), or lazy sequences. This allows working with potentially infinite sequences, reduces memory usage, and avoids unnecessary computation.

## When to Use

- Processing large datasets that don't fit in memory (log files, database cursors)
- Infinite sequences (natural numbers, sensor streams)
- Pipelines where intermediate results are never fully materialized
- When only the first N results are needed from an expensive computation

## When to Avoid

- When all values will definitely be consumed (lazy overhead with no benefit)
- When evaluation order matters for side effects
- Debugging is harder (stack traces show the consumption point, not the definition)
- When the deferred computation holds expensive resources (file handles, connections)

## Pseudocode

```
// Generator: lazily produces values
function* naturals():
    n = 0
    while true:
        yield n
        n += 1

// Lazy pipeline
result = naturals()
    |> filter(x => x % 2 == 0)   // lazy
    |> map(x => x * x)            // lazy
    |> take(5)                     // materializes only 5

print(result)  // [0, 4, 16, 36, 64]
```

## Python

```python
from typing import Iterator, Callable, TypeVar
import itertools
import sys

T = TypeVar("T")

# Generator: infinite sequence of natural numbers
def naturals() -> Iterator[int]:
    n = 0
    while True:
        yield n
        n += 1

# Lazy filter
def lazy_filter(predicate: Callable[[T], bool], iterable: Iterator[T]) -> Iterator[T]:
    for item in iterable:
        if predicate(item):
            yield item

# Lazy map
def lazy_map(fn: Callable[[T], T], iterable: Iterator[T]) -> Iterator[T]:
    for item in iterable:
        yield fn(item)

# Fibonacci generator (infinite)
def fibonacci() -> Iterator[int]:
    a, b = 0, 1
    while True:
        yield a
        a, b = b, a + b

# Thunk: deferred computation
class Thunk:
    def __init__(self, fn: Callable):
        self._fn = fn
        self._evaluated = False
        self._value = None

    def evaluate(self):
        if not self._evaluated:
            self._value = self._fn()
            self._evaluated = True
            print(f"  [Thunk] Evaluated: {self._value}")
        else:
            print(f"  [Thunk] Cached: {self._value}")
        return self._value

def main() -> None:
    # Lazy pipeline with infinite sequence
    print("=== Lazy Pipeline (infinite naturals) ===")
    pipeline = lazy_map(
        lambda x: x * x,
        lazy_filter(lambda x: x % 2 == 0, naturals())
    )
    first_five = list(itertools.islice(pipeline, 5))
    print(f"First 5 squares of evens: {first_five}")

    # Fibonacci: take first 10
    print("\n=== Lazy Fibonacci ===")
    fib_10 = list(itertools.islice(fibonacci(), 10))
    print(f"First 10 Fibonacci: {fib_10}")

    # Memory efficient: process large range
    print("\n=== Memory Efficient Processing ===")
    total = sum(x * x for x in range(1_000_000) if x % 2 == 0)
    print(f"Sum of squares of evens in 0..999999: {total}")
    # Note: no list created in memory

    # Thunk: deferred computation
    print("\n=== Thunks (Deferred Computation) ===")
    expensive = Thunk(lambda: sum(range(1_000_000)))
    print("Thunk created, not evaluated yet.")
    print(f"First access: {expensive.evaluate()}")
    print(f"Second access: {expensive.evaluate()}")

    # Chained lazy generators
    print("\n=== Chained Lazy Pipeline ===")
    def read_data() -> Iterator[str]:
        lines = ["Alice,30", "Bob,25", "Charlie,35", "Diana,28"]
        for line in lines:
            print(f"  Reading: {line}")
            yield line

    def parse_records(lines: Iterator[str]) -> Iterator[dict]:
        for line in lines:
            name, age = line.split(",")
            yield {"name": name, "age": int(age)}

    def filter_adults(records: Iterator[dict]) -> Iterator[dict]:
        for record in records:
            if record["age"] >= 28:
                yield record

    # Nothing happens until we consume
    pipeline = filter_adults(parse_records(read_data()))
    print("Pipeline built. Consuming:")
    for record in pipeline:
        print(f"  Result: {record}")

if __name__ == "__main__":
    main()
```

## Go

```go
package main

import "fmt"

// Generator via channels (lazy sequence)
func naturals(done <-chan struct{}) <-chan int {
	ch := make(chan int)
	go func() {
		defer close(ch)
		n := 0
		for {
			select {
			case <-done:
				return
			case ch <- n:
				n++
			}
		}
	}()
	return ch
}

// Lazy filter
func filter(done <-chan struct{}, in <-chan int, pred func(int) bool) <-chan int {
	out := make(chan int)
	go func() {
		defer close(out)
		for v := range in {
			if pred(v) {
				select {
				case <-done:
					return
				case out <- v:
				}
			}
		}
	}()
	return out
}

// Lazy map
func mapCh(done <-chan struct{}, in <-chan int, fn func(int) int) <-chan int {
	out := make(chan int)
	go func() {
		defer close(out)
		for v := range in {
			select {
			case <-done:
				return
			case out <- fn(v):
			}
		}
	}()
	return out
}

// Take first n values
func take(done <-chan struct{}, in <-chan int, n int) []int {
	result := make([]int, 0, n)
	for i := 0; i < n; i++ {
		select {
		case <-done:
			return result
		case v, ok := <-in:
			if !ok {
				return result
			}
			result = append(result, v)
		}
	}
	return result
}

// Fibonacci generator
func fibonacci(done <-chan struct{}) <-chan int {
	ch := make(chan int)
	go func() {
		defer close(ch)
		a, b := 0, 1
		for {
			select {
			case <-done:
				return
			case ch <- a:
				a, b = b, a+b
			}
		}
	}()
	return ch
}

// Thunk: lazy value
type Thunk[T any] struct {
	fn        func() T
	value     T
	evaluated bool
}

func NewThunk[T any](fn func() T) *Thunk[T] {
	return &Thunk[T]{fn: fn}
}

func (t *Thunk[T]) Get() T {
	if !t.evaluated {
		t.value = t.fn()
		t.evaluated = true
		fmt.Printf("  [Thunk] Evaluated\n")
	} else {
		fmt.Printf("  [Thunk] Cached\n")
	}
	return t.value
}

// Closure-based lazy iterator (alternative to channels)
func lazyRange(start, end int) func() (int, bool) {
	current := start
	return func() (int, bool) {
		if current >= end {
			return 0, false
		}
		val := current
		current++
		return val, true
	}
}

func main() {
	// Lazy pipeline with infinite naturals
	fmt.Println("=== Lazy Pipeline (infinite naturals) ===")
	done := make(chan struct{})
	pipeline := mapCh(done,
		filter(done, naturals(done), func(x int) bool { return x%2 == 0 }),
		func(x int) int { return x * x },
	)
	result := take(done, pipeline, 5)
	close(done)
	fmt.Printf("First 5 squares of evens: %v\n", result)

	// Fibonacci
	fmt.Println("\n=== Lazy Fibonacci ===")
	done2 := make(chan struct{})
	fib10 := take(done2, fibonacci(done2), 10)
	close(done2)
	fmt.Printf("First 10 Fibonacci: %v\n", fib10)

	// Thunk
	fmt.Println("\n=== Thunks (Deferred Computation) ===")
	expensive := NewThunk(func() int {
		sum := 0
		for i := 0; i < 1_000_000; i++ {
			sum += i
		}
		return sum
	})
	fmt.Println("Thunk created, not evaluated yet.")
	fmt.Printf("First access: %d\n", expensive.Get())
	fmt.Printf("Second access: %d\n", expensive.Get())

	// Closure-based lazy iterator
	fmt.Println("\n=== Closure-based Lazy Range ===")
	next := lazyRange(0, 5)
	for {
		val, ok := next()
		if !ok {
			break
		}
		fmt.Printf("  Got: %d\n", val)
	}
}
```

## JavaScript

```javascript
// Generator: infinite natural numbers
function* naturals() {
  let n = 0;
  while (true) {
    yield n++;
  }
}

// Lazy filter generator
function* lazyFilter(predicate, iterable) {
  for (const item of iterable) {
    if (predicate(item)) {
      yield item;
    }
  }
}

// Lazy map generator
function* lazyMap(fn, iterable) {
  for (const item of iterable) {
    yield fn(item);
  }
}

// Take first n items
function take(n, iterable) {
  const result = [];
  for (const item of iterable) {
    result.push(item);
    if (result.length >= n) break;
  }
  return result;
}

// Fibonacci generator
function* fibonacci() {
  let [a, b] = [0, 1];
  while (true) {
    yield a;
    [a, b] = [b, a + b];
  }
}

// Thunk: deferred computation
class Thunk {
  constructor(fn) {
    this._fn = fn;
    this._evaluated = false;
    this._value = undefined;
  }

  get() {
    if (!this._evaluated) {
      this._value = this._fn();
      this._evaluated = true;
      console.log(`  [Thunk] Evaluated: ${this._value}`);
    } else {
      console.log(`  [Thunk] Cached: ${this._value}`);
    }
    return this._value;
  }
}

function main() {
  // Lazy pipeline with infinite sequence
  console.log("=== Lazy Pipeline (infinite naturals) ===");
  const pipeline = lazyMap(
    (x) => x * x,
    lazyFilter((x) => x % 2 === 0, naturals())
  );
  const firstFive = take(5, pipeline);
  console.log(`First 5 squares of evens: [${firstFive}]`);

  // Fibonacci
  console.log("\n=== Lazy Fibonacci ===");
  const fib10 = take(10, fibonacci());
  console.log(`First 10 Fibonacci: [${fib10}]`);

  // Thunks
  console.log("\n=== Thunks (Deferred Computation) ===");
  const expensive = new Thunk(() => {
    let sum = 0;
    for (let i = 0; i < 1_000_000; i++) sum += i;
    return sum;
  });
  console.log("Thunk created, not evaluated yet.");
  console.log(`First access: ${expensive.get()}`);
  console.log(`Second access: ${expensive.get()}`);

  // Chained lazy pipeline
  console.log("\n=== Chained Lazy Pipeline ===");
  function* readData() {
    const lines = ["Alice,30", "Bob,25", "Charlie,35", "Diana,28"];
    for (const line of lines) {
      console.log(`  Reading: ${line}`);
      yield line;
    }
  }

  function* parseRecords(lines) {
    for (const line of lines) {
      const [name, age] = line.split(",");
      yield { name, age: parseInt(age) };
    }
  }

  function* filterAdults(records) {
    for (const record of records) {
      if (record.age >= 28) {
        yield record;
      }
    }
  }

  // Nothing happens until consumed
  const dataPipeline = filterAdults(parseRecords(readData()));
  console.log("Pipeline built. Consuming:");
  for (const record of dataPipeline) {
    console.log(`  Result: ${JSON.stringify(record)}`);
  }
}

main();
```

## Related Patterns

- **Proxy** — a lazy proxy defers the creation or loading of a real object until first access.
- **Iterator** — lazy generators are iterators; lazy evaluation and iterators are the same concept applied to sequences.
- **Memoization** — memoization caches the result of a deferred computation so it is only evaluated once.
