DEV Community

loading...
Cover image for Layman's Guide to Higher-Order Functions

Layman's Guide to Higher-Order Functions

nikoheikkila profile image Niko Heikkilä Originally published at nikoheikkila.fi ・4 min 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:AB f: A \to B

This is essentially the same as written above. We define a function f accepting a value of A and returning a value of B. Note that A and B 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 do only one thing and do it well.

So, what possibly could help us? Add 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 useState hook for managing component state is a higher-order function since it returns a function used for updating the state.

const App = () => {
  const [counter, setCounter] = useState(0)
  // typeof setCounter === 'function'
}
Enter fullscreen mode Exit fullscreen mode

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.

Pseudo-code for the Validator

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 Javascript.

/** Helper for printing the validator warnings */
const warn = msg => {
    console.warn('Invalid:', msg)
    return false
}

/** Validators */
const longEnough = (password, minLength = 12) => password.length >= minLength || warn(`Password should contain ${minLength} or more characters.`)
const hasUpperCase = password => /[A-Z]+/.test(password) || warn('Password should have at least one uppercase letter.')
const hasLowerCase = password => /[a-z]+/.test(password) || warn('Password should have at least one lowercase letter.')
const hasNumbers = password => /[0-9]+/.test(password) || warn('Password should have at least one number.')

/** Higher-order function to run the given validators */
const validate = password => (...fns) => fns.every(fn => fn(password))

const validator = validate('SUP3RsECREtP4ssW0rd')
console.log(validator(
    longEnough,
    hasUpperCase,
    hasLowerCase,
    hasNumbers,
)) // => true
Enter fullscreen mode Exit fullscreen mode

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 validate function which returns a new function accepting the validators as arguments. Doing this, we don't have to pass the password again for each of the validator functions. 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.

function validate(password) {
    return function(...fns) {
        return fns.every(function(fn) {
            return fn(password)
        })
    }
}
Enter fullscreen mode Exit fullscreen mode

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.

Discussion (6)

pic
Editor guide
Collapse
mrmowji profile image
Mojtaba Javan • Edited

I'm just wondering. It might make the code cleaner in some cases. But what about the speed?

I know that there could be some compiler optimizations which would make function calls better. But in your case, that separation of tasks (to achieve single-responsibility) doesn't seem to be a good choice.

I tested 3 versions of your code:

  • your code as it is
  • your code without higher order functions
  • your code without higher order functions and without separating comparisons (your code traverses the password from start to the end 3 times)

The time these codes take to finish could be simplified in this order: 3s, 2s, and 1s.
I'm not sure if that's worth it. Please let me know if I did something wrong.

function validate(password) {
  return password.length >= 12 && /[A-Za-z0-9]+/.test(password);
}

I see a lot of developers do the same thing, specially when solving online exercises (like Edabit or Exercism). But I think every one should consider naive alternatives first, then go for cleaner ones (if needed). The cleaner one isn't the best all the time.

Collapse
nikoheikkila profile image
Niko Heikkilä Author

Your point is very valid, and there are two schools here:

One might do this:

someBigArray
    .filter(conditionA)
    .filter(conditionB)
    .filter(conditionC)

Whereas, the other might do this:

someBigArray.filter(conditionA && conditionB && conditionC)

Both are fine, and the worst-case complexity is still O(n). I wouldn't be surprised if some compilers would actually interpret several similar filters as one unified filter.

"The cleaner one isn't the best all the time."

There are no right answers to this. I tend to choose readability over speed unless I know the problem domain is data-heavy and I expect arrays with millions of elements to be the typical input. However, in that case I would probably try to use another data structure than plain arrays to handle such data.

Collapse
mrmowji profile image
Mojtaba Javan • Edited

I think you misunderstood what I meant by not separating comparisons. That's my fault I guess.

With these two groups of codes you mentioned in the comment, your comparisons are still separated. That reflects to how deep you decide to abstract your code to get better single-responsibility. One might not stop there and go deeper and separate /A+/.test() from /B+/.test(), etc. and then define an arrow function for each. I mean you need a better example for your argument perhaps so that using higher order functions makes more sense.

Being O(n) is relative. If the number of your comparisons be equal to or greater than your n (the length of password) then we get O(n^2). And you architectured your code so it'd easily accept more comparisons/checks since you want single-responsibility.

IMO you over-engineered your code before the needs arise (the need to add more conditions which can be done easily even without higher-order functions). On the other hand I believe the speed/performance always matters, no matter how fast our computers become.

I'd go with speed, it’s a problem when it’s a problem, and readability, in no specific order, but having them all in mind.

Thread Thread
nikoheikkila profile image
Niko Heikkilä Author

Thanks for your input. Regular expressions were simply an example here to quickly whip out short functions.

In a real application, you could have validations for minimum length, ratio of symbols to alphabets, and even a side-effect introducing function that checks password from Have I Been Pwned database (or other service). As you know, using those as an example would have made the post confusing and I tried to keep this as beginner-friendly as possible.

Collapse
iquardt profile image
Iven Marquardt • Edited

Minor nitpicks: I think a function that returns a function is just a curried function rather than a higher order function, even if it has more than a single argument. Besides I think higher-order functions make a function more general, that is, applicable to a wider range of scenarios. Approaching a complex problem by decomposing it into smaller sub-problems, solving them and recompose these sub-problems again to solve the original problem is just called function composition, which is itself a higher order function.:

const comp = f => g => x => f(g(x));

Monads btw. are just a construct to compose functions with side effects.

Collapse
nikoheikkila profile image
Niko Heikkilä Author

I once read an article where every primitive value was constructed from a lambda function. Too bad I can't find it right now.

It's functions all the way down.