Wednesday, February 3, 2016

Sane ECMAScript 6 Generators

This post is cross posted in Medium, here.

I recently found one interesting thing about ES6 Generators. I come from Python background and I understood generators as in Python. So, I expected the following Python code's equivalent ECMAScript 6 code also to work as well.
>>> numbers = (num for num in range(10))
>>> for num in numbers:
...   if num == 3:
...     break
... 
>>> next(numbers)
4
You can find the online demo for this Python program, here.

But then, when I used Babel to transpile the following code and executed it
function* NumberGenerator() {
  for (var i = 0; i < 10; i += 1) {
    yield i;
  }
}

var numbers = NumberGenerator();

for (var num of numbers) {
  if (num === 3) {
    break;
  }
}

console.log(numbers.next());
// {"done":true}
You can find the online demo for this JavaScript program, made with Babel's REPL, here. As you see here, when I broke out of the loop, the Generator Object got closed. This was pointed out to me by Logan Smyth in Babel's Slack discussion. I was really surprised by this behavior and found the 13.7.5.13 Runtime Semantics: ForIn/OfBodyEvaluation ( lhs, stmt, iterator, lhsKind, labelSet ) section in the ECMAScript 6 Specification, which says
If LoopContinues(result, labelSet) is false, return IteratorClose(iterator, UpdateEmpty(result, V)).
I am not sure about the rationale behind that decision, but I am convinced that it would effectively limit the potential of the Generators. So I decided to fix this.

Sane Generators

To close the iterator, Iterator.prototype.return is called. (At the time of this writing, not many JavaScript Engines support this function. You can find the support for this feature by popular engines, here.) So I decided to override that and allow the actual return function to be invoked only when explicitly called with an argument.
function returnFunction(originalReturn, genObject) {
  return function(arg) {
    return arguments.length ? originalReturn.call(genObject, arg) : {
      done: false
    };
  };
}

function SaneGenerator(genObject) {
  var originalReturn = genObject['return'];

  if (typeof originalReturn === 'function') {
    Object.defineProperty(genObject, 'return', {
      value: returnFunction(originalReturn, genObject)
    });
  }

  return genObject;
}
You can see the actual and complete implementation in my GitHub repository, https://github.com/thefourtheye/sane-generator. Now, you can use the SaneGenerator like this
function* NumberGenerator() {
  for (var i = 0; i < 10; i += 1) {
    yield i;
  }
}

var numbers = SaneGenerator(NumberGenerator());

for (var num of numbers) {
  if (num === 3) {
    break;
  }
}

console.log(numbers.next());
// {"value":4,"done":false}
You can find the online demo for this JavaScript program, made with Babel's REPL, here.

NPM Module

This is available as an NPM module now. https://www.npmjs.com/package/sane-generator

No comments: