Functions are general concepts in a programming language that helps to group thought and return a single result. In this article, I will show you how to write your generator functions.
You will explore how to create generator functions, get familiar with its
methods and application programming interface - API (throw()
, return()
,
yield
). Get a better understanding of iterators and AsyncIterators, understand
how to types both iterators and generators for TypeScript usage, and explore
possible use cases for them.
At the end of this article, you will be able to differentiate between the functions and generator functions. Be comfortable using and explaining them to interviewers and co-workers when asked about them.
Prerequisites
The requirement to follow this tutorial is the basic understanding of functions, arrays and loops. Good knowledge of await/async, promises and asynchronous javascript will be helpful. The target is javascript developers who have not used the generator function at all. For typescript developers to learn how to type generators, check the section for typescript developers.
Examples
function myName() {
// i can return a string, number, array, object or promise that resolve to all the listed data types
return 'ojo'
}
myName()
function* myNameAgeGithubUsername() {
yield 'ojo'
yield 28
yield 'Oluwasetemi'
}
let generator = myNameAgeGithubUsername()
console.log(generator) // {[ Generator]}
console.log(generator.toString()) // [object Generator]
let name = generator.next()
console.log(name)
let age = generator.next()
console.log(age)
let githubUsername = generator.next()
console.log(githubUsername)
// you can use a for of loop on this generator
for (let each of myNameAgeGithubUsername()) {
console.log(each) // ojo, 28
}
// you can spread the value like an array
let arrayLike = [...myNameAgeGithubUsername()]
console.log(arrayLike)
Explaining the Keywords
Iteration is repeating a set of instructions over a period of a condition, also referred to as loops depending on the context of usage. Iterators help with sequence, that is, returning of consistent value one after another. If I say a string value is iterable, it means it can be loopped/iterated.
let name = 'generator'
for (let each = 0; each < name.length; each += 1) {
// looping through the string;
console.log(name[each]) // prints each letter in the string
}
Any object that implements the Iterator protocol can be looped. The iterator
protocol is an object that has a next method. The next method returns an object
containing value
- the next value and done
- a boolean value that denotes if
the iteration is complete or not. Once the iterator is created, it can be looped
explicitly by calling next()
and the for of loop
. When there is no more
value to yield, it returns {done: true}
.
Generators can return (“yield”) multiple values, one after another, on-demand. They work great with iterable, allowing to create data streams with ease.
Now that you have a clear understanding of all the major concepts you will be exploring in this tutorial, let’s get it rolling.
Built-in Iterables and Statements
String
, Array
, TypedArray
, Map
and Set
are all built-in iterable
because their prototype objects all have a Symbol.iterator
method.
Some statements and expressions expected are for-of loops
, yield*
, yield
and function*
.
Iterator Example
function makeEvenRangeIterator(start = 2, end = Infinity, step = 2) {
let nextIndex = start
let iterationCount = 0
const rangeIterator = {
next: function () {
let result
if (nextIndex < end) {
result = {value: nextIndex, done: false}
nextIndex += step
iterationCount++
return result
}
return {value: undefined, done: true, count: iterationCount}
},
}
return rangeIterator
}
let it = makeEvenRangeIterator(2, 10)
let result = it.next()
while (!result.done) {
console.log(result.value) // 2, 4, 6, 8, 10
result = it.next()
}
// adapted from [MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Iterators_and_Generators)
Note - The real thing working behind the scene in iterators is the
Symbol.iterator
. You can go further by implementing the previous example using the Iterator protocol.
const evenRange = {
from: 2,
to: 10,
}
evenRange[Symbol.iterator] = function () {
console.log(this.from)
return {
current: this.from,
last: this.to,
count: 0,
next() {
if (this.current < this.last) {
this.count++
return {done: false, value: (this.current += 2)}
} else {
return {done: true, count: this.count}
}
},
}
}
// you can loop with the traditional for of loop
for (let i of evenRange) {
console.log(i)
}
You should have a solid understanding of the iterators and their protocol, you should be able to create an iterator.
Creating Generators
Generators is used to yield value as opposed to the traditional function. It
takes iteration to another level of returning the value one at a time using a
function. There exist also async generator
- which will be discussed later in
this tutorial.
You will look at async generator and an example but let’s create a generator
example. Symbol.iterator
will help you create generators as also in the case
of iterators but the only difference is the *
in the function which enables it
to yield its values(it adds the generating effect).
In the example, you will create a range between two numbers with the value 1 as the step in between each values.
let rangeGenerator = {
from: 1,
to: 5,
*[Symbol.iterator]() {
// a shorthand for [Symbol.iterator]: function*()
for (let value = this.from; value <= this.to; value++) {
yield value
}
},
}
// this can be used in any for of loop, arrayLike, like an iterator.
// Please pause and write how this can be used as
// 1.array with array spread operator.
// 2.like iterator using `.next()`
// 3.consumed with a for of loop
You can drop your answers in the comment section.
Yielding means you should be expecting an answer to a question, it requires you
to wait till the answer is returned. You can compose generators using the
yield*
syntax. This means returning a generator within a generator. Generator
composition is a special feature of generators that allows transparent
“embeding” of generators in each other.
function* generateSequenceCompose(start, end) {
for (let i = start; i <= end; i++) yield i
}
function* generatePasswordCodes() {
// 0..9
// for (let i = 48; i <= 57; i++) yield i;
yield* generateSequenceCompose(48, 57)
// A..Z
// for (let i = 65; i <= 90; i++) yield i;
yield* generateSequenceCompose(65, 90)
// a..z
// for (let i = 97; i <= 122; i++) yield i;
yield* generateSequenceCompose(97, 122)
}
// used
let str = ''
for (let code of generatePasswordCodes()) {
str += String.fromCharCode(code)
}
console.log(str) // 0..9A..Za..z
// copied from https://javascript.info/generators
generator.throw
You can force a generator to throw an error in the place where you intended a yield. It is passed an error object, let’s consider an example:
function* myGen() {
yield 1
}
myGen().throw(new Error('throw error using the generator.throw'))
Use cases for using .throw
might be limited but it is a good tool to have if
you like to return an error when the network is out instead of allowing the API
to make a request. If you know any possible use case for this method, kindly
share using the comment section.
generator.return(value)
This is used to finish the generator execution before its time and return the
value passed to the generator.return
Let consider am example:
function* myGen() {
yield 1
yield 2
}
let gen = myGen()
console.log(gen.next()) // return {done: false: value: 1}
console.log(gen.return('I am done')) // {done: true, value: 'I am done'}
gen.next() // {done: true, value: undefined}
I cannot think of any use cases for this method, please share any in the comment section.
Async Generators
They are used to read the stream(constant inflow) of data. You can hit a
paginated endpoint and fetch all the data using an async generator at a go. They
can be used with the for await of loop
just like generators can be used with
the for of loop
. Before showing an example of Async generators, Recall that
iterators are created using Iterator protocol (@@iterator) - Symbol.iterator
.
Async generators are created using Symbol.asynciterator
or by adding an async
to the generator function to make it an async generator function.
async function* asyncGeneratorOneTwo() {
yield new Promise(resolve => resolve(1))
yield new Promise(resolve => resolve(2))
}
let asyncGen = asyncGeneratorOneTwo()
;(async () => {
for await (let each of asyncGen) {
console.log(each) // 1,2
}
})()
TypeScript Example
Please take note of the types for typescript users. AsyncIterableIterator<T>
and IterableIterator<T>
.
The example creates a delay promise using the setTimeout
, getRandomSetChars
function returns random string characters not more than 5 characters that can be
called in the getRandomSetChars
async generator that yields 3 values. It
yields a random string, then yields a delay of 200ms and finally an array of two
random strings.
function delay(ms: number): Promise<void> {
return new Promise<void>(resolve => {
setTimeout(resolve, ms)
})
}
function getRandomSetChars(): string {
const random = 1 + Math.floor(Math.random() * 5)
let wordString = ''
for (let i = 0; i < random; i++) {
const letter = 97 + Math.floor(Math.random() * 26)
wordString += String.fromCharCode(letter)
}
return wordString
}
// Step 3
async function* getRandomSetsChars(): AsyncIterableIterator<string> {
for (let i = 0; i < 10; i++) {
yield getRandomSetChars() // return a random set of char
await delay(200) // wait
yield* [getRandomSetChars(), getRandomSetChars()] // return two random sets of char
}
}
// Step 4
async function addWordsAsynchronously() {
for await (const x of getRandomSetsChars()) {
console.log('Iterator loop:' + x)
}
}
addWordsAsynchronously()
Real World Use Case With GitHub API
They are used in Rxjs and Redux-Saga. This use case allows a user to fetch all the commits on a GitHub repository using GitHub API iteratively until the commits get to 100 while logging to the console the name of the commit author.
The beauty is fetching from an endpoint in a repeated manner until you have
fetched the required 100 commits. It helps fetch all the data you will need to
load the next page using a feature that GitHub implements in all its paginated
endpoints where it returns the next page URL of a paginated resource in the
Link
field of the response.headers
. This allows hitting the next endpoint
asynchronously after fetching the 30 commits returned by Github API for the
paginated resource. The example is adapted from
javascript.info.
async function* fetchCommits(repo) {
let url = `https://api.github.com/repos/${repo}/commits`
while (url) {
const response = await fetch(url, {
// (1)
headers: {'User-Agent': 'Our script'}, // github requires user-agent header
})
const body = await response.json() // (2) response is JSON (array of commits)
// (3) the URL of the next page is in the headers, extract it
let nextPage = response.headers.get('Link').match(/<(.*?)>; rel="next"/)
nextPage = nextPage && nextPage[1]
url = nextPage
for (let commit of body) {
// (4) yield commits one by one, until the page ends
yield commit
}
}
}
;(async () => {
let count = 0
for await (const commit of fetchCommits(
'javascript-tutorial/en.javascript.info',
)) {
console.log(commit.author.login)
if (++count == 100) {
// let's stop at 100 commits
break
}
}
})()
Conclusion
In my experience, there are not many use cases for generators in the common day-to-day task, but exploring them shows us the possibilities within the JavaScript language. The ability to return data yielding it one at a time is awesome, more fascinating is the ability to await a yield value.
Attempt Quiz
What is the difference between the two code snippets below? You can drop your answers in the comment section.
function* myNameAgeGithubUsernameA() {
yield 'ojo'
yield 28
yield 'Oluwasetemi'
}
function* myNameAgeGithubUsernameB() {
yield 'ojo'
yield 28
return 'Oluwasetemi'
}
Credit
- Iterators and generator MDN
- Generators on javaScript.info, Ilya Kantor.
- Async iterators and generators on javascript.info, Ilya Kantor.