9 concepts you should know from functional programming
— functional-programming, JavaScript, programming-paradigms, function-composition — 4 min read
Let's begin by defining what functional programming is (FP from now on). FP is a programming paradigm where software is written by applying and composing functions. A paradigm is a "Philosophical or theoretical framework of any kind." In other words, FP is a way for us to think of problems as a matter of interconnecting functions.
This article aims to give a basic understanding of fundamental concepts in FP and some of the problems it helps solve.
Note: For practicality, I'll omit specific mathematical properties that define these concepts. This is not necessary for you to use these concepts and apply them in your programs.
Immutability
A mutation is a modification of the value or structure of an object. Immutability means that something cannot be modified. Consider the following example:
1const cartProducts = [2 {3 "name": "Nintendo Switch",4 "price": 320.0,5 "currency": "EUR"6 },7 {8 "name": "Play station 4",9 "price": 350.0,10 "currency": "USD"11 }12]1314// Let's format the price field so it includes the currency e.g. 320 €15cartProducts.forEach((product) => {16 const currencySign = product.currency === 'EUR' ? '€' : '$'17 // Alert! We're mutating the original object18 product.price = `${product.price} ${currencyName}`19})2021// Calculate total22let total = 023cartProducts.forEach((product) => {24 total += product.price25})2627// Now let's print the total28console.log(total) // Prints '0320 €350 $' 😟
What happened? Since we're mutating the cartProducts
object, we lose the original value of price.
Mutation can be problematic because it makes tracing the state changes in our application hard or even impossible. You don't want to call a function in a third party library and not know if it will modify the object you're passing.
Let's look at a better option:
1const cartProducts = [...]23const productsWithCurrencySign = cartProducts.map((product) => {4 const currencyName = product.currency === 'EUR' ? 'euros' : 'dollars'5 // Copy the original data and then add priceWithCurrency6 return {7 ...product,8 priceWithCurrency: `${product.price} ${currencyName}`9 }10})1112let total = 013cartProducts.forEach((product) => {14 total += product.price15})1617console.log(total) // Prints 670 as expected 😎
Now, instead of modifying the original object, we clone the data in the original cartProducts
by using the spread operator
1return {2 ...product,3 priceWithCurrency: `${product.price} ${currencyName}`4}
With this second option, we avoid mutating the original object by creating a new one that has the priceWithCurrency
property.
Immutability can actually be mandated by the language. JavaScript has the Object.freeze
utility, but there are also mature libraries such as Immutable.js
you can use instead. Nevertheless, before enforcing immutability everywhere, evaluate the tradeoff of adding a new library + the extra syntax; maybe you'd be better off creating an agreement in your team not to mutate objects if possible.
Function composition
It's the application of a function to the output of another function. Here's a small example:
1const deductTaxes = (grossSalary) => grossSalary * 0.82const addBonus = (grossSalary) => grossSalary + 50034const netSalary = addBonus(deductTaxes(2000))
In practice, this means we can split out algorithms into smaller pieces, reuse them throughout our application, and test each part separately.
Deterministic functions
A function is deterministic if, given the same input, it returns the same output. For example:
1const joinWithComma = (names) => names.join(', ')23console.log(joinWithComma(["Shrek", "Donkey"])) // Prints Shrek, Donkey4console.log(joinWithComma(["Shrek", "Donkey"])) // Prints Shrek, Donkey again!
A common non-deterministic function is Math.random
:
1console.log(Math.random()) // Maybe we get 0.69244934720439222console.log(Math.random()) // Maybe we get 0.4146573369082662
Deterministic functions help your software's behavior be more predictable and make the chance of bugs lower.
It's worth noting that we don't always want deterministic functions. For example, when we want to generate a new ID for a database row or get the current date in milliseconds, we need a new value to be returned on every call.
Pure functions
A pure function is a function that is deterministic and has no side-effects. We already saw what deterministic means. A side-effect is a modification of state outside the local environment of a function.
Let's look at a function with a nasty side-effect:
1let sessionState = 'ACTIVE'23const sessionIsActive = (lastLogin, expirationDate) => {4 if (lastLogin > expirationDate) {5 // Modify state outside of this function 😟6 sessionState = 'EXPIRED'7 return false8 }9 return true10}1112const expirationDate = new Date(2020, 10, 01)13const currentDate = new Date()14const isActive = sessionIsActive(currentDate, expirationDate)1516// This condition will always evaluate to false 🐛17if (!isActive && sessionState === 'ACTIVE') {18 logout()19}
As you can see, sessionIsActive
modifies a variable outside its scope, which causes problems for the function caller.
Now here's an alternative without side-effects:
1let sessionState = 'ACTIVE'23function sessionIsActive(lastLogin, expirationDate) {4 if (lastLogin > expirationDate) {5 return false6 }7 return true8}910function getSessionState(currentState, isActive) {11 if (currentState === 'ACTIVE' && !isActive) {12 return 'EXPIRED'13 }14 return currentState15}1617const expirationDate = new Date(2020, 10, 01)18const currentDate = new Date()19const isActive = sessionIsActive(currentDate, expirationDate)20const newState = getSessionState(sessionState, isActive)2122// Now, this function will only logout when necessary 😎23if (!isActive && sessionState === 'ACTIVE') {24 logout()25}
It's important to understand we don't want to eliminate all side-effects since all programs need to do some sort of side-effect such as calling APIs or printing to some stdout. What we want is to minimize side-effects, so our program's behavior is easier to predict and test.
High-order functions
Despite the intimidating name, high-order functions are just functions that either: take one or more functions as arguments, or returns a function as its output.
Here's an example that takes a function as a parameter and also returns a function:
1const simpleProfile = (longRunningTask) => {2 return () => {3 console.log(`Started running at: ${new Date().getTime()}`)4 longRunningTask()5 console.log(`Finished running at: ${new Date().getTime()}`)6 }7}89const calculateBigSum = () => {10 let total = 011 for (let counter = 0; counter < 100000000; counter += 1) {12 total += counter13 }14 return total15}161718const runCalculationWithProfile = simpleProfile(calculateBigSum)1920runCalculationWithProfile()
As you can see, we can do cool stuff, such as adding functionality around the execution of the original function. We'll see other uses of higher-order in curried functions.
Arity
Arity is the number of arguments that a function takes.
1// This function has an arity of 1. Also called unary2const stringify = x => `Current number is ${x}`34// This function has an arity of 2. Also called binary5const sum => (x, y) => x + y
That's why in programming, you sometimes hear unary
operators such as ++
or !
Curried functions
Curried functions are functions that take multiple parameters, only that one at a time (have an arity of one). They can be created in JavaScript via high-order functions.
Here's a curried function with the ES6 arrow function syntax:
1const generateGreeting = (ocassion) => (relationship) => (name) => {2 console.log(`My dear ${relationship} ${name}. Hope you have a great ${ocassion}`)3}45const greeter = generateGreeting('birthday')67// Specialized greeter for cousin birthday8const greeterCousin = greeter('cousin')9const cousins = ['Jamie', 'Tyrion', 'Cersei']1011cousins.forEach((cousin) => {12 greeterCousin(cousin)13})14/* Prints:15 My dear cousin Jamie. Hope you have a great birthday16 My dear cousin Tyrion. Hope you have a great birthday17 My dear cousin Cersei. Hope you have a great birthday18*/1920// Specialized greeter for friends birthday21const greeterFriend = greeter('friend')22const friends = ['Ned', 'John', 'Rob']23friends.forEach((friend) => {24 greeterFriend(friend)25})26/* Prints:27 My dear friend Ned. Hope you have a great birthday28 My dear friend John. Hope you have a great birthday29 My dear friend Rob. Hope you have a great birthday30*/
Great right? We were able to customize the functionality of our function by passing one argument at a time.
More generally, curried functions are great for giving functions polymorphic behavior and to simplify their composition.
Functors
Don't get intimidated by the name. Functors are just an abstractions that wraps a value into a context and allows mapping over this value. Mapping means applying a function to a value to get another value. Here's how a very simple Functor looks like:
1const Identity = value => ({2 map: fn => Identity(fn(value)),3 valueOf: () => value4})
Why would you go over the trouble of creating a Functor instead of just applying a function? To facilitate function composition. Functors are agnostic of the type inside of them so you can apply transformation functions sequentially. Let's see an example:
1const double = (x) => {2 return x * 23}45const plusTen = (x) => {6 return x + 107}89const num = 1010const doubledPlus10 = Identity(num)11 .map(double)12 .map(plusTen)1314console.log(doubledPlus10.valueOf()) // Prints 30
This technique is very powerful because you can decompose your programs into smaller reusable pieces and test each one separately without a problem. In case you were wondering, JavaScript's Array
object is also a Functor.
Monads
A Monad is a Functor that also provides a flatMap
operation. This structure helps to compose type lifting functions. We'll now explain each part of this definition step by step and why we might want to use it.
What are type lifting functions?
Type lifting functions are functions that wrap a value inside some context. Let's look at some examples:
1// Here we lift x into an Array data structure and also repeat the value twice.2const repeatTwice = x => [x, x]34// Here we lift x into a Set data structure and also square it.5const setWithSquared = x => new Set(x ** 2)
Type lifting functions can be quite common, so it makes sense we would want to compose them.
What is a flat function
The flat
function (also called join) is a function that extracts the value from some context. You can easily understand this operation with the help of JavaScript's Array.prototype.flat function.
1// Notice the [2, 3] inside the following array. 2 and 3 are inside the context of an Array2const favouriteNumbers = [1, [2, 3], 4]34// JavaScript's Array.prototype.flat method will go over each of its element, and if the value is itself an array, its values will be extracted and concatenated with the outermost array.5console.log(favouriteNumbers.flat()) // Will print [1, 2, 3, 4]
What is a flatMap function
It's a function that first applies a mapping function (map), then removes the context around it (flat). Yeah... I know it's confusing that the operations are not applied in the same order as the method name implies.
How are monads useful
Imagine we want to compose two type lifting functions that square and divide by two inside a context. Let's first try to use map and a very simple functor called Identity.
1const Identity = value => ({2 // flatMap: f => f(value),3 map: f => Identity.of(f(value)),4 valueOf: () => value5})67// The `of` method is a common type lifting functions to create a Monad object.8Identity.of = value => Identity(value)910const squareIdentity = x => Identity.of(x ** 2)11const divideByTwoIdentity = x => Identity.of(x / 2)1213const result = Identity(3)14 .map(squareIdentity)15 .map(divideByTwoIdentity) // 💣 This will fail because will receive an Identity.of(9) which cannot be divided by 216 .valueOf()
We cannot just use the map function and need to first extract the values inside of the Identity. Here's where the flatMap function comes into place.
1const Identity = value => ({2 flatMap: f => f(value),3 valueOf: () => value4})56...78const result = Identity(3)9 .flatMap(squareIdentity)10 .flatMap(divideByTwoIdentity)11 .valueOf()1213console.log(result); // Logs out 4.5
We are finally able to compose type lifting functions, thanks to monads.
Conclusion
I hope this article gives you a basic understanding of some fundamental concepts in functional programming and encourages you to dig deeper into this paradigm so you can write more reusable, maintainable, and easy-to-test software.