skip to content

How do you merge two sorted iter.Seq[string] sequences in order using iter.Pull?

level: middleimportance: should knowfreq 26%

answer

  1. two cursors alive at once
  2. prime each one before the loop
  3. compare heads, emit the smaller
  4. advance only the side you emitted
  5. one side dry means drain the other

basics

~20 s

Pull both sequences to get two independent cursors, prime each with one call to next, then repeatedly emit the smaller head and advance only that cursor. When one side's bool goes false, keep draining the other. Defer both stop functions.

solid answer

~40 s

Call `iter.Pull` on each sequence, so you hold `nextA, stopA` and `nextB, stopB`, and `defer` both stops immediately. Prime the merge by calling each `next` once, keeping the current value and its `ok` flag for each side. Then loop while either side is still `ok`: emit the smaller of the two heads and advance **only** the cursor you emitted from, so the other head stays available for the next comparison. When one side's `ok` is false, the comparison degenerates and you drain the remaining side. This is the canonical reason `iter.Pull` exists — a `for range` over each sequence cannot do it, because each range runs its sequence to completion before the next statement. Advancing both cursors per iteration is the classic bug; it silently drops every other element from one side.

code

go · 24 lines
go
func mergeLines(a, b iter.Seq[string], out io.Writer) error {
	nextA, stopA := iter.Pull(a)
	defer stopA()
	nextB, stopB := iter.Pull(b)
	defer stopB()

	va, okA := nextA()
	vb, okB := nextB()
	for okA || okB {
		switch {
		case okA && (!okB || va <= vb):
			if _, err := io.WriteString(out, va+"\n"); err != nil {
				return err
			}
			va, okA = nextA()
		default:
			if _, err := io.WriteString(out, vb+"\n"); err != nil {
				return err
			}
			vb, okB = nextB()
		}
	}
	return nil
}

go deeper

for a junior

Know the vocabulary: each iter.Pull gives you a cursor, and a merge needs two of them alive at once. Remember to prime each cursor with one call before the comparison loop starts.

for a middle

Be ready to write it. The interviewer is watching for advancing only the emitted side, draining the remaining side after one goes dry, and deferring both stop functions.

for a senior

Talk about what happens on the abnormal paths: a write error, an early break, a tie-breaking rule that must be deterministic, and what each suspended sequence is still holding when you return.

for a principal

Decide what the merge should publish. A helper returning iter.Seq keeps callers free to range or pull, while one writing to an io.Writer fixes the consumption model for everyone who uses it.

## The problem A log-shipping step reads two already-sorted streams — say two `io.Reader`s over rotated log files, each yielding lines in timestamp order as an `iter.Seq[string]` — and has to write one merged, still-sorted stream out. This is the textbook use of `iter.Pull`, and it is worth understanding why the push form cannot do it. With `for line := range a`, the loop body becomes the `yield` callback and the sequence drives: the loop runs to completion before the statement after it executes. You are never suspended in the middle of two sequences at once, so you can never compare their two current heads. `iter.Pull` inverts that: each pull gives you a cursor you advance on demand, and holding two cursors at once is exactly what a merge needs. ## The shape ``` nextA, stopA := iter.Pull(a) defer stopA() nextB, stopB := iter.Pull(b) defer stopB() ``` Two pulls, two defers, each defer on the line after its pull so no early return can skip it. Then **prime** both cursors — call each `next` once before the loop — and keep four variables: the current value and the current `ok` for each side. ``` va, okA := nextA() vb, okB := nextB() ``` The loop condition is `okA || okB`: keep going while *either* side still has something. Inside, pick the side to emit: - if only A is live, emit A; - if only B is live, emit B; - if both are live, emit whichever head sorts first. A `switch` with no expression expresses that compactly, with the two-sided comparison folded into one case: `case okA && (!okB || va <= vb):`. ## The rule that makes it correct **Advance only the cursor you emitted from.** The head you did not emit is still unconsumed and must remain available for the next comparison. Calling both `next` functions once per iteration is the most common mistake here, and it is quiet: the output is still sorted-looking, but half of one input has vanished. It is quiet in the other direction too — if you emit a head and forget to advance its cursor, you loop forever writing the same line. Priming matters for the same reason. If you call `next` inside the comparison, you consume a value in order to look at it and then have nowhere to keep it. The cursor gives you one value at a time; the merge state is the two values you are currently holding. ## Stability and ties Using `<=` rather than `<` when A and B tie makes the merge **stable** with respect to the argument order: equal elements from A come out before equal elements from B. That is a deliberate choice, and for log lines with identical timestamps it is usually the one you want, because it is deterministic. Flipping to `<` reverses tie order; both are sorted, only one is reproducible in the way readers expect. ## Errors and cleanup If the merge writes as it goes, a write error is an early `return`, and that is where the two deferred stops earn their place: they resume each suspended sequence with `yield` returning `false`, so each iterator returns and closes the reader it opened. Without the defers, a merge that returns early — on a write error, on a `break` after N lines, or simply because one side ran out and the code returned right there — leaves a suspended sequence behind on every call. If a sequence needs to report its own read error, the usual shape is `iter.Seq2[string, error]` and `iter.Pull2`, whose `next` returns `(string, error, bool)`. The merge logic is unchanged; you just carry an error alongside each head and abort when one is non-nil. ## Zipping is the same skeleton A zip — pair the nth element of A with the nth of B — is the same two-cursor structure with a simpler rule: advance **both** cursors every iteration and stop when either reports `false`. The reason both are natural here and neither is expressible with `range` is identical: you need two cursors alive at the same time. ## Cost Each `iter.Pull` sets up a coroutine and each `next` is a control switch rather than a plain call. For a merge over large streams that is entirely reasonable — the work per element dwarfs the switch. For merging two small in-memory slices, indexing them directly is simpler and faster; do not reach for pull cursors when you already have random access.

  • What changes if you want to zip the two sequences into pairs instead of merging them?
    The skeleton is the same two cursors, but the rule flips: advance **both** cursors every iteration and finish as soon as either reports `false`, since a pair needs one element from each side. There is no comparison, and the shorter sequence decides the length.
  • Why prime both cursors before the loop rather than calling next inside the comparison?
    Because `next` consumes. If you call it to look at a head you have already taken that element, and the loser of the comparison has nowhere to go. Holding one value plus one ok flag per side is the merge's state; the cursor only ever lends you the front element once.
  • How would the merge handle sequences that can fail mid-stream?
    Publish them as `iter.Seq2[string, error]` and pull with `iter.Pull2`, whose `next` returns `(string, error, bool)`. Carry the error next to each head and return as soon as one is non-nil. The deferred stops then shut down both sides, including the healthy one.

saying these in an interview costs you the question

  • Advances both cursors on every iteration, dropping elements
  • Emits a head but forgets to advance that cursor, looping forever
  • Returns as soon as one side is exhausted, truncating the other
  • Tries to nest two range loops and produces a cross product
  • Defers only one of the two stop functions