(Posts)

# Implement Functional Programming in Sass

12 Jul 2023 [SassClojure]
https://www.npmjs.com/package/lambda-sass

A few years ago, I started learning the ClojureScript language and quickly became fascinated with functional programming. One day, I wondered if it would be possible to implement a similar programming style in Sass, allowing for anonymous functions and threading macros. After conducting some experiments, I came up with the following Sass functions:

``````.map-function {
property: map(inc, 1, 2, 3); // (2, 3, 4)
}

.filter-function {
property: filter(odd, 1, 2, 3); // (1, 3)
}

.reduce-function {
property: reduce((plus _1 _2 _2), "#", [ "a", "b", "c" ]); // #aabbcc
}

.assoc-function {
property: assoc([1 2 3], 2, (a b)); // [1 (a b) 3]
}

property: thread-last(1, inc, (plus 10 5), dec, ("math.pow" 2)); // 65536
}``````

It turns out to be quite doable. The magic began with the Clojure Function Literal style anonymous function, `#(println %1 %2 %3)`, which can be passed to iterator functions and threading functions. For syntax reasons, I used `_` as the placeholder for a single parameter or `_1`, `_2`, and so on for multiple parameters.

In this post, I will explain how to accomplish this. Let’s take the `reduce()` function as an example:

``````@use "sass:list";

@function plus(\$args...) {
// Return the sum of \$args
}

.reduce-function {
property: reduce((plus _1 _2 _2), "#", [ "a", "b", "c" ]); // #aabbcc
}``````

In this case, `_1` represents the accumulator, and `_2` represents the current value. The function literal is a parentheses list, also known as the S-expression, and the Sass list is fully capable of handling this syntax. Therefore, the first step is to create a caller that accepts a Sass list, takes the first item in the list as the function name, and the rest items as arguments. It would look something like this:

``````@use "sass:meta";

@function call-fn(\$fn, \$args...) {
@return meta.call(meta.get-function(\$fn), \$args...);
}``````

As such, `call-fn(plus, 1, 2, 3)` is converted to `plus(1, 2, 3)`. The `meta.call()` is used to invoke the `\$fn` with `\$args`, and the `meta.get-function()` ensures that the passed-in function is callable.

This is just a basic concept. In the actual case, `\$fn` would be an S-expression, and we need to replace the `_` placeholder inside with the values in `\$args`. So let’s update the `call-fn()` to a `call-lambda()` function that can convert `call-lambda((plus, _1, _2), 10, 100)` to `plus(10, 100)`.

``````@use "sass:list";
@use "sass:meta";

@function first(\$list) {
// Return the first \$list
}
@function rest(\$list) {
// Return the rest items in \$list
}

@function replace-args(\$args, \$values) {
@for \$_i from 1 through list.length(\$values) {
@if \$_i == 1 {
\$_index: list.index(\$args, _) or list.index(\$args, _1);

@while \$_index != null {
\$args: list.set-nth(\$args, \$_index, list.nth(\$values, 1));
\$_index: list.index(\$args, _) or list.index(\$args, _1);
}
} @else {
\$_index: list.index(\$args, _#{\$_i});

@while \$_index != null {
\$args: list.set-nth(\$args, \$_index, list.nth(\$values, \$_i));
\$_index: list.index(\$args, _#{\$_i});
}
}
}

@return \$args;
}

@function call-lambda(\$lambda, \$args...) {
\$_fn: first(\$lambda);
\$_lambda-args: replace-args(rest(\$lambda), \$args);

@return meta.call(meta.get-function(\$_fn), \$_lambda-args...);
}``````

In the `replace-args()` function, use a `@while` loop to support cases where the `_` placeholder appears multiple times.

Here, we have a problem. The `meta.get-function()` cannot directly get a Sass module function. For example, `meta.get-function("list.index")` is invalid. The correct way would be `meta.get-function("index", false, "list")`. So, let’s create a new `get-function()` function to detect if the passed-in function is a Sass module method, and if so, extract the module name and the method name.

``````@use "sass:meta";
@use "sass:string";

@function get-function(\$fn) {
\$_dot-index: string.index(\$fn, ".");

@if \$_dot-index {
\$_module: string.slice(\$fn, 1, \$_dot-index - 1);
\$_fn: string.slice(\$fn, \$_dot-index + 1, -1);

@return meta.get-function(\$_fn, false, \$_module);
} @else {
@return meta.get-function(\$fn);
}
}``````

We’re almost done, there’s only one thing left. In iterator functions or threading functions, we can either pass an anonymous function or a named function. We already have the `call-fn()` and `call-lambda()` functions, so let’s add a new `auto-call()` function to detect the type of the passed-in function, and correspondingly choose to invoke `call-fn()` or `call-lambda()`:

``````@use "sass:meta";

@function auto-call(\$fn, \$args...) {
\$_type: meta.type-of(\$fn);

@if \$_type == string {
@return call-fn(\$fn, \$args...);
} @else if \$_type == list {
@return call-lambda(\$fn, \$args...);
} @else {
@error "#{\$fn} is not a function. Please make sure that #{\$fn} is defined as a function before calling it.";
}
}``````

Finally, based on the previous work, implementing the `reduce()` function or other functions becomes much simpler. Check the source code for more examples.

``````@function reduce(\$fn, \$init, \$list...) {
\$_result: \$init;

@each \$_i in \$list {
\$_result: auto-call(\$fn, \$_result, \$_i);
}

@return \$_result;
}``````

This project has been published as an NPM package named lambda-sass. The syntax was borrowed from Clojure, but the project name was inspired by the Lisp lambda function. I’d be delighted to see what you create using this library. If you find any functions missing, feel free to inform me or contribute by submitting a pull request.

If you're interested in continuing the discussion and receiving future updates, don't hesitate to follow me on Twitter, GitHub or DEV Community.