How to do Lazy evaluation in JavaScript

Functional programming for JS developers demystified

ยท

6 min read

Introduction

Lazy evaluation is an evaluation strategy that avoids unnecessary work until it is necessary to produce a result.
The name implies it is lazy, and it will stay lazy unless it is being called to do some work to evaluate a result. One benefit of this technique would be that we will be able to work with infinite lists and all perceived infinity.

In the language like JS by default computations will not be lazy the JS engine will be strictly evaluating expressions and, by default, all expressions will always be computed.

But lets see which mechanisms we can use to defer computation when needed.

In the example, the engine computes the 1 + 1 in the parameters of the function, and inside the function body we work with the already computed value.


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

Passed arguments in the function 1 + 1 the expression is computed before we get into the function to compute the number + 2 (arguments passed).

That means that the engine will strictly evaluate expressions instead of lazily. A lazy evaluation would be that we compute 1 + 1 only when that is needed in the function call itself.

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.

So how we can go around these limitations of by default strict evaluation inside JavaScript?

If we want to postpone evaluations, we must wrap those expressions into the functions since functions cannot be evaluated before calling them.
With the example of the incrementing that would look something like this.


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 a list with data that is more suitable for us to present.

Let's use the example that you would certainly use in day-to-day life. For example, we have a list of eggs ['๐Ÿฅš', '๐Ÿฅš', '๐Ÿฅš'] but we want a list of chickens to present ['๐Ÿ“', '๐Ÿ“', '๐Ÿ“'].
Normally we would just use .map to transform 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 on screen to present. Now, just mapping through all the eggs would become a bit of resource-consuming without actually a need to do that.

So it is a good time to get in touch with our toolbox and use the lazy evaluation technique to help us with this issue and rewrite the standard mapping function to a lazy one.

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

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.

We have two parameters: start and end. If the end is not provided, it means we are getting only one item. However, if the end is also provided, we get a full list of items ranging from start to end.

Actual mapping will be done only when we call the function that is produced by lazyMap. No matter how huge an 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 even better?

When we execute a mapping function over the values that already have been mapped we would do the mapping every time we desire those items again which is not desired.
I briefly mentioned one optimization technique in my previous blog, memoization. Shortly, let's remember what that is. Basically, we would compute an expression once and store the result of that computation for later if we need that result again. So when we want to map the item again we would reech the already stored computed result instead of executing the mapping again.

First, we will create one function to help us remember the computation.

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 function, we used key and value storage to store computations. We return a function which determines if there is already computed value to pull from the key-value store or to evaluate, store and return the function result. We already spoke about pure functions in the previous blog post and this is only possible if our functions are idempotent. 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 values as an evaluated function that we would memoize.

Now we will make some small changes to our lazyMap function to use a the above 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 a 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.

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

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

Thanks for taking the time to read it.

ย