Using function generator

Wednesday, August 18, 2021 ☕ 5 min read

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 mandatory
predicate: ({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 object
if (typeof value === "object") {
// we will see this later
} else if (filter === value) {
// filter is equal to the value.
// When we pass a primitive value directly
yield 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 filter
if (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 object
yield* 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 predicate
if (filter.predicate) {
// We execute the predicate and we return the result
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;
}
}

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 more
if (filters.length === 0) {
yield values;
return;
}
for (filter of filters) {
// We iterate on the filters
let filteredValues = values.filter((item) => {
const generatorResult = checkFilterInObject(item, filter);
// we return the generator result
// From our checkFilterInObject function
return 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 :

ProjectStatusVersion
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

Source

@2021 Mickael Croquet. All Rights Reserved.