Ensar Bavrk
Ensar Bavrk's Blog

Ensar Bavrk's Blog

How to do Lazy evaluation in JavaScript

Photo by Manja Vitolic on Unsplash

How to do Lazy evaluation in JavaScript

Functional programming for JS developers demystified

Ensar Bavrk's photo
Ensar Bavrk
ยทJan 30, 2022ยท

6 min read

Introduction

Lazy evaluation is an evaluation strategy that avoids unnecessary work until it is absolutely necessary to produce a result. The name implies is lazy, and it will stay that way unless it is absolutely needed to do some work to evaluate a result. If you are wondering how this technique is useful, stick till the end of the post. One benefit would be that we will be able to work with infinite lists and all perceived infinity and all sorts of cool things which we will be exploring today.

The JS engine will be strictly evaluating expressions and, by default, all expressions will always be computed. But we can use some mechanisms to defer computation when needed. In the next example, we can see that the engines would eagerly compute 1 + 1.


const incrementNumber = number = number + 2
const value = incrementNumber(1 + 1)

Now, we passed the argument in the function, the value 1 + 1. And this expression will be computed before we go into the function to compute the number + 2. That means that the engine will strictly evaluate expressions instead of lazily. lazy evaluation would be that we compute 1 + 1 in the function call itself, since that is the time when we require a computed value of 1 + 1.

I know this is a very trivial example, but hopefully this should show what it would mean to have by default strict or lazy evaluation of expressions.

If we wanted to postpone evaluations, we could wrap those expressions into the functions and that would prevent the engine from evaluating it on the spot, only when the function is called


const incrementNumber = number = number() + 1
const value = incrementNumber(() => 1 + 1)

Hopefully, this will give a perspective on how it is possible to delay the evaluation of in - place expressions.

But let's take a look at an example where we could benefit from this technique. For example, we have a list, and we want to transform that list into the list with data that is more suitable for us to present it, Let's use the example that you would certainly use as software developer, we have a list of eggs ['๐Ÿฅš', '๐Ÿฅš', '๐Ÿฅš'] but we actually want a list of chickens to present ['๐Ÿ“', '๐Ÿ“', '๐Ÿ“'], normally we would map those eggs into the chickens and present them on our page.

['๐Ÿฅš', '๐Ÿฅš', '๐Ÿฅš'].map(() => '๐Ÿ“')

And now we have a list of grown chickens to present. But let's say we have a huge list of eggs like in thousands or a hundred thousand eggs to transform into chickens, and usually we only need a few chickens sometimes one and sometimes just two or three. Now, just mapping through all the eggs would become a bit of a resource - consuming without actually a need to do that, since we would most likely never require all the chickens. So it is a good time to get in touch with our tool box and use lazy evaluation technique to help us with this issue.

const lazyMap = (arr, fn) =>
 (start, finish?) => 
  !finish ?
    fn(arr[start]) :
    arr.slice(start,finish).map(fn)

What did we do in the little snippet above? We took an array and mapping function as parameters to our function that will help us to build a lazily evaluated map, and we returned a function which will be our API for accessing our lazy map of chickens. We accept two parameters : start and end, If the end is not present, that means we only want one chicken and if the end is present too, then we want a list of chickens from start to the end. And mapping of the eggs to the chicken will be done only when we call the function that is produced by lazyMap. Basically, no matter how huge array we supply to our map function, we will be mapping only those elements that would be affected by start - end parameters.

But can we do better when we run a mapping function over the values that already have been mapped, I briefly mentioned one optimization technique in my previous blog, the memoization. Shortly, let's remember what that is, basically we would compute an expression once and store the result of that computation and later when we need that result again we would just return previous computation instead of computing the result again. I guess this is a perfect time to go hand in hand to help us solve this issue.

First, we will create one function to help us remember the computation, and we will call that function you would never guess a memoize

const memoize = fn => {
  const map = new Map();
  return funarg => 
    map.has(funarg) ?
      map.get(funarg) :
      map.set(funarg, fn(funarg)).get(funarg)
}

First, we created a function that takes a function that we will evaluate and store the result. Then, inside the memoize function, we used JS Map data structure key, value storage with an easy API to check if we have a value inside a structure. And we return a new function that will evaluate and store the function result inside a map or if we have already evaluated an expression, we will just return the already computed value. We already spoke about pure functions in the previous blog post and this is only possible if our functions are idempotent. Basically, no matter how we call the function, if we use the same inputs we would always compute the same outputs. That is why we could store arguments as keys and value as an evaluated function that we would memoize.

Now we will make some small changes to our lazyMap function to use a new memoize function.

const lazyMap = (arr, fn) => {
  const memoFn = memoize(fn);
  return (start, finish?) =>  
      !finish ?
          memoFn(arr[start]) :
          arr.slice(start,finish).map(memoFn)
}

Nothing much changed, right? We only added a memoize call to our map function and inside the lazy function we used a memoized function instead of the original function, and now we will see just by adding a couple of lines of code we would not need to compute the transformation of egg to multiple times rather just once since pretty much nothing changes egg will always produce chicken.

const mapped =  lazyMap(['๐Ÿฅš', '๐Ÿฅš', '๐Ÿฅš','๐Ÿฅš', '๐Ÿฅš'], () => '๐Ÿ“')

console.log(
  mapped(0,2),
  mapped(1,2)
  mapped(4)
)

I'm quite sure now after all of these chicken and egg examples you are wondering about well known problem which one is older? And I am glad you got this far and, as a bonus to you, I will write a function to find a solution to that problem, and we will never wonder again about it.

const chickenEggProblem = () => 
  ['๐Ÿ“', '๐Ÿฅš'].sort()[0] === '๐Ÿ“' ? 
    'chicken is older' :
    'egg is older'

You can inspect the page and copy/paste a code into a console and run the code and see for yourself.

Thanks for taking the time to read it. If you liked the article, you might want to subscribe to my mailing list.

ย 
Share this