by author Federico Kereki
Common features in functional programming (FP) include passing functions around as parameters to other functions, and returning new functions as a result! In a strict theoretical sense, a Higher Order Function (HOF) takes one or more functions as arguments, and possibly also returns a function as a result.
In this article, we're interested in HOFs that return new functions. We may classify such HOFs into three groups:
Functionality-wrapping: the original functionality is kept, but some new feature is added. We saw prime examples of this in our articles on memoizing functions and memoizing promises. In both cases, the produced functions did the same work but more efficiently thanks to caching. Other examples of wrapping could be timing (getting time performance data) or logging (generating logs), and we'll see those below.
Functionality-altering: the original functionality is modified in some way. For example, we might have a boolean function that tests for a condition, and alter it to negate (invert) its result. Another case: we may fix the arity of a function to avoid problems that we saw in our article on Pointfree programming; these are the examples we'll look at.
Functionality-creating: these HOFs provide new functions of their own. As an example, we might decouple methods from objects to use them as common functions, or transform a function that works with callbacks into a promise for neater handling; we'll get into those.
Let's see examples of these transformations, so you can get an idea of the many possible usages for HOFs! The examples in this text are based on code from my Mastering JavaScript Functional Programming book, in which you will find several more examples of HOFs.
Wrapping the original functionality
Wrapping a function implies producing a new version with the exact same functionality as the original one, adding some interesting side effects.
Timing functions
Let's suppose we want to measure the performance of a function. We could modify the function to get the current time at the beginning and just before returning, but with a HOF we don't need that. We will write an addTiming(...)
function that will return a new function that will log timing data in addition to doing whatever it would normally do.
const addTiming = (fn) => (...args) => {
let start = performance.now(); /* [1] */
try {
const toReturn = fn(...args); /* [2] */
console.log("Normal exit", fn.name, performance.now()-start, "ms");
return toReturn;
} catch (thrownError) { /* [3] */
console.log("Exception thrown", fn.name, performance.now()-start, "ms");
throw thrownError;
}
};
The function that we'll return starts ([1]) by getting the initial time using performance.now() for better accuracy. Then it calls the original function ([2]) and if there's no problem it logs "Normal exit", the function name, and the time it took. If the function threw an exception ([3]) it logs "Exception thrown" plus the function name and the total time, and throws the same error again, for further process. Let's see an example.
function add3(x, y, z) {
for (let i = 1; i < 100000; i++);
return x + y + z;
}
add3 = addTiming(add3); /* [1] */
add3(22,9,60); /* [2] */
// logs: Normal exit add3 3.200000047683716 ms
// returns: 91
We change our original function for a new version that includes timing ([1]). When we call this new function ([2]) we get some logging in the console, and the returned value is whatever the original function calculated... With a HOF we can now time any function without modifying it!
Logging functions
Another common example of wrapping is adding a logging feature. A so modified function will let you see in the console output when it is called, with which arguments, and what it returns. As with timing, we don't want to modify the original function -- that's error-prone, and not a good practice!
Similarly to what we did in the previous section, we'll write an addLogging(...)
function that will take a function as a parameter and return a new function that will start by logging its arguments, then call the original function, log whatever it returned, and finally return that value to the caller. We will also take exceptions into account. A possible implementation could be the following.
const addLogging =
(fn) =>
(...args) => { /* [1] */
console.log("Enter", fn.name, ...args);
try {
const toReturn = fn(...args); /* [2] */
console.log("Exit ", fn.name, toReturn);
return toReturn;
} catch (err) {
console.log("Error", fn.name, err); /* [3] */
throw err;
}
};
We start ([1]) by logging the arguments of the function. Then ([2]) we call the original function and store its result. If there's no problem we just log "Exit" and the result, and we return to the caller. If there's some error, we log "Error" and the exception, and we throw the latter again.
We can use the function simply; let's re-use the example from the previous section.
function add3(x, y, z) { // same as avoe
for (let i = 1; i < 100000; i++);
return x + y + z;
}
add3 = addTiming(add3); /* [1] */
addLogging(add3)(22,9,60); /* [2] */
// logs: Enter add3 22 9 60
// logs: Exit add3 91
// returns: 91
We produce a new function ([1]) that will do its logging, and when we call it ([2]) we get extra output. By the way, you could add both logging and timing, with something like addTiming(addLogging(add3))
-- can you see how it would work?
Altering the original functionality
In this second category of changes, we'll see two simple situations: reversing a boolean decision to gain flexibility in functions like filtering, and changing the arity of functions to solve a problem with functions that expect optional parameters.
Open Source Session Replay
Debugging a web application in production may be challenging and time-consuming. OpenReplay is an Open-source alternative to FullStory, LogRocket and Hotjar. It allows you to monitor and replay everything your users do and shows how your app behaves for every issue.
It’s like having your browser’s inspector open while looking over your user’s shoulder.
OpenReplay is the only open-source alternative currently available.
Happy debugging, for modern frontend teams - Start monitoring your web app for free.
Negating a condition
Suppose you have written a function to test for some condition. For instance, you could have a function to filter accounts according to some internal criteria. With that function, you may write code as follows.
const goodAccounts = listOfAccounts.filter(isGoodAccount);
This code uses the isGoodAccount(...)
function to extract good accounts from a given list. Now, what would you do if you needed to extract the bad accounts instead? You could certainly write something like this, but it doesn't look as clean -- at least in the pointfree style that we've been using.
const badAccounts = listOfAccounts(v => !isGoodAccount(v));
We can get a better solution by using a HOF that will invert (negate) whatever a given function produces. We can write this HOF in just a single line, as follows.
const not = fn => (...args) => !fn(...args);
Given a function as an argument, not(...)
produces a new function that will invoke the original, and then negate whatever it returned. With this, you can now write very directly the following.
const badAccounts = listOfAccounts(not(isGoodAccount));
Now the code is as legible as the one to filter the good accounts; a win! You can extend the idea here to allow joining several conditions with boolean operators: for example, you may write and(...)
and or(...)
HOFs that would let you write something like this.
const goodInternationalAccounts = listOfAccounts.filter(and(isGoodAccount, isInternationalAccount));
And, of course, you could mix things up!
const badInternationalAccounts = listOfAccounts.filter(and(not(isGoodAccount), isInternationalAccount));
I'll leave these boolean HOF functions up to you, a nice exercise!
Changing the arity of functions
In our article on pointfree style we saw a problem.
const numbers = [22,9,60,12,4,56];
numbers.map(Number.parseFloat); // [22, 9, 60, 12, 4, 56]
numbers.map(Number.parseInt); // [22, NaN, NaN, 5, NaN, NaN]
Why did the second map(...) produce such weird results? The reason is that parseInt(...)
allows for a second (optional) argument, but parseFloat(...)
doesn't. In technical terms, the arity of these functions is 2 and 1 respectively. We can transform the former function into a unary (arity 1) function very simply by using a HOF.
const unary = fn => (arg0, ...args) => fn(arg0);
Given an fn
function, unary(fn)
produces a new function that, given several arguments, calls fn
with just the first, discarding the rest. With this, our problem is easily solved in a different way than what we saw in our previous article.
numbers.map(unary(Number.parseInt)); // [22, 9, 60, 12, 4, 56]
In the same way that we transformed a function into a unary one, it would be simple to write binary(...)
, ternary(...)
, and more functions to transform functions to arity 2, 3, etc.; we'll leave this as another exercise!
Creating new functionality
We'll see a couple of examples: we'll transform methods into functions, and callback-using functions into promises.
From methods to functions
Some methods (like map(...)
, say) are available for arrays, but if you wanted to use them elsewhere you'd be out of luck. We can, however, write a HOF that will convert any method into an equivalent function. Instead of object.method(args)
you would write method(object,args)
-- and now you got a function that you can pass around in true FP style!
How can we manage this? The bind(...) method is key, and a possibility is as follows.
const demethodize = fn => (...args) => fn.bind(...args)();
(By the way, there are more ways to implement demethodize(...)
, for instance using apply(...) or call(...) -- if you're up for a challenge, try to do this!)
Let's say you wanted to be able to use the .toUpperCase(...) method as a function. You'd write the following.
const toUpperCase = demethodize(String.prototype.toUpperCase);
console.log(toUpperCase("this works!"));
// THIS WORKS!
FP is more geared to functions than to methods, so being able to transform methods into functions helps you work in a better fashion.
From callbacks to promises
Let's go with an example from Node. In it, by definition all async functions require an "error first" callback such as (err, data) => { ... }
. If err
is null, the operation is assumed to have succeeded and data
has its result; otherwise, err
gives the cause for the error. However, we could prefer working with promises instead. We can write a HOF that will transform an async function that requires a callback into a promise that lets you use .then/.catch
or await
. (OK, Node already provides util.promisify() to do exactly this -- but let's see on our own how we'd go about it.) The needed transformation is not hard.
Given any function, we'll return a new one that will return a function. After calling the original function, the promise will be resolved or rejected depending on what was returned. We can write this quickly.
const promisify = fn => (...args) =>
new Promise((resolve, reject) =>
fn(...args, (err, data) => (err ? reject(err) : resolve(data)))
);
Now, how would we read a file in Node with the fs.readFile(...)
function? (Yes, Node also provides a fs/promises API that already returns promises. In actual production, we'd use that instead of promisifying things by ourselves.) We could do the following.
const ourRead = promisify((...args) => fs.readFile(...args));
ourRead("some_file.txt")
.then(data => /* do something with data */)
.catch(err => /* process error err */);
// or equivalently
try {
data = await ourRead("some_file.txt");
/* do something with data */
} catch (err) {
/* process error err */
}
Nice and easy!
Summary
In this article we have explored the concept of Higher Order Functions (HOF), a common feature in Functional Programming, and we've seen several examples of their usage to cover common, everyday needs in development. Getting used to HOFs will give you much leeway in writer shorter, clearer, and more efficient code; try to practice using them!
Top comments (0)