Functional programming is a hot topic. The 2021 Developer Survey from Stack Overflow ranked functional languages among the most loved. Popular JavaScript libraries like React and Angular let you use functional concepts in your components, traditionally object-oriented languages have added functional support... and yet there’s been some confusion about what functional programming actually means.
Traditionally, folks often think that it’s a concept that you should learn when you are deeper into your development career, but that’s not necessarily the case!
“I think functional programming is more accessible for someone trying to teach themselves to program. I’ve seen folks from all sorts of backgrounds come onto the Elixir Wizards podcast and tell me that Elixir clicked for them when they were starting out because of the pipe operator. The pipe ( `|>` ) makes it easier for beginners to recognize what their code is doing, with a clear pipeline of what they started with, how it changed, and the final outcome. Generally, I think functional programming reads more like spoken language.” - Sundi Myint, co-host of the Elixir Wizards podcast
If you’re ready for it, let’s take a deep dive into what functional programming is, how it differs from other paradigms, why you would use it, and how to get started!
What is functional programming?
There are three “types” of programming that you may or may not know: procedural programming, object-oriented programming, and functional programming. I’ll focus on the latter two.
In object-oriented programming (OOP), you create “objects” (hence the name), which are structures that have data and methods. In functional programming, everything is a function. Functional programming tries to keep data and behavior separate, and OOP brings those concepts together.
“Functional programming [is] a paradigm that forces us to make the complex parts of our system explicit, and that’s an important guideline when writing software.” - José Valim, creator of Elixir
What are the rules of functional programming?
There are two main things you need to know to understand the concept:
Data is immutable: If you want to change data, such as an array, you return a new array with the changes, not the original.
Functions are stateless: Functions act as if for the first time, every single time! In other words, the function always gives the same return value for the same arguments.
There are three best practices that you should generally follow:
Your functions should accept at least one argument.
Your functions should return data, or another function.
Don’t use loops!
Now, some of these things can happen in object-oriented programming as well. Some languages allow you to mix-and-match concepts, too, like JavaScript for example.
An example to show the difference between OOP and functional programming
Let’s say that you have a school and we have the records of each of the students in some database. Let’s say they all have a name and a grade point average (GPA). In object-oriented programming, you might write something like this:
class Student {
constructor(name, gpa) {
this.name = name;
this.gpa = gpa;
}
getGPA() {
return this.gpa;
}
changeGPA(amount) {
return this.gpa + amount;
}
}
If you wanted to initialize a student, you might do something like this:
let jacklyn = new Student('Jacklyn Ford', 3.95);
Now let’s say you want to change the GPAs of a bunch of students. With OOP, you could have an array of the students:
let students = [ new Student('Jacklyn Ford', 3.95),
new Student('Cassidy Williams', 4.0), new Student('Joe Randy', 2.2) ];
// for legal reasons, these people are not real
// and my GPA may have been perfect in college, maybe
And to change each of their GPAs, you could do a loop to increase the grades of each.
for (let i = 0; i < students.length; i++) {
students[i].changeGPA(.1);
}
…or something. Then you could loop through again to print out the results, or just work with the objects as you wish.
Now, if you wanted to solve the same type of problem with functional programming, you’d do it a little differently. This is how you might initialize the students:
let students = [
['Jacklyn Ford', 3.95],
['Cassidy Williams', 4.0],
['Joe Randy', 2.2],
];
We’re storing the students as plain arrays instead of objects. Functional programming prefers plain data structures like arrays and lists and hashes (etc.) to not “complicate” the data and behavior. So next, instead of writing just one changeGPA()
function that you loop over, you’d have a changeGPAs()
function and a changeGPA()
function.
function changeGPAs(students) {
return students.map(student => changeGPA(student, .1))
}
function changeGPA(student, amount) {
return [student[0], student[1] + amount]
}
The function changeGPAs()
would take in the students’ array. It would then call changeGPA()
for each value in the students array, and return the result as a new array. The job of changeGPA()
is to return a copy of the student passed in with the GPA updated.
The point of this is because functional programming prefers tiny, modular functions that do one part of a larger job! The job of changeGPAs()
is to handle the set of students, and the job of changeGPA()
is to handle each individual student. Also note that the original array doesn’t change because we treat data as immutable in functional programming. We create new datasets instead of modifying existing ones.
Why would I use functional programming?
When you think about well-structured software, you might think about software that’s easy to write, easy to debug, and where a lot of it is reusable. That’s functional programming in a nutshell! Granted, one might argue that it’s not as easy to write, but let’s touch on the other two points while you wrap your mind around the functional paradigm.
“Once you get used to it, it’s self-evident. It’s clear. I look at my function. What can it work with? Its arguments. Anything else? No. Are there any global variables? No. Other module data? No. It’s just that.” - Robert Virding, co-inventor of Erlang
Debugging
Debugging functional programming is arguably significantly easier than other programming paradigms because of its modularity and lack of side effects.
If something went wrong in your software using OOP, you would have to think about what other parts of your program might have done previously that could affect your program’s state. With functional programming, you can pinpoint the exact function where something went wrong, because certain things can only happen at one point.
For example, let’s say we had a counter that skipped the number 5.
let count = 0;
function increment() {
if (count !== 4) count += 1;
else count += 2;
return count
}
In this program, if you want to test it, you’d have to keep track of the global state of count, and run the `increment()` function 5 times to make sure it worked, every single time. `increment()` returns something different every time it is called, so you need to use a debugger to step through your program.
Meanwhile, if you wrote this function in a functional way:
function pureIncrement(count) {
if (count !== 4) return count + 1;
else return count + 2;
}
We don’t need to run `pureIncrement()` multiple times to check this. You can easily unit test the function because it will always return the same thing with the same input, and there is no variable being modified (remember, immutability)!
Now, that isn’t to say that you’ll never use a debugger (I won’t get into that in this article), but by having everything written into smaller chunks and free from side effects, your errors will be much faster to pinpoint, and won’t be dependent on the environment they’re being run in.
Reusability
As you saw in our student example, we broke down the functions into smaller functions. In every functional program you write, you break functions down to be as small as they can be. It’s kind of like breaking up a complex math problem into parenthesis.
Let’s say that you want to solve a math problem, like:
(6 * 9) / ((4 + 2) + (4 * 3))
If you were doing this by hand, you could break up the problem by adding/multiplying all of the numbers, combining what is in the parenthesis, and then dividing the results.
If you were doing this in a functional language, like Lisp, for example, it looks incredibly similar:
(define (mathexample)
(/
(* 6 9)
(+
(+ 2 4)
(* 4 3)
)
)
)
This is why functional programming is often referred to as “pure programming!" Functions run as if they are evaluating mathematical functions, with no unintended side effects.
“Purity” by xkcd is licensed under CC BY-NC 2.5
When you have such small, pure functions, you can often reuse them much more easily than your traditional object-oriented program. This is a controversial take (if you look up “functional programming reuse” you will find many discussions and podcasts on the subject), so hang in with me here: When you want to reuse a class in OOP and add a feature, typically you add conditionals and parameters, and your functions get larger. Your abstract classes and interfaces get pretty robust. You have to pay careful attention to the larger application architecture because of side effects and other factors that will affect your program (like we talked about before). In functional programming, it’s the opposite in that your functions get smaller and much more specific to what you want. One function does one thing, and whenever you want to do that one thing, you use that one function.
There are exceptions in every system, of course, but this is generally what you see in various codebases out in the world!
Okay, okay, I’m sold. How do I get started?
If you’re already comfortable with JavaScript or Python, you can get started right away trying out functional programming concepts like we’ve talked about here. If you’d like to get more into the nitty-gritty of “pure” languages that are designed for functional programming, you can try out the Lisp family (including Common Lisp, Scheme, and Clojure), the ML family (including OCaml and F#), Erlang, Elixir, Elm, or Haskell.
Functional programming can be a little mind-bending as you get used to it. But if you give it a chance and try it out, your development and production environments will be robust, your software will be easier to debug, and you’ll enjoy the guarantees you get from the solid foundations of functional programming!