Home > Blockchain >  When to set "done" to true in an iterator in javascript (off by one errors)
When to set "done" to true in an iterator in javascript (off by one errors)

Time:12-23

I've seen a few iterators implemented in javascript, and every time it seems like they're off by one (there's obviously something I'm missing) Here's an example

const numbers = [1, 2, 3];

numbers[Symbol.iterator] = function() {
  let idx = 0;
  return {
    next: function() {
      return {
        value: numbers[idx  ],
        done: idx > numbers.length,
      };
    },
  };
};

// Run the custom iterator:
for (const num of numbers) console.log(num);

In this example, done is set to true when idx > numbers.length. However doesn't this mean that numbers[3] will not be set as done = true (even though there's no value for numbers[3]? I don't fully understand why this code works.

Would appreciate any help — thanks!

CodePudding user response:

There are two things to realise:

  1. done does not indicate whether the returned value is the last one, but whether the iterator has no more value to return. As Mozilla contributors write:

    done

    A boolean that's false if the iterator was able to produce the next value in the sequence.

  2. idx evaluates and then increments, and so the expression for the done property works with an idx value that is one more than was used for setting the value property.

So we have these objects returned:

{ value: 1, done: false }
{ value: 2, done: false }
{ value: 3, done: false }
{ value: undefined, done: true }

Alternative code without side-effect

To eliminate the side effect of mentioned in point 2, it maybe helps to look at this version, which produces the exact same {value,done} objects:

const numbers = [1, 2, 3];

numbers[Symbol.iterator] = function() {
  let idx = -1; // <--- one less, so we can first increment
  return {
    next: function() {
      idx  ; // First increment
      return {
        value: numbers[idx], // then use
        done: idx >= numbers.length, // ...as expected
      };
    },
  };
};

// Run the custom iterator:
for (const num of numbers) console.log(num);

CodePudding user response:

The first time you call the iterator it sets value to numbers[0], increments idx to 1, then sets done to 1 > numbers.length. So it returns {value: 1, done: false}

The second time, it sets value to numbers[1], increments idx to 2, and sets done to 2 > numbers.length. So it returns {value: 2, done: false}.

The third time, it sets value to numbers[2], increments idx to 3, and sets done to 3 > numbers.length. So it returns {value: 3, done: false}.

The fourth time, it sets value to numbers[3], increments idx to 4, and sets done to 4 > numbers.length. So this time it returns {value: undefined, done: true}. This indicates that the iteration is complete.

The important thing is that the iterator doesn't set done: true on the last element, it does this when you've called it the next time after having reached the end. This design is necessary because some iterators may not be able to tell when they're returning the last element -- they need to try to get the next element and detect failure.

  • Related