Use-Cases For JavaScript Generators

bob.ts - Aug 12 '19 - - Dev Community

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 ...
Enter fullscreen mode Exit fullscreen mode

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);
  };
};
Enter fullscreen mode Exit fullscreen mode

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);
    }
  };
}
Enter fullscreen mode Exit fullscreen mode

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']();
  });

});
Enter fullscreen mode Exit fullscreen mode

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']();
      });
    }
  });

});
Enter fullscreen mode Exit fullscreen mode

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.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .