Project 6: Lazy Stream Library

Project 6: Lazy Stream Library

Build a lazy evaluation library with infinite data structures and stream fusion.

Quick Reference

Attribute Value
Difficulty Advanced
Time Estimate 2 Weeks
Language TypeScript
Prerequisites Projects 1-3, Understanding of closures
Key Topics Lazy Evaluation, Thunks, Infinite Streams, Fusion

1. Learning Objectives

By completing this project, you will:

  1. Understand the difference between eager and lazy evaluation
  2. Implement lazy values using thunks and memoization
  3. Build infinite data structures (streams)
  4. Implement lazy list operations (map, filter, take, drop)
  5. Understand and implement stream fusion
  6. Learn when lazy evaluation helps and hurts performance
  7. See connections to generators and iterators

2. Theoretical Foundation

2.1 Core Concepts

Eager vs Lazy Evaluation

Eager (strict) evaluation computes values immediately:

// Eager: All million items processed before slice
const result = [1, 2, 3, ...millionMoreNumbers]
  .map(x => x * 2)       // Process all million
  .filter(x => x > 100)  // Check all million
  .slice(0, 10);         // Only need 10!

Lazy evaluation defers computation until needed:

// Lazy: Only compute what's needed for 10 results
const result = lazyRange(1, Infinity)
  .map(x => x * 2)       // Not computed yet
  .filter(x => x > 100)  // Not computed yet
  .take(10)              // Now compute ~55 values
  .toArray();

Visual comparison:

Eager Evaluation Timeline:
Define      map(ร—2)       filter(>100)   take(10)
  โ†“           โ†“               โ†“            โ†“
[1..1M] โ†’ [2..2M] โ†’ [filtered...] โ†’ [10 items]
         โ†‘โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ†‘
         All million items processed upfront

Lazy Evaluation Timeline:
Define      map(ร—2)       filter(>100)   take(10)
  โ†“           โ†“               โ†“            โ†“
[recipe]  [recipe]       [recipe]     [execute!]
                                          โ†“
                                    Process ~55 items
                                          โ†“
                                      [10 items]

Thunks: Delayed Computation

A thunk is a function with no arguments that wraps a computation:

// Eager: Computed immediately
const eager = 1 + 2;  // 3

// Lazy: Wrapped in thunk
const lazy = () => 1 + 2;  // Function, not computed
lazy();  // Now compute: 3

Thunks enable lazy evaluation:

// Lazy value type
type Lazy<T> = {
  get: () => T;      // Force evaluation
  isEvaluated: boolean;
};

// Create lazy value
const lazy = <T>(thunk: () => T): Lazy<T> => {
  let computed = false;
  let value: T;

  return {
    get isEvaluated() { return computed; },
    get() {
      if (!computed) {
        value = thunk();  // Evaluate once
        computed = true;
      }
      return value;
    }
  };
};

// Usage
const expensive = lazy(() => {
  console.log("Computing...");
  return fibonacci(40);
});

console.log(expensive.isEvaluated);  // false
console.log(expensive.get());         // "Computing...", then result
console.log(expensive.isEvaluated);  // true
console.log(expensive.get());         // Same result, no recomputation

Lazy Lists / Streams

A lazy list is a linked list where the tail is a thunk:

// Eager list (all elements exist)
type EagerList<A> = Nil | Cons<A>;
interface Cons<A> { head: A; tail: EagerList<A>; }

// Lazy list (tail computed on demand)
type LazyList<A> = Nil | LazyCons<A>;
interface LazyCons<A> {
  head: A;
  tail: Lazy<LazyList<A>>;  // Thunk!
}

This enables infinite structures:

// Infinite list of natural numbers
const naturals = (n: number): LazyList<number> => ({
  head: n,
  tail: lazy(() => naturals(n + 1))  // Computed only when accessed
});

const nums = naturals(0);  // 0, 1, 2, 3, ...
nums.head;                  // 0
nums.tail.get().head;       // 1 (now computed)
nums.tail.get().tail.get().head;  // 2

Lazy Operations

// Map: Transform each element (lazily)
const map = <A, B>(f: (a: A) => B) => (list: LazyList<A>): LazyList<B> => {
  if (isNil(list)) return nil;
  return {
    head: f(list.head),
    tail: lazy(() => map(f)(list.tail.get()))
  };
};

// Filter: Keep elements matching predicate (lazily)
const filter = <A>(pred: (a: A) => boolean) => (list: LazyList<A>): LazyList<A> => {
  if (isNil(list)) return nil;
  if (pred(list.head)) {
    return {
      head: list.head,
      tail: lazy(() => filter(pred)(list.tail.get()))
    };
  }
  return filter(pred)(list.tail.get());  // Skip, continue
};

// Take: Get first n elements (forces evaluation)
const take = <A>(n: number) => (list: LazyList<A>): A[] => {
  if (n <= 0 || isNil(list)) return [];
  return [list.head, ...take(n - 1)(list.tail.get())];
};

Stream Fusion

When you chain operations, each creates an intermediate structure:

// Without fusion: Three intermediate arrays
[1,2,3].map(x => x * 2).filter(x => x > 2).map(x => x + 1);
// [2,4,6] โ†’ [4,6] โ†’ [5,7]

// With fusion: Single pass
// Combine into: x => (x * 2) > 2 ? ((x * 2) + 1) : skip

Stream fusion merges multiple operations into a single pass:

Before Fusion:
list โ†’ map(ร—2) โ†’ [2,4,6] โ†’ filter(>2) โ†’ [4,6] โ†’ map(+1) โ†’ [5,7]
       โ””โ”€โ”€ pass 1 โ”€โ”€โ”˜      โ””โ”€โ”€ pass 2 โ”€โ”€โ”€โ”€โ”˜     โ””โ”€โ”€ pass 3 โ”€โ”€โ”˜

After Fusion:
list โ†’ fused transformer โ†’ [5,7]
       โ””โ”€โ”€ single pass โ”€โ”€โ”˜

Implementation uses a โ€œtransducerโ€ pattern:

type Transducer<A, B> = <R>(
  reducer: (acc: R, val: B) => R
) => (acc: R, val: A) => R;

const mapT = <A, B>(f: (a: A) => B): Transducer<A, B> =>
  (reducer) => (acc, val) => reducer(acc, f(val));

const filterT = <A>(pred: (a: A) => boolean): Transducer<A, A> =>
  (reducer) => (acc, val) => pred(val) ? reducer(acc, val) : acc;

// Compose transducers
const comp = <A, B, C>(t1: Transducer<A, B>, t2: Transducer<B, C>): Transducer<A, C> =>
  (reducer) => t1(t2(reducer));

2.2 Why This Matters

Memory Efficiency:

  • Process data larger than RAM
  • No intermediate arrays

Performance:

  • Only compute whatโ€™s needed
  • Fusion reduces passes

Expressiveness:

  • Work with infinite data
  • Separation of concerns: describe what, not when

In Real Applications:

  • Generators: JavaScriptโ€™s function* uses lazy evaluation
  • RxJS: Observables are lazy streams
  • Big Data: Sparkโ€™s lazy transformations

2.3 Historical Context

  • 1970s: Lazy evaluation in research languages
  • 1987: Haskell makes lazy evaluation default
  • 1998: โ€œDeforestationโ€ paper on fusion
  • 2000s: Stream libraries in Java, Scala
  • 2015: TC39 iterators and generators
  • Now: Transducers in Clojure, Ramda

2.4 Common Misconceptions

โ€œLazy is always fasterโ€

  • Reality: Lazy adds overhead; best for early termination or infinite data

โ€œThunks are memoized automaticallyโ€

  • Reality: You must implement memoization explicitly

โ€œJavaScript is eager, so this doesnโ€™t applyโ€

  • Reality: Generators are lazy; you can implement lazy structures

3. Project Specification

3.1 What You Will Build

A lazy evaluation library with:

  • Lazy<T> type for delayed computation with memoization
  • LazyList<T> for lazy sequences
  • Infinite stream generators
  • Lazy operations (map, filter, flatMap, take, drop)
  • Stream fusion via transducers
  • Utilities for integration with iterators

3.2 Functional Requirements

  1. Lazy Values:
    • lazy(thunk): Create lazy value
    • force(lazy): Evaluate and get value
    • isEvaluated(lazy): Check if evaluated
    • map(lazy, fn): Transform lazy value
  2. Lazy List Construction:
    • empty: Empty list
    • cons(head, tail): Prepend element
    • from(array): From eager array
    • range(start, end?): Numeric range (possibly infinite)
    • iterate(seed, fn): Repeated application
    • repeat(value): Infinite repetition
    • cycle(list): Infinite cycle
  3. Lazy Operations:
    • map(fn): Transform elements
    • filter(pred): Keep matching elements
    • flatMap(fn): Map and flatten
    • take(n): First n elements
    • drop(n): Skip first n elements
    • takeWhile(pred): Take while predicate true
    • dropWhile(pred): Drop while predicate true
    • zip(other): Pair with another stream
    • zipWith(fn, other): Combine with function
  4. Consumers (Force Evaluation):
    • toArray(): Convert to array
    • forEach(fn): Iterate with side effect
    • reduce(fn, initial): Fold to single value
    • find(pred): Find first match
    • every(pred): All match predicate
    • some(pred): Any matches predicate
  5. Stream Fusion:
    • Transducer composition
    • Automatic fusion of map/filter chains
    • transduce(transducer, initial): Apply fused operations

3.3 Non-Functional Requirements

  • Memory: Handle very long (million+ element) streams
  • Stack Safety: No stack overflow on deep operations
  • Performance: Fusion should improve benchmarks
  • Type Safety: Full generics, inferred types

3.4 Example Usage / Output

// Infinite stream of natural numbers
const naturals = range(0);  // 0, 1, 2, 3, ...

// Lazy transformations (no computation yet)
const result = naturals
  .map(x => x * x)           // Squares
  .filter(x => x % 2 === 0)  // Even only
  .take(5);                   // First 5

// Force evaluation
console.log(result.toArray());  // [0, 4, 16, 36, 64]

// Fibonacci sequence
const fibs = iterate([0, 1], ([a, b]) => [b, a + b])
  .map(([a, _]) => a);

console.log(fibs.take(10).toArray());
// [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

// Find in infinite stream
const firstPrimeOver1000 = naturals
  .filter(isPrime)
  .dropWhile(x => x <= 1000)
  .head();
console.log(firstPrimeOver1000);  // 1009

// Stream fusion
const fused = LazyList.from([1, 2, 3, 4, 5])
  .pipe(
    compose(
      map(x => x * 2),
      filter(x => x > 4),
      map(x => x + 1)
    )
  )
  .toArray();
// Single pass instead of three!
console.log(fused);  // [7, 9, 11]

// Working with iterators
const gen = function*() {
  yield 1; yield 2; yield 3;
};
const fromGen = LazyList.fromIterator(gen());
console.log(fromGen.map(x => x * 10).toArray());  // [10, 20, 30]

4. Solution Architecture

4.1 High-Level Design

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                   Lazy Stream Library                       โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚                                                            โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”‚
โ”‚  โ”‚                    Lazy<T>                            โ”‚  โ”‚
โ”‚  โ”‚  Memoized thunk for delayed computation               โ”‚  โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ”‚
โ”‚                          โ”‚                                  โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”‚
โ”‚  โ”‚                   LazyList<T>                         โ”‚  โ”‚
โ”‚  โ”‚  Nil | Cons(head: T, tail: Lazy<LazyList<T>>)        โ”‚  โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ”‚
โ”‚                          โ”‚                                  โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”‚
โ”‚  โ”‚               Lazy Operations                         โ”‚  โ”‚
โ”‚  โ”‚  map, filter, flatMap, take, drop, zip                โ”‚  โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ”‚
โ”‚                          โ”‚                                  โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”‚
โ”‚  โ”‚                 Transducers                           โ”‚  โ”‚
โ”‚  โ”‚  Composable transformers for fusion                   โ”‚  โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ”‚
โ”‚                          โ”‚                                  โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”‚
โ”‚  โ”‚                  Consumers                            โ”‚  โ”‚
โ”‚  โ”‚  toArray, reduce, forEach, find                       โ”‚  โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ”‚
โ”‚                                                            โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

4.2 Key Components

Component Responsibility Key Decisions
Lazy<T> Memoized thunk Object with getter
LazyList<T> Lazy linked list Union with Nil
Operations Transform lazily Return new LazyList
Transducers Enable fusion Reducer transformers
Consumers Force evaluation Iterate/accumulate

4.3 Data Structures

// Memoized lazy value
interface Lazy<T> {
  readonly isEvaluated: boolean;
  force(): T;
}

// Lazy list - discriminated union
type LazyList<A> = Nil | Cons<A>;

interface Nil {
  readonly _tag: 'Nil';
}

interface Cons<A> {
  readonly _tag: 'Cons';
  readonly head: A;
  readonly tail: Lazy<LazyList<A>>;
}

// Transducer type
type Reducer<A, R> = (acc: R, val: A) => R;
type Transducer<A, B> = <R>(reducer: Reducer<B, R>) => Reducer<A, R>;

// Stream type (alternative implementation)
interface Stream<A> {
  map<B>(f: (a: A) => B): Stream<B>;
  filter(pred: (a: A) => boolean): Stream<A>;
  take(n: number): Stream<A>;
  toArray(): A[];
  // ... more methods
}

4.4 Algorithm Overview

Lazy Map:

const map = <A, B>(f: (a: A) => B) => (list: LazyList<A>): LazyList<B> => {
  if (list._tag === 'Nil') return nil;
  return cons(
    f(list.head),
    lazy(() => map(f)(list.tail.force()))
  );
};

Take (Force Evaluation):

const take = <A>(n: number) => (list: LazyList<A>): A[] => {
  const result: A[] = [];
  let current = list;
  let count = n;

  while (count > 0 && current._tag === 'Cons') {
    result.push(current.head);
    current = current.tail.force();
    count--;
  }

  return result;
};

Transducer Composition:

const compose = <A, B, C>(
  t1: Transducer<A, B>,
  t2: Transducer<B, C>
): Transducer<A, C> =>
  <R>(reducer: Reducer<C, R>) => t1(t2(reducer));

// Fused map + filter in single pass
const mapThenFilter = compose(
  mapT(x => x * 2),
  filterT(x => x > 10)
);

Complexity:

  • Lazy operations: O(1) to create
  • Force n elements: O(n)
  • Fusion: Reduces constant factor, same Big-O

5. Implementation Guide

5.1 Development Environment Setup

mkdir lazy-streams && cd lazy-streams
npm init -y
npm install --save-dev typescript ts-node jest @types/jest ts-jest
npx tsc --init
mkdir src tests benchmarks

5.2 Project Structure

lazy-streams/
โ”œโ”€โ”€ src/
โ”‚   โ”œโ”€โ”€ lazy/
โ”‚   โ”‚   โ”œโ”€โ”€ lazy.ts          # Lazy<T> implementation
โ”‚   โ”‚   โ””โ”€โ”€ index.ts
โ”‚   โ”œโ”€โ”€ list/
โ”‚   โ”‚   โ”œโ”€โ”€ types.ts         # LazyList type
โ”‚   โ”‚   โ”œโ”€โ”€ constructors.ts  # cons, range, iterate, etc.
โ”‚   โ”‚   โ”œโ”€โ”€ operations.ts    # map, filter, take, etc.
โ”‚   โ”‚   โ””โ”€โ”€ index.ts
โ”‚   โ”œโ”€โ”€ transducers/
โ”‚   โ”‚   โ”œโ”€โ”€ types.ts         # Transducer types
โ”‚   โ”‚   โ”œโ”€โ”€ core.ts          # mapT, filterT, etc.
โ”‚   โ”‚   โ”œโ”€โ”€ compose.ts       # Composition utilities
โ”‚   โ”‚   โ””โ”€โ”€ index.ts
โ”‚   โ”œโ”€โ”€ integration/
โ”‚   โ”‚   โ”œโ”€โ”€ iterators.ts     # To/from iterators
โ”‚   โ”‚   โ””โ”€โ”€ arrays.ts        # To/from arrays
โ”‚   โ””โ”€โ”€ index.ts             # Public API
โ”œโ”€โ”€ tests/
โ”‚   โ”œโ”€โ”€ lazy.test.ts
โ”‚   โ”œโ”€โ”€ list.test.ts
โ”‚   โ”œโ”€โ”€ transducers.test.ts
โ”‚   โ””โ”€โ”€ infinite.test.ts
โ”œโ”€โ”€ benchmarks/
โ”‚   โ””โ”€โ”€ fusion-benchmark.ts
โ””โ”€โ”€ package.json

5.3 Implementation Phases

Phase 1: Lazy Values (1-2 days)

Goals:

  • Implement memoized thunks
  • Test memoization behavior

Tasks:

  1. Create Lazy<T> interface with isEvaluated and force()
  2. Implement lazy(thunk) factory with memoization
  3. Implement map(lazy, fn) for lazy values
  4. Test that evaluation happens only once

Checkpoint:

let count = 0;
const expensive = lazy(() => { count++; return 42; });

expect(expensive.isEvaluated).toBe(false);
expect(count).toBe(0);

expect(expensive.force()).toBe(42);
expect(count).toBe(1);

expect(expensive.force()).toBe(42);
expect(count).toBe(1);  // Not recomputed!

Phase 2: Lazy List Basics (2-3 days)

Goals:

  • Define LazyList type
  • Implement constructors
  • Implement basic operations

Tasks:

  1. Define Nil and Cons<A> types
  2. Implement nil, cons(head, tail) constructors
  3. Implement range(start, end?) - infinite if no end
  4. Implement iterate(seed, fn) - repeated application
  5. Implement repeat(value) - infinite repetition
  6. Implement take(n) and toArray()

Checkpoint:

const nums = range(0);  // Infinite!
expect(take(5)(nums)).toEqual([0, 1, 2, 3, 4]);

const powers = iterate(1, x => x * 2);
expect(take(5)(powers)).toEqual([1, 2, 4, 8, 16]);

Phase 3: Lazy Operations (3-4 days)

Goals:

  • Implement map, filter, flatMap
  • Implement take/drop variants
  • Implement zip operations

Tasks:

  1. Implement map(fn) - lazy transformation
  2. Implement filter(pred) - lazy filtering
  3. Implement flatMap(fn) - lazy map + flatten
  4. Implement takeWhile(pred) and dropWhile(pred)
  5. Implement zip(other) and zipWith(fn, other)
  6. Implement concat(other) - lazy concatenation

Checkpoint:

const fibs = iterate([0, 1], ([a, b]) => [b, a + b]).map(([a]) => a);
expect(fibs.filter(x => x % 2 === 0).take(5).toArray())
  .toEqual([0, 2, 8, 34, 144]);

Phase 4: Transducers & Fusion (2-3 days)

Goals:

  • Implement transducer pattern
  • Enable fusion of operations
  • Benchmark performance

Tasks:

  1. Define Transducer<A, B> type
  2. Implement mapT, filterT, takeT
  3. Implement compose for transducers
  4. Create transduce function to apply to streams
  5. Benchmark fused vs unfused operations

Checkpoint:

const xform = compose(
  mapT(x => x * 2),
  filterT(x => x > 5),
  takeT(3)
);

const result = transduce(xform, [], [1, 2, 3, 4, 5, 6, 7, 8]);
expect(result).toEqual([6, 8, 10]);

Phase 5: Integration & Polish (2-3 days)

Goals:

  • Iterator integration
  • Generator support
  • Documentation and examples

Tasks:

  1. Implement fromIterator(iter)
  2. Implement toIterator() method
  3. Make LazyList iterable ([Symbol.iterator])
  4. Create comprehensive examples
  5. Add JSDoc documentation
  6. Performance benchmarks

Checkpoint:

// Generator integration
function* gen() { yield 1; yield 2; yield 3; }
const fromGen = LazyList.fromIterator(gen());

// Iterable
for (const x of range(0, 5)) {
  console.log(x);  // 0, 1, 2, 3, 4
}

5.4 Key Implementation Decisions

Decision Options Recommendation Rationale
List representation Class vs Object Object (discriminated union) More FP-style
Tail sharing Yes vs No Yes Memory efficiency
Stack safety Recursion vs Iteration Trampolined for deep ops Avoid overflow
Memoization Always vs Optional Always for Lazy Correct semantics

6. Testing Strategy

6.1 Test Categories

Category Purpose Examples
Memoization Thunks evaluated once Lazy value caching
Operations Correct transformations map, filter results
Infinite Handles infinite streams take from infinite
Fusion Performance improvement Benchmark comparisons

6.2 Critical Test Cases

  1. Memoization Works: ```typescript test(โ€˜lazy value is computed exactly onceโ€™, () => { let computeCount = 0; const lz = lazy(() => { computeCount++; return โ€˜resultโ€™; });

lz.force(); lz.force(); lz.force();

expect(computeCount).toBe(1); });


2. **Lazy Operations Are Lazy**:
```typescript
test('map does not evaluate until forced', () => {
  let mapCalled = 0;
  const list = range(0)
    .map(x => { mapCalled++; return x * 2; });

  expect(mapCalled).toBe(0);

  list.take(3).toArray();
  expect(mapCalled).toBe(3);  // Only 3 calls, not infinite!
});
  1. Infinite Streams Work:
    test('can work with infinite streams', () => {
      const primes = range(2).filter(isPrime);
      expect(primes.take(5).toArray()).toEqual([2, 3, 5, 7, 11]);
    });
    
  2. Fusion Reduces Passes: ```typescript test(โ€˜fused operations make single passโ€™, () => { let passCount = 0; const counting = mapT(x => { passCount++; return x; });

const xform = compose( counting, mapT(x => x * 2), filterT(x => x > 5) );

transduce(xform, [], [1, 2, 3, 4, 5]); expect(passCount).toBe(5); // Not 15 (3 operations ร— 5 elements) });


### 6.3 Test Data

```typescript
// Finite test lists
const finite = from([1, 2, 3, 4, 5]);
const empty = nil;
const single = cons(42, lazy(() => nil));

// Infinite generators
const naturals = range(0);
const ones = repeat(1);
const fibs = iterate([0, 1], ([a, b]) => [b, a + b]).map(([a]) => a);

// Edge cases
const veryLong = range(0, 1_000_000);

7. Common Pitfalls & Debugging

7.1 Frequent Mistakes

Pitfall Symptom Solution
Forgetting to memoize Same computation repeated Use memoizing lazy()
Stack overflow on deep lists Crash Use trampolining
Evaluating unnecessarily Performance issues Check lazy semantics
Memory leaks from holding head Out of memory Donโ€™t hold refs to consumed elements

7.2 Debugging Strategies

// Debug lazy evaluation
const trace = <A>(label: string) => (list: LazyList<A>): LazyList<A> => {
  if (list._tag === 'Nil') {
    console.log(`${label}: Nil`);
    return nil;
  }
  console.log(`${label}: Cons(${list.head}, ...)`);
  return cons(list.head, lazy(() => trace(label)(list.tail.force())));
};

// Usage
range(0, 10)
  .map(x => x * 2)
  .pipe(trace('after map'))
  .take(3);
// Logs: "after map: Cons(0, ...)"
//       "after map: Cons(2, ...)"
//       "after map: Cons(4, ...)"

7.3 Performance Traps

  • Holding head references: Prevents garbage collection
  • No fusion when using methods: Convert to transducers for fusion
  • Repeated traversal: Memoize or convert to array if needed multiple times

8. Extensions & Challenges

8.1 Beginner Extensions

  • head/tail methods: Safe accessors returning Maybe
  • length method: Count elements (finite only)
  • reverse: Reverse finite list

8.2 Intermediate Extensions

  • Parallel take: Use Promise.all for IO-bound operations
  • Chunk: Group elements into sub-lists
  • Interleave: Alternate between two streams

8.3 Advanced Extensions

  • Async streams: Handle Promise-producing operations
  • Backpressure: Handle producer/consumer speed mismatch
  • Pull-based Observable: Bridge to reactive programming

9. Real-World Connections

9.1 Industry Applications

  • JavaScript Generators: Built-in lazy sequences
  • RxJS Observables: Lazy push-based streams
  • Node Streams: Lazy IO processing
  • Spark RDDs: Lazy distributed transformations
  • transducers-js: https://github.com/cognitect-labs/transducers-js
  • Ramda transducers: https://ramdajs.com/docs/#transduce
  • IxJS: https://github.com/ReactiveX/IxJS - Iterable extensions

9.3 Interview Relevance

  • โ€œImplement lazy evaluationโ€: Direct application
  • โ€œProcess infinite dataโ€: Show stream approach
  • โ€œOptimize map/filter chainsโ€: Show fusion

10. Resources

10.1 Essential Reading

  • โ€œWhy Functional Programming Mattersโ€ by John Hughes - Lazy evaluation section
  • โ€œHaskell Programming from First Principlesโ€ Ch. 27 - Non-strictness
  • โ€œFunctional Programming in JavaScriptโ€ by Luis Atencio - Lazy evaluation chapter

10.2 Video Resources

  • โ€œGenerators and Iteratorsโ€ - Fun Fun Function (YouTube)
  • โ€œTransducersโ€ - Rich Hickey (YouTube)

10.3 Tools & Documentation

  • MDN Iterators: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Iterators_and_Generators
  • Transducers Explained: https://clojure.org/reference/transducers

11. Self-Assessment Checklist

Before considering this project complete, verify:

Understanding

  • I can explain eager vs lazy evaluation
  • I understand how thunks delay computation
  • I can explain stream fusion and why it helps
  • I understand memory implications of lazy structures

Implementation

  • Lazy values memoize correctly
  • Infinite streams donโ€™t crash
  • Operations are properly lazy
  • Transducers compose and fuse
  • Iterator integration works

Growth

  • I can recognize when lazy evaluation helps
  • I understand connection to generators
  • I can apply these concepts in production code

12. Submission / Completion Criteria

Minimum Viable Completion:

  • Lazy<T> with memoization
  • LazyList<T> with map, filter, take
  • Infinite range working
  • Basic tests passing

Full Completion:

  • All operations implemented
  • Transducer fusion working
  • Iterator integration
  • Comprehensive tests
  • Performance benchmarks showing fusion benefit

Excellence (Going Above & Beyond):

  • Async stream support
  • Published to npm
  • Comparison with existing libraries
  • Memory profiling

This guide was generated from FUNCTIONAL_PROGRAMMING_TYPESCRIPT_LEARNING_PROJECTS.md. For the complete learning path, see the parent directory.