What are first-class functions?
First-class functions are functions that can be treated like any other value. You can pass them to functions as arguments, return them from functions, and save them in variables. In this episode, we talk about why they are important for functional programming and what features we require of them.
Eric Normand: Our first-class functions. By the end of this video, you will know what they are, you will know some of the required features that we need for functional programming, and why they are important for functional programming. My name is Eric Normand, and I help people thrive with functional programming.
First-class functions are important because they're a basic requirement for writing higher-level abstractions with functions. You could do it with classes, but that's a different paradigm.
Without them, we wouldn't have tools like map, filter and reduce, and we wouldn't be able to compose functions up into new functions. This is something that is one of the main benefits of functional programming. Not the primary benefit, but I guess you could call it the main secondary benefit.
Let's go over a few points about first-class functions. What does it mean? A first-class function, first-class anything, means that the function can be, if it's first class, it means it can be treated like a value.
Anything you can do to a value such as a number or an array or vector, a list, a string, any of those values, what can you do with them? You can pass them to functions, you can store them in variables, you can return them from functions. All of those things make it first class. It means it has the same status.
Think about second-class citizens, they don't have the same status as the first-class citizens. That's the idea here, that a first-class function has the same status as other values. Functions are just values.
We can do all those things. We can put them in a variable, we can pass them as arguments to other functions, we can return them from other functions. This makes it so that we can manipulate functions like you would manipulate numbers or strings, and things like that.
This means you can start doing what is often called metaprogramming. You're programming your programs. We'll talk a little bit more about that in a minute.
You want your first-class functions to also be what are called closures. That's closure with an s. What this means is that the function can refer to variables in the scope where the function was defined.
You have some variable, let's call it x. In that scope where x is defined, the next line, you define a function, you can refer to x in the body of the function. Even if you return that function from wherever that scope is, that function somehow gets out of the scope. When you call that function, it's still referring to the same x that you had before. That's what it means.
For instance, as an example, Java for the longest time did not have closures. You could define an anonymous class that had a method, but that method couldn't refer to variables in the scope where that class was defined. Now it has lambdas, those are closures, and so everything's all right.
Without them, it makes it very difficult. You have to make a lot of leaps and do a lot of gymnastics to get the same functionality. You basically have to build an object — this is in Java — you'd have to build an object and assign all of the variables you might need later, all the values you might need later, into that object. It's a lot of ceremony for what should be syntactically very easy.
Another thing is, when you have first-class functions, we were talking about metaprogramming, one thing you want to do is to separate out concerns so that you can reuse part of the concern in another place.
One thing I find in languages that don't allow such abstraction, or where it's not very common at least, is you're writing a lot of low-level code. People call it boilerplate. You're writing for-loops all the time, or where you're repeating yourself over and over. How can you possibly turn that into a first-class operation?
In Java, let's give an example in Java, you can make a for-loop first-class. The way in Java you make things first class is you turn them into a class. You make a class and now you can instantiate it, and now you have an object. It's a value, like other objects. You can pass it around, it can be an argument to a function, etc.
You make an object called for-loop, a public class for-loop, and it takes some kind of operation to do at each step, and then when to end. You can make that, and then it's first-class, so you can pass it to something else, and you can reuse the functionality.
"We make a function called map that is this one use for a for-loop, and we have to pass it to function. The functions are pretty easy to define, and so now we've eliminated a whole bunch of moving parts, or they're taken care of automatically by map. We don't have to initialize the index variable, we don't have to increment it, we don't have to remember how to end the loop.
"All of that's taken care of for us, and all we get is the values. We tell it how to transform the value. We don't even have to initialize the array or anything, the thing that's returned. All of that stuff is handled for us."
This is what I mean by being able to have higher-level abstractions. The ability to pass functionality, which is what a function is, is a way of encapsulating code, some procedure or some calculation, into a little package, and that you can pass that package around.
Now you can say, "This is what I want you to do to my array," for instance. That gives us map, filter and reduce. Without higher-order functions, we couldn't have them. I don't know what else to say. It's functionality that is repeated, it's boilerplate, we get to get rid of it.
Another thing is something like, what are called combinators. Combinators, there's a technical definition for it, but it's mostly used as a function that will take one or more functions and transform them into a new function. There is a technical term in lambda calculus, a technical usage for it, but in practice, in the industry, that's what people use combinators for.
What would this be like? A good example is a memoize function. This is a function that memoizes another function.
There's a function called memoize. You pass it a function, and it will memoize it, meaning it will cache the results so that you don't have to call the function twice for the same results. It has a little object in there that stores the cache, and it checks in the cache before it calls the function. It does that by wrapping this function in another function. It's pretty simple to write.
There's another combinator that you might have where, let's say you have a function that makes a Web request, and that Web request could fail. What do you do?
You make another function that takes the function, and it will call it three times. It returns a new function that calls it three times. This will automatically retry if there's a failure. Of course the third time if it fails, then it will fail for real.
This is a way of capturing these concerns and separating them out from the specifics of the functionality. You're able to, say, try three times as separate from what you're trying three times, and now there's a way to combine them back together.
The last thing I want to talk about is you can do what I'm going to call basic functional programming, without first-class functions. What I mean by that is there is a lot of value in simply thinking about the difference between actions, calculations and data.
Actions are things that have an effect on the world, or it matters how many times you call them or when you call them. Calculations are timeless relationships between inputs and outputs. They're just computations. Data is inert stuff with structure and meaning that you can apply to it.
Just that, I believe, will give you a lot of value. Knowing that, "I have to be careful when I call this, but I can call this as many times as I want." That will tremendously improve your software.
There's another level which is to be able to get these benefits of using higher-order programming. That's not just using map, filter and reduce, but building your own. Realizing that there's a lot of benefit to reusing this one piece of functionality.
That's when you pull things apart, you can see how they work a little bit better. Higher-order functions let you pull things apart. You don't have to write them all together in continuous code. You can put stuff into a function and pass it to something else.
Let's recap. First-class functions are functions that we can treat like values, like strings or numbers. They need to be closures, closures with an s, meaning they can refer to variables in the scope that they're defined in. These are what led us to map, filter and reduce, and also developed combinators, which transform functions from one thing to another.
We can do basic FP, but we can't do more advanced functional programming, which is where it gets really interesting, the idea of being able to separate out this, I guess I want to call it boilerplate. [laughs] That's the term people use. Separate out all this boilerplate stuff, nail it down, capture it as a small piece of functionality that's totally reusable.
Do yourselves a favor. Have a look at your code and see if there's any places where you couldn't pull out first-class functions. See if there's things where you already used first-class functions. They're very powerful, and you should be using them more. Everybody should.
Do me a favor. If you liked this, I try to go deep on these topics where I could just give the definition, but I like to make it practical, bring in some deeper thinking on the idea. If you like this one, then you should subscribe, because there's more like it coming down the pipe.
If you have a question, a comment, you want me to talk about something, you want me to go deeper on something, or you disagree with me, that's cool too, email me at email@example.com. You can find me on Twitter, I'm @EricNormand with a d, or find me on LinkedIn and we can talk there. Awesome. See you later.