Yet another introduction to functional programming
What is Functional Programming?
Wikipedia:
In computer science, functional programming is a programming paradigm- a style of building the structure and elements of computer programs-that treats computation as the evaluation of mathematical functions and avoids changing-state and mutable data.
I love Wikipedia as much as the next person, but that definition doesn’t give a great first impression. Let’s break each piece of this definition down into small chunks, and by the end of this post we’ll be able to compose them back together to reach an understanding.
Functional programming or FP for short, is a programming paradigm — meaning it is a style of programming. Consider some other programming paradigms you’re likely familiar with: (procedural, object oriented, etc.). These are just rule sets we use to structure the way we as developers write code and think about the problems we’re solving.
Many languages have been built to enforce some of the important “laws” of functional programming and are designed entirely around the paradigm. These languages are very fun to use and I’d highly encourage you to try a few of them out. That being said, the purpose of this post is not to learn a functional language. We’re going to learn what functional programming is, and how you can use its concepts inside the code you write everyday.
What is our goal with functional programming?
We’ve established that functional programming is a style of writing programs and representing computations. Before going any further though, I think it’s important to outline an overall goal that this style of programming is trying to achieve. It’s hard to get excited about a new tool if you don’t know what its used for. That being said, we have one and only one goal with FP: make programs simpler.
Now, although that idea took three words to explain and sounds pretty trivial allow me to assure you that making programs simpler is NOT an easy task. In fact, you could argue that people have been working on this problem since computers have existed. Every programming language that has been written likely began with the goal of making our lives as programmers easier. The reason you and I don’t have to specify register addresses when we write a bubble sort is a clear benchmark that shows us programming has gotten simpler as time has progressed. Our brains can now think less about the computer’s inner-workings and spend more time thinking about our program’s process. Much progress has been made in the years before us, but we can still do better. Functional programming provides us with some rules for our code that can provide more simplicity in our programs. These rules can be broken up into three focus points:
- Abstracting out common processes
- Avoiding mutating state / Isolating state mutation
- Writing declarative and composable code
If some of these words or phrases don’t make sense right now that is totally fine. I want to cover each of these in good detail and provide some code examples along the way. Before doing a detailed deep-dive let’s start with a question: how do we “do” functional programming?
One of the more obvious ways you might think to “do” functional programming is to express our program in terms of functions. And that’s actually a great place to start! Writing our programs as a composition of smaller bite-sized functions is a key part of FP. To get a better idea of what that means let’s transform some non-functional code into functional code.
Non-Functional:
const greeting = "Hi, I'm ";
const name_1 = "Jay";
const name_2 = "Eric";
const name_3 = "Pranshu";
console.log(greeting + " " + name_1); // "Hi, I'm Jay"
console.log(greeting + " " + name_2); // "Hi, I'm Eric"
console.log(greeting + " " + name_3); // "Hi, I'm Pranshu"
Functional:
const name_1 = "Jay";
const name_2 = "Eric";
const name_3 = "Pranshu";
function greet(name) {
return "Hi, I'm " + name;
}
console.log(greet(name_1)); // "Hi, I'm Jay"
console.log(greet(name_2)); // "Hi, I'm Eric"
console.log(greet(name_3)); // "Hi, I'm Pranshu"
Nothing too ground-breaking here. We’ve abstracted out a common process in our program into a function which we can reuse. In short, our program has has been expressed in terms of functions instead of statements. Let’s look at a more complex snippet of code and try to apply this same style of thinking.
Non-Functional:
function willCarRun(car, driver) {
let assembled;
if (car.isTotaled) {
if (driver.isMechanic) {
assembled = true;
} else {
assembled = false;
}
} else {
assembled = true;
}
return assembled && car.hasGas;
}
Functional:
function isAssembled(car, driver) {
return !car.isTotalled || driver.isMechanic;
}
function willCarRun(car, driver) {
return isAssembled(car, driver) && car.hasGas;
}
One of our focus areas of FP is avoiding mutation. We’ll go into more detail on the benefits of immutability later, but a good rule of thumb for making our programs more functional is to avoid reassignment. In the functional version of the code, we have the exact same logic as before, but we’ve extracted it out to a separate function making it much easier to reason about.
Now that we’ve seen an overview of what functional code looks like, we can get more into the nitty-gritty details. We’ll dive into other techniques for writing functional programs, and more importantly I’ll walk through the benefits of writing our code this way.
Immutability & Pure Functions
Before we begin, I’d like to take a second and apologize on behalf of the entire functional programming community. We are going to be seeing a lot of terminology in the next few sections of this post. Bare with me. It’s not difficult to understand, but it sounds intimidating at first. With that being said I’ll do my best to explain all the terms I use!
A core piece of the functional programming pie is avoiding state mutation as much as possible. State mutation can be anything from changing a variable’s value, writing to a file, or updating an array entry. Although it may not be obvious, avoiding data mutation in our code is a great way to simplify programs. When state change is minimized, designing, debugging, and reasoning about our programs becomes much easier. Now, unfortunately we can’t totally eliminate state mutation. Without state changing, our programs would be useless! Even the most basic “Hello, World!” program writes to a stdout file. That being said, we can work on minimizing and isolating state mutation in our code to help simplify things.
One way we can put this idea of immutability into action is by defining our programs as a composition of these things called, pure functions.
A pure function is just a regular ol’ function that satisfies a few properties:
1.) It is a deterministic function (the same input ALWAYS gives the same output).
2.) The output of the function solely depends on the function’s input (no reading of state outside the function)
3.) No side-effects take place inside the function.
A side-effect is anything a function does that affects the “world” outside of the function. This can be anything from writing to a file or mutating a global variable. A good way to think about it: if there is some way to tell that a function was invoked inside your program, that function contains side-effects.
Impure function:
let count = 0;
function incrementCountBy(x) {
// side effect because we are mutating state outside the function
count += x;
// writing to IO stream is a side effect
console.log("Incremeneted by: ", x);
// return value is non-deterministic, and isn't dependent on our input
return Math.random();
}
Pure function:
function incrementBy(count, x) {
return count + x;
}
How do pure functions and immutability benefit us?
When we write a pure function we’re giving ourselves a free guarantee that no state will change during the call of that function. The ONLY thing we have to be concerned with is the function’s input and output. This means we don’t have to think about the “world” outside the function. We don’t need to think about what values are mutated where and by what in our program.
Looking back at our Wikipedia definition:
In computer science, functional programming is a programming paradigm-a style of building the structure and elements of computer programs-that treats computation as the evaluation of mathematical functions and avoids changing-state and mutable data
“Mathematical function” is just another term for the pure functions we’ve been looking at. Just like a math function, pure functions take an input and return a deterministic output using nothing but the input data.
Mathematics: f(x) = x * 2
Javascript: function f(x) { return x * 2 }
The only difference is syntax!
Alright, pure functions are great, but at some point we need to update state. So, where should we do it? Unfortunately, there isn’t a straightforward answer to this, and every situation will be a bit different. Later, we’ll see examples of common ways to avoid local state mutation by abstracting out code that mutates data into separate functions. Notice I didn’t say we got rid of mutation, I said we abstracted it out. This is a common way to write more “immutable” code. You don’t necessarily have to get rid of mutation, you just need to isolate it away from your program logic and make it reusable. A similar technique of isolating mutation is also common when managing global state in our programs. You may have heard of state management libraries like Redux or Taskflow. These libraries take on the task of storing our global state, handling updates, and logging all data changes. These libraries are incredibly powerful tools that can help you write maintainable software.
Higher-Order Functions
A higher-order function is a function that does either/both of the following:
1.) Accepts one or more functions as arguments
2.) Returns a function
Higher order functions (HOFs) are another useful tool to help write us write abstract and composable code. When our language lets us pass functions into other functions and return functions from functions we can create all sorts of super nice abstractions that decrease repetitive code and improve the readability of our programs.
Example of a HOF:
function do(action, input) {
return action(input)
}
All our do
function does is invoke the action
function passed to it with our input
value. So, to make use of our do
function we could write something like...
function increment(x) {
return x + 1
}
console.log(do(increment, 5)) // 6
Hopefully that example gave you some familiarity with HOFs. Now let’s look at a slightly more practical example:
function greetWith(greeting) {
return function (name) {
return greeting + name;
};
}
const morningGreeting = greetWith("Good morning, ");
const students = ["Samantha", "Kunal", "George"];
let morningGreetings = [];
for (let i = 0; i < students.length; i++) {
morningGreetings.push(morningGreeting(students[i]));
}
console.log(morningGreetings); // [ "Good morning, Samanatha",
// "Good morning, Kunal",
// "Good morning, George" ]
Above we’ve created a greetWith
function that takes a single argument, greeting
, and returns a function that concatenates this greeting with a name
. Taking a look at the line beneath our definition we can see how it's used. morningGreeting
is set to the return value of greetWith("Good morning, ")
which is our inner function that takes a name
argument. This technique is very common in the FP. It's so common that there is actually a name for it, currying. A more descriptive name that it often goes by is function specialization. All our outer function is doing is creating a specialized function based on the greeting
we provide it, and returning it. We can use the returned function exactly as you'd expect - we pass it a name, and it returns to us our full greeting string.
Never write a for-loop again (higher order functions continued)
Some of the most common code you’ll see in day-to-day programming is iterating over a list. Likely you will filter out certain data from the list, or manipulate each entry in the list. These operations are SO common that they’ve been abstracted into their own higher order functions. These are map, filter, and reduce. I've found that making use of these three functions is a good first step toward adopting a more functional style into your every day development. So, if there is one section of this post you should really pay attention to, it's this one!
Sidenote: The below code is all written in JS, but map, filter, and reduce are common patterns built into most programming languages.
map
Map is a function that takes an array and an operation function. Map creates a new array and fills it with the results of calling the operation function on all of the original array entries.
const words = ["dog", "plant", "bike"];
function makePlural(word) {
return word + "s";
}
const plurals = words.map(makePlural);
console.log(words); // ['dog', 'plant', 'bike']
console.log(plurals); // ['dogs', 'plants', 'bikes']
The plurals
array is a copy of the words
array except that every entry has been thrown through our makePlural
function. Here is a better way to visualize the creation of plurals
:
const plurals = [makePlural("dog"), makePlural("plant"), makePlural("bike")];
If map
still feels too abstract that's ok. Something that really helped me understand map
for the first time was seeing an implementation of it. It's important to remember that none of the functions you see here are magic. They can all be reimplemented using the programming language features you're already familiar with.
Our own map implementation:
function map(list, fn) {
let result = [];
for (let i = 0; i < list.length; i++) {
result[i] = fn(list[i]);
}
return result;
}
// Usage:
const nums = [1, 2, 3];
function double(x) {
return x * 2;
}
const doubled_nums = map(nus, double);
console.log(doubled_nums); // [2, 4, 6]
This code should look very familiar to you. It's just a simple for loop that updates a new array and returns it. One final note before we move on is that map
does NOT mutate our old array - it creates a new one! (+1 for not mutating data)
For more info on map
in Javascript check the MDN Docs
filter
Another common process is extracting certain entries of a list into a new list (aka filtering out items). The filter function takes an array of items and a predicate function. Filter creates a new array and fills it with the elements of the original list that pass the predicate function test.
Example:
const animals = [
{ name: 'keith', genus: 'lion' },
{ name: 'fluffy', genus: 'bear' },
{ name: 'norm', genus: 'tiger' },
{ name: 'lucy', genus: 'bear' },
{ name: 'rachel', genus: 'tiger' },
{ name: 'mittens', genus: 'lion' }
]
function is_lion(animal) {
return animal.genus === 'lion'
}
const lions = animals.filter(is_lion)
console.log(lions)
=> [
{ name: 'keith', genus: 'lion' },
{ name: 'mittens', genus: 'lion' }
]
Filter is a pretty intuitive if you understand map
. It just iterates over your list and only adds entries to a new list if they pass your predicate function. For good measure though, let's write our own filter
function.
function filter(list, predicate) {
let filtered_list = [];
for (let i = 0; i < list.length; i++) {
if (predicate(list[i])) {
filtered_list.push(list[i]);
}
}
return filtered_list;
}
// Usage
const nums = [1, 2, 3, 4, 5];
function isEven(x) {
return x % 2 === 0;
}
const evens = filter(nums, isEven);
console.log(evens); // [2, 4]
For more info on filter
in Javascript check the MDN Docs
reduce
Reduce (or fold as it is referred to in languages like Scala and Haskell) is another higher-order function for list iteration. This is the least intuitive of the three, but we'll get through it. The reduce function takes two arguments: a reducer function and an initial value. Each entry in the original array is passed through this reducer function to create a final value. Examples are the only way this will begin to make sense.
const nums = [12, 8, 22, 95];
const sum = nums.reduce(function (accumulator, currentValue) {
return accumulator + currentValue;
}, 0);
As mentioned above, our first argument to reduce is a function. Annoyingly enough this function is referred to as a reducer function. So, our reducER function takes an accumulator and a current value. For every entry in the array we'll call this function with an accumulator value and the array entry itself. Whatever is returned from the function will be the accumulator value for the next array entry (the accumulator value on the first item in the array will be our inital value I mentioned earlier). Once we've run out of array entries to process reduce will return our final accumulator value.
Tracing through reduce is quite helpful, so here is what our accumulator and currentValue look like each time we invoke the reducer function.
1.) accumulator = 0, currentValue = 12
2.) accumulator = 0 + 12, currentValue = 8
3.) accumulator = 0 + 12 + 8, currentValue = 22
4.) accumulator = 0 + 12 + 8 + 22, currentValue = 95
After executing our reducer for each member in our nums
array, reduce will return our final value of: 0 + 12 + 8 + 22 + 95
, or 137
for those of us that like our expressions reduced (ha).
Implementation of reduce:
function reduce(list, f, startVal) {
let acc = startVal;
for (let i = 0; i < list.length; i++) {
acc = f(acc, list[i]);
}
return acc;
}
For more info on reduce
in Javascript check the MDN Docs
Writing Declarative Code
What are the perks of using abstractions like map
, filter
, and reduce
, aside from avoiding the strain of typing more? Are there any?
Yes! Using these functions helps us in a couple of important ways:
1.) Minimize code
Writing less code means there is less code to debug. No more making mistakes in simple
for
loops. Remember, code is not an asset, it's a liability.
2.) Code Readability
Using abstractions in our code reduces the amount of code we have to read, and it also makes our programs more declarative.
Declarative code can be thought of as the opposite of imperative code. Instead of writing imperative programs where we show how each step in our logic works, we want to write declarative programs that describes what we're doing.
Imperative - describes every step of the process.
"Shows how it is done"
const counts = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
let double_counts = [];
for (let i = 0; i < counts.length; i++) {
double_counts[i] = counts[i] * 2;
}
Declarative - Describes the end goal of our data. Accomplishes this by using abstractions around common procedures.
"Describes what is done."
function double(x) {
return x * 2;
}
const counts = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const double_counts = counts.map(double);
When we write declarative code we're making our future lives MUCH easier. There is less code to comb through, it's simpler to read, and most importantly you don't have to "be" the computer to understand what is going on. Consider reading a piece of code that has multiple nested for loops. To really understand what is happening you have to "become" a computer and keep track of the all indexing logic in your head. Most of us can't do that effectively, or correctly for that matter. And even if you can, why waste the time? Investing time in writing more humanized programs is an effort you won't regret. Future you and your co-workers will really appreciate it.
To wrap things up I wanted to give more examples of refactoring imperative code to be more functional. I think learning the laws of FP is just as important as learning how to apply them to real code. If you can't use these concepts in your day-to-day programming, then what's the use?
Refactoring to Functional
#####Block 1: (Greet the students)
function greetWith(greeting) {
return function(name) {
return greeting + name
}
}
const morningGreeting = greetWith("Good morning, ")
const students = ["Samantha", "Kunal", "George"]
let morningGreetings = [];
for(let i = 0; i < students.length; i++) {
morningGreetings.push(morningGreeting(students[i]))
}
morningGreetings
=> [ "Good morning, Samanatha",
"Good morning, Kunal",
"Good morning, George" ]
#####Refactor of Block 1:
function greetWith(greeting) {
return function(name) {
return greeting + name
}
}
const morningGreeting = greetWith("Good morning, ")
const students = ["Samantha", "Kunal", "George"]
const morningGreetings = students.map(morningGreeting)
morningGreetings
=> [ "Good morning, Samanatha",
"Good morning, Kunal",
"Good morning, George" ]
#####Block 2: (Find max element in list)
const counts = [23, 15, 6, 79, 12]
let maxCount = -Infinity
for(let i = 0; i < counts.length; i++) {
if(counts[i] > maxCount) {
maxCount = counts[i]
}
}
maxCount
=> 79
#####Refactor of Block 2
function max(x, y) {
if(x > y) {
return x;
}
return y;
}
const counts = [23, 15, 6, 79, 12]
const maxCount = counts.reduce(max, -Infinity)
maxCount
=> 79
#####Block 3: (create a new list containing incremented versions of the even numbers in the original list)
Imperative
const old_nums = [1,2,3];
let new_nums = [];
for (let i = 0; i < old_nums.length; i++) {
if (i % 2 == 0) {
new_nums.push(old_nums[i] + 1);
}
}
new_nums
=> [3, 5]
#####Refactor of Block 3
function increment(x) { return x + 1 }
function isEven(x) { return x % 2 == 0 }
const old_nums = [1, 2, 3, 4, 5]
const new_nums = old_nums.filter(isEven).map(increment);
new_nums
=> [3, 5]
Summary
Functional programming is a paradigm of programming that can help us simplify our code. This is done by expressing our programs in terms of small, composable functions, applying a more declarative pattern to our code, and avoiding data mutation when possible. Although some of the concepts may be a bit difficult to grasp at first, the benefits of code readability, program simplicity, and easy debugging are well worth the effort. Our goal is not to write functional code - our goal is simplicity, and functional programming is the tool we're using to achieve it.
More Resources!
Below are links to articles and videos that helped me better understand the foundations of functional programming. So, if you want to learn more, I can't recommend these enough.
A Practical Introduction to Functional Programming
Higher Order Functions: Using Filter, Map and Reduce for More Maintainable Code
What's functional programming all about?
Talks/Video Playlists
Fun Fun Function (Functional Programming in Javascript)