September 12, 2020

Generator Functions

edit

Have you ever heard of the Baader-Meinhof phenomenon? It’s a cognitive illusion where once you become aware of some thing, you see that thing every where. It’s called an illusion because the thing was always there, you were just unaware of its presence.

I feel like generator functions are a bit of a Baader-Meinhof phenomenon for me. I never had a use for them, but once I figured out a single use for them, now I see reasons to use them everywhere.

I recently used them while doing some maze generation experiments during some time off. Check out the tweets in this thread if that interests you. They were perfect for the problem I had at hand, and I want to take some time to explain what they are and why they might be useful to you.

What’s a Generator Function?

A generator function is a special function, denoted by the use of the * in its declaration.

function* myFirstGenerator() {
  // do stuff here
}

This function is different. Calling this function will not return the value of your function’s body. Not right away, at least. More on this shortly. Instead, it returns a Generator object. This object has several methods on it, but the one we’re most concerned with is the next method. Calling next() on our generator object is how we return the next generated result from our generator function.

This result is an object with two properties: value and done. Let’s discuss how we get these values first.

The most common way we will “return” values from a generator functions is with the yield keyword. Let’s give it a try in a very rudimentary way.

function* myFirstGenerator() {
  yield 42
}

const generatorObject = myFirstGenerator()

console.log(generatorObject.next()) // { value: 42, done: false }
console.log(generatorObject.next()) // { value: undefined, done: true }

I want to draw your attention to a few things. First, as I pointed out, we call our generator function to get our generatorObject. Calling next returns an object with the yielded value and a done property. But why is done false on the first yield? There’s nothing else returned in our function body, right?

Wrong.

Just like all normal JavaScript functions, there is an implicit return undefined if there isn’t an explicit return statement in the function body. This implicit return undefined is what we get on the second call of the next method. Hence why value is undefined and done is now true.

From this, we can deduce that using return instead of yield is how to stop a generator.

Let’s combine the use of return with multiple yields. Yes, that’s right. You can yield as many times as you would like in a generator function.

function* countTo(number) {
  let i = 0

  while (i < number - 1) {
    i++
    yield i
  }

  return number
}

const countTo3 = countTo(3)

console.log(countTo3.next()) // { value: 1, done: false }
console.log(countTo3.next()) // { value: 2, done: false }
console.log(countTo3.next()) // { value: 3, done: true }
// Let's call it an extra time to see what happens
console.log(countTo3.next()) // { value: undefined, done: true }

As you can see, our function “counts” as we expect and is done when we expect, too.

Something Mildly More Interesting

Let’s try this on something mildly more interesting. Are you familiar with the Collatz conjecture. It’s an algorithm in mathematics that returns a sequence of numbers. The algorithm is as follows:

Given any positive integer, if the number is even, divide it by 2, otherwise, multiply it by 3 and add 1. Repeat until you arrive at 1.

Writing this algorithm as a generator function is pretty straightforward and we can generate each step in the sequence.

const isOdd = num => Boolean(num % 2)
const collatz = num => (isOdd(num) ? 3 * num + 1 : num / 2)

function* collatzSequence(num) {
  let current = num

  while (current !== 1) {
    yield current
    current = collatz(current)
  }

  return current
}

const sequence = collatzSequence(17)

I want to draw your attention to something I find interesting about how we’re able to write this algorithm with a generator. Because the function essentially pauses execution on the yield statement, we’re able to yield the current number before mutating the current variable with the next collatz result. I’ve made a simple React component that will allow you to try out different numbers and generate the Collatz sequence.

Collatz Sequencer

Choose a starting number and type it into the input. Then click the “Next” button to generate the sequence.

Isn’t that interesting? We’re able to get each step in the sequence very easily, and the code is pretty simple to write as well.

Something Practical

Alright, let’s make one more generator function, but use it to solve a practical problem. In fact, I’m going to use it to solve the very problem that inspired me to write this blog post: loan repayment.

If you follow me on Twitter, you probably know of my trevails with student loans. We’ve been paying them off aggressively for years, and still have a few more years to go, but how many to be exact? And how much will an extra $100 here or there help me? All questions that can be answered with a small program. So let’s build a simplified version of it.

A loan has a principal (starting amount), an interestRate, a compounding period (how often it compounds), and a payment.

Let’s start with the simplest part, a few helper functions:

// Basic compounding formula
const compound = (amount, rate) => amount + amount * rate

// Keeps floats to a max of 2 places past the decimal
const to2 = num => Number(num.toFixed(2))

Now, let’s set about writing a makePayment generator function. I’ll bake some details specific to student loans in the function, and make comments when I’m doing so.

function* makePayment(principal, rate, payment) {
  // Student loans compound daily, so we create a daily interest rate
  // by dividing our annual rate with the number of days in a year
  const dailyInterestRate = rate / 365
  let remaining = principal
  let totalInterestPaid = 0
  let paymentsCount = 0

  while (remaining > 0) {
    let beginWith = remaining
    let endWith = remaining

    // For simplification, we're going to treat each payment period
    // as 30 days long
    for (let i = 0; i < 30; i++) {
      endWith = compound(endWith, dailyInterestRate)
    }

    // Student loans pay the accrued interest first, then the principal
    // We're keeping this formula rudimentary, but this let's us see what
    // interest accrued since the last payment
    const interestAccrual = to2(endWith - beginWith)
    totalInterestPaid = to2(totalInterestPaid + interestAccrual)

    endWith = to2(endWith - payment)
    remaining = to2(endWith)
    paymentsCount++

    if (endWith <= 0) {
      endWith = 0
      remaining = 0

      return {
        beginWith,
        endWith,
        interestAccrual,
        paymentsCount,
        totalInterestPaid,
      }
    }

    yield {
      beginWith,
      endWith,
      interestAccrual,
      paymentsCount,
      totalInterestPaid,
    }
  }
}

Simple Payment Generator

As of the time of this writing, looks like I have about ~33 payments to go. It’s pretty cool to see how much changing the payment can change how long it will take to pay the rest of the loan.

If I wanted to spend more time on this in this particular post, you could make some very cool data visualizations with generator functions sequencing each update to the visualization. Perhaps I’ll explore this in a future post.

Summary

Generator functions are a special function that allow you to pause execution, yielding multiple (if not infinite) values from them. You might have to stretch your imagination to find a good use for them, but they’re a handy tool to have in your tool chest.

If you’d like to read more about them, I suggest looking at the MDN docs on them.

Caveat

Generator functions will not work in any version of IE. Womp womp.


Liked the post?
Give the author a dopamine boost with a few "beard strokes". Click the beard up to 50 times to show your appreciation.
Kyle Shevlin's face, which is mostly a beard with eyes

Kyle Shevlin is the founder & lead software engineer of Agathist, a software development firm with a mission to build good software with good people.

Logo for Introduction to State Machines and XState
Introduction to State Machines and XState
Check out my courses!
If you enjoy my posts, you might enjoy my courses, too. Click the button to view the course or go to Courses for more information.
Sign up for my newsletter
Let's chat some more about TypeScript, React, and frontend web development. Unsubscribe at any time.