16

I came up with this simple experiment after reading the documentation on generators from MDN:

var nodes = {
    type: 'root',
    value: [
        { type: 'char', value: 'a' },
        { type: 'char', value: 'b' },
        { type: 'char', value: 'c' },
    ],
};

function* recursiveGenerator(node) {
    if (node.type === 'root') {
        node.value.forEach(function (subnode) {
            for (var suffix of recursiveGenerator(subnode)) {
                yield suffix;
            }
        });
    }

    else {
        yield node.value;
    }
}

for (generated of recursiveGenerator(nodes)) {
    console.log(generated);
}

Running it on node.js v0.11.9 with the --harmony flag set produces the following error:

alix@900X4C:~$ node --version
v0.11.9
alix@900X4C:~$ node --harmony test.js 

/home/alix/test.js:14
                yield suffix;
                      ^^^^^^
SyntaxError: Unexpected identifier

I also tried using for ... in ... and the let keyword instead of var, but without any success.

I don't understand what yield* does exactly, but if I use it within the for loop I get instead:

alix@900X4C:~$ node --harmony test.js 

/home/alix/test.js:14
                yield* suffix;
                ^
ReferenceError: yield is not defined

If I replace the yield in the for with console.log() it outputs a, b and c. What am I doing wrong?


Edit

Here's a minimalistic generator, showing that node.js knows what to do with generators:

function* alpha() {
    yield 'a';
    yield 'b';
    yield 'c';
}

for (var suffix of alpha()) {
    console.log(suffix);
}

Output:

alix@900X4C:~$ node --harmony y.js 
a
b
c

Solution (thanks @Andrew)

function* recursiveGenerator(node) {
    if (node.type === 'root') {
        for (var i = 0; i < node.value.length; ++i) {
            var subnode = node.value[i];

            for (var suffix of recursiveGenerator(subnode)) {
                yield suffix;
            }
        }
    }

    else {
        yield node.value;
    }
}

for (generated of recursiveGenerator(nodes)) {
    console.log(generated);
}
Community
  • 1
  • 1
Alix Axel
  • 141,486
  • 84
  • 375
  • 483
  • 1
    I'm a little confused by the `*`s in here at random. Are these actually in your code? They don't seem to be part of the spec you linked. – Chris Hayes Dec 30 '13 at 05:55
  • @ChrisHayes: I've added another link (look for `yield*` in the question). From what I gather, `function* ...` is a generator function (i.e., one that contains a `yield` keyword) and `yield*` is way of delegating the generator to another generator (I can't make it work tho). – Alix Axel Dec 30 '13 at 05:57
  • @ChrisHayes: Ah, I see what you mean... The MDN website doesn't uses `*` anywhere. Well, the [ECMAScript wiki](http://wiki.ecmascript.org/doku.php?id=harmony%3agenerators#syntax) does. And yes, this is actually in my code (trying to solve [this broader problem](http://stackoverflow.com/questions/20815278/porting-invregex-py-to-javascript-node-js) out), the reason I'm using `function*` is because node doesn't seem to recognize the asterisk-free version as a generator (even if it has the `yield` keyword within). – Alix Axel Dec 30 '13 at 06:01
  • 1
    Got it, thanks. In the case of `yield*`, from the error message, I think node is expecting `yield` to be a variable name. In both cases it certainly seems to me that it isn't identified as a keyword. – Chris Hayes Dec 30 '13 at 06:05
  • @ChrisHayes: Yeah, I would think the same thing, but the example I just added shows that generators clearly work with the `--harmony` flag. The question is, why only "simple" ones. – Alix Axel Dec 30 '13 at 06:10
  • 2
    I'm not familiar with generators enough yet to post as an answer, but I believe your problem is in the `forEach()`. That creates its own scope (like `setTimeout()`). If you change that to a `for (var i = 0; i < node.value.length; i++)` you should see it working. – Andrew Dec 30 '13 at 06:22
  • Ah ah! That was it! I get it now, the lambda function was not a generator and thus the yield keyword was invalid there. Well spotted @Andrew, thanks a bunch! – Alix Axel Dec 30 '13 at 06:26
  • 1
    @Andrew Great spot. Worth an answer IMO. Curious that node doesn't output a better error message here, but I guess it's early days for `yield`. – Chris Hayes Dec 30 '13 at 08:06

2 Answers2

28

Summarizing the comments: you can't use yield inside a regular function, so you can't use yield with forEach. Here an example of "generatorized" foreach:

function * foreach (arr, fn) {
  var i

  for (i = 0; i < arr.length; i++) {
    yield * fn(arr[i])
  }
}

function * gen (number) {
  yield number + 1
  yield number + 2
  yield number + 3
}

function * other () {
  yield * foreach([1, 2, 3], gen)
}

for (var i of other()) {
    console.log(i)
}

UPDATE Also the original problem can be solved quite elegantly using such a helper:

var nodes = {
  type: 'root',
  value: [
    { type: 'char', value: 'a' },
    { type: 'char', value: 'b' },
    { type: 'root', value: [
        { type: 'char', value: 'c' },
        { type: 'char', value: 'd' },
        { type: 'char', value: 'e' },
      ] 
    },
  ],
}

function * foreach (arr, fn) {
  var i

  for (i = 0; i < arr.length; i++) {
    yield * fn(arr[i])
  }
}

function * value (val) {
  yield val
}

function * recursiveGenerator(node) {
  yield * node.type === 'root' ?  foreach(node.value, recursiveGenerator) : value(node.value)
}

for (var generated of recursiveGenerator(nodes)) {
  console.log(generated);
}

So the generator itself becomes a one-liner!

vkurchatkin
  • 12,170
  • 2
  • 40
  • 50
  • 1
    *You can't use `yield` inside a function`* That's misleading. Inside a generator function is the *only* place you can use `yield`. +1 for the example code however. – Chris Hayes Dec 30 '13 at 19:57
  • @ChrisHayes: Will fix it. =) Thanks for the example, the `yield*` does actually help understanding what it does. – Alix Axel Dec 31 '13 at 06:59
  • @ChrisHayes what is exactly misleading? generator function is the only place you can use yield => you can use yield inside a function. Or do you mean it's better to say something like 'you can't use yield inside non-generator function'? – vkurchatkin Dec 31 '13 at 10:25
  • @vkurchatkin Yes, that's what I meant. Sorry, my comment was confusing there. – Chris Hayes Dec 31 '13 at 10:47
3

You've found your solution, but just for the record here is another example a little different that print the types of all nodes in the tree (I added some deepness and vars)

var nodes = {
    type: 'root',
    value: [
        { type: 'char', value: 'a' },
        { type: 'char', value: 'b' },
        { type: 'char', value: [{type: 'int', value: 'c'}] },
    ],
};

var flattenTree = function* (root) {
    yield root.type;
    var subvalues = root.value;
    for(var i in subvalues) {
        var gen = flattenTree(subvalues[i]);
        val = gen.next();
        while(!val.done) {
            if(val.value != undefined)
                yield val.value;
            val = gen.next();
        }
    }
}

var printTree = function() {
    console.log("begin tree");
    var generator = flattenTree(nodes);
    var next = generator.next();
    while(!next.done) {
        console.log(next);
        next = generator.next();
    }
    console.log("finish tree");
}

printTree();

Outputs:

~/workspace/tmp$ ../node/node --harmony test-gen.js 
begin tree
{ value: 'root', done: false }
{ value: 'char', done: false }
{ value: 'char', done: false }
{ value: 'char', done: false }
{ value: 'int', done: false }
finish tree
nico
  • 584
  • 9
  • 22