- Published on
Functional Programming
Functional programming in JavaScript emphasizes immutability, utilizing pure functions, and leveraging higher-order functions like map, filter, and reduce for concise and declarative code.
Table of Contents
Immutability
Immutability refers to the principle of not changing the state of data once it's created. In JavaScript, this means not modifying objects or arrays directly but instead creating new ones with the desired changes.
Example:
// Mutable approach (not recommended)
let mutableArray = [1, 2, 3]
mutableArray.push(4) // Modifies original array
console.log(mutableArray) // Output: [1, 2, 3, 4]
// Immutable approach (recommended)
let immutableArray = [1, 2, 3]
let newArray = [...immutableArray, 4] // Create a new array with the desired change
console.log(newArray) // Output: [1, 2, 3, 4]
console.log(immutableArray) // Output: [1, 2, 3] (original array remains unchanged)
Pure Functions
Pure functions are functions that, given the same input, will always return the same output and have no side effects. They don't modify variables outside their scope or rely on external state.
Example:
// Impure function (not pure)
let counter = 0
function impureAdd(x) {
counter++ // Modifies external state
return x + counter
}
console.log(impureAdd(5)) // Output: 6
console.log(impureAdd(5)) // Output: 7 (side effect: counter incremented)
// Pure function
function pureAdd(x, y) {
return x + y // No side effects, always returns the same result for the same inputs
}
console.log(pureAdd(5, 3)) // Output: 8
console.log(pureAdd(5, 3)) // Output: 8 (no side effects)
Map, Filter, Reduce
These are higher-order functions commonly used in functional programming to operate on arrays and produce a result without mutating the original array.
Map: Transforms each element of anarray into another valueusing a provided function.Filter: Creates anew arraywith all elements that pass the test implemented by the provided function.Reduce: Reduces an array to asingle valueby applying a function to each element and accumulating the result.
Example:
// Map
const numbers = [1, 2, 3, 4]
const doubled = numbers.map((num) => num * 2)
console.log(doubled) // Output: [2, 4, 6, 8]
// Filter
const evenNumbers = numbers.filter((num) => num % 2 === 0)
console.log(evenNumbers) // Output: [2, 4]
// Reduce
const sum = numbers.reduce((acc, curr) => acc + curr, 0)
console.log(sum) // Output: 10 (1 + 2 + 3 + 4)
Reduce
reduce is a higher-order function typically employed to transform a list of values into a single value. It iterates through each element of the list, applies a specified function to pair them together, and accumulates the results.
const array = [1, 2, 3, 4, 5]
// Using reduce to sum up the array elements
const sum = array.reduce((accumulator, currentValue) => accumulator + currentValue, 0)
console.log(sum) // Output: 15
array.reduce()takes two arguments: acallback functionand an optionalinitial valuefor theaccumulator.- The callback function passed to
reducetakesfour arguments:accumulator,currentValue,currentIndex(optional), and thearray itself(optional). However, in most cases, you'll only need the first two arguments:accumulatorandcurrentValue. - The
accumulatorparameteraccumulatesthereturn valuesof the callback function. It starts with theinitial valueprovided (if any), or with thefirst elementof thearrayif no initial value is provided. - The
currentValueparameter represents thecurrentelement being processed in the array.
Here's a breakdown of the example:
accumulatorstarts with an initial value of 0.- For each
elementof the array, thecallbackfunction adds thecurrentelement (currentValue) to theaccumulator(accumulator). - After iterating through all elements,
reducereturns thefinalaccumulated value.
// Flattening an Array of Arrays
const arrays = [
[1, 2],
[3, 4],
[5, 6],
]
const flattenedArray = arrays.reduce((accumulator, currentValue) => {
return accumulator.concat(currentValue)
}, [])
console.log(flattenedArray) // Output: [1, 2, 3, 4, 5, 6]
// Counting occurrences of elements in an array
const words = ['apple', 'banana', 'apple', 'orange', 'banana', 'apple']
const wordCount = words.reduce((acc, word) => {
acc[word] = (acc[word] || 0) + 1
return acc
}, {})
console.log(wordCount)
// Output: { apple: 3, banana: 2, orange: 1 }
// Here `acc` is an `object` where each key represents a `unique` word,
// and the value represents the `count` of occurrences of that word.
// If the word doesn't exist in the accumulator yet, it initializes its count to 0 before incrementing.
// Transforming an array of objects
const people = [
{ name: 'Kumar', age: 30 },
{ name: 'Rajnish', age: 25 },
{ name: 'Rahul', age: 35 },
]
const peopleDetails = people.reduce(
(acc, person) => {
acc.names.push(person.name)
acc.totalAge += person.age
return acc
},
{ names: [], totalAge: 0 }
)
console.log(peopleDetails)
// Output: { names: ['Kumar', 'Rajnish', 'Rahul'], totalAge: 90 }
// Here reduce is utilized to transform an array of objects
// containing name and age properties into a single object
// containing an array of names (names) and the total age (totalAge) of all people.
// The initial value of the accumulator is an object with empty names array and totalAge set to 0.
Question:
Given an array of integers nums and an integer target, return indices of the two numbers such that they add up to target.
You may assume that each input would have exactly one solution, and you may not use the same element twice.
You can return the answer in any order.
const twoSum = function (nums, target) {
const map = {}
return nums.reduce((result, num, index) => {
const complement = target - num
if (map.hasOwnProperty(complement)) {
result.push(map[complement], index)
}
map[num] = index
return result
}, [])
}
Here's a breakdown of the example:
-
result: This is theaccumulatorvariable. In this context, it's an array where the indices of the two numbersaddingup to thetargetwill be stored.Initially, it's anemptyarray. -
num: This represents thecurrent elementof the numsarraybeing processed during each iteration of thereducemethod. -
index: This represents theindexof thecurrent element(num) within the nums array. -
The
fourth argumentis not explicitly used in thecallback function. In JavaScript,reduceprovides thecurrent arraybeing processed as thefourthargument to the callback function, but it's not utilized in this specific example.
Here's how the code works:
- For each
element(num) in thenumsarray, it calculates thecomplement, which is thedifferencebetween thetargetand thecurrentnum. - It then
checksifmap(an object) has apropertywith thekeyequal to thecomplement. If it does, it means that thecurrentnum plus thecomplement(which is already seen before) equals thetarget. In this case, itaddstheindicesof thecomplementand thecurrentnum to theresultarray. - It then
storesthecurrentnum and itsindexin themapobject. - Finally, it
returnstheresultarray, whichcontainstheindicesof thetwo numbersthat add up to thetarget. If no such pair is found, it returns anempty array[].
/**
* @param {number[]} prices
* @return {number}
*/
var maxProfit = function (prices) {
return prices.reduce(
(maxProfit, currentPrice) => {
if (currentPrice < maxProfit.minPrice) {
maxProfit.minPrice = currentPrice
} else if (currentPrice - maxProfit.minPrice > maxProfit.maxProfit) {
maxProfit.maxProfit = currentPrice - maxProfit.minPrice
}
return maxProfit
},
{ minPrice: Infinity, maxProfit: 0 }
).maxProfit
}
Explanation:
- We use the
reduce()function on thepricesarray with an initialaccumulatorobject containingminPriceinitialized toInfinityandmaxProfitinitialized to 0. - For each element in the
pricesarray, we compare it with theminPricein theaccumulator. If it'slessthanminPrice, we updateminPriceto thecurrentprice. - If the
differencebetween thecurrentprice andminPriceisgreaterthan themaxProfitin theaccumulator, we updatemaxProfitaccordingly. - Finally, we return the
maxProfitfrom theaccumulator.
function productExceptSelf(nums) {
const totalProduct = nums.reduce((acc, num) => acc * num, 1)
return nums.map((num) => totalProduct / num)
}
// Example usage
const nums = [1, 2, 3, 4]
console.log(productExceptSelf(nums)) // Output: [24, 12, 8, 6]
Explanation:
- We first calculate the
totalproduct of allelementsin thenumsarray usingreduce(). - Then, for each
elementin thenumsarray, wedividethetotalproduct by thecurrentelement to get theproductof allelementsexcept thecurrentelement. - Finally, we return the
resultingarray.
Why Mutable approach (not recommended)
In JavaScript, mutability can lead to unintended side effects, especially in complex applications or when working in a team environment. Here's why mutating data directly is often discouraged:
Unintended Side Effects:Mutatingdata directly can lead to unexpected behavior, especially when dealing with sharedstate or asynchronous code. It can introduce bugs that are hard to track down and debug.Functional Programming Paradigm: Functional programming promotesimmutabilityas a core principle. Embracingimmutabilityleads to more predictable and easier-to-understand code, which aligns well with the functional programming paradigm.Debugging and Testing:Immutabledata makes debugging and testingeasiersince you can trust thatdata won't change unexpectedly. Testing pure functions and immutable data structures tends to be simpler and more reliable.
While mutability may offer performance benefits in some cases, modern JavaScript engines are optimized to handle immutable data efficiently, and the performance trade-off is often negligible compared to the benefits gained in code maintainability and reliability.