Skip to content

Victor and Code

9 concepts you should know from functional programming

functional-programming, JavaScript, programming-paradigms, function-composition4 min read

Functional programming

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]
13
14// 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 object
18 product.price = `${product.price} ${currencyName}`
19})
20
21// Calculate total
22let total = 0
23cartProducts.forEach((product) => {
24 total += product.price
25})
26
27// Now let's print the total
28console.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 = [...]
2
3const productsWithCurrencySign = cartProducts.map((product) => {
4 const currencyName = product.currency === 'EUR' ? 'euros' : 'dollars'
5 // Copy the original data and then add priceWithCurrency
6 return {
7 ...product,
8 priceWithCurrency: `${product.price} ${currencyName}`
9 }
10})
11
12let total = 0
13cartProducts.forEach((product) => {
14 total += product.price
15})
16
17console.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.8
2const addBonus = (grossSalary) => grossSalary + 500
3
4const 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(', ')
2
3console.log(joinWithComma(["Shrek", "Donkey"])) // Prints Shrek, Donkey
4console.log(joinWithComma(["Shrek", "Donkey"])) // Prints Shrek, Donkey again!

A common non-deterministic function is Math.random:

1console.log(Math.random()) // Maybe we get 0.6924493472043922
2console.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'
2
3const sessionIsActive = (lastLogin, expirationDate) => {
4 if (lastLogin > expirationDate) {
5 // Modify state outside of this function 😟
6 sessionState = 'EXPIRED'
7 return false
8 }
9 return true
10}
11
12const expirationDate = new Date(2020, 10, 01)
13const currentDate = new Date()
14const isActive = sessionIsActive(currentDate, expirationDate)
15
16// 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'
2
3function sessionIsActive(lastLogin, expirationDate) {
4 if (lastLogin > expirationDate) {
5 return false
6 }
7 return true
8}
9
10function getSessionState(currentState, isActive) {
11 if (currentState === 'ACTIVE' && !isActive) {
12 return 'EXPIRED'
13 }
14 return currentState
15}
16
17const expirationDate = new Date(2020, 10, 01)
18const currentDate = new Date()
19const isActive = sessionIsActive(currentDate, expirationDate)
20const newState = getSessionState(sessionState, isActive)
21
22// 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}
8
9const calculateBigSum = () => {
10 let total = 0
11 for (let counter = 0; counter < 100000000; counter += 1) {
12 total += counter
13 }
14 return total
15}
16
17
18const runCalculationWithProfile = simpleProfile(calculateBigSum)
19
20runCalculationWithProfile()

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 unary
2const stringify = x => `Current number is ${x}`
3
4// This function has an arity of 2. Also called binary
5const 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}
4
5const greeter = generateGreeting('birthday')
6
7// Specialized greeter for cousin birthday
8const greeterCousin = greeter('cousin')
9const cousins = ['Jamie', 'Tyrion', 'Cersei']
10
11cousins.forEach((cousin) => {
12 greeterCousin(cousin)
13})
14/* Prints:
15 My dear cousin Jamie. Hope you have a great birthday
16 My dear cousin Tyrion. Hope you have a great birthday
17 My dear cousin Cersei. Hope you have a great birthday
18*/
19
20// Specialized greeter for friends birthday
21const 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 birthday
28 My dear friend John. Hope you have a great birthday
29 My dear friend Rob. Hope you have a great birthday
30*/

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: () => value
4})

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 * 2
3}
4
5const plusTen = (x) => {
6 return x + 10
7}
8
9const num = 10
10const doubledPlus10 = Identity(num)
11 .map(double)
12 .map(plusTen)
13
14console.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]
3
4// 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 Array
2const favouriteNumbers = [1, [2, 3], 4]
3
4// 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: () => value
5})
6
7// The `of` method is a common type lifting functions to create a Monad object.
8Identity.of = value => Identity(value)
9
10const squareIdentity = x => Identity.of(x ** 2)
11const divideByTwoIdentity = x => Identity.of(x / 2)
12
13const result = Identity(3)
14 .map(squareIdentity)
15 .map(divideByTwoIdentity) // 💣 This will fail because will receive an Identity.of(9) which cannot be divided by 2
16 .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: () => value
4})
5
6...
7
8const result = Identity(3)
9 .flatMap(squareIdentity)
10 .flatMap(divideByTwoIdentity)
11 .valueOf()
12
13console.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.

© 2024 by Victor and Code. All rights reserved.