So you have that one hipster "functional programmer" coworker... They ramble on about their side projects in Elm, why JavaScript is too mainstream, how they've learned Haskell because it's a different way of thinking about things, and have tried explaining what currying and a monad is to you several times. With a slew of new terms and languages you haven't even heard of, it's easy to dismiss your coworker's functional programming ideas as fringe concepts.
You're a JavaScript developer that has heard about functional programming in passing, but haven't committed the time to dive into it fully. What if I told you are probably already a functional programmer in disguise? You're probably already using loads of functional programming concepts without even knowing it. Before we explore some ways in you're already writing functional code, let's define some basic functional programming concepts.
Functional programming dumbed down without much extra terminology
Ignore currying, monads, and other terms that are often associated with functional programming. Functional programming at its core is to code with pure functions. There's two rules of pure functions:
- The same inputs always return the same output. No matter how many times the function is called, what order it's called in, or what environment the function is running, it will always have a consistent output.
- The function does not have an effect on anything outside the function. No modifying the parameters, changing a variable outside of the function scope, or making http calls. This is often called no side effects.
Advantages of using pure functions over impure functions
- Pure functions are more testable and predictable, because the same inputs return the same outputs.
- Pure functions are usually more readable (easier to reason about), because you don't need to think about the effects of outside state on your function. Refactoring becomes easier; if you have a function that is confusing, you can write a unit test for the existing function and replace or rewrite it with the new implementation without worrying too much about breaking existing code.
- Pure functions are usually more reusable. Once you start writing pure functions, they'll typically be smaller because you can't rely on outside state. Small functions typically only do one thing, so they're inherently more reusable across your application.
How you're already using pure functions
Chances are, you've written many pure functions before. Here are some clues that you're actually already a functional programmer.
Using [].map instead of [].forEach or a for loop
Like forEach or a for loop, map()
iterates over an array. The difference is, map will not change (or mutate) the original array. Map always returns a new array. Let's take an example function that capitalizes all items in a list and implement it with for, forEach, and map:
const list = ['apple', 'banana', 'carrot'];
const capitalized = capitalizeList(list);
Using for
function capitalizeList(list) {
for (let i = 0; i < list.length; i++) {
list[i] = list[i].toUpperCase();
}
return list;
}
Using forEach
function capitalizeList(list) {
let newList = [];
list.forEach(item => {
newList.push(item.toUpperCase());
});
return newList;
}
Using map
function capitalizeList(list) {
return list.map(item => item.toUpperCase());
}
You may have written the third option before or prefer it due to its conciseness. It's also the most pure. The for loop example modifies the original array, so it is impure. The capitalizeList forEach example will always return the same input and output, but the forEach function inside of capitalizeList is not pure because it relies on outside state. The map example is completely pure; both capitalizeList and the map function do not produce any side effects. Preferring map over the other options means you're probably writing many pure functions.
Using filter, reduce, find, or a host of other array pure functions
Like map()
, filter()
and reduce()
will also not change the original array.
Filter using for
function filterByLetter(list, letter) {
for (let i = 0; i < list.length; i++) {
if (!list[i].startsWith(letter)) {
list.splice(i, 1);
}
}
return list;
}
[].filter
function filterByLetter(list, letter) {
return list.filter(item => item.startsWith(letter));
}
Finally, reduce can be used to take an array and turn it into a new data type.
Summing numbers using for
function sumNumbers(numbers) {
let sum = 0;
for (let i = 0; i < numbers; i++) {
sum += numbers[i];
}
return sum;
}
Summing numbers using [].reduce
function sumNumbers(numbers) {
return numbers.reduce((a, b) => a + b, 0);
}
Reduce is a little more advanced and merits its own article, but understanding it and using it should help with building more pure functions. Here are some more examples of pure functions in JavaScript that you may have used before:
Pure Function | What's it for? |
---|---|
[].map() | Returning a new array of new items |
[].filter() | Filtering arrays |
[].reduce() | Morphing arrays into new data structures |
[].find() | Finding first occurrence of item |
[].some() | Checking if array has at least one item matching criteria |
[].includes() | Checking if array has at least one item matching raw param value |
[].every() | Checking if array has ALL items matching criteria |
[].slice(start, end) | Trims array at positions |
[].concat() | Merging two arrays together |
[].join() | Converting array to a single string |
[].flatMap() | Converting a 2D-array into a single array |
It's worth mentioning some common impure functions that modify the original array:
Impure Function | What's it for? |
---|---|
[].push() | Adding to an array |
[].pop() | Removing an item from array |
[].sort() | Sorting |
[].shift() | Removing first item in array |
[].unshift() | Adding items to the beginning of the array |
[].splice() | Removing/replacing items in array |
[].reverse() | Reversing the order |
Using const instead of var or let
JavaScript essentially phased out var
and replaced it with let
and const
in 2016. If you've ditched var, you're already on the right track. let
(and var) allows you to reassign variables:
let vegetable = 'asparagus';
vegetable = 'broccoli'; // valid JavaScript
var carb = 'rice';
carb = 'bread'; // valid JavaScript
var carb = 'beans'; // also valid JavaScript
const
will not allow you to reassign variables
const legume = 'chickpea';
legume = 'soybean'; // syntax error
In the above capitalizeList example, notice how let
is present in the impure examples. If you program only with const
, you're forced to write more pure functions.
Using Object spread {...} notation
It's worth mentioning that const isn't completely immutable - you can still modify objects:
const snacks = {
healthyOption: '',
unhealthyOption: 'Cookies'
}
const addHealthyOption = (snacks, healthyOption) => {
snacks.healthyOption = healthyOption;
return snacks;
}
const newSnackObject = addHealthyOption(snacks, 'Edamame');
console.log(newSnackObject) // 😊 { healthyOption: 'Edamame', unhealthyOption: 'Cookies' }
console.log(snacks.healthyOption) // 😦 'Edamame'
In this example, addHealthyOption mutated the original object. This can be avoided using the object spread syntax:
const addHealthyOption = (snacks, healthyOption) => {
return {...snacks, healthyOption}
}
const newSnackObject = addHealthyOption(snacks, 'Edamame');
console.log(newSnackObject) // 😊 { healthyOption: 'Edamame', unhealthyOption: 'Cookies' }
console.log(snacks.healthyOption) // 😊 ''
Using array spread [...x]
Like the above example, array spreading is similar to object spreading. Let's refactor the above example to take in arrays and look at the two implementations.
const snacks = {
healthyOptions: [],
unhealthyOptions: ['Cookies']
}
const addHealthyOptions = (snacks, healthyOptions) => {
snacks.healthyOptions.push(healthyOptions);
return snacks;
}
const newSnackObject = addHealthyOptions(snacks, ['Edamame', 'Hummus and Veggies']);
console.log(newSnackObject) // 😊 { healthyOptions: ['Edamame', 'Hummus and Veggies'], unhealthyOptions: ['Cookies'] }
console.log(snacks.healthyOptions) // 😦 ['Edamame', 'Hummus and Veggies']
Notice how snacks was mutated. Writing this in a pure way can be accomplished by using the array spread feature:
const snacks = {
healthyOptions: [],
unhealthyOptions: ['Cookies']
}
const addHealthyOptions = (snacks, healthyOptions) => {
return {
...snacks,
healthyOptions: [...snacks.healthyOptions, healthyOptions]
}
}
const newSnackObject = addHealthyOptions(snacks, ['Edamame', 'Hummus and Veggies']);
console.log(newSnackObject) // 😊 { healthyOptions: ['Edamame', 'Hummus and Veggies'], unhealthyOptions: ['Cookies'] }
console.log(snacks.healthyOptions) // 😊 []
Summary
When we write pure functions (same input, same outputs and no side effects) we're doing functional programming. These features can help us write pure functions:
- Using .map() and other array methods like filter, find, and reduce that do not modify the original array
- Using const instead of let or var
- Using {...x} or [...x] to create new objects and arrays
If you've used any of these features, you've probably already written many pure functions. You can call yourself a functional programmer. Inevitably, it becomes harder and harder not to produce side effects or rely on outside state in your functions. This is where advanced functional programming concepts like closures, higher order functions, and currying come in. I did not focus on these advanced topics, because if you're new to functional programming, you're probably not already currying functions on a day-to-day basis. After you've mastered the basics, check out some of these resources to take you functional programming game to the next level:
- https://www.youtube.com/watch?v=qtsbZarFzm8: Anjana Vakil — Functional Programming in JS: What? Why? How? - Great talk on functional programming
- https://mostly-adequate.gitbook.io/mostly-adequate-guide/: Professor Frisby's Mostly Adequate Guide to Functional Programming - In-depth, free book that explains more advanced concepts.
Top comments (0)