DEV Community

artydev
artydev

Posted on

The laziness of generators is a great property.

I won't describe what are generators, many people will do it way better than I could.

Here is a very good documentation JSINFO

You know now what they are,
Time to explore their laziness property.

Let's first create two generators functions (thanks to James Sinclair):

function* filter(p, xs) {
  for (const x of xs) if (p(x)) yield x;
}

function* map(fn ,xs) {
   for (const x of xs) yield fn(x)
}

Enter fullscreen mode Exit fullscreen mode

Testing their laziness :


let data = [1,2,3,4,5,7,8,9,10]

let evens = filter(x => x % 2 == 0, data);

console.log(evens) // empty array

Enter fullscreen mode Exit fullscreen mode

The result of our filter function is an empty array !!!

The rule is that a generator does not produce any value until you really iterate on it.

Let's try the following :


let data = [1,2,3,4,5,7,8,9,10]

let evens = filter(x => x % 2 == 0, data);

console.log(Array.from(evens)) // [ 2, 4, 8, 10 ]

Enter fullscreen mode Exit fullscreen mode

This time we get an actual value, great.

It would be nicer if we could wrote :


let results = evens.toArray ()

Enter fullscreen mode Exit fullscreen mode

This method does not exists on generators, so let's create an extension method, to allow it :


Object.defineProperty(
 Object.getPrototypeOf(function* () {}).prototype,
 "toArray",
 {
   value: function () { return Array.from(this) }
 }
);

Enter fullscreen mode Exit fullscreen mode

Let's try it :


let data = [1,2,3,4,5,7,8,9,10]

let evens = filter(x => x % 2 == 0, data);

console.log(evens.toArray()) // [ 2, 4, 8, 10 ]

Enter fullscreen mode Exit fullscreen mode

Great, that works.

Recall, until we are not applying our toArray extension, nothing is evaluated, executed, generators are lazy by nature.

Let's go a little further :

Given this array :


const data = [1,2,4,5,6,7,8,9,10]

Enter fullscreen mode Exit fullscreen mode

Let's return the even values greater than 4 and multiplied by 5:


let evens = filter(x => x % 2 == 0, data);

let evensSup4 = filter(x => x > 4, evens);

let mul5evensSup4 =  map(x => x * 5, evensSup4);

Enter fullscreen mode Exit fullscreen mode

demo

Note, that our functions are provided with actual data, and nothing is executed, awesome !!!

Time to unveil the results :

We can pass the generator with 'its' context to a function.


function showMul5Evens (gen) {
  console.log(gen.toArray())
}

showMul5Evens(mul5evens) // [ 30, 40, 50 ]

Enter fullscreen mode Exit fullscreen mode

Demo

We can "pipe" our predicates like so :


let pipefilter = (...predicates) => xs => {
  return predicates.reduce((acc, predicate) => {
    return filter(predicate, acc)
  }, xs)
}

Enter fullscreen mode Exit fullscreen mode

Here is an application :



const users = [
  {
    nom : "paul" ,
    age: 24,
    city: "Paris"
  },
    {
    nom : "henri" ,
    age: 16,
    city: "Paris"
  },
  {
    nom : "paul" ,
    age: 23,
    city: "Sens"
  }    
]

let pipefilter = (...predicates) => xs => {
  return predicates.reduce((acc, predicate) => {
    return filter(predicate, acc)
  }, xs)
}

let f = pipefilter (
  user => user.age < 18, 
  user => user.city == "Paris"
)

console.log(f(users).toArray()) // [ { nom: 'henri', age: 16, city: 'Paris' } ]

Enter fullscreen mode Exit fullscreen mode

Demo

Generators allow to anticipate any transformations with your data, without wasting cpu cycles, and preserving memory.

And example of preserving memory is when you have to read gigabytes files size.
In order to avoid to put all the file in memory, you can read one line at a time using generators.

And they are capable of much more :-)

Beware, for very large arrays and complex transformations better use transducers, but that is another beast :-)

Top comments (0)