In Computer Science, Functional Programming is "a programming paradigm where solutions are built by composing functions. Differently than in imperative programming, in the functional approach, functions are considered first-class citizens, which means they can be passed as parameters to other functions or even be returned from them as any other data type". (Source: https://en.wikipedia.org/wiki/Functional_programming)
One of the main concepts behind Functional Programming is Pure Functions. This concepts helps us to avoid side effects by guaranteeing that whenever a function is called with the same input it will always returns the same output. Below there are some articles that explain what Pure Functions are in detail:
JavaScript and Functional Programming
JavaScript is, by nature, "a multi-paradigm language that allows you to mix object-oriented, procedural and functional programming techniques". (Source: https://opensource.com/article/17/6/functional-javascript).
JS Applications built using functional programming tend to be more modularized, testable and maintainable. So, let's take a look at how a procedural implementation may become a functional one.
Note: in this article, I'm not saying that the functional approach is the best for EVERY case. This will vary according to the scope and expertise involved in the project you are working on.
From Procedural to Functional
Let’s start with a simple JS example built using an imperative approach. Consider the following list of developers hired by a tech company. Each one of them is represented by a JSON object that contains their name, age, sex, level and their earnings of the last three months.
const developers = [
{
id: 1,
name: 'John Doe',
age: 29,
sex: 'male',
level: 'senior',
earnings: [
{
month: 'February',
year: 2021,
amount: 12500
},
{
month: 'March',
year: 2021,
amount: 12000
},
{
month: 'April',
year: 2021,
amount: 13100
}
]
},
{
id: 2,
name: 'Peter Johnson',
age: 27,
sex: 'male',
level: 'mid',
earnings: [
{
month: 'February',
year: 2021,
amount: 9800
},
{
month: 'March',
year: 2021,
amount: 8600
},
{
month: 'April',
year: 2021,
amount: 10000
}
]
},
{
id: 3,
name: 'Jane Doe',
age: 22,
sex: 'female',
level: 'mid',
earnings: [
{
month: 'February',
year: 2021,
amount: 10450
},
{
month: 'March',
year: 2021,
amount: 11340
},
{
month: 'April',
year: 2021,
amount: 11050
}
]
},
{
id: 4,
name: 'Mary Jane',
age: 35,
sex: 'female',
level: 'senior',
earnings: [
{
month: 'February',
year: 2021,
amount: 14600
},
{
month: 'March',
year: 2021,
amount: 15230
},
{
month: 'April',
year: 2021,
amount: 14200
}
]
},
{
id: 5,
name: 'Bob Taylor',
age: 19,
sex: 'male',
level: 'junior',
earnings: [
{
month: 'February',
year: 2021,
amount: 6700
},
{
month: 'March',
year: 2021,
amount: 5900
},
{
month: 'April',
year: 2021,
amount: 6230
}
]
},
{
id: 6,
name: 'Ted Talker',
age: 48,
sex: 'male',
level: 'senior',
earnings: [
{
month: 'February',
year: 2021,
amount: 18450
},
{
month: 'March',
year: 2021,
amount: 17660
},
{
month: 'April',
year: 2021,
amount: 17995
}
]
}
]
Based on this list, we need to create a routine that appends to each one of the developers their average salary and if it is below, equal or above the average salary for their positions in a given place.
Procedural Example
const JUNIOR_AVERAGE_SALARY = 7000
const MID_AVERAGE_SALARY = 10000
const SENIOR_AVERAGE_SALARY = 13000
for(let developer of developers) {
let lastThreeMonthsTotalEarnings = 0
for(let earning of developer.earnings) {
lastThreeMonthsTotalEarnings += earning.amount
}
developer.averageSalary = lastThreeMonthsTotalEarnings / developer.earnings.length
if(developer.level === 'junior') {
if(developer.averageSalary === JUNIOR_AVERAGE_SALARY) {
developer.averagePosition = 'equal'
} else if(developer.averageSalary > JUNIOR_AVERAGE_SALARY) {
developer.averagePosition = 'above'
} else {
developer.averagePosition = 'below'
}
}
if(developer.level === 'mid') {
if(developer.averageSalary === MID_AVERAGE_SALARY) {
developer.averagePosition = 'equal'
} else if(developer.averageSalary > MID_AVERAGE_SALARY) {
developer.averagePosition = 'above'
} else {
developer.averagePosition = 'below'
}
}
if(developer.level === 'senior') {
if(developer.averageSalary === SENIOR_AVERAGE_SALARY) {
developer.averagePosition = 'equal'
} else if(developer.averageSalary > SENIOR_AVERAGE_SALARY) {
developer.averagePosition = 'above'
} else {
developer.averagePosition = 'below'
}
}
}
Notice that there are reassignments within for loops, which breaks one of the rules of functional programming and, besides that, the code is kind of messy and cannot be easily tested because it is too procedural. Besides, this loop structure is single thread which blocks IO and any resource outside its scope.
Migrating to Functional
Thinking about the functional approach and how we use it to build solutions composing functions, we can make use of lodash and its FP package. This package was built to take advantage of composable and chainable functions and implement solutions using Functional Programming.
By chaining and currying functions using pipe
and curry
methods, we are able to implement the same logic but using pure functions that are executed one after another.
import { pipe, get, reduce, map, curry } from 'lodash/fp'
const AVERAGE_SALARIES = {
junior: 7000,
mid: 10000,
senior: 13000
}
const AVERAGE_POSITIONS = {
equal: 'equal',
above: 'above',
below: 'below'
}
function appendSalaryInfo(developers) {
return pipe(
map(developer => pipe(
appendAverageSalary,
appendAveragePosition,
)(developer))
)(developers)
}
function getAveragePosition(developer) {
const { averageSalary, level } = developer
const averageSalaryReference = get(level, AVERAGE_SALARIES)
if(averageSalary === averageSalaryReference) {
return AVERAGE_POSITIONS.equal
} else if(averageSalary > averageSalaryReference) {
return AVERAGE_POSITIONS.above
} else {
return AVERAGE_POSITIONS.below
}
}
function calculateAverageSalary(developer) {
const earnings = get('earnings', developer)
return pipe(
reduce((result, { amount }) => result += amount, 0),
curry(calculateAverage)(earnings.length)
)(earnings)
}
function calculateAverage(length, total) {
return total / length
}
function appendAverageSalary(developer) {
const averageSalary = calculateAverageSalary(developer)
return {
...developer,
averageSalary
}
}
function appendAveragePosition(developer) {
const averagePosition = getAveragePosition(developer)
return {
...developer,
averagePosition
}
}
Notice that I have taken the chance and refactored the references for constant variables as well.
The whole refactoring made the code more maintainable and readable and, besides that, made it much easier to implement tests that guarantee the functions are returning the correct result.
The code above also implements immutability, one of the key principles of Functional Programming, avoiding variables' states to be changed during the execution. Every function that manipulates the developer
variable, return new values.
With this approach, no matter what the inputs are, the respective outputs will always be the same.
And, last but not least, by using reduce
function, the code takes advantage of parallelism which increases performance.
The whole code can be found on this gist
I hope you liked it! Please, comment and share.
Cover image by: @kellysikkema
Top comments (0)