Introduction to Functional Abstractions in JavaScript

First-order Functions

A function, simply put, is an action that has been saved for later. It can store a set of behaviors, allowing programmers to reference this entire set of behaviors by calling its name.

First order functions are functions that take data as input and then operate on that data. These functions are used to store behaviors that act on data alone.

var firstOrderAdder = function(num1, num2) {
return num1 + num2;

}; // Function has been stored. Now I can add two numbers without using addition directly
var someNumber = 4;
var otherNumber = 6;
var sum = firstOrderAdder(someNumber, otherNumber);
console.log(sum);

We can group statements together under a single name and call that name to execute those statements. The function firstOrderAdder is pretty contrived since it only saves one statement in its function body. Let’s look at an example that better illustrates the practicality of the “statement-saving” capacity of functions.

Scenario: Given an array of numbers, return a new array in which every number is doubled.

Solution:

function doubleEveryNumber(array) {
var doubledNumbers = [];

for (var i = 0; i < array.length; i++) {
doubledNumbers.push(array[i] * 2);
};

return doubledNumbers;

};
console.log(doubleEveryNumber([1,2,3]));

The above solution is a first-order function: it doesn’t take other functions as arguments, nor does it return a function. It is also written in an imperative style, using a manual for-loop rather than built-in abstractions.

It would be a pain to have to write all of the code in the function body every single time we wanted to double the values in an array. Not only because of all the repetition we would go through, but because each time we wanted to accomplish this task, we would have to choose different variable names for the counter in the for loop as well as the array that the function returns. This nuisance is avoided since functions define an inner local variable scope that is inaccessible by the outer scope. Whenever we want to double the values in an array, we just pass that array into the doubleEveryNumber function.

Functions are values themselves, and can therefore be passed as arguments to other functions and assigned to variables. However, unlike other values, functions can store unexecuted statements. Values exist only after an executed statement initializes them. Functions are values, yet they store usable code.

Higher-order Functions

Higher-order functions are functions that either take a function as an argument or return a function after execution. This is extremely important because it means that programmers can abstract over actions, not just data.

If higher-order functions didn’t exist, functions would still be useful since they can be used to store behavior that acts on data. But higher-order functions provide an ability far more powerful: they can be used to store behaviors that acts on other behaviors. Said another way, higher-order functions treat behavior like a form of data.

Say we need to write a function that took an array of characters as input and capitalized each one of them.

This description is asking us to transform each element in the array in some way. This is very similar to the doubled numbers question above. In either case, transforming the elements of the array is the primary operation.

Our first attempt at solving the problem may look like this:

function capitalizeEveryLetter(array) {
var capitalizedLetters = [];

for (var i = 0; i < array.length; i++) {
capitalizedLetters.push(array[i].toUpperCase());

};

return capitalizedLetters;

};
console.log(capitalizeEveryLetter(['a', 'b', 'c']));

Just like the doubleEveryNumber function, the above function initializes a new array. Then, it iterates over the old array, performing some action on each element and pushing it into the new array. When all elements in the old array have been transformed and pushed into the new array, the new array is returned. This is, once again, an imperative approach that does not make use of built-in abstractions.

If we needed both doubleEveryNumber and capitalizeEveryLetter in the same program, our program would have a lot of repetition. The two functions are nearly identical.

Wouldn’t it be nicer if we had a function that could take an array as an argument, transform every element in any way we wanted, and return a new array of transformed elements? This function would solve our repetition problem: we would no longer have to re-implement the general pattern of transformation (i.e. the for loop, the insertion of a transformed value into the new array for each element in the old array, and then returning the new array of transformed values). All that we would have to do is specify the specific transformation we want to occur (e.g. doubling numbers or capitalizing letters).

This is a perfect use-case for higher order functions. In fact, JavaScript provides a built-in higher order function for transformation. Its name is Array.prototype.map… We will call it “map” for short.

The beauty of map is that it takes a function as an argument: the function that specifies what the transformation of each value in the array will be. It will then iterate over each element in the array, and pass the element into the argument function (termed the “callback” function). It will then place the return value of the callback into a new array. When map is finished iterating, it will return the new array.

Let’s re-implement the above two functions using map:

function doubleNumbers(numbers) {

return numbers.map(function(number) {

return number * 2;
});

};
console.log(doubleNumbers([1, 2, 3]));
function capitalizeLetters(letters) {

return letters.map(function(letter) {

return letter.toUpperCase();
});

};
console.log(capitalizeLetters(['a', 'b', 'c']));

As the above code shows, by using map, a higher-order function, the only implementation we have to worry about is passing a function to map as an argument that performs the desired transformation. map will then take care of the rest by calling this function for each element in the array, placing the return values of the callback into a new array, and returning the new array once it has finished iterating.

The Ingredients of Abstractions

This article introduced two different types of abstractions: abstractions over actions on data and abstraction over actions on other actions.

When we write a function, the function’s behavior is the responsibility of the function body, and the function’s data is the responsibility of the arguments provided. When we later invoke a first-order function, we have control only on the arguments we pass to the function: we do not have control over how the function will behave: we wrote its behavior during “implementation-time”. In other words, the set of possible behaviors of the function is already determined at “use-time.”

This division is blurred with the use of higher-order functions. By passing functions as arguments to other functions, we can influence the functions’ behaviors during “use-time” and not just during implementation time.

With higher-order functions, the division is not between behaviors and data. It is between general-purpose behaviors (the function body), case-by-case supplemental behaviors (the “callback” function; the function passed as an argument), and the data.