Who uses Generators here?

Anybody using Generators in their Meteor projects?

I’d like to learn more about them, but – like Promises, I feel like it’s something you just have to use to actually learn. Reading a ton about them won’t do squat.

What are you using them for and why is it beneficial for your use case?

Simple code examples would be appreciated too

I was just reading about them here, and I guess they can be used to generate a list of objects to be processed further in another function. So for example, if the rows in your db need a lot of data added/modified before they can be used for some particular purpose, you can use a generator on them.

I’m sure others on this forum can provide additional info.

1 Like

There are three types you should learn about

You can create them without any function* syntax. I use some simplified python range function as an example, a sequence starting at start and ending at stop - 1:

Definition: An Iterator must have a next method/property that returns {done: boolean, value?: any}.

Intrinsically an Iterator must maintain some kind of state.

const iterator = (start, stop) => {
  let current = start;
  return {
      next: () => {
        if (current >= stop) return {done: true};
        return {done: false, value: current++};
      }
  };
};

You can consume an instantiated Iterator it = iterator(start, stop) by repeatedly calling it.next();. The done key tells you whether the iterator is “done”: if the done key is true, the value has no meaning and further calls to next will the done key set to true. If the done key is false, then value has meaning.

Definition: An Iterable must define a [Symbol.iterator] method/property that returns an Iterator.

const iterable = (start, stop) => ({
    [Symbol.iterator]: () => iterator(start, stop)
});

You can consume an Iterable either by calling its [Symbol.iterator] method/property to get an Iterator that you consume as above, or by using the for (const value of iterable(start, stop)) syntax that iterates over the values yielded by the iterable’s iterator, and breaks as soon as done is true. You can also use Array.from to create an Array from a (finite) iterable.

Definition: A Generator is both an Iterable and an Iterator.

It’s more idiomatic to use prototype or a class but it also works with an object literal:

const generator = (start, stop) => {
  let current = start;
  const self = {
    [Symbol.iterator]: () => self,
    next: () => {
      if (current >= stop) return {done: true};
      return {done: false, value: current++};
    }
  };
  return self;
};

:warning: Generator also gives back meaning to value even when done is true with the return method/property. It also defines a throw method/property. I would consider this advanced usage. I have never used it myself.

The function* syntax creates a GeneratorFunction that returns a Generator. The values yielded are the ones you yield with the yield and yield* syntax.

Iterative:

const generator = function* (start, stop) {
  let current = start;
  while (current < stop) yield current++;
};

Recursive:

const generator = function* (start, stop) {
  if (start < stop) {
    yield start;
    yield* generator(start+1, stop);
  }
};

Some remarks:

:warning: For some reason, TypeScript defines IterableIterator ~instead of~ in addition to Generator, ~merges~ also has the additional return and throw Generator methods/properties as optional into Iterator, and forces the method/property [Symbol.iterator] of IterableIterator to return an IterableIterator, and the method/property [Symbol.iterator] of Generator to return a Generator.

:bulb: You can also use class or prototype syntax to create iterables and iterators, for instance:

class MyIterator {
  constructor (start, stop) {
    this.current = start;
    this.stop = stop;
  }
  next() {
    if (this.current >= this.stop) return {done: true};
    return {done: false, value: this.current++};
  }
}

or

function MyIterator (start, stop) {
  this.current = start;
  this.stop = stop;
}

MyIterator.prototype.next = function {
  if (this.current >= this.stop) return {done: true};
  return {done: false, value: this.current++};
};

:bulb: From there you can easily derive asynchronous iterators/iterables/generators:

  • The next method/property of an AsyncIterator must return a Promise<{done: boolean, value?: any}>
  • [Symbol.iterator] of Iterable is replaced by [Symbol.asyncIterator] of AsyncIterable and returns an AsyncIterator
  • for (const ... of someIterable) syntax is replaced by for await (const ... of someAsyncIterable)
  • Unfortunately, you have to roll out your own version of the asynchronous version of Array.from as it does not exist yet (and Promise.all is a false friend: it needs a synchronous Iterable of Promises to execute all promises in parallel, so it will not work with an asynchronous iterable).
  • AsyncGenerators combine function*/yield and async/await syntaxes:
const asyncGenerator = async function* (urls) {
  for (const url of urls) {
    const response = await fetch(url);
    yield response;
  }
};
5 Likes

I’ve used a generator in one of my projects, although that code was not Meteor specific. They can be very nice in some use cases, but there’s always a way to do it without as well.

Here’s the code, it’s a function that returns one of a set of colors in a loop:

export function *loop(): Generator<string, null, void> {
	const colors = [ mint, pink, lightblue, keylime, mint, sunshine, crimson ]
	while ( true ) yield *colors
}

When using it (in a list or something), you would first do

const color = loop()

And then for each item

<Item color={color.next().value} />

As said, it’s nice but there’s definitely other ways of doing this as well. I have not found other use cases in daily coding myself.

2 Likes

I’ve never seen this kind of syntax in JS. Reminds me of a List in Java

Ah, guessing that was just comment for function args

That’s typescript generic syntax. It’s not part of the JS.

2 Likes

Thanks for clarifying. Haven’t used TS yet. Do many people implement it with Meteor?

And here’s the answer to the question “What are you using them [generators] for?”: Everywhere I want to produce an array of stuff. Here is why:

  • yield item syntax is more declarative than const results = []; results.push(item); return results;: when you write yield it yields.
  • Since the responsibility for consumption is delegated to the caller, you can stop early. A corollary is that the sequence of yielded values can be infinite, contrary to arrays. Example: generate a sequence of candidate return values and stop as soon as you have a passing candidate.
  • If I ever want an Array from a (finite) Iterable I call Array.from.

The only case where I revert back to Array is when there is proven critical performance improvement. And this might not always be the case: if your sequence is large, it may actually be better to process each item one at a time, to avoid having one large temporary array filling the memory.

5 Likes