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.
Top comments (20)
I've found some more cases personally that have actually helped and are much smaller examples. My 3 favorites are:
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!
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):
Here is the infinitely looping array:
Here is how to make an iterable from an object:
and using a similar method, you can convert objects to Maps, which can become super useful in some instances:
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:
.includes()
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!
@emnudge regarding your examples:
Could you explain / demonstrate 4. ?
I'm also not sure if I understand 3.
Why not use object.values().includes() ?
Hey there! Just saw this now.
We can iterate through objects in many ways. The demonstration was showing that we can also iterate through objects in particular ways, as per our own use case.
We might want to create an iterator that only spits out object properties that are numbers or fall under some filter.
We could create an array of all properties and then filter, but this is creating 2 arrays whereas a generator allows us to create 0 arrays. We simply iterate over the returned iterable and deal with it as necessary
I love the idea of using a generator for this. I might have to try an alternate!
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
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?
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 keywordscontinue
andbreak
) with a custom object seems self evident? Or are you more simply wondering why someone wouldn't just extendArray
(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.