iterator next method returning new object

# Mark Volkmann (10 years ago)

I know there was a discussion about this recently, but I don't recall seeing a reason why it would be problem for a custom iterator to return the same object over and over with different values for the value and done properties. I tried this in some sample code using Traceur and Babel and it works fine. What are the potential problems with doing that?


R. Mark Volkmann Object Computing, Inc.

# Jeremy Martin (10 years ago)

Seems handy for low allocation, performance critical iterators. As long as the contract was clear, I would love something like this.

# Benjamin (Inglor) Gruenbaum (10 years ago)

Well, if the user caches the result (in order to pass it to another iterator) they'll get unexpected results.

Iterator A yields:


{value: 1, done: false}
{value: 2, done: false}
{value: 3, done: false}
{value: 4, done: false}

There are two consumers for the iterator (sending the result to remote servers that perform a calculation) which do something like:

function consume(){
    var toSend = iterator.next();
    if(toSend.done) return Promise.resolve();
    return fetch("/mydata?id="+toSend).then(function(result){
         return {value: toSend.value, response: result};
    });
}

Returning a mutable object will cause a race condition which will cause incorrect results sometimes. Whether or not people will actually do this is an interesting question but IMO returning mutable objects is a big lose here.

# Jason Orendorff (10 years ago)

The most important things here are:

  1. Like all performance hacks, unless you've measured a speed improvement under fairly realistic workloads, you shouldn't do this.

  2. Whenever you need a custom iterator, try writing a generator instead. It's amazing. You can get the behavior you want in a fraction of the developer time, with clearer and much smaller code.

As to your question, here are some (additional) seeds of doubt as requested:

  • Storing this object isn't entirely free. You have to create a property (or closed-over variable, same deal, roughly) to stash it in, and each time .next() is called, you have to access that property or variable. Also, storing the object prevents the most-recently-iterated value from being GC'd. (Normally this won't matter. It could matter if you have thousands of iterators going at once, and each one is only occasionally consulted.)

  • If the caller is a jerk, they might do something like freeze the object (which means your .next() method will keep getting called forever, since it can't set .done to true) or replace the .value property with an accessor, and then perhaps your next() method would throw or something. You probably never care about stuff like this, and that's OK. Certain very rare users do have to care. Worse, your JS engine's JIT does have to care.

  • The statement return {value: v, done: false}; can't fail, whereas cachedObject.value = v can fail, in the unlikely cases described above, or can trigger a setter, and therefore may require your JS engine to check something each time you do it. That's extra work and it could slow things down.

  • The JS engine might be smart enough (if not now, then a year down the road) to optimize away the temporary object created by return {value: v, done: false};. If it does, then using a new object is probably better for your JS engine's optimizer, because getting .value or .done off of that object is guaranteed to produce v and false, respectively, with no side effects and no possibility of triggering any getters, setters, proxy traps, etc.

# Andrea Giammarchi (10 years ago)

about preventing jerkiness around the object passed around ...

with some logic similar to this one:

function proxiedOwnValues(obj) {
  return Object.freeze(Object.keys(obj).reduce(function (o, k) {
    return Object.defineProperty(o, k, {
      get: function () {
        return obj[k];
      }
    });
  }, Object.create(null)));
}

we can have one object per generator, and one passed around via .next() so that

// not exposed
var internal = {value: undefined, done: false};

// returned via .next()
var passedAround = proxiedOwnValues(internal);

// when the value gets updated
internal.value = 123;

// is reflected in the outer world
passedAround.value; // 123

// when everything is done
internal.done = true;

// is reflected as well
passedAround.done; // true

So we have preserved contract, people stopping polluting objects they don't own, and read only operations that could be optimized even more behind the scene.

Does any of this make sense?

Best