Justin Wallace
Justin Wallace

Reputation: 102

Force a consumer to use and entire iterator

Is there a way to force a consumer to use an entire iterator?

For example:

const [first] = tuple // throws "Need to use both"
const [first, second] = tuple // works

I was hoping something like:

*[Symbol.iterator]() {
    const tuple = this.error
      ? ([null, this.error] as const)
      : ([this.value, null] as const);

    yield tuple[0];

    return {
      done: true,
      value: (() => {
        throw new Error("Must destructure both values from tuple");
      })(),
    };
  }

Would work, but doesn't. Is this even possible? I can't really think of a solid way but would love some help from the big brains.

Upvotes: 1

Views: 90

Answers (1)

Hao Wu
Hao Wu

Reputation: 20867

It is possible but I'm not sure why you're doing this. It's time to abuse finally to mess up the control flow if you really insist.

const tuple = {
  *[Symbol.iterator]() {
    let hasError = true;
    try {
      yield 1;
      hasError = false;
      yield 2;
    } finally {
      if(hasError) {
        throw new Error('Must destructure both values from tuple');
      }
    }
  }
}

const [first] = tuple;  // Error: Must destructure both values from tuple
console.log(first);

const tuple = {
  *[Symbol.iterator]() {
    let hasError = true;
    try {
      yield 1;
      hasError = false;
      yield 2;
    } finally {
      if(hasError) {
        throw new Error('Must destructure both values from tuple');
      }
    }
  }
}

const [first, second] = tuple;  // works fine
console.log(first, second);

Why It Works

According to try...catch

Control flow statements (return, throw, break, continue) in the finally block will "mask" any completion value of the try block or catch block. In this example, the try block tries to return 1, but before returning, the control flow is yielded to the finally block first, so the finally block's return value is returned instead.

function doIt() {
  try {
    return 1;
  } finally {
    return 2;
  }
}

doIt(); // returns 2

finally is a bit cursed (but makes sense), it always executes when exiting a try block, regardless of how the exit happens.

Bare that in mind and let's disect this statement:

const [first] = tuple
  1. tuple is referenced, which is the object with an iterator.

  2. const [first] is executed, it's a destructuring statement, that causes *[Symbol.iterator]() to run, and requesting for exactly one value.

let hasError = true;
try {
  yield 1;
  hasError = false;
  yield 2;
} finally {
  if(hasError) {
    throw new Error('Must destructure both values from tuple');
  }
}
  1. After yield 1 is executed, the requested quantity of values (one) has met, so the generator function is halt and ready to exit, but before exiting the try body, the finally has to run.

  2. Because it hasn't reached hasError = false part, the if(hasError) resolves true thus the error is thown before the function truly exits.

However on step 2, if the statement was const [first, second], it's requesting two values instead. So it will not try to exit the try body after yield 1, so hasError = false has reached, thus when the finally block is executed after yield 2, if(hasError) will fail and the error will no longer be thrown.

Upvotes: 4

Related Questions