So far, I have written seven Sass libraries. Most of them are just a superset of functions which can be included then used in your projects to give you more power over the code.
For instance, SassyLists is a collection of functions to manipulate Sass lists. They help you reverse a list, insert an item at a specific index, slice a list between two indexes and so on.
SassyLists can be imported as a Compass Extension, but I've noticed sometimes developers only want to use a very specific function from SassyLists so they copy/paste it into their code base. The problem is that they don't always pay attention to dependencies (for example, other functions).
I decided to start working on a dependency checker. The idea is quite simple: each function with dependencies will first run through the dependency checker; if the latter finds that some functions are missing it warns the developer that the function won't be able to run correctly.
Buildin’ it!
The dependency checker is a simple function accepting an unlimited number of arguments (required functions' names).
@function missing-dependencies($functions...) { // Check for dependencies }
With this we use the function-exists()
function, introduced in Sass 3.3, which checks for the existence of a given function in the global scope.
Note: Sass 3.3 also offers mixin-exists()
, variable-exists()
and global-variable-exists()
.
@function missing-dependencies($functions...) { @each $function in $functions { @if not function-exists($function) { @return true; } } @return false; }
If for some reason a function doesn't exist in the scope, then missing-dependencies
returns true
. If all functions are okay, then there are no missing dependencies so it returnsfalse
.
You would therefore use it like this:
// @requires my-function // @requires my-other-function @function dummy() { @if missing-dependencies(my-function, my-other-function) { @warn "Oops! Missing some functions for `dummy`!"; @return null; } // `dummy` function's core, // obviously needing `my-function` and `my-other-function` to work. }
So that's pretty cool.
Pushing Things Further
It would be even better if we could identify which function is missing, so the developer knows what to do in order to fix the problem. Also, having to type the warning everytime is kind of annoying so we could move that to the missing-dependencies
function.
The idea is not so different than what we've previously done. The difference is that now we will store the name of missing functions rather than directly returning. Then, if there are missing functions, we will throw a warning to the developer, and finally return a boolean as we did earlier.
@function missing-dependencies ($functions...) { $missing-dependencies: (); @each $function in $functions { @if not function-exists($function) { $missing-dependencies: append($missing-dependencies, $function, comma); } } @if length($missing-dependencies) > 0 { @warn "Unmet dependencies! The following functions are required: #{$missing-dependencies}."; } @return length($missing-dependencies) != 0; }
As you can see, it is not much more complex than our previous version. Plus, the way to use it is even simpler:
@function dummy() { @if missing-dependencies(my-function, my-other-function) { @return null; } // `dummy` function's core, // obviously needing `my-function` and `my-other-function` to work. }
See? We can ditch the @warn
directive, yet if there is one or more functions missing, the developer will be prompted with:
Unmet dependencies! The following functions are required: my-function, my-other-function.
Preventing the Dependency Checker From Being a Dependency
The major problem I can see with this feature is that, you need the missing-dependencies
function! At this point, the dependency checker is basically a dependency... You see the irony.
This is because missing-dependencies(...)
is treated as a string in situations where missing-dependencies
doesn't refer to any functions, and a string always evaluates to true
. So when doing @if missing-dependencies(...)
, you are effectively doing @if string
, which is always true, so you'll always end up meeting that condition.
To avoid this, there is a clever work around. Instead of simply doing @if missing-dependencies(...)
, we could do @if missing-dependencies(...) == true
. In Sass, ==
is like ===
in other languages, which means it not only checks the value but also the type.
@function dummy() { @if missing-dependencies(my-function, my-other-function) == true { @return null; } // `dummy` function's core, // obviously needing `my-function` and `my-other-function` to work. }
If the function doesn't exist, then as we saw earlier the call will be treated as a string. While a string evaluates to true
, it isn't strictly equal to true
, because it's a String
type, not a Bool
type.
So at this point if the missing-dependencies
function doesn't exist, you won't match the condition, so the function can run normally (though crash if there is a missing dependency somewhere in the code). Which is cool, because we are only enhancing things without breaking them.
Different Types of Dependencies
One other issue with this feature is that it only checks for missing functions, and not mixins or global variables. That being said, this is easily doable by tweaking the dependency checker code.
What if each argument passed to the function could be a list of two items, with the type of dependency as a first argument (either function
, mixin
or variable
), and the name of the dependency as a second one? For instance:
missing-dependencies(function my-function, variable my-cool-variable);
If we are more likely to use functions as dependencies, we could make function
the default, so that the previous call would look like this:
missing-dependencies(my-function, variable my-cool-variable);
Basically, this is like asking to check whether the function my-function
exists, and the variable my-cool-variable
exists, because they are needed for a given task. Clear so far?
Now let's move on to the code. We can use the call()
function, to call {{ TYPE }}-exists({{ NAME }})
. Everything else is just the same as previously.
@function missing-dependencies ($dependencies...) { $missing-dependencies: (); @each $dependency in $dependencies { $type: "function"; // Default type of dependency @if length($dependency) == 2 { $type: nth($dependency, 1); $type: if(index("function" "mixin" "variable", $type), $type, "function"); $dependency: nth($dependency, 2); } @if not call("#{$type}-exists", $dependency) { $missing-dependencies: append($missing-dependencies, $dependency, comma); } } @if length($missing-dependencies) > 0 { @warn "Unmet dependencies! The following dependencies are required: #{$missing-dependencies}."; } @return $missing-dependencies != 0; }
Pretty cool, heh? This does make the code slightly more complex, so unless you are likely to have different types of dependencies (which isn't my case with SassyLists), I suggest you stick with the first version we saw.
Note: you might want to automatically turn a variable
type into global-variable
in order to call global-variable-exists
, because it is likely that a required variable is a global one.
Final Thoughts
That's pretty much it folks. Obviously, this is not an everyday Sass feature. But I think that in many cases, especially when building libraries, frameworks and extensions, these kinds of tips can come in handy. Let me know what you think in the comments!
Comments