Code Sample

Making Function Calls Across Sass Versions

Using safe-get-function and safe-call utilities

The Sass 3.5 Release Candidate includes support for first-class functions. What are they, how do we use them, and what tools can we use to future-proof our Sass toolkits in advance?

The Sass 3.5 Release Candidate includes support for first-class functions and the resulting get-function() function. I just said “function” too many times in a single sentence. Get used to it, there’s more. We’ll explain the problem, and help you call all the functions in every version of Sass!

Calling Functions

Normally, when we’re using functions in Sass, we know what function we need, and we can reference it directly:

// Using Susy's "span" function directly, with a single argument
.span {
  width: span(3);
}

But when we build toolkits in Sass, it’s common that we don’t know for sure what function we’ll be calling. In OddBird’s Accoutrement tools we even let the user pass in arbitrary functions and arguments that we’ll call at the right time to manipulate CSS colors and sizes.

In order to call functions without knowing the function name in advance, we have to use the call() function. Here’s how it works on the current versions of Sass:

// This could change!
$function: 'span';

// Calling some unknown function, with a single argument
.span {
  width: call($function, 3);
}

Those two code samples will return the same results. The first is more direct, but the second is more flexible for use in a toolkit.

Introducing the get-function function

Sass is taking a first step towards modular namespacing – expected to land in the 4.0 release. This will allow you to include third-party tools without any concern for naming conflicts.

Functions will be namespaced locally to a given Sass file – something like susy.span(), though the syntax hasn’t been settled. The new get-function() allows us to capture a snapshot of a function into a variable, and pass that snapshot into new namespacing contexts.

// type-of($my-function) == 'function'
$my-function: get-function('susy.span');

In Sass 3.5 and later, the call() function only accepts first-class functions, where it used to accept function names as a string. In brief, we have to start using get-function('function-name') before calling a function using call() – but only in new versions of Sass.

In demo code, people often write it like this:

$call: call(get-function('susy.span'), $arguments...);

That code is misleading. I always wondered why get-function isn’t baked into call, so we can pass a first-class function or a string. I still think that would have been the most backwards-compatible option, without any clear downsides. That’s a question for Chris and Natalie.

But, once the modular system is in place, it will be very rare to call a function from the local scope. The call option is most useful for handling functions defined elsewhere (e.g. third-party tools) that won’t have guaranteed access to the functions being called. Using get-function, we can pass a first-class function to a third-party toolkit, without worrying about differences in namespace. So, a more accurate demonstration might be:

// third-party.scss
@mixin three($function) {
  .three {
    width: call($function, 3);
  }
}

// my-local.scss
@import 'susy';
@include three(get-function('susy.span'));

This still creates a problem for toolkits and frameworks (like Susy) that already use call() internally to handle user input. How do we support old and new versions of Sass, while allowing users to pass in either strings or first-class functions?

Kaelig provides one solution in a great article with more details. It’s a good start, but it doesn’t cover all the use cases I need. What if users pass in a first-class function that they’ve already captured – as they likely should in Sass 3.5+? Here’s my slightly-expanded solution.

Safe get-function

We need to use get-function() in new versions of Sass, but we can’t use it in old versions. We also don’t want to use get-function() on a function we’ve already got. That gives us several options we have to cover in our safe-get-function().

  • If the user passes in a string, and we’re using an older version of Sass => do nothing.
  • If the user passes in a string, and we’re using a newer versions of Sass => use get-function.
  • If the user passes in a first-class function, we can assume we’re using the latest Sass version => do nothing.

The result looks something like this:

@function safe-get-function(
  $function
) {
  // find out what's been passed in
  $type: type-of($function);

  // if it's a first-class function, do nothing
  @if ($type == 'function') {
    @return $function;
  } @else if ($type == 'string') {
    // if it's a string, but we can get a function, we should
    @if function-exists('get-function') {
      @return get-function($function);
    }

    // if it's a string, and we can't get a function, return the string
    @return $function;
  }

  // if it's not a string or a function, we know there's a problem
  @error 'Invalid function name, [#{$type}] `#{$function}` must be a function or string';
}

This safe-get-function accepts one argument, either a string or a first-class function, and returns the proper value (also a string or a function) for the version of Sass being used.

Safe call

I also wrote a very small safe-call() wrapper function that passes all function-calls through our safe-get-function() before calling them.

@function safe-call(
  $name,
  $args...
) {
  $name: safe-get-function($name);
  @return call($name, $args...);
}

This function accepts the same arguments required by Sass’s internal call() function, a name (or first-class function), and arguments to pass-through when the function is called. You can use it right away like this:

$result: safe-call('span', 3);

And that should continue to work just fine when you upgrade to Sass 3.5 or later.

Ship it!

After adding those two functions to a project, I can search-and-replace every instance of call( with safe-call(, and I’m ready to support the latest in Sass technology.

This should work on all versions of Sass, for all expected forms of input.

Have you played with Sass 3.5 already? Did we miss anything important? Let us know via Twitter or our public Slack channel!

Miriam Suzanne is a product manager, user-experience designer, writer, speaker, and open source developer.

Let’s Build Something Together!

We want to hear all about your software ideas. Fill out our contact form, join our public Slack chat, or tweet @oddbird to start the conversation.