What are higher-order functions?
This is an episode of Thoughts on Functional Programming, a podcast by Eric Normand.
Higher-order functions are functions that take a function as an argument and/or return a function. We use them a lot in functional programming. They are a way to define reusable functionality, as we do with map, filter, and reduce.
Eric Normand: What are higher-order functions? By the end of this episode, you'll know what higher-order functions are. You'll have some examples of higher-order functions that you probably are familiar with. You'll know why they are used so much in functional programming.
My name is Eric Normand, and I help people thrive with functional programming.
High-order functions are an important way of getting leverage when you're programming. They're used, extensively, in functional programming. We were always on the lookout for new higher-order functions to write.
I think that they are the second level of skill. This is how I think of the near-second level of skill in functional programming, the first being seeing actions, calculations, and data.
The second level, you start to think in terms of higher order abstractions like higher order functions.
What are they? Higher order functions, pretty simple definition, but the definition might not really reveal why they're so important. Here's the definition. Higher order function is a function that takes a function as an argument and/or returns a function as a return value.
Let's say a function about a function. It's a function that acts on a function or it's a function that creates a function. It's higher order because you could say, "Well, there's functions that just operate on values." Those are low order. We're talking about addition. We're talking about string concatenation.
These take two strings, return a string. Addition takes two numbers, return to a number. They're low order. Once you have a function that operates on a function, boom, you're getting to a higher order.
Whether they're better, they're higher, that's not really what it's about. It's about the fact that it's about itself. It's functions about function. That's what makes it higher order.
Some examples that you're probably used to already, map, filter and reduce, those are common in a lot of languages now. They are functions, and they take functions as arguments.
Map takes a function and a list, or an array, or something like that, and it applies that function. Map, the function, applies the function that you pass at the argument to all of the elements in that array.
Filter takes a function and a sequence, an array or something, and it applies that function. Any of the elements that don't return true for that function will not make it into the output function.
Reduce is similar. It takes a function, and an initial value, and an array, and it will call that function on each of the elements in the array.
The common thing, though, is that they take functions as arguments. That's why they're higher-order functions.
Why do we do this? You notice you could solve the same problems of map, filter and reduce with for-loops, but for-loops have the advantage of being syntax in your language.
The body of the for-loop, the stuff in the curly braces of the for-loop, is privileged. The compiler is going to look at that code and say, "I'm not going to execute it right away. What I'm going to do is every iteration through this loop, I'm going to execute this code."
We need a way of deferring the execution of code until later. One way you could do that is you wrap that code in a function, and then you make sure that you have all the arguments you need to make that function work out of context, in some other place. We're encapsulating this code, wrapping it up in a function.
If your language has first-class functions, then you can take that function and pass it around. We can make another function that says, "I need to know what you want me to do to each of these things in the array," like a map.
Pass me that as a function and I promise I will call that function for each element in the array and I'll pass it as the argument.
This is a way that you can pass code around. You can pass functionality around. If I wanted to speak in my own terms, I would say you're passing calculations around. You're able to say, you're not just passing data, you're passing calculations. These calculations can then be run on different inputs, different arguments.
They let us take something that is basically syntax like a for loop and turn it into something that is not as privileged. That is first class. Usually, you can't pass a for-loop around but if you wrap it up in a function, you can.
You can also say that at this point you going to start reusing it. Map is much more amenable to re-use. It's going to give you shorter code, people will talk about that.
I don't know if those are the really significant things though. I don't know if it's so important when you've called map it's shorter. What's important is that you're able to operate at a higher level of abstraction. You're able to make functions that operate on other functions instead of having to write out all the code yourself.
You could write, just as an example, if you wanted to process an array four times with different code, you could write four different for-loops. It does the first one, and then does the second one, does the third one and then does the fourth one.
That's great. You could replace all of those for-loops with maps. I would argue it's probably about the same amount of code. You haven't done any savings yet.
Then because you're operating on the same array, right the same array, you can say, "Whoa, I don't need to write out all these maps. I can put the functions that get passed to map into an array."
"Because the functions are first class, I can put these four functions into an array and then I can map over those. Now I'm really saving some code." You're taking it to a higher level. Yes, that's the leverage that I'm talking about.
Not something that I often see when I see people doing functional programming, taking it to that next level to say, "We don't need to write out everything we do. We can have the machine do that for us."
Often this is called metaprogramming. You're programming about programming because it lets you get away with not having...like you're making a new syntax. Instead of a for-loop which has a body, you're doing a map which has a function and so in the function it has a body.
You're able to do a little syntax. You're able to program about programming. Functional programmers don't think that higher-order functions. They don't really call them metaprogramming. They call them programming. That's just what you do.
That is what programming is all about. It's about finding the right level, the right abstractions at the right level for the problem that you're solving. Sometimes that involves making higher-order functions that operate on other functions.
I do want to emphasize that you need first-class functions to do this or something equivalent. You could have blocks, some places they're called lambdas. You could do it with anonymous inner classes in Java, but now they have lambdas so you probably don't need to do it.
Let's talk about functions that return functions. We talked about maps and filter reduce. Those take them as arguments, but they don't return them. Let's talk about two common functions that return functions. One is composition. In the last episode, I talked about function composition as a function.
It takes two functions and it returns a new function that is the composition of the two. You're actually making a function inside that function and returning it. This is an example of a returning a function.
You could say, "I want a function that adds five to a number." You could write that function out. It takes X and it returns X plus five. Then you write one. I need one that returns X plus 10, and I need one that returns X plus 100.
You could say, "Wait a second, I see a pattern here. I'm going to make a function. That takes a number N or Y and it returns a function that takes an X and then it will return X plus Y." You're taking a function, you're making a function that returns a new function. Now you're able to leverage this repetition into common functionality.
Finally, there's another one that you might be familiar with. Maybe not, but that's OK. You have something like filter. It's a function that you pass the filter returns true if you want to keep the element and it returns false if you want to eliminate the elements from the array.
What if you have the function positive? You have a function positive, so it's a positive number. Returns true if it's a positive number and false if it's not positive. You want the opposite of that. You want to keep all of the non-positive numbers and reject all the positive numbers.
You could make a new function that just takes an X and it returns not positive X. You could write that. Then you could also say, "Well, I have a function that tells that me if this is a capitalized string, so is capital."
I need a function that is the opposite of that. Is not capital. I'm going to make a new function call "is not capital" and it will return not is capital X.
You can write all these functions that are just the opposite of an existing function. Then you see the pattern. "Whoa, I'm writing a new function each time that's just the not of an existing function.
"Why don't I make a function that will do that for me?" I'm operating at just a slightly higher level. Instead of writing all these functions myself, I will make the computer write the function for me.
You could call this function opposite or you can call it complement. What it does is it takes a function and it makes a new function that just returns not of calling that functions. Then that's it.
Now you have a function called the opposite, or complement, or negate. Whatever you want to call it. Now, you can use that and pass it to filter and you'll get your non-positive numbers out and eliminate the positive numbers.
That's an example of returning a function. You notice I did these examples so that you could see like, "Oh yes, this is something I would use a lot and I would want to be able to abstract that."
This is what we're doing. We're abstracting these things and you could write yourself. You could write opposite, the opposite of capitalized or the opposite of is capital. You can write the opposite of is positive. You can write the opposite of a bunch of functions all by hand, but this is a way of doing it automatically.
You might have in something like higher-order react components, where you're taking a component or taking a function and returning a new function, that's a functional component, is possible. I just want to mention that. Let me recap.
A higher-order function is one that either takes the function as an argument or returns a function as the return value or both. It could do both. Some common examples, map, filter, reduce, function composition, complement or opposite. There's one that returns an adder that adds a certain number to it. Why do we do it?
Really for the ability to reuse that functionality that you would normally have to write out yourself. You could say that it gives shorter code, but it lets you take a function that represents functionality and use that in some other way. It's a way to make what you might normally think of as the new syntax.
You can do that with just passing in a function because you don't need a block. You need first-class functions for this to work. You need to be able to pass them as arguments obviously.
You might think of this as metaprogramming, programming about programming, but that's kind of limited because this is just what programming is to functional programmers. This is just what we do.
Sometimes, we're writing a function to convert a string to a different kind of string. It's just lower order. Just value-to-value. Sometimes, we notice a pattern, and we say, "Oh, I can make this out of higher order functions. If I just pass-in this function, I will be able to eliminate this duplicated code."
Or, "This is exactly the kind of abstraction that will help my software." Or whatever it is. We just do that. We just do it naturally. We don't think of it as a separate activity, like metaprogramming.
You're probably familiar with callbacks, but the returning of function is also an important thing too.
A little assignment for you that might help you consolidate your learning. Do you have functions that return other functions in your code? You might be able to find them.
Do you have functions that take functions as arguments? Just look at those, find some examples in your code, and ask what they are helping you do. Why are they there? What is it about them that makes them worth having?
Please do me a favor. If you've liked this discussion about higher order functions, please subscribe, because then you'll get more episodes like it as they come out.
That's all I have to say. If you like to get into a discussion with me, I love getting emails. Please email me at firstname.lastname@example.org. I try to reply to everything. In fact, I think I pretty much do reply to everything.
You can also find me on Twitter, @ericnormand with a D. You can also find me on LinkedIn.
I'll see you next time.