The Advanced JavaScript Guide: Generators. Part 2, a simple use case



The behavior of the generators described in the previous article is not complex, but it is definitely surprising and may seem confusing at first. So instead of learning new concepts, we will now pause and look at an interesting example of using generators.



Let's have a function like this:



function maybeAddNumbers() {
    const a = maybeGetNumberA();
    const b = maybeGetNumberB();

    return a + b;
}

      
      





Functions maybeGetNumberA



and maybeGetNumberB



return numbers, but sometimes they can return null



or undefined



. This is evidenced by the word "maybe" in their names. If this happens, do not try to put these values (for example, the number and null



), it is better to stop and return, say null



. Namely null



, and not some unpredictable value obtained by adding null



/ undefined



with a number or other null



/ undefined



.



So you need to check that the numbers are actually defined:



function maybeAddNumbers() {
    const a = maybeGetNumberA();
    const b = maybeGetNumberB();

    if (a === null || a === undefined || b === null || b === undefined) {
        return null;
    }

    return a + b;
}

      
      





Everything works, but if it a



is null



or undefined



, then there is no point in calling the function maybeGetNumberB



. We know that it will be returned anyway null



.



Let's rewrite the function:



function maybeAddNumbers() {
    const a = maybeGetNumberA();

    if (a === null || a === undefined) {
        return null;
    }

    const b = maybeGetNumberB();

    if (b === null || b === undefined) {
        return null;
    }

    return a + b;
}

      
      





So. Instead of three simple lines of code, we quickly bloated it to 10 lines (not counting empty ones). And functions are now applied if



, which you have to wade through to understand what the function does. And this is just an educational example! Imagine a real codebase with much more complex logic that makes such checks even more difficult. I would like to use generators here and simplify the code.



Take a look:



function* maybeAddNumbers() {
    const a = yield maybeGetNumberA();
    const b = yield maybeGetNumberB();

    return a + b;
}

      
      





What if we could let the expression yield <smething>



test if it is a <smething>



real value and not null



or undefined



? If it turns out to be not a number, then we just stop and return null



, as in the previous version of the code.



That is, you can write code that looks like it only works with real, defined values. The generator can check this and take appropriate action for you! Magic, right? And it's not only possible, it's easy to write!



Of course, the generators themselves do not have this functionality. They just return iterators, and you can insert values ​​back into the generators if you want. So we need to write a wrapper, so be it runMaybe



.



Instead of calling the function directly:



const result = maybeAddNumbers();

      
      





we will call it as a wrapper argument:



const result = runMaybe(maybeAddNumbers());

      
      





This pattern is very common in generators. By themselves, they don't know much, but with the help of self-written wrappers, you can give the generators the desired behavior! This is what we need now.



runMaybe



- a function that takes one argument: an iterator created by the generator:



function runMaybe(iterator) {

}

      
      





Let's run this iterator in a loop while



. To do this, you need to call the iterator for the first time and start checking its property done



:



function runMaybe(iterator) {
    let result = iterator.next();

    while(!result.done) {

    }
}

      
      





Inside the loop, we have two possibilities. If result.value



is null



or undefined



, then the iteration should be stopped immediately and returned null



. Let's do this:



function runMaybe(iterator) {
    let result = iterator.next();

    while(!result.done) {
        if (result.value === null || result.value === undefined) {
            return null;
        }
    }
}

      
      





Here, we return



immediately stop the iteration with help and return from the wrapper null



. But if it result.value



is a number, then you need to "return" to the generator. For example, if the yield maybeGetNumberA()



function maybeGetNumberA()



is a number, then you need to replace the yield maybeGetNumberA()



value of that number. Let me explain: let's say the result of the calculation maybeGetNumberA()



is 5, then we replace const a = yield maybeGetNumberA();



with const a = 5;



. As you can see, we do not need to change the extracted value, it is enough to pass it back to the generator.



We remember that you can replace yield <smething>



with some value by passing it as an argument to the method next



in an iterator:



function runMaybe(iterator) {
    let result = iterator.next();

    while(!result.done) {
        if (result.value === null || result.value === undefined) {
            return null;
        }

        // we are passing result.value back
        // to the generator
        result = iterator.next(result.value)
    }
}

      
      





As you can see, the new result is now stored in a variable again result



. This is possible because we specifically declared result



using let



.



Now, if the generator detects null



/ when retrieving a value undefined



, we simply return null



from the wrapper runMaybe



.



It remains to add something else so that the iteration process ends without detecting null



/ undefined



. After all, if we get two numbers, then we need to return their sum from the wrapper!



The generator maybeAddNumbers



ends with an expression return



. We understand that the presence return <smething>



in the generator causes it to return next



an object from the call { value: <smething>, done: true }



. When this happens, the loop while



stops because the property done



gets a value true



. But the last value returned (in our particular case, this a + b



) will still be stored in the property result.value



! And we can just return it:



function runMaybe(iterator) {
    let result = iterator.next();

    while(!result.done) {
        if (result.value === null || result.value === undefined) {
            return null;
        }

        result = iterator.next(result.value)
    }

    // just return the last value
    // after the iterator is done
    return result.value;
}

      
      





And it's all!



Let's create functions maybeGetNumberA



and maybeGetNumberB



, and let them return real numbers first:



const maybeGetNumberA = () => 5;
const maybeGetNumberB = () => 10;

      
      





Let's run the code and log the result:



function* maybeAddNumbers() {
    const a = yield maybeGetNumberA();
    const b = yield maybeGetNumberB();

    return a + b;
}

const result = runMaybe(maybeAddNumbers());

console.log(result);

      
      





As expected, the number 15 will appear in the console.



Now, replace one of the terms with null



:



const maybeGetNumberA = () => null;
const maybeGetNumberB = () => 10;

      
      





When executing the code, we get null



!



However, it is important for us to make sure that the function is maybeGetNumberB



not called if it maybeGetNumberA



returns null



/ undefined



. Let's check again that the calculation was successful. To do this, just add to the second function console.log



:



const maybeGetNumberA = () => null;
const maybeGetNumberB = () => {
    console.log('B');
    return 10;
}

      
      





If we have written the wrapper correctly runMaybe



, then when this code is executed, the letter will B



not appear in the console.



Indeed, when executing the code, we will see simply null



. This means that the wrapper actually stops the generator as soon as it detects null



/ undefined



.



The code works as intended: it produces null



any combination:



const maybeGetNumberA = () => undefined;
const maybeGetNumberB = () => 10;
const maybeGetNumberA = () => 5;
const maybeGetNumberB = () => null;
const maybeGetNumberA = () => undefined;
const maybeGetNumberB = () => null;

      
      





Etc.



But the benefit of this example does not lie in the execution of this particular code. It lies in the fact that we have created a universal wrapper that can work with any generator that can extract values null



/ undefined



.



Let's write a more complex function:



function* maybeAddFiveNumbers() {
    const a = yield maybeGetNumberA();
    const b = yield maybeGetNumberB();
    const c = yield maybeGetNumberC();
    const d = yield maybeGetNumberD();
    const e = yield maybeGetNumberE();
    
    return a + b + c + d + e;
}

      
      





You can do it in our wrapper without any problems runMaybe



! In fact, it doesn't even matter to the wrapper that our functions return numbers. After all, we did not mention the numeric type in it. So you can use any value in the generator - numbers, strings, objects, arrays, more complex data structures - and it will work with our wrapper!



This is what inspires developers. Generators allow you to add custom functionality to your code, which looks very common (apart from calls, of course yield



). You just need to create a wrapper that iterates the generator in a special way. Thus, the wrapper adds the necessary functionality to the generator, which can be anything! Generators have almost limitless possibilities, it's all about our imaginations.



All Articles