Blog | Niko Heikkilä

Layman's Guide to Higher-Order Functions

An introduction to higher-order functions I wish I've had in school.

Niko Heikkilä / 27.09.2022 / ☕️ 5 minutes read

The single most important topic in functional programming is to understand what a function is. Inherently, a function is a way to map the input value of some type to output value of another type. To put it in other words, you give your function a problem, and it returns a solution.

In mathematics, you may have stumbled upon the formal definition of a function: f:ABf: A \to B

This is essentially the same as written above. We define a function f(A)f(A) accepting a value of type AA and returning a value of type BB. Note that both AA and BB could be of the same type, but for the sake of this example, we keep them separate.

In programming, problems are bound to grow more difficult over time, and thus solutions grow more complex. Typically, the larger the problem, the larger our function grows in size. Following the principles of clean code — single-responsibility principle, to be accurate — we need to keep in mind that functions should have only one reason to change. Thus, we should split our logic to small cognitively easy pieces.

So, what possibly could help us? Adding more functions!

When solving a large problem, the important approach is to divide and conquer. First, break the problem into small parts (divide) and then solve each of them one by one (conquer). We can use the concept of higher-order functions to achieve this.

Anatomy of a Higher-Order Function

A higher-order function is defined to have either of the following two properties:

  1. It takes one or more functions as its arguments
  2. It returns another function (a closure)

React developers know that for example, the React.useState hook for managing component state is a higher-order function since it returns a function used for updating the state.

TypeScript
1const App = () => {
2    const [counter, setCounter] = useState<number>(0);
3    // typeof setCounter === 'function'
4};

At first, higher-order functions seemed to me like an overcomplicated problem-solving tool. Why not write a single function and call other functions from inside? Truthfully speaking, I thought as much about object-oriented programming before grasping how different design patterns improve the code.

This was my mind before I understood the value of declarative programming over imperative. In declarative programming, you define what things are, whereas, in imperative programming, you define what things do.

Solving problems in a declarative way is a perfect demonstration of divide and conquer. Let's take an example.

Use Case: Password Validation

Assume we are given a user password for validation. Our function should return true if the password is valid, and false otherwise. We have received the following requirements for validating passwords:

  • password must contain 12 or more characters
  • password must contain at least one uppercase and one lowercase character
  • password must contain at least one number

What an easy task, you might think. Write a function with a couple of conditional blocks and after having run through all of them return the intended result. Let's grab a keyboard and start defining our function.

Haskell
1validate password
2    | not (longEnough password)     = false
3    | not (hasUpperCase password)   = false
4    | not (hasLowerCase password)   = false
5    | not (hasNumbers password)     = false
6    | otherwise                     = true

This is perfectly fine for a lax validation. However, what if requirements keep on coming, and you need to add more and more conditionals into your function? Your function could quickly grow to a convoluted, unmaintainable, and unreadable mess.

One solution is to define each validator as a function and pass it as an argument. The example below is in TypeScript.

TypeScript
1/** Helper for chaining and printing the validator warnings **/
2const warn = (msg: string): boolean => {
3    console.warn("Invalid:", msg);
4    return false;
5};
6
7type Validator = (password: string) => boolean;
8
9const longEnough = (password: string, minLength = 12) =>
10    password.length >= minLength ||
11    warn(`Password should contain ${minLength} or more characters.`);
12
13const hasUpperCase = (password: string) =>
14    /[A-Z]+/.test(password) ||
15    warn("Password should have at least one uppercase letter.");
16
17const hasLowerCase = (password: string) =>
18    /[a-z]+/.test(password) ||
19    warn("Password should have at least one lowercase letter.");
20
21const hasNumbers = (password: string) =>
22    /[0-9]+/.test(password) ||
23    warn("Password should have at least one number.");
24
25const validator =
26    (...fns: Validator[]) =>
27    (password: string) =>
28        fns.every((fn) => fn(password));
29
30const isValidPassword = validator(
31    longEnough,
32    hasUpperCase,
33    hasLowerCase,
34    hasNumbers
35);
36
37const validPasswords = [
38    "passwordpassword",
39    "Passwordpassword",
40    "Passwordpassword1",
41].filter(isValidPassword);
42
43console.log("Valid passwords are:", validPasswords);
44// Valid passwords are: ['Passwordpassword1']

Breaking this down you can see that longEnough, hasUpperCase, hasLowerCase, and hasNumbers are each a closure passed to the validator function. Using variadic arguments — known as the spread operator (...) in JavaScript — we can pass any number of validators and our code takes care of the rest.

The Array.prototype.every() function returns true if the array satisfies all the conditions passed so here we pass predicate (boolean) functions as conditions.

Another sweet aspect of higher-order functions is the ability to curry your functions. Here we pass our password to the isValidPassword function, which itself is built on from curried functions. Doing this, we don't have to define the validation rules again each time we wish to validate a given password. This makes the code easier to read again.

Perhaps your head is spinning fast right now, so let's write the validate function without the ES6 arrow notation to examine it further.

TypeScript
1function validator(...fns: Validator[]) {
2    return function(password: string) {
3        return fns.every(function(fn) {
4            return fn(password);
5        };
6    };
7};

After removing the arrows, we have a function satisfying both pre-conditions of being a higher-order function. In my opinion, arrow functions have made writing especially JavaScript way more succinct since we can write all of this in one line and without using a single return statement. No more nested code, also known as hadouken code.

Higher-order functions provide a clean way of solving a large problem by composing smaller solutions together. Now, instead of having to maintain a long and cumbersome validator function, we can define smaller validators elsewhere in our codebase and import them. Want to remove a certain validation? Remove it from the list of arguments. Need to change how the validation logic? There's no need to touch the main validator at all.

I wrote this post because I had very much trouble understanding different functional programming concepts when studying. Unfortunately, typical computer science education tends to lean on the way of defining high-level theories and proving them using mathematical constructs. This is something you almost certainly won't find in a professional software development environment. If you have managed to achieve such a position without a degree as I have, I hope this post is helpful to you.


Cover image by Ilija Boshkov on Unsplash.

Back to postsEdit PageView History