7

I tried to filter a generator and had the expectation that this kind of general functionality must be defined anywhere in JavaScript, because it is defined for Arrays, but I can not find it. So I tried to define it. But I can not extend the built-in generators.

I have an example generator

function make_nums ()
{
  let nums = {};
  nums[Symbol.iterator] = function* () {
    yield 1;
    yield 2;
    yield 3;
  };
  return nums;
}

generating some numbers.

[...make_nums()] // => Array [ 1, 2, 3 ]

If I build an array, I can filter the array by the use of the filter function for arrays.

[...make_nums()].filter(n => n > 1) // => Array [ 2, 3 ]

But I do not want to build an array. Instead I want to take the old generator and build a new filtering generator. For this I wrote the following function.

function filtered (generator, filter)
{
  let g = {};
  g[Symbol.iterator] = function* () {
    for (let value of generator)
      if (filter(value))
        yield value;
  };
  return g;
}

which can be used to do what I want.

[...filtered (make_nums(), n => n > 1)] // => Array [ 2, 3 ]

But this is a very general function, which can be applied to every generator in the same way the filter function can be applied to every Array. So I tried to extend generators in general, but I do not understand how.

The MDN documentation for generators suggests somehow that Generator.prototype may exist, but it does not seem to exist. When I try to define something in Generator.prototype, I get the error

ReferenceError: Generator is not defined

How can I extend the built-in Generator class?

ceving
  • 21,900
  • 13
  • 104
  • 178
  • "*But this is a very general function*" - what's wrong with that? – Bergi Nov 28 '17 at 15:14
  • Why would you want to extend `Generator`s when you could also extend [all iterators](http://www.ecma-international.org/ecma-262/6.0/#sec-%25iteratorprototype%25-object)? – Bergi Nov 28 '17 at 15:19

2 Answers2

6

With the traditional caveat that extending built-in prototypes is not necessarily the best idea, and that it's something to be done with caution, you can get the generator function prototype with

const genproto = Object.getPrototypeOf(function*(){});

With that you could add a filter() capability:

Object.defineProperty(genproto, "filter", {
  value: function*(predicate) {
    for (let value of this())
      if (predicate(value)) yield value;
  }
});

And thus:

console.log([... function*() {
    for (let i = 0; i < 10; i++) yield i;
  }.filter(value => value % 2 === 0)
]);

will print [0, 2, 4, 6, 8].

To be clear: this answer is about extending the prototype for generator functions, not the generator objects themselves. Thus this will ensure that every generator function in the program can use that .filter() method and any other similar extension.

Pointy
  • 405,095
  • 59
  • 585
  • 614
  • OP wants to extend `Generator`, not `GeneratorFunction` – Bergi Nov 28 '17 at 15:16
  • @Bergi well I could not figure out how to make sense of that; you may be right however. This is something I'd never thought of but it does seem kind-of useful once I forgive myself for extending that prototype object. Things like `.take()` etc are really easy to implement. – Pointy Nov 28 '17 at 15:21
  • Just change `getPrototypeOf(function*(){})` to `getPrototypeOf(function*(){}())` and `this()` to `this`. – Bergi Nov 28 '17 at 15:23
  • @Bergi well sure, but what I meant was that the generator object itself doesn't necessarily inherit from that prototype; the iterable/iterator protocols are just that: protocols. Thus it wouldn't be possible to know that a given generator object would have the extended capabilities. But maybe I'm missing something; it makes my head hurt a little. – Pointy Nov 28 '17 at 15:41
  • [This diagram](http://www.ecma-international.org/ecma-262/8.0/#sec-generatorfunction-objects) may help (probably both to clarify, and to make your head hurt even more :P). All generator objects (created by calling a generator function) do inherit from `GeneratorPrototype` and `IteratorPrototype`. – Bergi Nov 28 '17 at 16:14
  • @Bergi right, I get that. And I think I was wrapped around a sort-of bogus concern that "home-made" generators would make code fragile; they would, but I don't think making home-made generators is a very good idea in the first place. Thank you for the link. – Pointy Nov 28 '17 at 19:23
5

It turned out that I was confused about Generator and Iterable. I thought I had to extend Generator, but it is actually sufficient to extend Iterable. And the fact that Iterable does not seem to be defined by JavaScript either, makes it easy to define it, to avoid problems with modifications of built-in prototypes.

This actually does what I tried to achieve.

class Iterable {
  constructor (generator) {
    this[Symbol.iterator] = generator;
  }
}

Iterable.prototype.filter = function (predicate) {
  let iterable = this;
  return new Iterable (function* () {
    for (let value of iterable)
      if (predicate (value))
        yield value;
  });
};

I can create Iterables

make_nums = new Iterable(function* () { yield 1; yield 2; yield 3; });

can use them

[...make_nums] // => Array [ 1, 2, 3 ]

and can filter them

[...make_nums.filter(n => n > 1)] // => Array [ 2, 3 ]
ceving
  • 21,900
  • 13
  • 104
  • 178