loading...

A Quick, Practical Use Case for ES6 Generators: Building an Infinitely Repeating Array

sreisner profile image Shawn Reisner Originally published at itnext.io on ・3 min read

Preface

You’ve probably heard of ES6 generators, perhaps you’ve even learned the syntax, and you may be wondering what they’re actually useful for in real life.

Definition (from MDN)

Generators are functions which can be exited and later re-entered. Their context (variable bindings) will be saved across re-entrances.

You may be thinking, “Okay, but why would I want to do that?” As it turns out, there are a whole range of use cases ranging from simple to complex, many of which involve Promises to make asynchronous requests (async/await is built on top of generators). My goal is to give you the first baby step into understanding how they work with a simple, real-life example so that you begin noticing when a generator is the most suitable solution to problems in your own code. Here we go.

Use Case

I’m building an app in which my users can calculate a 3-week workout cycle, with a setting to work out between 3 and 7 days per week during the cycle. Each individual workout is based on one of the following 4 lifts: squat, bench press, deadlift, and overhead press, and each successive workout must be based on the next lift in that order:

  • Squat
  • Bench
  • Deadlift
  • Overhead press
  • Squat
  • Bench
  • Deadlift
  • Overhead press

You can probably see where this is going.

I need my code to say, “Give me the lift for the next workout, then the next, then the next, etc. When the end of the list of lifts is reached, start over from the beginning and keep repeating forever, until I’ve generated all the workouts for the 3-week cycle.” Here’s a simplified version of how I initially implemented it, without generators:

Not too bad, but it could be more declarative. Wouldn’t it be nice if we didn’t have to keep track of that currentLiftIndex directly in our workout generation code? It decreases the readability of the code and feels like it belongs in its own function. Here’s the code using a generator function, I’ll explain it below.

Here, the code is more declarative and readable. We abstracted the index-tracking logic into a general-purpose utility function called repeatedArray. The function * syntax tells JavaScript that this is a generator function. All we have to do is ask for the next item in the “repeated array” and our generator gives it to us. The best part is we don’t have to worry about how it’s happening outside of our generator function.

Here’s what’s happening:

repeatedArray returns an iterator object for the repeatedArray function itself (read that twice) when we call it on line 9. The iterator is stored in a variable named nextLiftGenerator. It’s important to understand that the code in the function hasn’t been executed at this point. The function is only executed when we call the next() function on the nextLiftGenerator iterable, and it’s only executed up until it hits a yield. Our generator gives us the value, then waits until the next call to continue execution until it hits another yield, then returns that value. Make sense? That’s it!

This is obviously a very simple example, but hopefully it helped you understand how generators work, and also why generators are such a powerful feature in JavaScript.

If you liked this article, follow me on Twitter and Instagram for more nerdy content!

Happy coding!


Posted on by:

Discussion

markdown guide
 

Cool article! thanks for the nice read, I always wanted a use case for generators but I was never able to decide when to use them (some times I keep this idea) but I remember usin Generators to retrieve paginated mongo cursors from a collection with a registry over hundreds of thousand records, we had to process each record individually (at least that was what I was tasked to do), but you know expecting things to be faster than the blink of an eye.

something like this (pardon me about syntax errors or not being logic at all, the original snippet is somewhere and I do remember do further modifications to it)

function* getCursorList(count, size, collection) {
  const batchList = [];
  let i = 0;
  let n = count;
  while (i < n) {
    yield collection.find().clone().limit(size).skip(i += size);
  }
}
const cursors = getCursorList(count, 10, collection);
for (const cursor of cursors) {
    //blablabla
}

and then processed each cursor on a stream for given collection, generators were my salvation because they were lazily evaluated once the previous stream finished processing the whole cursor, that also prevented my streams from clogging and losing data. I tried a similar solution without the generators, but I was losing data and my streams were hanging too

 

Thank you Shawn for the wonderful article.

As I was reading your article, I thought about an infinite scrolling so decided to stand on the shoulders of giants.

 

Editorial tip: your 2nd code sample is identical to the 1st. 🤓

 

So how would you relate that to a closure? better, worse or just different?