Representative interview topic

General interview: How does Iterator.concat compose lazy data sources?

GeneralHard
Offer.cc Editorial TeamPublished Updated

Question

Implement a lazy iterator that combines an array, a Set, and a generator, then explain the boundaries between Iterator.concat, Array.concat, Iterator.from, and flatMap.

1. Prompt and scope

A log aggregator reads an in-memory array, a deduplicated Set, and a paginated generator in priority order; the consumer needs only the first 100 records. Use Iterator.concat while preserving laziness, and explain infinite inputs, exceptions, and cleanup when consumption stops early.

2. What the interviewer is testing

  • Distinguish Iterable from Iterator and know that Iterator.concat requires Iterable arguments.
  • Explain that the result is a new lazy Iterator that pulls inputs in sequence only when consumed.
  • Identify that an infinite source hides every later source and that the argument list must remain finite.
  • Handle return cleanup, TypeError cases, compatibility fallback, and unnecessary array materialization.

3. Questions to clarify first

  1. Is each input a repeatable Iterable or an already-advanced single-use Iterator?
  2. Does the consumer need a streaming interface or an array at the boundary?
  3. Do any inputs own a file, network, or database cursor that must close on early stop?
  4. Do target browsers and Node provide native Iterator.concat, or is a polyfill allowed?

4. Thirty-second answer

I would verify that every argument implements Symbol.iterator, then create a lazy Iterator with Iterator.concat. It acquires each input in order and reads values only when the consumer calls next, take, or spreads the result. take(100) stops pulling and gives the underlying iterator a chance to close. Put infinite inputs last or bound consumption; on older runtimes, use a generator fallback with the same laziness and cleanup semantics.

5. Step-by-step deep dive

Step 1: Establish the Iterable boundary

Iterator.concat accepts Iterables, not merely objects with next. Arrays, Sets, and generator objects qualify. A bare Iterator should first be wrapped with Iterator.from; this gives concat a clear way to obtain and close each input iterator.

Step 2: Build the lazy composition

js
function* pages() {
  yield { source: "page", id: 1 };
  yield { source: "page", id: 2 };
}

const memory = [{ source: "memory", id: 1 }];
const unique = new Set([{ source: "set", id: 1 }, { source: "set", id: 2 }]);
const merged = Iterator.concat(memory, unique, pages());
const firstThree = merged.take(3).toArray();

Creating merged does not traverse any input; toArray starts consumption. Values are emitted in memory, Set, and pages order, and concat does not deduplicate or transform them.

Step 3: Understand consumption state

The returned Iterator stores the current input and position. After one consumer advances it, another consumer sees only the remainder. Replaying requires fresh Iterators from the source Iterables. If only the first N values are needed, apply take after concat instead of spreading first.

Step 4: Bound infinite inputs

Any input may be infinite, making the result infinite. If the first input never ends, later inputs are unreachable; place finite batches first or enforce take, timeout, and cancellation at the consumer boundary. Do not write Iterator.concat(...infiniteIterables()), because spreading the argument list never finishes.

Step 5: Cleanup, errors, and fallback

When consumption stops or throws, the current iterator should get an opportunity to run return, releasing cursors, file handles, or connections. A custom iterator can record cleanup in finally. On runtimes without native support, a generator can yield* inputs in order and propagate return; the fallback must not silently copy every input into an array.

6. Model high-quality answer

I would validate Iterable inputs, create a lazy composition with Iterator.concat, and call take(100) or toArray only at the consumer boundary. concat acquires inputs in order and does not deduplicate their values. An infinite first input makes later sources unreachable, so I would bound consumption and test early-stop cleanup through return. For older runtimes, a generator fallback can preserve laziness, ordering, single-use state, and cleanup without materializing the sources.

7. Common mistakes

  • Passing an object with only next → TypeError → wrap it with Iterator.from or implement Symbol.iterator.
  • Assuming concat executes immediately → hidden I/O → remember that consumption pulls values.
  • Putting an infinite Iterable first → later sources are never reached → reorder and bound consumption.
  • Spreading everything first → memory and latency spikes → call toArray only at an explicit array boundary.
  • Ignoring early-stop return → leaked cursors → test a counting Iterable and assert cleanup.

8. Follow-up questions

Follow-up 1: How does it differ from Array.concat?

Array concat processes materialized arrays immediately and returns an array. Iterator.concat accepts Iterables and returns a lazy Iterator that can represent generators and infinite sources.

Follow-up 2: Why not pass every Iterator directly?

A bare Iterator that is not Iterable leaves ownership and closure of not-yet-reached inputs ambiguous. Wrap it with Iterator.from to establish the Iterable boundary.

Follow-up 3: When is flatMap preferable?

When the input itself is a large or unbounded sequence of Iterables, flatMap can produce and flatten them incrementally. concat is clearer for a finite, known set of inputs.

Follow-up 4: How do you verify cleanup?

Use a test Iterable implementing next and return, record pulls and closes, and assert that take, consumer interruption, and callback errors stop pulling and call return.

Public sources

Related questions