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"]
]
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();
}
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' ]
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 😉
}
}
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;
}
}
}
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),
)
})();
Repl
Recap
Destructuring parameters from next via yield
Let's look at this very peculiar statement.
let link = yield genealogy[i]
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"]
}
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"]
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)