< / >
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 an array into another value using a provided function.
  • Filter: Creates a new array with all elements that pass the test implemented by the provided function.
  • Reduce: Reduces an array to a single value by 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: a callback function and an optional initial value for the accumulator.
  • The callback function passed to reduce takes four arguments: accumulator, currentValue, currentIndex (optional), and the array itself (optional). However, in most cases, you'll only need the first two arguments: accumulator and currentValue.
  • The accumulator parameter accumulates the return values of the callback function. It starts with the initial value provided (if any), or with the first element of the array if no initial value is provided.
  • The currentValue parameter represents the current element being processed in the array.

Here's a breakdown of the example:

  • accumulator starts with an initial value of 0.
  • For each element of the array, the callback function adds the current element (currentValue) to the accumulator (accumulator).
  • After iterating through all elements, reduce returns the final accumulated 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 the accumulator variable. In this context, it's an array where the indices of the two numbers adding up to the target will be stored. Initially, it's an empty array.

  • num: This represents the current element of the nums array being processed during each iteration of the reduce method.

  • index: This represents the index of the current element (num) within the nums array.

  • The fourth argument is not explicitly used in the callback function. In JavaScript, reduce provides the current array being processed as the fourth argument to the callback function, but it's not utilized in this specific example.

Here's how the code works:

  • For each element (num) in the nums array, it calculates the complement, which is the difference between the target and the current num.
  • It then checks if map (an object) has a property with the key equal to the complement. If it does, it means that the current num plus the complement (which is already seen before) equals the target. In this case, it adds the indices of the complement and the current num to the result array.
  • It then stores the current num and its index in the map object.
  • Finally, it returns the result array, which contains the indices of the two numbers that add up to the target. If no such pair is found, it returns an empty array [].

Question:

/**
 * @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 the prices array with an initial accumulator object containing minPrice initialized to Infinity and maxProfit initialized to 0.
  • For each element in the prices array, we compare it with the minPrice in the accumulator. If it's less than minPrice, we update minPrice to the current price.
  • If the difference between the current price and minPrice is greater than the maxProfit in the accumulator, we update maxProfit accordingly.
  • Finally, we return the maxProfit from the accumulator.

Question:

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 total product of all elements in the nums array using reduce().
  • Then, for each element in the nums array, we divide the total product by the current element to get the product of all elements except the current element.
  • Finally, we return the resulting array.

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: Mutating data directly can lead to unexpected behavior, especially when dealing with shared state or asynchronous code. It can introduce bugs that are hard to track down and debug.
  • Functional Programming Paradigm: Functional programming promotes immutability as a core principle. Embracing immutability leads to more predictable and easier-to-understand code, which aligns well with the functional programming paradigm.
  • Debugging and Testing: Immutable data makes debugging and testing easier since you can trust that data 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.