Exploring Maps In Sass 3.3(Part 2): Sass Maps & Memoization

Micah Godbolt, Developer
#Mapping | Posted

In the first blog post of this blog series, I introduced Maps in Sass 3.3,  In this second installation, I will explain one of the more powerful uses of Sass Maps: memoization. Memoization is a technique that has been long employed in other languages to improve performance. It involves caching the results of a function so that if a function is called twice with the same parameters, the cached copy can be used instead of calculating that value again.

Say we have a couple of simple function calls like this:

  1. $length1: length(cat, dog, fish);
  2. $length2: length(cat, dog, fish);

In this case, Sass will call the length() function on list (cat, dog, fish) twice, each time calculating that the list is 3 items long.

The overhead of this calculation is obviously quite small, but lets consider a different function:

  1. // Fibonacci Function
  2.  
  3. @function fibonacci($n) {
  4.  $fib: 0 1;
  5.  @for $i from 1 through nth($n,1) {
  6.    $new: nth($fib, length($fib)) + nth($fib, length($fib) - 1);
  7.    $fib: append($fib, $new);
  8.  }
  9.  @return $fib;
  10. }

This is the famous Fibonacci function which creates a string of numbers where each number is a sum of the 2 preview numbers. This function can take multiple seconds to compile PER use of the function. So imagine this situation:

  1. $fib1: fibonacci(1000);
  2. $fib2: fibonacci(1000);

You have just asked Sass to perform the same exact, time intensive, calculation twice! I doubt you’ll be needing a Fibonacci series very often, but imagine you had a complex function in your grid system, a function that might get called hundreds of times in a single project.

Maps allow us to implement memoization so that when we call fibonacci(1000)  a second time, we no longer have to calculate that value again. Instead, we store those values in a map, and then look that value up any time we need it.

Lets take a look at how we can build memoization into our project.

You can follow along by opening this gist in Sassmeister.

We start off by creating a global variable of $memo, and assign its value to an empty map.

$memo:();

The first helper functions we're going to need here is our ‘setter’ memo-update() :

  1. @function memo-update($function, $params, $value) {
  2.  $result:();
  3.  @if map-has-key($memo, $function) {
  4.    $sub-map-new: map-merge(map-get($memo,$function),($params: $value));
  5.    $result: map-merge($memo, ($function: ($sub-map-new) ));
  6.  }
  7.  @else {
  8.    $result: map-merge($memo, ($function: ($params: $value)));
  9.  }
  10.  
  11.  @return $result;
  12. }

This function checks to see if any entries have already been made for this function @if map-has-key($memo, $function) , and either updates that sub-map with the new entry, or creates a new sub-map for that function name. The new, updated $memo  map is then returned.

The second helper function will be our ‘getter’ memo-get() :

  1. @function memo-get($function, $params) {
  2. $result: map-get(map-get($memo,$function),$params);
  3. @return $result;
  4. }

This function reaches into the $memo map by $function key, and then reaches further in using $params key to return the value set by our ‘setter’ function.

Lastly is the heart of our system: call-function() .  This function calls the setters and getters.

  1. @function call-function($function, $params...) {
  2. @if map-has-key($memo, $function) {
  3. $result: memo-get($function, $params);
  4. @if $result != null {
  5. @return $result;
  6. }
  7. }
  8. $result: call($function, $params...);
  9. $memo: memo-update($function, $params, $result) !global;
  10. @return $result;
  11. }

After passing in a function param pair, call-function()  checks to see if that combination has been called before. If it has, it calls memo-get()  to retrieve the value. If it has not been called before it will use call()  to invoke that function, return the result, and call memo-update()  to add that combination to $memo .

With these functions in place, we can pass call-function() any function name, along with a list of parameters. It will either pull the return value from our $memo map, or will calculate the value and add it to the map.

There is much more we could (and should do) with these functions to make them bulletproof, but this is a great place to start!

Next week I’ll wrap up this blog series talking about a few ways we can improve maps and how maps allow us to call variables, with variables.  In the meantime, check out Mike Crittenden's Blog series on Aurora and Sass!

 

Micah Godbolt

Developer