Search This Blog

2024/03/30

Javascript: Generator Functions


What is generator function?

A generator function is a special type of function in JavaScript
that allows pausing and resuming its execution during runtime.
Unlike regular functions, which run to completion, generator functions
can be paused and resumed multiple times, making them particularly useful
for dealing with asynchronous operations, handling large datasets,
and writing custom iterators.

Generator functions are defined using the function* syntax, and they
use the yield keyword to pause the function's execution and produce a
value. When a generator function is called, it returns an iterator object,
which can be used to control the function's execution.

Syntax Of Generator Function:

The syntax of generator function is:

Syntax:
function* generatorFunction() {
// Generator function body
yield value1;
yield value2;
// ...
}

Explanation:
The function* keyword indicates that this is a generator function.
Inside the generator function body, the yield keyword is used to
pause the function and produce a value to the caller.

Iterators and generators in javascript:

Iterators and generators are related concepts in JavaScript, often used
together to create iterable objects that produce a sequence of values.
Here's an overview of each concept:

Iterator:
1) An iterator is an object that provides a way to access the elements
of a collection one at a time,typically in a sequential manner.

2) Iterators have a next() method that returns an object with two
properties: value (the next value in the sequence) and done (a boolean
indicating whether the end of the sequence has been reached).

3) You can manually iterate over an iterator using a loop or consume its
values using other methods like for...of loop, Array.from, or
destructuring assignment.

4) Iterators are commonly used with iterable objects such as arrays,
strings, maps, sets, etc., to provide a standardized way of accessing
their elements.

Generator:
1) A generator is a special type of iterator that can be defined using a
function with an asterisk (function*). Generators allow you to define an
iterative algorithm by writing code that looks like a standard function
but behaves like an iterator.

2) Generators use the yield keyword to yield values one at a time. When a
generator function is called, it returns an iterator object that can be
used to iterate over the values generated by the generator.

3) Unlike regular functions, generator functions can maintain their state
between successive calls. They pause their execution at each yield
statement and resume it when the generator's next() method is called again.

4) Generators are often used to create sequences of values lazily or
on-demand, making them useful for scenarios where you need to generate a
large or potentially infinite sequence of values without consuming
excessive memory.

Return value of generator function:
Return value of generator function is in following format.

{ value: value, done: true | false}

Explanation:
The object has two properties value and done . The value contains
the value to be yielded. Done consists of a Boolean (true|false)
which tells the generator if .next() will yield a value or undefined.

Examples of generator Function:

Example 1:
function* evenNumberGenerator(max) {
let first = 0;
while (first < max) {
first = first + 2;
yield first;
}
}

//looping using for of loop
for (let val of evenNumberGenerator(10)) {
console.log(val);
}

//looping using classic for loop
const iteratorThree = evenNumberGenerator(10);
for (let resultThree = iteratorThree.next();
!resultThree.done; resultThree = iteratorThree.next()) {
console.log(resultThree.value);
}

//looping using while with assumption that done is
// true when function reaches end
const iteratorTypeOne = evenNumberGenerator(10);
let resultTypeOne = iteratorTypeOne.next();
while (!resultTypeOne.done) {
console.log(resultTypeOne.value);
resultTypeOne = iteratorTypeOne.next();
}

//looping using while with assumption that value is undefined when
//function reaches end
const iteratorTwo = evenNumberGenerator(10);
let resultTypeTwo = iteratorTwo.next();
while (resultTypeTwo.value) {
console.log(resultTypeTwo.value);
resultTypeTwo = iteratorTwo.next();
}
Explantion:
Output of all these four loop is same.For each loop output is

Output for each loop:
2
4
6
8
10

The return() Method:
The return() method allows us to force a generator to complete before it
reaches the end. It can take an optional argument that will be returned
as the final value of the generator.

We will try to reuse above code here
Code:
function* evenNumberGenerator(max) {
let first = 0;
while (first < max) {
first = first + 2;
yield first;
}
}

const iteratorTypeOne = evenNumberGenerator(10);
let resultTypeOne = iteratorTypeOne.next();
while (resultTypeOne.value) {
console.log(resultTypeOne);
if (resultTypeOne.value == 6) {
resultTypeOne = iteratorTypeOne.return("Finished!");
} else {
resultTypeOne = iteratorTypeOne.next();
}
}
console.log(resultTypeOne);

Output:
{ value: 2, done: false }
{ value: 4, done: false }
{ value: 6, done: false }
{ value: 'Finished!', done: true }
{ value: undefined, done: true }
Explanation:
The statement resultTypeOne = iteratorTypeOne.return("Finished!");
passed
value as finished & done as true so on nect iteration get that value
i.e.
{ value: 'Finished!', done: true }
it forces the interation to end in sense next value will be undefined &
done as true.
i.e.
{ value: undefined, done: true }



We Can't foresee that next iteration value:
when working with generators, you typically won't know beforehand
if the next iteration will be the last one (i.e., if done will be
true or false) until you actually call next() on the iterator.

Throwing exception:
Consider code below:
Code:
function* generate() {
try {
yield "2 + 2 = ?"; // Error in this line
yield "4 + 4 = ?";
console.log("The execution does not reach here,
because the exception is thrown above");
}
catch (exp) {
console.log(exp); // shows the error
}
}

let generator = generate();

let question = generator.next();
console.log("Question:", question);

try {
generator.throw(new Error("The answer is not found in my database"));
} catch (e) {
console.log(e); // shows the error
}
let another = generator.next();
console.log("another:", another);

Output:
Question: { value: '2 + 2 = ?', done: false }
Error: The answer is not found in my database
another: { value: undefined, done: true }
Explanation:
The code after try catch complete is
let another = generator.next()
console.log("another:", another)
it runs & print another: { value: undefined, done: true }
saying end is reached.

To pass an error into a yield, we should call generator.throw(err).
In that case, the err is thrown in the line with that yield on which
current control lies.
Here the yield of "2 + 2 = ?" leads to an error.

Psudo Random Generator:
One of them is testing. We may need random data: text, numbers, etc.
to test things out well.In JavaScript, we could use Math.random().
But if something goes wrong, we’d like to be able to repeat the test,
using exactly the same data.For that, so called “seeded pseudo-random
generators” are used. They take a “seed”, the first value, and then
generate the next ones using a formula so that the same seed yields
the same sequence, and hence the whole flow is easily reproducible.
We only need to remember the seed to repeat it.

An example of such formula, that generates somewhat uniformly
distributed values is
next = previous * 16807 % 2147483647

If we use 1 as the seed, the values will be:

16807
282475249
1622650073
…and so on…

Code (Using normal function):
function pseudoRandom(seed) {
let value = seed;
return function () {
value = (value * 16807) % 2147483647;
return value;
};
}
let generator = pseudoRandom(1);

console.log(generator()); // 16807
console.log(generator()); // 282475249
console.log(generator()); // 1622650073


Code (Using Generator Function):
function* pseudoRandom(seed) {
let prevVal = seed;
while (true) {
prevVal = (prevVal * 16807) % 2147483647;
yield prevVal;
}
}

var iterator = pseudoRandom(1);
console.log(iterator.next());// 16807
console.log(iterator.next());// 282475249
console.log(iterator.next());// 1622650073
Output:
{ value: 16807, done: false }
{ value: 282475249, done: false }
{ value: 1622650073, done: false }



Passing value in next() call:

Consider code below:
Code:
function* foo(x) {
var y = 2 * (yield x + 1);
console.log(y)
var z = yield y / 3;
console.log(z)
return x + y + z;
}

//calling code
var it = foo(5);// x becomes 5 in foo function

// note: not sending anything into `next()` here
console.log(it.next()); // { value:6, done:false }

console.log(it.next(12)); // { value:8, done:false }
console.log(it.next(13)); // { value:42, done:true }
Output:
{ value: 6, done: false }
y: 24
{ value: 8, done: false }
z: 13
{ value: 42, done: true }
Explanation:
1) In calling code the first line is
var it = foo(5);
which make value of x as 5.
2) The second line in calling code is
console.log(it.next()).
Here we are not passing any value to next() call as in first call even
if it is passed it get ignored.
The first line in generator function code is
var y = 2 * (yield x + 1);
thing that is on right side of yield is evaluated and returned ,so x+1
becomes 6 & 6 is returned to calling code.
control still remain on 1st line of generator function code i.e.
var y = 2 * (yield x + 1);
3) The third line in calling code is
console.log(it.next(12));
As we are passing value 12 in next() call.As control is still on first
line
var y = 2 * (yield x + 1);
12 passed will replace (yield x + 1) & y becomes 24.yield on this
line already ran so it moves further
prints y as per its value in console.log statement.moves further as no
yield encountered yet.next line is
var z = yield y / 3;
Expression on right side of yield is evaluated y/13 is effectively
24/3 so it becomes 8 and its returned to calling code.
Control is still on 3rd line o generator function code i.e.
var z = yield y / 3;
4) The forth line in calling code is
console.log(it.next(13));
Here we are passing value 13 in next() call.As control is still on
line
var z = yield y / 3;
the yield y/3 becomes 13,so z becomes 13.As No yield statement found
control moves to next line.
print 13 the value of z in console.log statement.It moves on next line
but x is 5,y is 24 & z become 13.
so the line
return x + y + z;
will return 5+24+13 meaning 42.but this is last line so done become
true.

An async generator function:
An async generator function is declared using async function* syntax. Inside,
the yield keyword can be combined with await to handle asynchronous operations
gracefully. This means we can yield promises, fetch data and seamlessly
integrate
it into our data stream.

Async Generators can be iterated using a for-await-of loop.

Code:
// Async function that returns a promise,delay can be added with settimeout
async function fetchData(id) {
return new Promise(async (resolve, reject) => {
const data = await fetch(`https://reqres.in/api/users/${id}`);
resolve(data.json());
});
}

// Async generator function to yield data based on IDs
async function* asyncDataGenerator(ids) {
for (const id of ids) {
// Wait for the promise to resolve
const data = await fetchData(id);
yield data;
}
}

// Array of IDs for fetching data
const ids = [1, 2, 3, 4, 5];

// Iterate over the async data generator using for-await-of
(async () => {
try {
for await (const item of asyncDataGenerator(ids)) {
console.log(item);
}
} catch (e) {
console.error(e);
}
})();


Code:
// Async generator function to yield data based on IDs
async function* asyncDataGenerator(ids) {
for (const id of ids) {
const data = fetch(`https://reqres.in/api/users/${id}`)
.then((res) => {
return res.json();
});
yield data;
}
}

// Array of IDs for fetching data
const ids = [1, 2, 3, 4, 5];

// Iterate over the async data generator using for-await-of
(async () => {
try {
for await (const item of asyncDataGenerator(ids)) {
console.log(item);
}
} catch (e) {
console.error(e);
}
})();

output of Both Code is same.

Output:
{
data: {
id: 1,
email: 'george.bluth@reqres.in',
first_name: 'George',
last_name: 'Bluth',
avatar: 'https://reqres.in/img/faces/1-image.jpg'
},
support: {
url: 'https://reqres.in/#support-heading',
text: 'To keep ReqRes free, contributions towards server costs are
appreciated!'
}
}
{
data: {
id: 2,
email: 'janet.weaver@reqres.in',
first_name: 'Janet',
last_name: 'Weaver',
avatar: 'https://reqres.in/img/faces/2-image.jpg'
},
support: {
url: 'https://reqres.in/#support-heading',
text: 'To keep ReqRes free, contributions towards server costs are
appreciated!'
}
}
{
data: {
id: 3,
email: 'emma.wong@reqres.in',
first_name: 'Emma',
last_name: 'Wong',
avatar: 'https://reqres.in/img/faces/3-image.jpg'
},
support: {
url: 'https://reqres.in/#support-heading',
text: 'To keep ReqRes free, contributions towards server costs are
appreciated!'
}
}
{
data: {
id: 4,
email: 'eve.holt@reqres.in',
first_name: 'Eve',
last_name: 'Holt',
avatar: 'https://reqres.in/img/faces/4-image.jpg'
},
support: {
url: 'https://reqres.in/#support-heading',
text: 'To keep ReqRes free, contributions towards server costs are
appreciated!'
}
}
{
data: {
id: 5,
email: 'charles.morris@reqres.in',
first_name: 'Charles',
last_name: 'Morris',
avatar: 'https://reqres.in/img/faces/5-image.jpg'
},
support: {
url: 'https://reqres.in/#support-heading',
text: 'To keep ReqRes free, contributions towards server costs are
appreciated!'
}
}

Asynchronous Operations with Generator Functions:

Traditionally, asynchronous tasks in JavaScript were managed using callbacks
or promises, which could lead to callback hell or complex promise chains.
Generator functions, in combination with yield and next(), offer a more
intuitive way to write asynchronous code.

Code:
function fetchData(url) {
return new Promise((resolve, reject) => {
setTimeout(() => {
const data = fetch(url).then((res) => {
return res.json();
});
resolve(data);
}, 2000);
});
}

function* fetchDataGenerator() {
const data1 = yield fetchData("https://reqres.in/api/users/1");
console.log(data1);

const data2 = yield fetchData("https://reqres.in/api/users/2");
console.log(data2);

const data3 = yield fetchData("https://reqres.in/api/users/3");
console.log(data3);
}

const iterator = fetchDataGenerator();
const promise = iterator.next().value;
promise.then((data) => {
iterator.next(data).value.then((data1) => {
iterator.next(data1).value.then((data2) => {
iterator.next(data2);
});
});
});

Output:
{
data: {
id: 1,
email: 'george.bluth@reqres.in',
first_name: 'George',
last_name: 'Bluth',
avatar: 'https://reqres.in/img/faces/1-image.jpg'
},
support: {
url: 'https://reqres.in/#support-heading',
text: 'To keep ReqRes free, contributions towards server costs are
appreciated!'
}
}
{
data: {
id: 2,
email: 'janet.weaver@reqres.in',
first_name: 'Janet',
last_name: 'Weaver',
avatar: 'https://reqres.in/img/faces/2-image.jpg'
},
support: {
url: 'https://reqres.in/#support-heading',
text: 'To keep ReqRes free, contributions towards server costs are
appreciated!'
}
}
{
data: {
id: 3,
email: 'emma.wong@reqres.in',
first_name: 'Emma',
last_name: 'Wong',
avatar: 'https://reqres.in/img/faces/3-image.jpg'
},
support: {
url: 'https://reqres.in/#support-heading',
text: 'To keep ReqRes free, contributions towards server costs are
appreciated!'
}
}

Explanation:
In the above example, we have a generator function fetchDataGenerator()
that yields promises obtained from the fetchData() function. Each yielded
promise is resolved inside a then() block, and the resulting data is passed
back to the generator function using next().

Benefits of Generator Functions
1) Simpler Asynchronous Code: Generator functions provide a cleaner and more
sequential way to handle asynchronous operations compared to a traditional
callback or promise-based approaches.

2) Lazy Evaluation: Generator functions allow for lazy evaluation of data
streams. They produce values on demand, reducing memory usage and improving
performance when dealing with large datasets.

3) Custom Iterators: Generator functions simplify the creation of custom
iterators, making it easier to iterate over custom data structures or
implement unique traversal patterns.

4) Stateful Execution: Generator functions retain their state
between invocations, allowing for resumable computation and maintaining
context across multiple function calls.

No comments:

Post a Comment