loading...
Cover image for Javascript Underdogs: Part 1 - The WeakMap

Javascript Underdogs: Part 1 - The WeakMap

kepta profile image Kushan Joshi ใƒป6 min read

Hello 2018! It has been 3 years since we first saw Javascript 2015 (aka ES6). During this time most of us have focused on the cosmetic changes like Arrow => Functions or the fancy destructing operator โ€ฆ .

gif1

Everyone needs something exciting like the upcoming fancy |> pipe operator. Who cares if ES6 also added things like WeakMap, WeakSet, Iterables, Map or Set. Even looking at this thing called WeakMap, feels so depressing ๐Ÿ˜ž.

Keeping the sarcasm aside, let us talk about WeakMaps๐Ÿ’ƒ.

Why you would need something Weak

I have to agree the name WeakMap is definitely a misnomer. If it were me I would have named it SuperMap. Before we get into definitions, let us actually take a moment and understand why we need WeakMap's in our apps.

Imagine itโ€™s 1990 ๐Ÿก and you create an app of all the countries ๐ŸŽŒ present at that time.

var USSR = {
  name: 'Soviet Union',
  capital: 'Moscow',
  ..
  ..
}

var countries = [ Afganishtan, Albania, Algeria, ..., USSR, ..., Zimbabwe ]

A user can click any country and get detailed information which also includes the area of the country. Below is a hypothetical area calculation function.

async function calcArea(country) {
  const boundaries = await fetch(country);

  area = calculateArea(country, boundaries); // takes a long time

  return area;
}

Caching the Area

Every time a user clicks a country you calculate the area. But we have a problem! If a user clicks a country multiple times you have to repeat this enormous asynchronous calculation, which is something we should totally avoid. There are generally two ways to solve this kind of problem.

  1. Debounce the function
  2. Cache the function

Debouncing is a peaceful way to calm down multiple aggressive invocations in a short interval of time. (Imagine an impatient user clicking the refresh button multiple times). Debounce allows us to only take the last invocation and discard the rest.

Since countries don't change area that often, we can simply cache the result of calcArea.

We can use both caching and debouncing to make our application performant. Below is a generic caching function which we will use to cache calcArea.

function cachify(fn) {
  // its a good idea to hide you cache inside the closure
  var cache = new Map();
  return arg => {
    if (cache.has(arg)) {
      return cache.get(arg);
    }
    var computed = fn(arg);
    cache.set(arg, computed);
    return computed;
  };
}

cachedCalcArea = cachify(calcArea);

cachedCalcArea(USSR); // goes and computes the area
cachedCalcArea(USSR); // already computed, returns the cached area

Great! We made some serious performance improvements.

But we have another problem, USSR just broke into 15 new countries. This would mean we remove USSR and add the newly formed countries to our countries array.

countries.remove(USSR);
// add the new countries
countries.add([Armenia, Azerbaijan, ...., Uzbekistan]);

Removing USSR just from the array doesn't help, as our cache still contains USSR and the calculated area. A naive solution would be to monkey patch our cachify function to remove USSR, but if the world continues to break into smaller countries we have got ourselves a memory leak.

We need a smart way to clean up our cache which scales well. There are multiple ways fellow developers would approach this problem:

  1. Maintain a precomputed area array and keep it in sync with countries.
  2. Figure out some smart cache eviction like LRU, time-based, etc.

Precomputing the area for every country seems to be a waste of computation, as most of the users won't ever be seeing every country.

We can use a smart caching strategy like Least Recently Used caching, this caching automatically removes the entry which is least recently used. But we aren't running out of memory with 160+ countries and LRU doesn't seem all that magical and seamless.

What about WeakMap?

wmap

WeakMap is the missing jigsaw piece for our caching problem. It automatically removes* any unused references from it.

"The WeakMap object is a collection of key/value pairs in which the keys are weakly referenced. The keys must be objects and the values can be arbitrary values." - MDN

I like to say WeakMap is nothing but a regular Map with dementia. It is a very forgiving data structure, it will forget things which no longer matter. (We should be like that too :P)

We can simply replace the Map with WeakMap in our caching function.

function weakCache(fn) {
  var cache = new WeakMap(); // <-- Behold the Weak!
  return (arg) => {
    if (cache.has(arg)) {
      return cache.get(arg);
    }
    var computed = fn(arg);
    cache.set(arg, computed);
    return computed;
  }
}
cachedCalcArea = weakCache(calcArea);

cachedCalcArea(USSR); // cache miss
cachedCalcArea(USSR); // cache hit

Now let USSR break into the 15 countries. We just need to take care of removing all references pointing to the USSR obj in our app and our cachedCalcArea function will automatically forget the USSR entry in the cache. Hence, avoiding the memory leak!

How does it forget things?

WeakMap works similar to a regular Map but in order to be a forgetful version of Map it imposes these constraints:

  • Primitive data type keys are not allowed (Numbers, String, null, true, etc)
  • You cannot list all the values inside the WeakMap

Let us see a hypothetical example of WeakMap

  • Imagine a WeakMap instance to be a building with thousands of ๐Ÿšช doors.
  var building = new WeakMap();
  • Each door has a unique key and we own a key ๐Ÿ”‘ for our ๐Ÿšช101. Due to the constraints mentioned above the key can only be an object.
  var key = {
    password: '๐Ÿ”‘'
  };
  • We can lock/unlock our door with this key.
  building.set(key, '๐Ÿšช101');

  building.get(key); // ๐Ÿšช101
  • Now a thief has seen our key (Its Javascript duh!) and he tries to fabricate a duplicate key.
  var fake_key = {
    password: '๐Ÿ”‘'
  };
  • Since we live in a Javascript world we clearly know even though they look same, they are not equal.
  fake_key === key // false
  • Our thief didn't read this awesome article and he tries to get into our building using his fake key only to fail :(.
  building.get(fake_key); // undefined

What happens if we lose the key

As long as some variable holds the reference to our original key we are safe. But if there comes a time when no variable in the entire app is holding a reference to our key, we lose the access to our ๐Ÿšช101.

This is exactly what powers the smart caching of a WeakMap. If we lose the key, the GC can deduce that there is no way to access the thing associated with the key and it can safely remove it from the memory.

Note: This is the crucial difference between a WeakMap and Map. WeakMap removes <key,value>if you lose the key, but in a Map, you can simply list all the keys to find the lost key.

Coming back to USSR problem, when USSR breaks into the 15 countries and we just need to take care of removing all references to the USSR obj in our app.

countries.remove(USSR); // remove from array

USSR = undefined; // unset the variable

// at this point there is no way to get the cached area of USSR since it doesn't exist anymore

As you can see after the above steps, there is no way of accessing the USSR object in the current state of app and with this knowledge Javascript garbage collector automatically clears the memory it reserved for the area of USSR. Notice the removing happens behind the scenes and all we did was replace Map with WeakMap. Isn't that powerful?

WeakMap Takeaways

  • Remember not to mutate the key object because in Javascript the object reference stays the same even if you mutate the object.
var obj = {name: '๐Ÿ•'};
weakMap.set(obj, 'animal');

obj.name = '๐Ÿ™โ€โ™‚๏ธ';
weakMap.get(obj); // 'animal'
  • WeakMap cannot accept primitive javascript values as keys. You should use Map if you wanna use them as your key.
weakMap.set('key', 'value'); // Error!
  • Sometimes it is faster to not cache a function. If your function barely takes a millisecond to execute, you would end up slowing it down by caching.
  • You can use anything as a value for WeakMap/Map. Yes even promises!
  • The eviction of an unsued key doesn't happen immediately. It depends on the garbage collector's mood. You shouldn't worry about this part though.
  • WeakMap works great for derived state. A lot of times your application has state which can simply be derived from other state. In example below, you can see deriving a value using cached function is much more maintainable and easier to reason with.
var user = {
    name: "Kushan Joshi"
}

var websites = ['Facebook', 'Github', 'Twitter', 'Dev.to', 'Medium'];

var memberOf = (user) => websites.filter(website => isUser(user));

// save the websites and keep track of it, too complicated ๐Ÿคฎ !
user.memberOf = memberOf(user);

// deriving the value using weakMaps, awesomo ๐Ÿค–!
cachedMemberOf = weakCache(memberOf); // avoid recomputing everytime
// or derive it everytime you need it
console.log(cachedMemberOf(user)); 
render(cachedMemberOf(user))

I really hope this article helped you in understanding WeakMaps. I love using it with libraries like Immutable.js or Redux since they enforce immutability. Even if you don't use these libraries, as long as you don't mutate the object you can reap benefits from WeakMap.

I am planning on writting a Part-2 of Javascript Underdogs, let me know in the comments what Javascript feature you think is amazing but underapreciated.

If you โค๏ธ this article, please share this article to spread the words.

Reach out to me on Twitter @kushan2020.

Posted on by:

kepta profile

Kushan Joshi

@kepta

I <3 Javascript and ๐Ÿ‡ฎ๐Ÿ‡ณ

Discussion

markdown guide
 

ES6 Proxies are a great underrated feature to write about. I would certainty y like to learn some more about them.

 

Very informative!

One potential correction:

If we lose the key, the javascript compiler can deduce that there is no way to access the thing associated with the key and it can safely remove it from the memory.

I think "compiler" should instead be "garbage collector".

 

Fixed, thanks so much for pointing that out

 
 
 

Wonderful post, congratulations Joshi, you explain in a simple and funny way.

 
 

Very clear and informative!!!
Looking forward to the new post.:...

 

Great article, really helped me.