3

How do I extend Array with functions that returns a generator object and be able to chain them?

I have tried extending the Array to see if I can chain generator functions. The first call works, but the next chain would not, because it returns a generator object and I have no idea how to extend that.

Array.prototype.select = function* (fn) {
    let it = this[Symbol.iterator]();
    for (let item of it) yield fn(item);
}

Array.prototype.where = function* (fn) {
    let it = this[Symbol.iterator]();
    for (let item of it) {
        if (fn(item)) yield item;
    }
}

I want to be able to chain generators like this to an array

let result = arr.select(v => v * 2)
                .where(v => v % 3 === 0);

for (let item of result) console.log(item);
Kamalesh M. Talaviya
  • 1,422
  • 1
  • 12
  • 26
  • 4
    Why not use built-in methods? `arr.map(v => v * 2).filter(v => v % 3 === 0)` – adiga May 24 '19 at 12:11
  • Extending the prototype is a risky proposition. See [Why is extending native objects a bad practice?](https://stackoverflow.com/q/14034180/215552). – Heretic Monkey May 24 '19 at 13:01
  • @adiga Im new to javascript and im experimenting how i can implement LINQ deferred execution. Im just curious with generators cause it reminds me of C# IEnumerator<> interface. – Jerome Gamo May 24 '19 at 13:15
  • @adiga checkout this [repl](https://repl.it/@JeromeGamo/CaringGleamingFile) to see what Im trying to achieve. – Jerome Gamo May 24 '19 at 14:05
  • 1
    @Kierkegaard I've already done [exactly this](https://github.com/patrickroberts/enumerable-ts). It's written in TypeScript but you can use it in node with JavaScript-only as it's pre-compiled. I do plan to update it soon to be umd format. – Patrick Roberts May 24 '19 at 20:25
  • @PatrickRoberts That is a really neat stuff you got there. – Jerome Gamo May 26 '19 at 10:08

3 Answers3

1

You could store both methods as a prototype function of Object and handover the iterator object, because both functions take a generator and returns an iterable object.

Object.prototype.select = function* (fn) {
    for (let v of this) yield fn(v);
}

Object.prototype.where = function* (fn) {
    for (let v of this[Symbol.iterator]()) if (fn(v)) yield v;
}

var array = [1, 2, 3, 4, 5, 6, 7, 8, 9],
    result = array[Symbol.iterator]()
        .select(v => v * 2)
        .where(v => v % 3 === 0);

console.log(...result);
Nina Scholz
  • 376,160
  • 25
  • 347
  • 392
  • Oh yeah, forgot about the Object, I am too caught up with translating c# inheriting to a specific class i need to extend. But is there a more specific way? or I should not make it complicated? – Jerome Gamo May 24 '19 at 13:23
1

Without modifying Object, you can still make these methods chainable by using a technique I've coined as "superclassing".

You start by defining your base class which Array will extend, then you modify the prototype chain of Array to artificially extend your base class.

Note how the select() and where() methods encapsulate your original generator functions to return new instances of the class so that the methods are chainable.

class Enumerable {
  constructor (getIterator) {
    this[Symbol.iterator] = getIterator;
  }

  select (fn) {
    return new Enumerable(function * () {
      for (const value of this) {
        yield fn(value);
      }
    }.bind(this));
  }

  where (fn) {
    return new Enumerable(function * () {
      for (const value of this) {
        if (fn(value)) yield value;
      }
    }.bind(this));
  }
}

Object.setPrototypeOf(Array, Enumerable);
Object.setPrototypeOf(Array.prototype, Enumerable.prototype);

const array = [1, 2, 3, 4, 5, 6, 7, 8, 9];
const result = array
  .select(v => v * 2)
  .where(v => v % 3 === 0);

for (const item of result) {
  console.log(item);
}
Patrick Roberts
  • 49,224
  • 10
  • 102
  • 153
0

If you do not mind overextending, the first iterator returns just an object. You can check by doing console.log(typeof arr.select(v => v * 2));.

Hence, you can simply define: Object.prototype.where = function* (fn) {};

Array.prototype.select = function* (fn) {
    let it = this[Symbol.iterator]();
    for (let item of it) {
      yield fn(item);
    }
};

Object.prototype.where = function* (fn) {
    let it = this[Symbol.iterator]();
    for (let item of it) {
        if (fn(item)) yield item;
    }
};


const arr = [1, 2, 3, 4, 5, 6, 7, 8, 9];
let result = arr.select(v => v * 2)
                .where(v => v % 3 === 0);

for (let item of result) {
  console.log(item);
}

If you want that the order does not matter, you could extend both Array and Object like this.

Array.prototype.select = function* (fn) {
        let it = this[Symbol.iterator]();
        for (let item of it) {
          yield fn(item);
        }
    };

Array.prototype.where = function* (fn) {
    let it = this[Symbol.iterator]();
    for (let item of it) {
        if (fn(item)) yield item;
    }
};

Object.prototype.select = Array.prototype.select;
Object.prototype.where = Array.prototype.where;


const arr = [1, 2, 3, 4, 5, 6, 7, 8, 9];

// Chain 1.
let result1 = arr.select(v => v * 2).where(v => v % 3 === 0);

console.log('Chain 1');
for (const item of result1) {
  console.log(item);
}

// Chain 2.
let result2 = arr.where(v => v % 3 === 0).select(v => v * 2);

console.log('Chain 2')
for (const item of result2) {
  console.log(item);
}
nitobuendia
  • 1,228
  • 7
  • 18