lesson

Advanced Iterator Patterns

Advanced Iterator Patterns

Internal vs External Iterators

There are two fundamental approaches to iteration: internal and external. Each has distinct advantages and use cases.

External Iterators

With external iterators, the client controls the iteration process:

typescript
1interface ExternalIterator<T> {
2 next(): IteratorResult<T>;
3 hasNext(): boolean;
4}
5
6class RouteIterator implements ExternalIterator<DeliveryStop> {
7 private position = 0;
8
9 constructor(private stops: DeliveryStop[]) {}
10
11 hasNext(): boolean {
12 return this.position < this.stops.length;
13 }
14
15 next(): IteratorResult<DeliveryStop> {
16 if (this.hasNext()) {
17 return {
18 done: false,
19 value: this.stops[this.position++]
20 };
21 }
22 return { done: true, value: undefined as any };
23 }
24}
25
26// Client controls iteration
27const iterator = new RouteIterator(stops);
28while (iterator.hasNext()) {
29 const stop = iterator.next().value;
30
31 // Client decides when to continue
32 if (stop.priority === 'high') {
33 processImmediately(stop);
34 } else {
35 // Can skip or delay processing
36 break;
37 }
38}

Advantages:

  • Client has full control over iteration flow
  • Can pause, resume, or stop iteration
  • Can maintain multiple iterators simultaneously
  • Can compare positions across iterators

Disadvantages:

  • More verbose client code
  • Client must manage iterator state

Internal Iterators

With internal iterators, the collection controls the iteration:

typescript
1class DeliveryRoute {
2 private stops: DeliveryStop[] = [];
3
4 // Internal iterator
5 forEach(callback: (stop: DeliveryStop, index: number) => void | boolean): void {
6 for (let i = 0; i < this.stops.length; i++) {
7 const result = callback(this.stops[i], i);
8 // Allow early termination by returning false
9 if (result === false) break;
10 }
11 }
12
13 // Filter with internal iteration
14 filter(predicate: (stop: DeliveryStop) => boolean): DeliveryStop[] {
15 const results: DeliveryStop[] = [];
16 this.forEach((stop) => {
17 if (predicate(stop)) {
18 results.push(stop);
19 }
20 });
21 return results;
22 }
23}
24
25// Collection controls iteration
26route.forEach((stop, index) => {
27 console.log(`Stop ${index}: ${stop.address}`);
28});

Advantages:

  • Simpler client code
  • Collection can optimize traversal
  • Easier to parallelize

Disadvantages:

  • Less control for client
  • Harder to pause or compare iterators
  • May process more than needed

Hybrid Approach: Generators

TypeScript generators provide the best of both worlds:

typescript
1class DeliveryRoute implements Iterable<DeliveryStop> {
2 private stops: DeliveryStop[] = [];
3
4 // External-style: client controls
5 *[Symbol.iterator](): Generator<DeliveryStop> {
6 yield* this.stops;
7 }
8
9 // Internal-style: collection controls
10 forEach(callback: (stop: DeliveryStop) => void): void {
11 for (const stop of this) {
12 callback(stop);
13 }
14 }
15}

Filtering Iterators

Filtering iterators apply predicates during iteration without creating intermediate arrays.

Basic Filtering Iterator

typescript
1class FilteringIterator<T> implements Iterator<T> {
2 private position = 0;
3
4 constructor(
5 private items: T[],
6 private predicate: (item: T) => boolean
7 ) {}
8
9 next(): IteratorResult<T> {
10 // Skip items that don't match predicate
11 while (this.position < this.items.length) {
12 const item = this.items[this.position++];
13 if (this.predicate(item)) {
14 return { done: false, value: item };
15 }
16 }
17 return { done: true, value: undefined as any };
18 }
19}

Generator-Based Filtering

typescript
1class DeliveryRoute {
2 private stops: DeliveryStop[] = [];
3
4 *filterStops(predicate: (stop: DeliveryStop) => boolean): Generator<DeliveryStop> {
5 for (const stop of this.stops) {
6 if (predicate(stop)) {
7 yield stop;
8 }
9 }
10 }
11
12 // Specific filters
13 *byPriority(priority: string): Generator<DeliveryStop> {
14 yield* this.filterStops(stop => stop.priority === priority);
15 }
16
17 *byZone(zone: string): Generator<DeliveryStop> {
18 yield* this.filterStops(stop => stop.zone === zone);
19 }
20
21 *incompleteStops(): Generator<DeliveryStop> {
22 yield* this.filterStops(stop => !stop.completed);
23 }
24}
25
26// Usage
27const route = new DeliveryRoute();
28
29// Only iterate incomplete high-priority stops
30for (const stop of route.filterStops(s => s.priority === 'high' && !s.completed)) {
31 deliverUrgently(stop);
32}

Chaining Filters

typescript
1class IteratorChain<T> {
2 constructor(private source: Iterable<T>) {}
3
4 *filter(predicate: (item: T) => boolean): Generator<T> {
5 for (const item of this.source) {
6 if (predicate(item)) {
7 yield item;
8 }
9 }
10 }
11
12 *map<U>(mapper: (item: T) => U): Generator<U> {
13 for (const item of this.source) {
14 yield mapper(item);
15 }
16 }
17
18 *take(count: number): Generator<T> {
19 let taken = 0;
20 for (const item of this.source) {
21 if (taken >= count) break;
22 yield item;
23 taken++;
24 }
25 }
26}
27
28// Usage
29const route = new DeliveryRoute();
30const chain = new IteratorChain(route)
31 .filter(stop => stop.zone === 'A')
32 .filter(stop => stop.priority === 'high')
33 .take(5);
34
35for (const stop of chain) {
36 console.log(stop.address);
37}

Lazy Evaluation

Lazy evaluation defers computation until values are actually needed, improving performance for large datasets.

Eager vs Lazy

typescript
1class PackageCollection {
2 private packages: Package[] = [];
3
4 // ❌ Eager: processes everything immediately
5 getHighPriorityPackages(): Package[] {
6 return this.packages
7 .filter(pkg => pkg.priority === 'high')
8 .map(pkg => this.enrichPackage(pkg))
9 .slice(0, 10);
10 // Processes ALL packages, even though we only need 10
11 }
12
13 // ✅ Lazy: processes only what's needed
14 *getHighPriorityPackagesLazy(): Generator<Package> {
15 let count = 0;
16 for (const pkg of this.packages) {
17 if (pkg.priority === 'high') {
18 yield this.enrichPackage(pkg);
19 if (++count >= 10) break; // Stop after 10
20 }
21 }
22 }
23
24 private enrichPackage(pkg: Package): Package {
25 // Expensive operation
26 return { ...pkg, route: calculateOptimalRoute(pkg) };
27 }
28}

Lazy Database Queries

typescript
1class PackageRepository {
2 // Lazy loading from database
3 async *findPackages(criteria: PackageCriteria): AsyncGenerator<Package> {
4 let offset = 0;
5 const batchSize = 100;
6
7 while (true) {
8 // Fetch one batch at a time
9 const batch = await this.queryDatabase({
10 ...criteria,
11 offset,
12 limit: batchSize
13 });
14
15 if (batch.length === 0) break;
16
17 // Yield each package
18 for (const pkg of batch) {
19 yield pkg;
20 }
21
22 offset += batchSize;
23
24 // If batch is smaller than batchSize, we've reached the end
25 if (batch.length < batchSize) break;
26 }
27 }
28
29 private async queryDatabase(query: any): Promise<Package[]> {
30 // Database query implementation
31 return [];
32 }
33}
34
35// Usage: only loads what you process
36const repo = new PackageRepository();
37
38for await (const pkg of repo.findPackages({ zone: 'A' })) {
39 await processPackage(pkg);
40
41 // Can stop early without loading remaining data
42 if (pkg.isUrgent) {
43 await escalate(pkg);
44 break; // Remaining packages never queried
45 }
46}

Lazy Transformations

typescript
1class LazySequence<T> {
2 constructor(private source: Iterable<T>) {}
3
4 // All transformations are lazy
5 *filter(predicate: (item: T) => boolean): Generator<T> {
6 for (const item of this.source) {
7 if (predicate(item)) {
8 yield item;
9 }
10 }
11 }
12
13 *map<U>(mapper: (item: T) => U): Generator<U> {
14 for (const item of this.source) {
15 yield mapper(item);
16 }
17 }
18
19 *flatMap<U>(mapper: (item: T) => Iterable<U>): Generator<U> {
20 for (const item of this.source) {
21 yield* mapper(item);
22 }
23 }
24
25 // Terminal operations that trigger evaluation
26 toArray(): T[] {
27 return Array.from(this.source);
28 }
29
30 first(): T | undefined {
31 for (const item of this.source) {
32 return item; // Only processes until first item
33 }
34 return undefined;
35 }
36
37 reduce<U>(reducer: (acc: U, item: T) => U, initial: U): U {
38 let acc = initial;
39 for (const item of this.source) {
40 acc = reducer(acc, item);
41 }
42 return acc;
43 }
44}
45
46// Usage: transformations don't execute until terminal operation
47const packages = new LazySequence(allPackages)
48 .filter(pkg => pkg.weight > 10) // Not executed yet
49 .map(pkg => calculateShipping(pkg)) // Not executed yet
50 .filter(pkg => pkg.shippingCost < 50) // Not executed yet
51 .first(); // NOW executes, stops at first match

Infinite Iterators

Infinite iterators generate values indefinitely, useful for IDs, sequences, or event streams.

ID Generators

typescript
1function* generateTrackingIds(prefix: string): Generator<string> {
2 let counter = 1;
3 while (true) {
4 yield `${prefix}-${Date.now()}-${counter.toString().padStart(6, '0')}`;
5 counter++;
6 }
7}
8
9// Usage
10const idGenerator = generateTrackingIds('PKG');
11const id1 = idGenerator.next().value; // PKG-1734820800000-000001
12const id2 = idGenerator.next().value; // PKG-1734820800000-000002

Fibonacci Sequence

typescript
1function* fibonacci(): Generator<number> {
2 let [prev, curr] = [0, 1];
3 while (true) {
4 yield curr;
5 [prev, curr] = [curr, prev + curr];
6 }
7}
8
9// Take first 10 Fibonacci numbers
10const fibs = [];
11const fibGen = fibonacci();
12for (let i = 0; i < 10; i++) {
13 fibs.push(fibGen.next().value);
14}
15console.log(fibs); // [1, 1, 2, 3, 5, 8, 13, 21, 34, 55]

Event Stream Iterator

typescript
1class RouteUpdateStream {
2 private listeners: ((update: RouteUpdate) => void)[] = [];
3 private updates: RouteUpdate[] = [];
4
5 emit(update: RouteUpdate): void {
6 this.updates.push(update);
7 this.listeners.forEach(listener => listener(update));
8 }
9
10 async *[Symbol.asyncIterator](): AsyncGenerator<RouteUpdate> {
11 let position = 0;
12
13 while (true) {
14 // Yield existing updates
15 while (position < this.updates.length) {
16 yield this.updates[position++];
17 }
18
19 // Wait for new update
20 await new Promise<void>(resolve => {
21 const listener = () => {
22 this.listeners = this.listeners.filter(l => l !== listener);
23 resolve();
24 };
25 this.listeners.push(listener);
26 });
27 }
28 }
29}
30
31// Usage
32const stream = new RouteUpdateStream();
33
34(async () => {
35 for await (const update of stream) {
36 console.log('Route updated:', update);
37 }
38})();
39
40// Emit updates
41stream.emit({ routeId: 'R1', status: 'in-progress' });
42stream.emit({ routeId: 'R1', status: 'completed' });

Bidirectional Iteration

Some collections benefit from traversing in both directions.

Bidirectional Iterator Interface

typescript
1interface BidirectionalIterator<T> extends Iterator<T> {
2 next(): IteratorResult<T>;
3 previous(): IteratorResult<T>;
4 hasNext(): boolean;
5 hasPrevious(): boolean;
6}
7
8class DeliveryRouteIterator implements BidirectionalIterator<DeliveryStop> {
9 private position = 0;
10
11 constructor(private stops: DeliveryStop[]) {}
12
13 hasNext(): boolean {
14 return this.position < this.stops.length;
15 }
16
17 hasPrevious(): boolean {
18 return this.position > 0;
19 }
20
21 next(): IteratorResult<DeliveryStop> {
22 if (this.hasNext()) {
23 return {
24 done: false,
25 value: this.stops[this.position++]
26 };
27 }
28 return { done: true, value: undefined as any };
29 }
30
31 previous(): IteratorResult<DeliveryStop> {
32 if (this.hasPrevious()) {
33 return {
34 done: false,
35 value: this.stops[--this.position]
36 };
37 }
38 return { done: true, value: undefined as any };
39 }
40
41 // Jump to position
42 seek(position: number): void {
43 if (position >= 0 && position <= this.stops.length) {
44 this.position = position;
45 }
46 }
47
48 // Reset to beginning
49 reset(): void {
50 this.position = 0;
51 }
52
53 // Reset to end
54 resetToEnd(): void {
55 this.position = this.stops.length;
56 }
57}
58
59// Usage
60const iterator = new DeliveryRouteIterator(stops);
61
62// Move forward
63console.log(iterator.next().value); // Stop 1
64console.log(iterator.next().value); // Stop 2
65
66// Move backward
67console.log(iterator.previous().value); // Stop 1
68console.log(iterator.previous().value); // Stop 0
69
70// Jump to position
71iterator.seek(5);
72console.log(iterator.next().value); // Stop 5

Doubly-Linked List Iterator

typescript
1class LinkedListNode<T> {
2 constructor(
3 public value: T,
4 public next: LinkedListNode<T> | null = null,
5 public prev: LinkedListNode<T> | null = null
6 ) {}
7}
8
9class DoublyLinkedList<T> implements Iterable<T> {
10 private head: LinkedListNode<T> | null = null;
11 private tail: LinkedListNode<T> | null = null;
12
13 append(value: T): void {
14 const node = new LinkedListNode(value);
15 if (!this.tail) {
16 this.head = this.tail = node;
17 } else {
18 node.prev = this.tail;
19 this.tail.next = node;
20 this.tail = node;
21 }
22 }
23
24 // Forward iterator
25 *[Symbol.iterator](): Generator<T> {
26 let current = this.head;
27 while (current) {
28 yield current.value;
29 current = current.next;
30 }
31 }
32
33 // Backward iterator
34 *reverse(): Generator<T> {
35 let current = this.tail;
36 while (current) {
37 yield current.value;
38 current = current.prev;
39 }
40 }
41
42 createBidirectionalIterator(): BidirectionalIterator<T> {
43 let current = this.head;
44
45 return {
46 hasNext: () => current !== null && current.next !== null,
47 hasPrevious: () => current !== null && current.prev !== null,
48
49 next: () => {
50 if (current && current.next) {
51 current = current.next;
52 return { done: false, value: current.value };
53 }
54 return { done: true, value: undefined as any };
55 },
56
57 previous: () => {
58 if (current && current.prev) {
59 current = current.prev;
60 return { done: false, value: current.value };
61 }
62 return { done: true, value: undefined as any };
63 }
64 };
65 }
66}
67
68// Usage
69const list = new DoublyLinkedList<string>();
70list.append('A');
71list.append('B');
72list.append('C');
73
74// Forward iteration
75for (const item of list) {
76 console.log(item); // A, B, C
77}
78
79// Backward iteration
80for (const item of list.reverse()) {
81 console.log(item); // C, B, A
82}

Composite Iterators

Composite iterators combine multiple iterators into a single traversal.

Merging Multiple Routes

typescript
1class RouteCollection implements Iterable<DeliveryStop> {
2 private routes: DeliveryRoute[] = [];
3
4 addRoute(route: DeliveryRoute): void {
5 this.routes.push(route);
6 }
7
8 // Iterate all stops across all routes
9 *[Symbol.iterator](): Generator<DeliveryStop> {
10 for (const route of this.routes) {
11 yield* route;
12 }
13 }
14
15 // Merge by priority across all routes
16 *byPriority(priority: string): Generator<DeliveryStop> {
17 for (const route of this.routes) {
18 yield* route.byPriority(priority);
19 }
20 }
21
22 // Interleave stops from multiple routes (round-robin)
23 *interleave(): Generator<DeliveryStop> {
24 const iterators = this.routes.map(r => r[Symbol.iterator]());
25 let activeIterators = iterators.length;
26
27 while (activeIterators > 0) {
28 for (let i = 0; i < iterators.length; i++) {
29 const result = iterators[i].next();
30 if (!result.done) {
31 yield result.value;
32 } else if (activeIterators > 0) {
33 activeIterators--;
34 }
35 }
36 }
37 }
38}
39
40// Usage
41const collection = new RouteCollection();
42collection.addRoute(route1);
43collection.addRoute(route2);
44collection.addRoute(route3);
45
46// All stops from all routes
47for (const stop of collection) {
48 console.log(stop.address);
49}
50
51// High priority stops from all routes
52for (const stop of collection.byPriority('high')) {
53 priorityDelivery(stop);
54}

Best Practices

1. Choose the Right Iterator Type

  • External: When you need fine-grained control
  • Internal: When you want simplicity and collection optimization
  • Generator: For most cases, provides flexibility and clarity

2. Prefer Lazy Evaluation

typescript
1// ✅ Lazy: efficient for large datasets
2*processLarge(): Generator<Result> {
3 for (const item of this.items) {
4 yield transform(item);
5 }
6}
7
8// ❌ Eager: processes everything upfront
9processAll(): Result[] {
10 return this.items.map(item => transform(item));
11}

3. Use Filtering Iterators

typescript
1// ✅ Filter during iteration
2*filterActive(): Generator<Stop> {
3 for (const stop of this.stops) {
4 if (!stop.completed) yield stop;
5 }
6}
7
8// ❌ Create intermediate array
9getActiveStops(): Stop[] {
10 return this.stops.filter(s => !s.completed);
11}

4. Implement Bidirectional When Needed

Only implement bidirectional iteration when it provides clear value (undo/redo, navigation history, etc.).


Key Takeaways

  1. External iterators give clients full control over iteration
  2. Internal iterators simplify client code but limit control
  3. Filtering iterators avoid creating intermediate collections
  4. Lazy evaluation defers computation until values are needed
  5. Infinite iterators generate values indefinitely
  6. Bidirectional iterators enable forward and backward traversal
  7. Composite iterators combine multiple iterators into one

Next Steps

In the upcoming workshop, you'll implement a complete delivery route iterator system with:

  • Iterable delivery routes
  • Filtering by zone and priority
  • Lazy evaluation with generators
  • Advanced traversal patterns

These advanced iterator patterns will help you build efficient, flexible data traversal systems for complex logistics operations.