Generator Functions

Introduction

The Generator function (function*) is a powerful feature in JavaScript that allows you to iterate over a sequence of values lazily, meaning they produce values only when needed.

Generator functions behave differently from regular functions in javascript. While a regular function returns a value on execution, a generator function can return multiple values, one after the other, on-demand. When a generator function is called, it does not runs its code and instead, it returns a special object call generator object called generator, to manage the execution.

Syntax

javascript
	function* name(param1) {
	  statements;
	}

Parameters

name: The function name.

param1: The name of a formal parameter for the function. It is optional.

Statements: The body of the function. It is optional.

Generator Methods

The primary method is next(), which is used to retrieve the next value yielded by the generator. The next() method of a generator function returns an object with two properties: value and done. While value property represents the value yielded by the generator function. If the generator function contains a yield statement, the value property will be set to the value specified in the yield statement. If the generator function has finished executing or there are no more values to yield, the value property will be undefined, the done property indicates whether the generator function has finished executing (true) or not (false). If the generator function has finished executing, done will be true. If there are more values to yield, done will be false. See example:

javascript
	function* generateFunc() {
	  yield 1;
	  yield 2;
	  return 3;
	}
	
	const generator = generateFunc();
	
	const firstValue = generator.next();
	
	console.log(JSON.stringify(firstValue)); // output: {"value":1,"done":false}
	
	let secondValue = generator.next();
	
	console.log(JSON.stringify(secondValue)); // output: {"value":1,"done":false}

However, there are a couple of other methods associated with generator objects:

return()

The return() method is used to forcefully terminate the generator. It can be called with an optional argument, which becomes the value of the yield expression that caused the generator to finish.

Example:

javascript
	function* myGenerator() {
	    yield 1;
	    yield 2;
	    yield 3;
	    yeild 5
	}
	
	const generator = myGenerator();
	console.log(generator.next()); // Output: { value: 1, done: false }
	console.log(generator.return(4)); // Output: { value: 4, done: true }
	console.log(generator.next()); // Output: { value: undefined, done: true }

throw()

The throw() method is used to throw an exception into the generator. It can be caught within the generator using a try…catch block.

Example:

javascript
	function* myGenerator() {
	  try {
	    yield 1;
	    yield 2;
	  } catch (error) {
	    console.log('Error caught:', error);
	  }
	}
	
	const generator = myGenerator();
	console.log(generator.next()); // Output: { value: 1, done: false }
	console.log(generator.throw('Error occurred')); // Output: Error caught: Error occurred
	console.log(generator.next()); // Output: { value: undefined, done: true }

Generators are iterables

There is different between a generator and a generator function. A generator is the response returned from the execution of a generator function. Generators are iterable because you can loop over the values using for..of loop. See example below:

javascript
	function* myGenerator() {
	  yield 1;
	  yield 2;
	  yield 3;
	}
	
	const generator = myGenerator();
	
	for (let value of generator) {
	  console.log;
	}

Optionally, a generator can be looped over by tranforming it to an array and then use array method to loop over it as below:

Using spread operator (...)

You can use the spread operator to expand the generated values of a generator function into an array or another iterable object.

Example:

javascript
	function* myGenerator() {
	  yield 1;
	  yield 2;
	  yield 3;
	}
	
	const generator = myGenerator();
	const values = [...generator]; // Spread the generated values into an array
	console.log(values); // Output: [1, 2, 3]

Using Array.from()

You can use the Array.from() method to create an array from the generated values of a generator function.

Example:

javascript
	function* myGenerator() {
	  yield 1;
	  yield 2;
	  yield 3;
	}
	
	const generator = myGenerator();
	const values = Array.from(generator); // Create an array from the generated values
	console.log(values); // Output: [1, 2, 3]

Generator Composition

Generator composition refers to the practice of combining multiple generator functions to create new generator functions. This allows you to modularize and reuse generator logic, similar to how functions are composed in functional programming.

Generator composition is achieved by using yieldstatements within a generator function. The yield statement delegates the execution of another generator function from within the current generator function, effectively chaining the two generators together.

Here is an example

javascript
	function* generator1() {
	  yield 'a';
	  yield 'b';
	}
	
	function* generator2() {
	  yield '1';
	  yield '2';
	}
	
	function* composedGenerator() {
	  yield* generator1();
	  yield* generator2();
	}
	
	const generator = composedGenerator();
	
	console.log(generator.next()); // Output: { value: 'a', done: false }
	console.log(generator.next()); // Output: { value: 'b', done: false }
	console.log(generator.next()); // Output: { value: '1', done: false }
	console.log(generator.next()); // Output: { value: '2', done: false }
	console.log(generator.next()); // Output: { value: undefined, done: true }

In the example above, composedGenerator is a new generator function that combines the logic of generator1 and generator2 using yield* statements. When composedGenerator is executed, it yields values from generator1 until it is exhausted, then it yields values from generator2. This demonstrates how generator composition can be used to create more complex generator functions by reusing existing generator logic.

Practical Example

Generator can be handy when performance is key. Here are examples demonstrating how generator function can be used to improve the performance of your logics:

Fetching and paginate data from the server

javascript
	async function fetchData(url, page, pageSize) {
	  const response = await fetch(`${url}?_page=${page}&_limit=${pageSize}`);
	
	  if (!response.ok) {
	    throw new Error(`Failed to fetch data: ${response.statusText}`);
	  }
	
	  const data = await response.json();
	  return data;
	}
	
	async function* paginateData(url, pageSize) {
	  let currentPage = 1;
	  while (true) {
	    const data = await fetchData(url, currentPage, pageSize);
	
	    if (data.length === 0) {
	      break; // No more data
	    }
	
	    yield { data, currentPage };
	
	    currentPage++;
	  }
	}
	
	// Wrapper to handle pagination
	(async () => {
	  const url = 'https://jsonplaceholder.typicode.com/posts'; // Example API endpoint
	  const pageSize = 10; // Number of items per page
	  let currentPage = 1;
	  let dataGen = paginateData(url, pageSize);
	
	  try {
	    // Function to get the next set of data
	    async function getNextPage() {
	      const result = await dataGen.next();
	      if (!result.done) {
	        currentPage = result.value.currentPage;
	        return result.value.data;
	      } else {
	        return null; // No more data
	      }
	    }
	
	    // Function to get the previous set of data
	    async function getPreviousPage() {
	      // Todo: complete the logic to fetch the previous list of data
	      console.log('This is intended. You should complete it.');
	    }
	
	    console.log('First batch:', await getNextPage()); // return the first batch
	    console.log('Next batch:', await getNextPage()); // return the next batch
	    console.log('Previous batch:', await getPreviousPage()); // return the previous batch
	  } catch (error) {
	    console.error('Error fetching data:', error);
	  }
	})();

In the example above, the API endpoint used is https://jsonplaceholder.typicode.com/posts, which is a placeholder API that provides fake posts for testing and prototyping. The above code fetch data from the api, paginate and lazy load them.

Generating an infinite sequence of values.

javascript
	// 1. Generator function to produce unique order IDs
	function* orderIdGenerator() {
	  let currentId = 1;
	  while (true) {
	    yield `ORDER-${currentId.toString().padStart(6, '0')}`;
	    currentId++;
	  }
	}
	
	// 2. Simulating an order processing system
	const orderGen = orderIdGenerator();
	
	function processOrder(customerName, orderDetails) {
	  const orderId = ''; // TODO: using the orderIdGenerator, generate the order id
	  const order = {
	    id: orderId,
	    customerName: customerName,
	    orderDetails: orderDetails,
	    timestamp: new Date().toISOString()
	  };
	
	  // Simulate order processing (e.g., saving to database, sending confirmation)
	  console.log('Processing Order:', order);
	}
	
	// Usage
	processOrder('Alice', { item: 'Laptop', quantity: 1 });
	processOrder('Bob', { item: 'Smartphone', quantity: 2 });
	processOrder('Charlie', { item: 'Book', quantity: 3 });
	// Continue processing orders as needed

In the example above, the orderIdGenerator generates an infinite sequence of values and assign to variable orderId.

Additional Resources