DEV Community

Tyler Roberts
Tyler Roberts

Posted on

JS Symbol Iterators & Generators - An Exercise


What is function*, yield and yield*?


Brief

When you iterate over lists it's probably intuitive now for most to use Array.map(). However, many of us also like to generate our lists based on some range, not by way of data but some application logic defined number. Usually, I'll import lodash.range or create a range helper. What about specific sequences like Fibonacci? Well here we can take the power of large, possibily infinite size lists. Normally large lists would hurt performance if it's particularly large even in O(n).

Essentially we're creating a lazy loaded sequence.

In a lot of libraries today we have lazy load as a common way of handling lists of data that may be large in length or in size per element; image galleries.

Without writing a lot of helpers, importing libraries, or getting caught in cumbersome type associations as boilerplate, we can look at built-in Generators.

Now when we define our applications sequences or even the json we may use, we can immediately "close the faucet" of the flow of that data. Only opening it when we need it, making it reusable, and allowing us to completely throw it out if we need to start over.


A Linked List

Given a list of data we can look at a list of lists to get started:

const familyTree = [
    ["Adam", "Jane", "Doe"],
    ["Jane", "Peter", "Mary"],
    ["Mary", "Liam", "Olivia"],
    ["William", "Ava", "Lucas"]
]
Enter fullscreen mode Exit fullscreen mode

Here we have a "sorted" list going from familyTree[0] being the earliest generation and last index being the oldest.

Let's assume that the first of each is the "Child" and the other two are biological "Parents".

Iterator Logic

Let's start by creating our familyTree iterator logic.

function* genList(p1, p2) {
    const genealogy = [...familyTree].reverse();
}
Enter fullscreen mode Exit fullscreen mode

I choose to work backwards from generation, given our data, and spread operator to prevent mutation.

In this data our familyTree contains the newest generation at the head or first of the list. So we can reverse the list before we start.

yield*

We could easily create a map of each element very quickly with yield* to simply "iterate" the given data, and give us each Array within familyTree, but where's the fun in that. Our generator should have some logic, and yield genealogy[i] conditionally!

To clarify what * does, we can look at yield*:

function* genList() {
   yield* [...familyTree].reverse();
}

let i = genList();
console.log(i.next().value); //  [ 'William', 'Ava', 'Lucas' ] 
console.log(i.next().value); //  [ 'Mary', 'Liam', 'Olivia' ] 
Enter fullscreen mode Exit fullscreen mode

Now let's search to find who we're actually looking for with p2 or person2

  • Let's imagine it's "Olivia"
    • ["William", "Ava", "Lucas"] is first since we reversed, so we can skip over it

Reverse the data

function* genList(p1, p2) {
    const genealogy = [...familyTree].reverse();
    let start = 0, end = genealogy.length - 1;
    for (const i in genealogy) {
        if (genealogy[i].includes(p2)) {
            start = +i // coerce typeof i to number from string
        }
        if (genealogy[i].includes(p1)) {
            // Exercise: what would go here, and why?
            // leave a comment below 😉
        }
    }
Enter fullscreen mode Exit fullscreen mode

Now that we can rule out names that aren't even in here.

Let's loop through our reduced list by finding links in each family array, for preceding arrays;

  • or "blood" relation
  • see: Linked List

Did the child become a parent?

main.js

function* genList(p1, p2) {
    [...]


    // |-> Read along
    // Iterator Logic
    for (let i = start; i <= end; i++) {
        // yield will send over the first family
        let link = yield genealogy[i]
        // with .next(Child) we can pass over a name 
        // from the above: yield genealogy[i]
        // to the above: link
        if (link && (i + 1) <= end) {
            let [_, ...parents] = genealogy[i + 1]
            // Did that child became a parent?                      
            // Let's see if parents include the link
            if (parents.includes(link)) {
                yield genealogy[i]
            }
        } else {
            // but if there's no subsequent links...
            break;
        }
    }
}

Enter fullscreen mode Exit fullscreen mode

Let's Apply it and Test

main.js

/**
 * 
 * @param p1 Child
 * @param p2 Relative
 */
const isGenerational = (p1, p2) => {
    let link;

    // generate genealogy with lower and upper bounds
    const ancestry = genList(p1, p2)
    // get Child from each family and yield links 
    for (const [ancestor] of ancestry) {
        (ancestor === p1)
            // if p1 is found, we can throw the list away
            ? link = ancestry.return(true)
            // if linked list continues
            : link = ancestry.next(ancestor)

    }
    return (link.done && link.value)
}

(async () => {
    console.log(
        (isGenerational("Adam", "Olivia") === true),
        (isGenerational("Adam", "Lucas") === false),
        (isGenerational("Jane", "Liam") === true),
        (isGenerational("Mary", "Ava") === false),
    )
})();
Enter fullscreen mode Exit fullscreen mode

Repl

Recap

Destructuring parameters from next via yield

Let's look at this very peculiar statement.

let link = yield genealogy[i]
Enter fullscreen mode Exit fullscreen mode

It's the initialization that makes it useful.

We can send data over initially, and wait for any data that may be contextual.

let whatAboutThis = yield context[i]
if (whatAboutThis) {
    // perform check, updates, hydrate, whatever
    await updateContext(whatAboutThis)
    yield context["imaginary"]
}
Enter fullscreen mode Exit fullscreen mode

Essentially when our function obtains something we can have our iterator pass it up to the generator and allocate a new yielded value.

const iterator = contextSequence(); // generates sequence of "context"
let water = iterator.next("hydrate"); // <- gets passed to `whatAboutThis` 
water.value // -> value stored of context["imaginary"] 
Enter fullscreen mode Exit fullscreen mode

I can imagine reactive state handlers here. Imagine a federal reserve of some data that is only accessed when stores are low; lazy loaded galleries.

Great way to handle large queues that run async but not AOT.

I'm thinking debounce function for subscribed events that aren't time critical. Although I'll need to play around with it a bit. Almost every example showcases take on infinite lists, so it's very functional.

Top comments (0)