What is functional programming?
Functional programming is a programming paradigm, meaning that it is a way of thinking about software construction based on some fundamental, defining principles, much in the way we have imperative programming, wherein you say, "Do this this way, and then do that that way," or object oriented programming, where you have mutable objects with methods and variables that allow you to interact with other pieces of code. Functional programming is a paradigm where functions rule supreme.
Functional programming is also a style of code organization, writing code, and approaching projects and problems.
Finally, functional programming is one of the hot new buzzwords working its way through tech hubs.
To give a more clinical definition:
Functional programming (often abbreviated FP) is the process of building software by composing pure functions, avoiding shared state, mutable data, and side-effects. Functional programming is declarative rather than imperative, and application state flows through pure functions. Contrast with object oriented programming, where application state is usually shared and colocated with methods in objects.
- Eric Elliott
Why write functional JavaScript?
When we broach the subject of writing functional code in JavaScript, I ask that you table your object orientation and pursue FP with a clear and open mind.
Functional JavaScript tends to be safer and easier to debug and maintain as you build out a project. The boom of functional JavaScript has lead to more and more libraries and maintainers adopting the paradigm, meaning interacting with those libraries, and as a result, learning FP as a new adopter will be dramatically more straight forward. Functional code tends to be more concise, more predictable, and easier to test than imperative or object oriented code.
Most importantly (not really but if you've ever dealt with it yo uknow what I mean), we get rid of this
. . . More on this
later.
How do we do it? What does it mean?
Pure Functions
One key to functional programming is understanding the anatomy of a pure function.
A pure function is a function which, given the same input, will always return the same output.
input -> output
Let's look at a simple addition function.
const add = (a: number, b: number) => {
return a + b;
};
console.log(add(1, 1)); // Logs 2
console.log(add(2, 2)); // Logs 4
This function is an example of a pure function. Easy to write, easy to interpret, easy to test.
I know that when I call my add(a,b)
function with any two given numbers, it will always return the same value given the same two numbers as inputs.
Pure functions are pure because the produce no side effects.
A side effect is anything a function might do that is not computing it's output from the input(s) given, and returning that output.
A function loses its purity when it
- Uses or modifies any external variable or object property
- Logs something to the console
- Writes to the screen
- Writes to a file
- Writes to the network
- Triggers any external process
- Calls any other function with side effects
Effectively, anything that adds a degree of unpredictability to a function is an impurity.
Here is a high level example of an impure function:
// variable declared outside the function scope
const name = "Mason Smith";
// name variable is not provided as an argument
const impureGreet = () => {
// No return value
console.log(`Hi, I'm ${name}`);
};
impureGreet(); // Logs "Hi, I'm Mason Smith"
const pureGreet = (name: string) => {
return `Hi, I'm ${name}`;
};
console.log(pureGreet(name)); // Logs "Hi, I'm Mason Smith"
As you can see, our impure function could manipulate some data, is dependent on a consistent name
variable, has no return value,
and would fail tests given different values.
Contrast that to our pureGreet
function and we have reliable output for any given input
Higher Order Functions (first class functions and function composition)
Another key to functional programming is the use of higher order functions, abbreviated to HOCs for all of you React developers reading.
A higher order function is a function that can take a function as its input and/or return a function as its output.
We can use higher order functions to:
- Abstract or isolate actions, effects, or async flow control using callback functions, promises, etc...
- Create utilities which can act on a wide variety of data types
- Partially apply a function to its arguments or create a curried function for the purpose of reuse or function composition
- Take a list of functions and return some composition of those input functions
Note: I definitely plan on writing about currying and partial applications as I have recently fallen in love with them. So watch out for that.
Consider a case in which we need to find a specific submodule in an array of submodules:
interface ModuleInterface {
module: [{ name: string }];
}
// Function to select a sub-module by a given name
// Returns an object with a name property
const selectSubModule = (module: ModuleInterface) => {
// Pass in the sub-module name to isolate and return it
return (subModuleName: string) => {
// Iterate through the module array to find the subModule by name
return module.find((subModule) => subModule.name === subModuleName);
};
};
We have a few options to use the above function.
// Our array of modules
const module = [
{ name: "Sub Module One" },
{ name: "Sub Module Two" },
{ name: "Sub Module Three" },
];
/**
* This is an example of "Currying"
* (note the quotes) . . .
* more on that later
*/
const curriedSubModuleOne = selectSubModule(module)("Sub Module One");
// => { name: 'Module One' }
const curriedSubModuleTwo = selectSubModule(module)("Sub Module Two");
// => { name: 'Module Two' }
/**
* This is an example of partial application
*/
const moduleSelector = selectSubModule(module);
// And we can partially apply moduleSelector
const partialSubModuleOne = moduleSelector("Sub Module One");
// => { name: 'Module One' }
const partialSubModuleTwo = moduleSelector("Sub Module Two");
// => { name: 'Module Two' }
In the above example, we see that selectSubModule
is our higher order function because it returns another function for us to use
THIS IS KEY TO UNDERSTANDING FUNCTIONAL PROGRAMMING AND HIGHER ORDER FUNCTIONS because as we will see as time goes on, we will need these higher order functions to avoid some of the tricks we may have encountered in other programming paradigms. For many, myself included, learning to think in a functional way is heavily influenced by understanding higher order functions.
In my opinion, all of the important concepts of functional programming flow through and from the concept of higher order functions.
You may even be surprised to find that if you use methods like .map()
, .reduce()
, and .filter()
, you are using higher order functions and
embracing the functional programming paradigm.
Instead of iterating over an array with a for
or while
loop, we can use a higher order function like .map()
, in which we can not only
provide an array as it's input, but also a function that we then apply to it.
These higher order functions also make for an easier time employing the concept of immutability, which brings us to our next section.
Immutability and Shared State
Something we want to avoid in functional programming is mutating data. When we mutate data, we risk introducing unpredictable behavior elsewhere in our project where another method may be interacting with that same piece of data.
An immutable object is an object that can’t be modified after it’s created. Conversely, a mutable object is any object which can be modified after it’s created.
Consider the following mutation:
let animals = ["dog", "cat", "bird"];
animals[2] = "lizard";
console.log(animals); // Logs ["dog", "cat", "lizard"]
Let's say elsewhere in my project I have a method that relies on the animals
array to have "bird" at index 2. Well, since here I have
mutated the array to have "lizard" instead of "bird", the method that relies on using the "bird" value will inevitably break. When you have a number of
methods relying on the "bird" value being present, things can quickly go sour. What we have done is damage the referential integrity of our animals
array.
So how do we update "bird" in our animals
array while maintaining the array's referential integrity? We make a copy of the array and use that instead.
const animals = ["dog", "cat", "bird"];
const newAnimals = animals.map((animal) => {
if (animal === "bird") {
return "lizard";
}
return animal;
});
console.log(animals); // => ["dog", "cat", "bird"]
console.log(newAnimals); // => ["dog", "cat", "lizard"]
This may be a bit more verbose, of course, but importantly, our referential integrity is maintained and we trust that this method will not break data being used by any other methods in our project.
An Aside on Performance
A possible drawback of this method is that if we make a copy of every array, particularly with large arrays, we can introduce memory and performance ineffecencies in our project.
One way around this shortoming is to use persistent data structures for efficient immutability.
Let's represent an array as a tree, such that each leaf node of the tree is n number of things we want to store.
// /\
// /\ \
const arrayOne = [1, 2, 3];
// If we want to change something in arrayOne,
// we don't need to replace the entire array;
// we can just make a new node, like the variable four
const four = 4;
// We make a new node which connects our
// [1, 2] node to our 4 node
const arrayTwo = [1, 2, 4];
How the heck do we do that?
Well, frankly I use libraries. There is Mori, which is a "simple bridge to ClojureScript's persistent data structures and supporting APIs for vanilla JavaScript", or ImmutableJS, which "provides many Persistent Immutable data structures including: List, Stack, Map, OrderedMap, Set, OrderedSet and Record".
That's all I have for now
This is obviously not an in depth breakdown of functional programming, but a high level overview of some of the paradigm's most important concepts. I strongly recommend further reading and experimentation as, speaking from personal experience, adopting functional programming has changed the way I srite, structure, and think about code. Predictability is my new best friend, and my software is all the better for it.
None of this is to say that you have to write only functional JavaScript. The great thing about JavaScript is that it is so dynamic, even when using type systems like flow or TypeScript. Embrace the dynamism offered by JavaScript and use it's openness to multiple paradigms to your advantage.