Using function generator
What is a function generator?
A function generator is a function that can be declared like function*
, it permits you to have control of the iteration of the function.
You will need to use .next()
to get the next value of the generator and in your function use yield
to return the value. Sometimes, we want to iterate the function on herself with new data from the result inside this function, to do that we use yield*
.
Example
Here is a simple example of how to use a function generator. This example has been taken from Let me hack better - workshop 2 (Tony Gorez / Thomas Gentillome)
const workshopInput = {name: "banana",items: [{name: "hibou",items: [{name: "apple",items: [],},{name: "kangourou",items: [],},],},{name: "foobar",items: [],},],};function* solution(item) {yield item.name;for (children of item.items) {yield* solution(children);}}const iterator = solution(workshopInput);console.log([...iterator]);// will log [ 'banana', 'hibou', 'apple', 'kangourou', 'foobar' ]}
Real-world example
Context
In the real world, sometimes, we need to make complex filters on a list of items. To exclude some items based on the filter we send to it, most of the time, we will define multiple functions to filter those items.
In those cases, you could use a function generator to make those filters, we will iterate on those items and inside them, we will iterate on the subitems if needed.
Example
We will have a structure like this :
const activities = [{status: "draft",libelle: "A Project Item",version: "1",},{status: "draft",libelle: "A Project Item",version: "2",},{status: "draft",libelle: "A Project Robot",version: "1",},{status: "published",libelle: "A Project Robot",version: "2",},];
In this list, we want to filter by type
and salut
or by a value like "toto" directly. We will need to iterate the activities
but in this list, we have could have one or multiple nested objects.
Also, we will pass all ours filters like this in this structure :
[{field: "status",value: "toto", // not mandatorypredicate: ({values, value, field}) => ["draft", "published"].includes(value); // also not mandatory.},"toto" // you could passed directely a value]
We will need to use a function generator to iterate the list and those nested objects, first let define the function that will iterate the current filter on the list and the current item.
function* checkFilterInObject(value, filter) {// first we check if our current value(item) is an objectif (typeof value === "object") {// we will see this later} else if (filter === value) {// filter is equal to the value.// When we pass a primitive value directlyyield true;return;}}
Now let's get focus on the internal if condition for the typeof value
is an object. We will need to check each value of the object and we will need to iterate if one of them is also an object.
If the filter.value
is equal, to the value of the current key of value
, we will return true|false
and we stop the function generator because we have a match or a mismatch.
function* checkFilterInObject(values, value, filter) {if (typeof value === "object") {for (const item in value) {// we check that the value of the item is equal to the filterif (filter?.field === item) {// We will see this later} else {yield filter.value === value[item];return;}} else {// if it's not equals, we will iterate the nested objectyield* checkFilterInObject(values, value[item], filter);}}} else if (filter === value) {yield true;return;}}
One last thing, as I said before, we maybe want to have some predicate that will return true|false
depending on the callback we passed in our filters.
To have some custom filter based on multiple properties from the object, we will check in our list.
function* checkFilterInObject(values, value, filter) {if (typeof value === "object") {for (const item in value) {if (filter?.field === item) {// we check that our filter as a predicateif (filter.predicate) {// We execute the predicate and we return the resultyield filter.predicate({values,value,field: filter.field,});return;} else {yield filter.value === value[item];return;}} else {yield* checkFilterInObject(values, value[item], filter);}}} else if (filter === value) {yield true;return;}}
Now we got our function to check in an item if the current filter is ok! We need one more function to iterate on an array of objects and an array of filters.
With this function, we will have full control over our filters and the sample data we want to filter.
function* filterItems(values, filters) {// we return only the last values from our array// Only if we don't have a filter any moreif (filters.length === 0) {yield values;return;}for (filter of filters) {// We iterate on the filterslet filteredValues = values.filter((item) => {const generatorResult = checkFilterInObject(item, filter);// we return the generator result// From our checkFilterInObject functionreturn generatorResult.next().value;});filters.shift();yield* filterItems(filteredValues, filters);}}
Solution code and a working React solution
Now let's compile every code we saw before and how we will use it. It's quite easy now!
const activities = [{status: "draft",libelle: "A Project Item",version: "1",},{status: "draft",libelle: "A Project Item",version: "2",},{status: "draft",libelle: "A Project Robot",version: "1",},{status: "published",libelle: "A Project Robot",version: "2",},];function* checkFilterInObject(values, value, filter) {if (typeof value === "object") {for (const item in value) {if (filter?.field === item) {if (filter.predicate) {yield filter.predicate({values,value,field: filter.field,});return;} else {yield filter.value === value[item];return;}} else {yield* checkFilterInObject(values, value[item], filter);}}} else if (filter === value) {yield true;return;}}function* filterItems(values, filters) {if (filters.length === 0) {yield values;return;}for (const filter of filters) {let filteredValues = values.filter((item) => {const generatorResult = checkFilterInObject(values, item, filter);return generatorResult.next().value;});filters.shift();yield* filterItems(filteredValues, filters);}}const filteredValues = filterItems(activities, [{field: "status",predicate: ({ value }) => ["draft"].includes(value.status),},]);console.log(...filteredValues);/*{status: "draft",libelle: "A Project Item",version: "V1",},{status: "draft",libelle: "A Project Item",version: "V2",},{status: "draft",libelle: "A Project Robot",version: "V1",}*/
And then we can use it in our code base, here is an example with some react components, there is a debounce on the search bar :
Project | Status | Version |
---|---|---|
A Project Item | draft | 1 |
A Project Item | draft | 2 |
A Project Robot | draft | 1 |
A Project Robot | published | 2 |
And the code of those components: Complete code of the component showed