loading...
Cover image for Use-Cases For JavaScript Generators

Use-Cases For JavaScript Generators

rfornal profile image bob.js Updated on ・5 min read

In one of my many deep-dives about JavaScript, I came across generators. They looked interesting.

Then, I looked for some use-cases for generators. And looked. And looked.

Eventually, I found a simple generator throttle example. After all this research, I resolved to see how I could use them. Since I was working on an Asynchronous JavaScript talk (JavaScript Enjoys Your Tears), I wrote a state machine to facilitate positioning within the slide deck and managing font size on the presentation side.

What I found is documented here ...

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

The ability of functions to be paused and then resumed again. A generator returns an iterator. On creation, the code inside the generator is not executed.

  • Solves "reasoning about" issues.
  • Allows for non-"run-to-completion" behavior. Localized blocking only.
  • Syntactic form of a state machine.
  • Cooperative concurrency versus preemptive concurrency.

Advantages of Generators

Lazy Evaluation

This is an evaluation model which delays the evaluation of an expression until its value is needed. That is, if the value is not needed, it will not exist. It is calculated on demand.

Memory Efficient

A direct consequence of Lazy Evaluation is that generators are memory efficient. The only values generated are those that are needed. With normal functions, all the values must be pre-generated and kept around in case they need to be used later. However, with generators, computation is deferred.

Use-Cases

Here are some Generator use-cases ...

Infinitely Repeating Array

This is the article (by Shawn Reisner) that got me interested in this topic in the first place.

Generating Unique Identifiers

This is from a post (by Nick Scialli @nas5w ): TWEET.

An interesting use case for a javascript generator: generating an infinite number of unique identifiers!

function * idCreator() {
  let i = 0;
  while (true) yield i++;
}

const ids = idCreator();

console.log(ids.next().value); // 0
console.log(ids.next().value); // 1
console.log(ids.next().value); // 2
// etc ...

Throttle Generator

This generator will throttle a function for an amount of time (in milliseconds).

export function * throttle(func, time) {
  let timerID = null;
  function throttled(arg) {
    clearTimeout(timerID);
    timerID = setTimeout(func.bind(window, arg), time);
  }
  while(true) throttled(yield);
}

export class GeneratorThrottle {

  constuctor() {};

  start = () => {
    thr = throttle(console.log, 3000);
    thr.next('');
  };

  toString = () => {
    console.log(throttle);
    console.log('start =', this.start);
  };
};

Content State-Machine

export class ContentStateMachine {
  _content;
  _default;
  _statePatterns;
  _returnState;
  _changeAlgorithm;

  _machine;

  constructor(settings) {
    this._content = settings.content;
    this._default = settings.defaultIndex;
    this._statePatterns = settings.statePatterns;
    this._returnState = settings.returnState;
    this._changeAlgorithm = settings.changeAlgorithm;

    const machineSettings = {
      'content': this._content,
      'defaultIndex': this._default,
      'statePatterns': this._statePatterns,
      'returnState': this._returnState
    };
    this._machine = this.stateMachine(machineSettings);
    return this._machine;    
  };

  stateMachine = function * stateMachine(settings) {
    const content = settings.content;
    const defaultIndex = settings.defaultIndex;
    const statePatterns = settings.statePatterns;
    const returnState = settings.returnState;

    let currentIndex = defaultIndex;
    while (currentIndex >= 0 && currentIndex < content.length) {
      if (this._changeAlgorithm) {
        const states = returnState(content, currentIndex);
        this._changeAlgorithm(states, currentIndex);
      }
      const changeType = yield returnState(content, currentIndex);
      currentIndex = statePatterns[changeType](content, currentIndex);
    }
  };
}

Use as a font state-machine ...

import { ContentStateMachine } from '/scripts/presentation/_content-state-machine.js';

$(document).ready(() => {

  const main = $('.main');
  const upButton = $('.up');
  const downButton = $('.down');
  const resetButton = $('.reset');

  const channel = new BroadcastChannel('le-slides-font-size');
  const actions = {
    init: () => {
      upButton.hide();
      downButton.hide();
      resetButton.hide();
    },

    'trigger-up': () => {
      fontStateMachine.next('up');
    },
    'trigger-reset': () => {
      fontStateMachine.next('reset');      
    },
    'trigger-down': () => {
      fontStateMachine.next('down');
    },

    'report-states': () => {
      channel.postMessage({
        upDisabled: upButton.hasClass('disabled'),
        downDisabled: downButton.hasClass('disabled')
      });
    }
  };
  channel.onmessage = (triggerAction) => {
    actions[triggerAction.data]();
  };

  const sizes = [
    'fsm05', 'fsm04', 'fsm03', 'fsm02', 'fsm01',
    'fs00',
    'fsp01', 'fsp02', 'fsp03', 'fsp04', 'fsp05'
  ];
  const defaultIndex = Math.floor(sizes.length / 2);
  const changeFont = (classes, currentIndex) => {
    for (var i = 0, len = classes.length; i < len; i++) {
      if (i === currentIndex) {
        main.addClass(classes[i]);
      } else {
        main.removeClass(classes[i]);
      }
    }

    if (currentIndex === 0) {
      downButton.addClass('disabled');
    } else {
      downButton.removeClass('disabled');
    }

    if (currentIndex === classes.length - 1) {
      upButton.addClass('disabled');
    } else {
      upButton.removeClass('disabled');
    }

    actions['report-states']();
  };
  const statePatterns = {
    'up': (content, index) => {
      const max = content.length - 1;
      return (index + 1 <= max) ? index + 1 : index;
    },
    'down': (content, index) => {
      return (index - 1 > 0) ? index - 1 : 0;
    },
    'reset': (content, index) => {
      return defaultIndex;
    }
  };
  const returnState = (content, currentIndex) => {
    return content;
  };

  const settings = {
    'content': sizes,
    'defaultIndex': defaultIndex,
    'statePatterns': statePatterns,
    'returnState': returnState,
    'changeAlgorithm': changeFont
  };

  const fontStateMachine = new ContentStateMachine(settings);

  fontStateMachine.next('reset');

  upButton.on('click', () => {
    actions['trigger-up']();
  });

  resetButton.on('click', () => {
    actions['trigger-reset']();
  });

  downButton.on('click', () => {
    actions['trigger-down']();
  });

});

Use as a navigation state-machine ...

import { ContentStateMachine } from '/scripts/presentation/_content-state-machine.js';

$(document).ready(() => {

  $('.notes').load('/templates/cards.html', function() {
    let slideStateMachine;

    const nextButton = $('.next');
    const previousButton = $('.previous');

    const channel = new BroadcastChannel('le-slides-position');
    const actions = {
      init: () => {
        nextButton.hide();
        previousButton.hide();
      },

      'trigger-previous': () => {
        slideStateMachine.next('previous');
      },
      'trigger-next': () => {
        slideStateMachine.next('next');
      },

      'report-states': (index) => {
        channel.postMessage({
          currentIndex: index,
          previousDisabled: previousButton.hasClass('disabled'),
          nextDisabled: nextButton.hasClass('disabled')
        });
      }
    };
    channel.onmessage = (triggerAction) => {
      actions[triggerAction.data]();
    };

    let cardData = [];
    let cardTitles = [];

    $.getJSON('/data/card-data.json')
    .done((data) => {
      cardData = data;
    })
    .fail((data) => {
      console.log('fail', data);
      if (data.status!==200) {
        const error = $('<div/>').text('Error loading JSON file');
        content.append(error);
      }
    })
    .always(() => {
      if (cardData.length > 0) {
        initTitles();      
      }
    });

    function initTitles() {
      for (let i = 0, len = cardData.length; i < len; i++) {
        cardTitles.push(cardData[i].id);
      }

      init();
    }

    function init() {
      const changeCurrentCard = (cards, currentIndex) => {
        const title = cards[currentIndex];
        const currentCard = $(`.note[card="${title}"]`);
        const previousTitle = (currentIndex - 1 < 0) 
          ? '' : cardTitles[currentIndex - 1];
        const nextTitle = (currentIndex + 1 > maxCards - 1) 
          ? '' : cardTitles[currentIndex + 1];
        const keep = [title];

        currentCard.addClass('slide');
        currentCard.attr('style', 'left:0;');

        if (previousTitle.length > 0) {
          keep.push(previousTitle);

          previousButton.removeClass('disabled');
          $(`[card="${previousTitle}"]`)
            .attr('style', 'left:-100%;')
            .removeClass('slide');
        } else {
          previousButton.addClass('disabled');
        }

        if (nextTitle.length > 0) {
          keep.push(nextTitle);

          nextButton.removeClass('disabled');
          $(`[card="${nextTitle}"]`)
            .attr('style', 'left:100%;')
            .removeClass('slide');
        } else {
          nextButton.addClass('disabled');
        }

        $('.n').text(currentIndex + 1);

        actions['report-states'](currentIndex);

        for (let i = 0, len = cards.length; i < len; i++) {
          const element = $(`[card="${cards[i]}"`);
          if (!keep.includes(cards[i])) {
            element.attr('style', 'display:none;');
          }
        }
      };

      const statePatterns = {
        'previous': (content, index) => {
          return (index - 1 > 0) ? index - 1 : 0;
        },
        'next': (content, index) => {
          const max = content.length - 1;
          return (index + 1 <= max) ? index + 1 : index;
        },
        'reset': (content, index) => {
          return 0;
        }
      };

      const returnState = (content, currentIndex) => {
        return content;
      };

      const settings = {
        'content': cardTitles,
        'defaultIndex': 0,
        'statePatterns': statePatterns,
        'returnState': returnState,
        'changeAlgorithm': changeCurrentCard
      };

      const maxCards = cardTitles.length;
      $('.max').text(maxCards);

      slideStateMachine = new ContentStateMachine(settings);

      slideStateMachine.next('reset');

      nextButton.on('click', (event) => {
        actions['trigger-next']();
      });

      previousButton.on('click', (event) => {
        actions['trigger-previous']();
      });
    }
  });

});

Conclusions

After a ton of research, I found very few practical examples of JavaScript Generators. I wanted to find ways to use them. After working with them on an Asynchronous JavaScript talk (JavaScript Enjoys Your Tears), I found a state machine to facilitate positioning within the slide deck and managing font size on the presentation side to be an excellent example.

Could I have managed state in other ways? Certainly. But, I wouldn't have learned nearly as much as I did with the code above.

Posted on by:

rfornal profile

bob.js

@rfornal

REAL, FUN, GEEK who is passionate about Front-End!

Discussion

pic
Editor guide
 

I've found some more cases personally that have actually helped and are much smaller examples. My 3 favorites are:

  • for loops which need to be paused and resumed at a later date
  • infinitely looping over an array and having it reset to the beginning once it's done
  • creating iterables to use in for of loops from non-iterable objects using [Symbol.Iterator]

Generators also help to loop through structures where you'd normally need to keep track of multiple variables at a time. Instead of having multiple pieces of global state, you just need to call .next() on the generator.

I'd like to hear more about generators being a mistake though. I haven't yet heard that and I would like to know the reasoning behind it.

Cheers!

 

Love the comments.

As to Generators being a mistake, I heard that in a talk a while back and vaguely remember agreeing with the arguments at the time. Since I can't remember the talk or the arguments, I'll change the language here.

Thanks again!
Bob

 

Can you provide "practical" code uses with your examples that I can include?

 

Here is the first example (looping arrays using generators to pause and resume):

function* makeGenLoop(arr) {
  for (const item of arr) {
    yield item;
  }
}

const myGen = makeGenLoop([0, 3, 7, 2, 5]);
console.log(myGen.next());

Here is the infinitely looping array:

function* makeGenLoop(arr) {
  for (let i = 0;; i++) {
  if (i === arr.length) i = 0;
    yield arr[i];
  }
}

const myGen = makeGenLoop([0, 3, 7, 2, 5]);
console.log(myGen.next());

Here is how to make an iterable from an object:

const myObj = {
  name: 'Stanley', 
  Job: 'Web Dev', 
  age: 28
}

myObj[Symbol.iterator] = function* () {
  for (const prop in this) {
    yield this[prop];
  }
}

console.log([...myObj]); // > ["Stanley", "Web Dev", 28]
for (const val of myObj) {
  console.log(val)
}

and using a similar method, you can convert objects to Maps, which can become super useful in some instances:


const myObj = {
  name: 'Stanley', 
  Job: 'Web Dev', 
  age: 28
}

myObj[Symbol.iterator] = function* () {
  for (const prop in this) {
    yield [prop, this[prop]];
  }
} 

const objectMap = new Map(myObj);
console.log(objectMap.get('name')); // > "Stanley"

OK, the code examples are good ... and I get where you are going with this.

By "practical," I am trying to find actual use-cases ... not just "Hello World" type of code.

I'm not going to bootstrap an entire project for an example, but I can give you a basic use case for them:

  1. You'd want to play and pause loops if someone hit a pause button on an animation and the loop is a tween.
  2. You'd want an infinitely looping array for things like carousels or lists of data which wrap around
  3. You'd want to iterate over objects to check if any of its properties include a specific value. You get the array and then check with .includes()
  4. You'd want a map if you have objects already and want to keep the object like structure, but also want to check if properties exist on an object that are falsy.

I hope you understand that I'm not going to code these entire use cases. It's be much too long for a comment and I'd sooner make my own post than a comment with the full code.

This is more than enough ... if you are OK with it, I'll do a follow-up post to this one with your examples included (attributed to you, of course) down the road.

Sounds good.
If you want one or two more, Dr. Axel Rauschmayer provides some use cases with async code

Awesome ... thanks for the additional references!

 

Let me share an example using async generators that I recently implemented.

tl;dr: async generators for streaming

I had to find all people who have commented on issues at a particular GitHub repo. GitHub's rest API provides us with an endpoint that can list issue comments. With each page having a few results, in order to find all the commentors, we have to traverse all the pages. Now we can do this in a single run, and then show user the results, which will take really really long time of user seeing nothing (on a repo with 250 pages, it took 5-6 min). What can be better for user experience is to keep emitting unique commentors as we find them, hence creating a streamed output so user knows things are really in progress and not broken.

You can find the code for this at: github.com/sidvishnoi/respec-githu... and github.com/sidvishnoi/respec-githu...

 

Can't wait to see the code!

 

The first example can be done using just a simple plain old function. Why would you use generators for that?

function idCreator() {
  let i = 0;
  return () => i++;
}

const nextId = idCreator();

console.log(nextId()); // 0
console.log(nextId()); // 1
console.log(nextId()); // 2
// etc ...

It's even less code than the generator example. I'm sure the other examples can be done without generators too.

 

This article was more about the generators ... and how they can be used, not proving it better or worse than traditional methods. The idea wasn't to come up with examples that couldn't be done in regular JavaScript. The idea was to come up with examples of practical generator usage.

Yes, some of these examples can be done in Vanilla JS. But, given the generator's ability to be a "state machine," it can provide a more elegant solution in a few cases ... although, again, that was not the focus of the article.

 

You left out combining the generators API with Symbol.iterator to create an object which can be used with the browser's native for/of syntax. developer.mozilla.org/en-US/docs/W... Which, from my perspective, is the most obvious/common use case for generators.

Separately, the open source recurring dates library rSchedule is built entirely upon ES6 generators. They are a necessary abstraction given that many calendar schedules are infinitely recurring.

Pretty much every object in the library is a generator. The generator code for the Rule object can be seen here: gitlab.com/john.carroll.p/rschedul....

When you build out a recurrence object in rSchedule, you are really building a generator out of many generators.

 

Didn’t leave out the Symbol.iterator ... was looking for “practical” use-cases. Can you show, in code, a use case beyond the Hello World examples?

For the rSchedule ... THAT is what I was looking for. Can’t wait to dig through their code!

 

I'll let rSchedule be my practical example.

Though I'm getting the impression that we might have different conceptions of the word "practical". From my perspective, a for loop is one of the most basic javascript keywords. Used in pretty much every app ever. The practicality of being able to hook into that keyword (and related keywords continue and break) with a custom object seems self evident? Or are you more simply wondering why someone wouldn't just extend Array (or expose the data as an array via a property)?

I believe I might need to extend my definition of practical. I was looking for real-world use-cases since some of the examples seem challenging to see applied in production code. I’m trying to showcase code that is more than just an example.