This is Mongolia steppe. As flat as your future async code.

Genuine Novice Intro to Generators

Ana Makarochkina
9 min readFeb 18, 2017

Suppose you have this small app, like magic 8 ball. It is your pet project, Freecodecamp zip line or whatever else. You already avoided the greatest horrors of callback hell, and fetching Promises, catching errors, chaining then’s takes no effort.

But you know, it’s 2017 already, async is all the rage, new standard es2017 already came out, and you still investigating es2015. So here’s one of the es2015 features of interest here — how to use generators in such apps?

Generators already have had reputation of something too complex to understand and it’s been explained quite comprehensively as well. The main point is that generators can stop themselves and then resume from the place they were stopped.

How can it be used?

  • code like this var i = 0; while(true) do i++; always got you thrilled? Finally you have an option to use it in a generator without crashing your browser. Little hint ahead: that’s possible because — once again — generator stops itself at each next() step.
  • you successfully shifted to Promises on your way out the callback hell? wow, that’s symbolic
Orpheus tried to get Eurydice from hell, but broke the promise, so you’re more lucky!

… now you can flatten the code and make asynchronous look like synchronous even more!

I am going to show you examples of going asynchronous using generators in three variations on a quite basic app — magic 8-ball, which you ask the question, shake, and wait for an answer.

Short recap: JavaScript is a single-threaded language, meaning that all code runs line after line. However, when you make the request to some external source, you wouldn’t really like stop all your program from running. For instance, in my 8-ball app I would like the 8-ball to continue shaking (because its fun) and to prepare for showing the “answer” even after the request for the quotes is made.

What is a generator?

(I did not understand it from the first reading of one article… or another… so just relax if it is not all clear at once)

Generator is a function, which looks like this

function *justAgenerator() { yield 'grapes' }

or

function* justAgenerator() { yield 'grapes' } 

The asterisk near the function or function name denotes not only that it is a generator function, which returns iterable Generator object, but also the exclusivity of yield keyword usage. (More on iterables you can read here, basically an iterable is an object whose prototype has built-in iteration method — Array, String, TypedArray, Set, Map)

You store Generator object in a variable:

var gen = justAgenerator()

and iterate with next(), which will get you an object with properties done and value, done shows whether there are more yields to iterate and can be true or false, and value can be any value.

console.log(gen.next())
// -> { done: false, value: 'grapes' }

return with return()

console.log(gen.return('and tomatoes'))
// -> { done: true, value: 'and tomatoes' }
or insteadconsole.log(gen.return())
// -> { done: true, value: undefined }

throw errors with throw()

function* justAgenerator() {
while(true) {
try {
yield 'harmony';
} catch(e) {
console.log('oh why did you do that mistake! we all doomed');
}
}
}

var gen = justAgenerator();
console.log(gen.next());
// -> { value: 'harmony', done: false }
gen.throw(new Error('Something went wrong'));
// -> 'oh why did you do that mistake! we all doomed'
console.log(gen.next());
// -> { value: 'harmony', done: false }

You could also go through Generator values with come custom function or with for... of loop, which runs generator to completion, but ignores the return statement (or some more methods listed here).

Also note yield* keyword, which is used to give out each value one by one of another generator or iterable object.

function* gen(x) {
console.log(76)
yield* x
console.log(96)
return 5
}
var it = gen('hi');let arr=[]//for...of loop to iterate and store yielded values in an array
// or console.log([...it]) would output the same
for (x of it) {
arr.push(x)
}
//76
//96
//["h","i"]

Brace yourselves! More complexity is coming.

Yield may take value in along as give a value out. Let’s take a look at generator example first:

function* generator(x) {var y = yield x;
var z = yield y;
var w = yield z;
return w;
}

Suppose you feed it some values:

var gen = generator(1);console.log(gen.next(2)); 
// -> { done: false, value: 1 }
console.log(gen.next(3));
// -> { done: false, value: 3 }
console.log(gen.next(4));
// -> { done: false, value: 4 }
console.log(gen.next(5));
// -> { done: true, value: 5 }
console.log(gen.next(6));
// -> {value: undefined, done: true}

What happened? Generator took your value, proceeded until next yield and stopped, returning some value (in this case value is the same); on the next next() call it substituted the previous yield and proceeded until the next again, and so it goes until the last yield expression or return. As it goes, yield smth expression (where generator stopped before) is replaced by incoming value. All of it happens as follows:

function* generator(x) {
// here generator 1) returns value of x I initialized generator with,
// and 2) stops at yield x.
var y = yield x;// second gen.next() call
// 1) substitutes yield x for given value (3),
// 2) y = 3,
// 3) y value is returned,
// 4) function stops at yield y
var z = yield y; // third gen.next() call does the same:
// returns value z = 4,
// function runs until next yield z on the next line
var w = yield z; // fourth call: no more yields,
// generator still returns value:5 (value of w),
// but iteration is done, so done: true;
return w;
}
var gen = generator(1); // initialize generator with x=1console.log(gen.next(2)); // any value given here is thrown away, and initializing variable (or operation with it) is returned
// -> { done: false, value: 1 }
console.log(gen.next(3));
// -> { done: false, value: 3 }
console.log(gen.next(4));
// -> { done: false, value: 4 }
console.log(gen.next(5)); // this value is kept and returned, but not by yield, by return, iteration is done
// -> { done: true, value: 5 }
console.log(gen.next(6)); // no more yields, no return, so no value is returned
// -> {value: undefined, done: true}

Here you can experiment with this code for better understanding.

How did I apply it

Honestly saying, applying generators, alone or with a flow-control library like co is not that hard (if you have examples! like mine :) or other resources like the ones I listed in the end of the post). I struggled much more on making the ball wiggle as I liked — that is, shaking as is if you’re shaking real magic 8-ball.

Shake it!

Here are code samples of all four Promise-based variants you can use by now. For Promises, I used fetch API, and also polyfill for it. For all 4 examples logic is the following: you get the JSON file from Firebase with all quotes. Now, randomly getting a single one from that is fairly simple — but suppose for training purposes I still need to make yet another request to get actual quote, using random id, generated based on the JSON length.

Just Promises

The first example is simply fetching and chaining Promises. This one you probably know. For short recap, Promise is a proxy object for value which is not yet here with three states: pending, fulfilled and rejected, and such superpowers as chaining Promise methods .then() and .catch() one after another, each returning Promises as well.

function getQuote() {// getting 1st JSON
fetch('https://ball-7d2f9.firebaseio.com/.json')
.then(function(response) {
if (response.ok) {
return response.json();
}
throw new Error('Network response was not ok.');
})
.then(function(response) {
// getting random id to fetch one quote
var id = '' + (Math.floor(Math.random() * response.length) + 1);
fetch(`https://ball-7d2f9.firebaseio.com/${id}.json`)
.then(function(answer) {
if (answer.ok) {
return answer.json();
}

throw new Error('Network response was not ok.');
})
.then(function(theAnswer) {
document.getElementById('test').innerHTML = JSON.stringify(theAnswer).slice(1, theAnswer.length + 1);
})
})
.catch(function(error) {
// if nothing loaded, at least 'Yes' or 'No' will guide your executive decision
if (Math.random() > 0.5) {
document.getElementById('test').innerHTML = 'Yes'
} else {
document.getElementById('test').innerHTML = 'No'
}
});
}
getQuote()

Same with GENERATORS

Syntactic sugar or not, I am just a fan of generators already, with generators code is one level flatter, and it is clear, when does the function stop and resume.

To use generator function in your app write a generator function, which yields, takes in and passes down the Promises, and outputs the answer in HTML. Wrap all of this into try...catch and write the case if there is an error — if nothing is loaded, you can randomly output ‘Yes’ or ‘No’. Then you need a function, that will run this generator and respond to errors, which may work as follows:

  • run next() method on generator to get yield value
  • if yielded value is not last in the generator (e.g. done: false) you work with the Promise that generator gives you back: if fulfilled, you call execute function to iterate generator once again with returned value; if rejected, you throw error and generator randomly returns ‘Yes’ or ‘No’.

You can look at the same magic 8-ball app using generators. Not much difference, may be just a bit slower.

//generator function, that yields and resolves each Promise step by stepfunction* getQuote() {let theAnswer;try {
let allFetch = yield fetch('https://ball-7d2f9.firebaseio.com/.json');
let allInfo = yield allFetch.json();
let l = allInfo.length;
let id = Math.floor(Math.random() * l + 1);
let theAnswerFetch = yield fetch(`https://ball-7d2f9.firebaseio.com/${id}.json`);
let theAnswer = yield theAnswerFetch.json();

} catch (e) {
let theAnswer = Math.random() >=0.5 ? 'Yes' : 'No'

}
document.getElementById('test').innerHTML = theAnswer;
}
// function,running the generator until it is exhaustedfunction execute(generator, yieldValue) {let next = generator.next(yieldValue);if (!next.done) {
next.value.then(
result => execute(generator, result),
err => generator.throw(err)
)
}
}
execute(getQuote());

Generators with CO

To use co you may add it via CDN (https://cdnjs.cloudflare.com/ajax/libs/co/4.1.0/index.js) or npm install co(https://github.com/tj/co). In Codepen, I added CDN link for app axample with co (using code below).

You could apply the yield* to fancy things up a bit, and just wrap the last generator in co:

function* fetchQuote(url) {
let allQuotesFetch = yield fetch(url);
let allQuotes = yield allQuotesFetch.json();
let l = allQuotes.length;
let id = Math.floor(Math.random() * l + 1);
let quote = yield fetch(`https://ball-7d2f9.firebaseio.com/${id}.json`);
let q = yield quote.json();
return q;
}
function* getQuote() { let quote; try {
quote = yield* fetchQuote('https://ball-7d2f9.firebaseio.com/.json');
} catch(e) {
quote = 'Yes'
}
document.getElementById('test').innerHTML =
JSON.stringify(quote).slice(1, quote.length+1)

}
co(getQuote());

Or you could just take generator function from previous example, and simply run it with co:

function* getQuote() {let theAnswer;try {
let allFetch = yield fetch('https://ball-7d2f9.firebaseio.com/.json');
let allInfo = yield allFetch.json();
let l = allInfo.length;
let id = Math.floor(Math.random() * l + 1);
let theAnswerFetch = yield fetch(`https://ball-7d2f9.firebaseio.com/${id}.json`);
let theAnswer = yield theAnswerFetch.json();

} catch (e) {
let theAnswer = Math.random() >=0.5 ? 'Yes' : 'No'

}
document.getElementById('test').innerHTML = theAnswer;
}
co(getQuote())

Async functions

To use async functions you would need to use Babel and Babel Polyfill (https://cdnjs.cloudflare.com/ajax/libs/babel-core/5.8.29/browser-polyfill.min.js). The code below is live here.

As you saw before, generators need an additional function or an external library to get them going. So the async functions are intended to alleviate the pain of always adding these boilerplates, basically containing the generator inside. You may just take the previous generator function, rename it and replace yield with await.

async function getQuote() {
let theAnswer;
try {
let resultResponse = await fetch('https://ball-7d2f9.firebaseio.com/.json');
let response = await resultResponse.json()
let l = response.length;
let id = Math.floor(Math.random() * l + 1);
let answerResponse = await fetch(`https://ball-7d2f9.firebaseio.com/${id}.json`)
let answer = await answerResponse.json();
theAnswer = JSON.stringify(answer).slice(1, answer.length + 1);
} catch (err) {
console.log(err.message)
theAnswer = Math.random() >=0.5 ? 'Yes' : 'No'
}
document.getElementById('test').innerHTML = theAnswer;
}
getQuote();

That is pretty much it! :)

Last thing I’d like to mention is some great resources that helped me to build understanding of this topic, and which you can use to know more:

And no matter how hard this topic may go, just go ahead and

Be the miracle.

--

--