Higher-order Functions
A higher-order function is a function that accepts another function as an argument value (hofFunc1
below) or a function whose return value is another function (hofFunc2
below):
function hofFunc1 (argumentFunc) { console.assert(typeof argumentFunc === 'function'); } hofFunc1(function() {}); // ************** function hofFunc2 () { return function() {}; } console.assert(typeof hofFunc2() === 'function');
typeof and console.assert
This is the first time you are seeing the typeof
keyword. It is a special operator that returns a string indicating the type of its operand:
typeof 3.14 === 'number'; typeof 'hello' === 'string'; typeof true === 'boolean'; typeof {a: 1} === 'object'; typeof [1, 2, 4] === 'object'; typeof new Date() === 'object'; typeof function() {} === 'function';
Note how the type of arrays and dates were reported as objects. Do not rely on typeof to check for those types. |
This is also the first time you are seeing the console.assert
function. Assertions give us a simple tool to use to test our code. There are many assertion libraries that you can use, but the simplest assert function is the one that is built into the console
object, which is available in your browser. If the expression you pass to console.assert
is evaluated as false, you get an Assertion failed
error. The expressions used for the above assertions are all true.
Callbacks
We have used higher-order functions in previous articles but they did not have the fancy name yet back then.
Here is an example:
function squareNumbersInList(list) { return list.map(number => number * number); }
The map
function above is a higher-order function because when we called it we passed another function as its only argument value. The map
function applies its argument function to each element of the list on which it was called.
The function that we pass in as the argument of a higher-order function is sometimes referred to as a callback. The callback arguments and return value are both important and what they mean depends on the higher-order function that is consuming the callback.
In the map example above, the callback function is an inline anonymous function.
To make the definition of higher-order functions more obvious, we can define a separate function, name it square
, have it take one argument, and return its squared value:
function square(e) { return number * number; }
We can now pass this new square
function reference as the callback argument value for the map
function.
list.map(square);
Note how we pass a reference to the function. We do not invoke the square function. The map
function is going to invoke the square
function on every element. This is true for all higher-order functions: the value we pass to them is just a function reference that represents the definition of a function.
Exercise:
Filter is another higher-order function. In the template below, you have an array and a callback function and your task is to pass the correct argument value for the filter function.
function isOdd(e) { return e % 2 === 1; } // Complete the following line // to make newArray = [1, 3] using a filter call const newArray = [1, 2, 3].filter; console.log(newArray);
Solution: jscomplete.com/playground/ByJqGyzgG
Functions are First-Class Objects
The most important fundamental thing to understand about JavaScript is the function concept. The statement that functions are first-class citizens or objects simply means that functions in JavaScript can be treated just like any other JavaScript Object. Anywhere you can use an object you can also use a function.
Functions can be assigned to variables, array entries, and properties of other objects:
const add = (a, b) => a + b; const arr = [add, 1, 2]; const obj = { operation: add, args: arr.slice(1), }; console.log(obj.operation(...obj.args));
They can be declared with literals and we can define properties on them. We can also access those properties from within the functions:
function multiplyBy(a) { return multiplyBy.factor * a; } multiplyBy.factor = 5;
They can also be passed as other function parameters and be returned as values from other functions:
function host(func) { // do something with func return func; } const result = host(multiplyBy)(3); console.log(result);
The code above makes the host
function a higher-order one.
Functions are basically objects with the special capability of being callable.
Let’s Build a Simple Calculator
Now that we understand higher-order functions and the concept of functions as first-class objects in JavaScript. Let’s write a simple but funky calculator! Here is how I would like this calculator to be used:
// Define 3 operations, add, multiply, subtract // const add = (a, b) => a + b; // const multiply = ... // const subtract = ... // **** The Desired API **** // // calculator(3) // Start with 3 // (add, 4) // Add 4 to 3 // (multiply, 5) // Multiply 5 by 7 // (subtract, 6, function(result) { // console.log(result); // After subtracting 6 from 35 // });
When we invoke the desired calculator
function, it returns what seems to be another function that we can execute with an operation and an argument. This makes the calculator
function a higher-order one simply because it returns another function.
The function that calculator
returns is itself a higher-order function because we are passing it other functions. We can pass it an operation (like add, multiply, and subtract
), an operand value to carry that operation on the calculator
value. It also accepts an optional third-argument callback function to do something with the result (like the callback for the subtract
call above).
Let’s build this calculator
one step at time. Try to solve the following challenges by yourself first.
Challenge #1: Multiply and Subtract
Your first simple challenge: I already defined the add
function above. Go ahead and define both multiply
and subtract
in the same fashion. Here are some assertions you can use:
You want to make sure that all expressions in console.assert
statements are true.
Solution:
We simply define multiply
to return the product of its two arguments and subtract
to do a subtraction operation on its two arguments.
const multiply = (a, b) => a * b; const subtract = (a, b) => a - b;
We have our operations, so let’s now define our Calculator function.
Challenge #2: Functions that Return Functions
Your next challenge is to make the following assertion pass:
// Define the calculator function console.assert(typeof calculator() === 'function');
Basically, you need to define a calculator
function that returns another function.
Solution:
To make the assertion above pass, all we need to do is define a calculator function and make it return another function. Let’s name the returned function doOperation
:
function calculator(total) { function doOperation() {} return doOperation; }
Challenge #3: Functions that Return Functions that Return Functions
We have a function that returns another function. However, in the desired API above, we chained function calls, so not only is calculator
returning a function, but the function it returns should also return a function.
Your challenge: Make the following two assertions pass.
console.assert(typeof calculator()() === 'function'); console.assert(typeof calculator()()() === 'function');
Solution:
We can simply make the doOperation
function return itself.
function calculator(total) { function doOperation() { return doOperation; } return doOperation; }
This way, every time we execute doOperation
, the return value is also doOperation
and we can keep doing operations as we designed in the API.
Challenge #4: Functions as Arguments
Let’s now work on the doOperation
arguments. We designed the API so that this doOperation
function takes two arguments and an optional third callback function. Let’s begin with the first two required arguments.
-
An operation, which itself is a function
-
An operand value to be carried as one of the operation arguments
The calculator function receives its initial value as its only argument. This is the starting state of this simple application. I named that value total
. The calculator function needs to keep track of that. Operations need to change that.
function calculator(total) { function doOperation(operation, operand) { // do something on total console.log(total); return doOperation; } return doOperation; } // Test with calculator(3) (add, 4) (multiply, 5) (subtract, 6, function(result) { console.log(result); });
Your challenge: Inside the doOperation
function, change the total
variable to be the result of executing the operation
function on both total
and the operand
value for operation
.
When executing your solution with the desired API example, you should see three lines in the output: 7, 35, and 29.
Solution:
We simply assign a new value to total. This value is coming from invoking the operation
function with two arguments of its own. The first argument is current total
value and the second argument is the operand
argument that we pass to doOperation
.
function calculator(total) { function doOperation(operation, operand) { total = operation(total, operand); console.log(total); return doOperation; } return doOperation; }
Challenge #5: The Optional Callback
The last argument that we can pass to doOperation
is an optional callback. The API defined that to receive a result
argument. The result
it receives is the calculator’s total
value so far.
function calculator(total) { function doOperation(operation, operand, callback) { total = operation(total, operand); // maybe invoke the callback... // "result" inside the callback is "total" return doOperation; } return doOperation; }
Try to implement this callback function on your own first. Remember it is optional.
Solution:
We simply use an if
statement. If there is a callback value, then we want to invoke it using the total
variable as its arguments:
function calculator(total) { function doOperation(operation, operand, callback) { total = operation(total, operand); if (callback) { callback(total); } return doOperation; } return doOperation; }
Note that while this calculator uses the higher-order function concept, it is not really completely written with a functional programming style. The calculator uses a shared mutable state (total
) and it has manual instructions (like the if
statement). Making the calculator above completely functional will require more code and more concepts to understand.
I think it’s okay to have a minimally contained mutable state that can be tested with many known cases. However, you need to learn how to work with all programming paradigms and then make decisions on which one to use. Each application will have different requirements.
In the next article, let’s explore some unique challenges to get you more comfortable with declarative programming.