Benefits of functional programming

By Ravindre Ramjiawan

4 min read

Writing code in a functional way can help to solve complex problems in a efficient and in a reusable manner, for creating clean and maintainable software.

Benefits of functional programming
Authors

Table of Contents

Background

The functional programming1 paradigm2 has been around since the early 1930s. It has its roots in a mathematical theory named lambda calculus3, a notation for describing mathematical functions and programs. In lambda calculus functions are first-class citizens4, meaning functions can be used as input and returned as output. JavaScript also treats functions as first-class citizens which is the reason why we can apply functional programming in JavaScript.

Imperative vs. Declarative

The benefit of writing code in a functional way is that functional programming focuses on what should be done rather than on how it should be done. This is also known as imperative5 and declarative6 programming, where imperative does it in a procedural7 style and declarative in a functional style. It can also be described as explicit and implicit programming.

Imperative

// Focuses on how it is done
const imperativeDoubleMap = (numbers) => {
  // Create a new array to push our numbers to
  const doubledNumbers = []
  // Loop through the numbers array
  for (let i = 0; i < numbers.length; i += 1) {
    // Push the doubled number to our new array
    doubledNumbers.push(numbers[i] * 2)
  }
  // Return the doubled numbers array
  return doubledNumbers
}

// Results in our numbers being doubled
console.log(imperativeDoubleMap([2, 3, 4])) // [4, 6, 8]

Declarative

// Focuses on what should be done
// The map function loops over the numbers array for us
// and creates a new array based on what is returned
// Arrow functions have an implicit return
const declarativeDoubleMap = (numbers) => numbers.map((number) => number * 2)

// Results in our numbers being doubled
console.log(declarativeDoubleMap([2, 3, 4])) // [4, 6, 8]

As shown above, both programming paradigms yield the same result but which one is easier to understand?

Reuse

In functional programming the focus on the reuse8 of logic is a very important concept. Reusing functions avoids writing code from scratch and adheres to the DRY principle9.

Before

const imperativeDoubleMap = (numbers) => {
  const doubledNumbers = []

  for (let i = 0; i < numbers.length; i += 1) {
    // The logic for doubling numbers is inside the for loop
    // Not reusable unless you put it in a (you guessed it) function
    doubledNumbers.push(numbers[i] * 2)
  }

  return doubledNumbers
}

// The logic of doubling numbers is already inside a function which map accepts
// Meaning we can now extract it to its own reusable function
const declarativeDoubleMap = (numbers) => numbers.map((number) => number * 2)

After

// Reusable function for doubling a number
const double = (number) => number * 2

const imperativeDoubleMap = (numbers) => {
  const doubledNumbers = []

  for (let i = 0; i < numbers.length; i += 1) {
    // Logic of doubling numbers is now handled through the reusable function
    doubledNumbers.push(double(numbers[i]))
  }

  return doubledNumbers
}

// Same idea but making this a lot more concise
const declarativeDoubleMap = (numbers) => numbers.map((number) => double(number))

// As expected our numbers are still doubled
console.log(imperativeDoubleMap([2, 3, 4])) // [4, 6, 8]
console.log(declarativeDoubleMap([2, 3, 4])) // [4, 6, 8]

Point-free

Functional programming allows for the ability to write code in point-free10 programming style. This means that you can omit function arguments if they match the parameters of the function you pass it to.

const double = (number) => number * 2

// The map function expects a function that accepts a value argument
// on its first parameter position
// Since our double function does exactly that, we can just pass it in directly
// and map will call the double function for us
const declarativeDoubleMap = (numbers) => numbers.map(double)

// As expected we get the same results
console.log(declarativeDoubleMap([2, 3, 4])) // [4, 6, 8]

Higher-order function

Functional programming has the concept of higher-order functions11. This means a function that takes a function as an argument and/or returns a function as its output.

const double = (number) => number * 2

// The array function "map" is a higher-order function
// because it takes in a function as argument
const numbersMap = (numbers) => numbers.map(double)
// Multiply is a higher-order function because it returns a function
const multiply = (a) => (b) => a * b

// By calling multiply with the first argument
// we get our final function back to be used
const multiplyBy3 = multiply(3)

console.log(multiplyBy3(4)) // 12

Purity

In functional programming functions are expected to be pure12. This means a function that has no external dependencies other than its given inputs and always produces the same output. This makes pure functions deterministic13 and are easier to test.

// Pure function
const double = (number) => number * 2

// No matter how often this function is called with the same arguments
// it will always produce the same result
console.log(double(2)) // 4
console.log(double(2)) // 4
console.log(double(2)) // 4

Impure

A function that has side effects14 is called impure. This means a function that relies on external dependencies such as global variables or shared states and mutates or produces randomized values. This makes impure functions nondeterministic15 and are harder to test. Side effects can lead to unexpected results, frustrations, and bugs.

// Some global state
const globalState = { number: 0 }

// Impure function
const double = (number) => (globalState.number += number * 2)

// No matter how often this function is called with the same arguments
// it will always produce a different result
console.log(double(2)) // 4
console.log(double(2)) // 8
console.log(double(2)) // 12

Referential Transparency

Another positive aspect of functional programming are referentially transparent16 functions. This means that the output of a function can be replaced by its value directly without having any influence on the program. This is only possible if a function is pure. This allows for compilers17 to perform code optimizations18 by swapping out function calls with their return values.

// A pure function that is also referentially transparent
const add = (a, b) => a + b

const something = () => add(1, 2) + 5
// A compiler could swap out the add function call for its return value
const something2 = () => 3 + 5

// Both functions return the same output
console.log(something()) // 8
console.log(something2()) // 8

Recursion

Another benefit of functional programming is a concept called recursion19. This means a function that keeps calling itself until the desired result is achieved. Recursion is a form of looping also known as the recursive loop.

// Factorial
const factorial = (number) => {
  // Base case
  if (number < 2) return 1
  // Here we call ourselves to enter recursion
  // Until the base case is hit it will keep calling itself
  // Hence it being called the recursive loop
  return number * factorial(number - 1)
}

console.log(factorial(10)) // 3628800

Memoization

Functional programming allows for a technique called memoization20. Since functions are expected to be pure and therefore deterministic we can cache the output of computationally expensive function calls.

// The memoize function will normally be provided
// by a functional programming library
// Memoize is a higher-order function since it accepts a function as argument
// and also returns a function as output
const memoize = (func) => {
  // The cache is simply an object which will be populated by key value pairs
  const cache = {}
  // Return a function that can accept any amount of arguments
  return (...args) => {
    // To create a unique key we simply turn them into a string
    const key = JSON.stringify(args)
    // If the key exists in the cache we take can return value
    if (cache[key]) return cache[key]
    // Otherwise we call the function and store its output in the cache
    return (cache[key] = func(args))
  }
}

// Function to measure execution time
// measureTime is also a higher order function just like memoize
const measureTime = (func) => {
  return (...args) => {
    // Start track of time
    const startTime = performance.now()
    // Execute function
    const result = func(args)
    // End track of time
    const endTime = performance.now()
    // Log the difference in time taken
    console.log(`Time taken: ${endTime - startTime} milliseconds`)
    return result
  }
}

// Fibonacci sequence
const fibonacci = (number) => {
  // Base case
  if (number < 2) return number
  // Fibonacci enters recursion twice
  return fibonacci(number - 1) + fibonacci(number - 2)
}

// Memoize our fibonacci function
const memoizedFibonacci = measureTime(memoize(fibonacci))

// On our first call fibonacci has to be calculated
// Subsequent calls with the same arguments will not recalculate fibonacci
// since it will return the already calculated value from the cache
console.log(memoizedFibonacci(40)) // 102334155 - Time taken: 700 milliseconds
console.log(memoizedFibonacci(40)) // 102334155 - Time taken: 0 milliseconds
console.log(memoizedFibonacci(40)) // 102334155 - Time taken: 0 milliseconds

Intermediate values

Functional programming enables us to eliminate intermediate values. An intermediate value is often a temporary variable created to store partial results for the next step in a process leading up to the final output. Often these intermediate values are not used further in the program and just add syntactic noise21 to the code.

Before

const double = (number) => number * 2
const square = (number) => number * number
const subtract = (number) => number - 1

const calculateNumber = (number) => {
  // As you can see we are storing each part of the process in a temporary variable
  // To be used by the next step in the process
  const doubledNumber = double(number)
  const squaredNumber = square(doubledNumber)
  const finalResult = subtract(squaredNumber)
  return finalResult
}

console.log(calculateNumber(4)) // 63

After

const double = (number) => number * 2
const square = (number) => number * number
const subtract = (number) => number - 1

// Instead we can let the intermediate results be represented by the inputs and outputs of functions instead
// by passing functions to each other producing the same result but more succinct
const calculateNumber = (number) => subtract(square(double(number)))

console.log(calculateNumber(4)) // 63

This demonstrates the flow of intermediate values going from input to output from one function to the next. This is also known as function composition22. The downside is that this can quickly become unreadable if we keep nesting23 more functions.

Pipe & Compose

In functional programming there is the concept of piping and composing. Pipe executes functions in succession from left to right. This allows for a natural reading experience (at least in the western hemisphere) when trying to follow the execution24 flow. Compose executes functions in succession from right to left. Compose reflects exactly how functions would execute if you would not use compose at all, namely from the innermost function to the outermost function as seen in the previous example.

Pipe

// The pipe function will normally be provided by a functional programming library
// Pipe is a higher-order function
// Pipe accepts any amount of functions which will return a function that accepts a value
// By using reduce we accumulate the outputs of function starting from the left
// and pass to the next function to the right
// Until the last function is called
const pipe =
  (...funcs) =>
  (value) =>
    funcs.reduce((currentValue, func) => func(currentValue), value)
const double = (number) => number * 2
const square = (number) => number * number
const subtract = (number) => number - 1

// Pipe flows from left to right
const calculateNumber = pipe(double, square, subtract)

console.log(calculateNumber(4)) // 63

Compose

// The compose function will normally be provided by a functional programming library
// Compose is a higher-order function
// Compose accepts any amount of functions which will return a function that accepts a value
// By using reduceRight we accumulate the outputs of function starting from the right
// and pass to the next function to the left
// Until the first function is called
const compose =
  (...funcs) =>
  (value) =>
    funcs.reduceRight((currentValue, func) => func(currentValue), value)
const double = (number) => number * 2
const square = (number) => number * number
const subtract = (number) => number - 1

// Compose flows from right to left
const calculateNumber = compose(double, square, subtract)

console.log(calculateNumber(4)) // 18

As shown, this allows us to have the benefits of function composition and not sacrifice any readability.

Curry

Another benefit of functional programming is a concept called currying25. Currying can be useful when you want to create a new function that has some arguments already fixed, or when you want to pass a function with a smaller number of arguments to another function that expects a function with a larger number of arguments.

// The curry function will normally be provided by a functional programming library
// Curry is a higher-order function and keeps returning functions until the final
// argument is passed
const curry = (func) =>
  function curried(...args) {
    if (args.length >= func.length) return func(...args)
    return (...moreArgs) => curried(...args, ...moreArgs)
  }

// Add function with multiple arguments
const add = (a, b, c) => a + b + c

// Curried version of add
const curriedAdd = curry(add)

console.log(curriedAdd(1)(2)(3)) // 6
console.log(curriedAdd(1, 2)(3)) // 6
console.log(curriedAdd(1)(2, 3)) // 6
console.log(curriedAdd(1, 2, 3)) // 6

Conclusion

I hope this gives a better understanding of how functional programming can help reduce code complexity in a code base and allow for reliable, efficient, reusable, and maintainable code. For further insight on the topic, I highly recommend checking out some popular functional programming libraries such as Lodash or Ramda which provide a large number of utility functions that are optimized for performance and conciseness. More information about the theoretical side of functional programming can be found by checking out this website.

Footnotes

  1. https://en.wikipedia.org/wiki/Functional_programming

  2. https://en.wikipedia.org/wiki/Programming_paradigm

  3. https://en.wikipedia.org/wiki/Lambda_calculus

  4. https://en.wikipedia.org/wiki/First-class_citizen

  5. https://en.wikipedia.org/wiki/Imperative_programming

  6. https://en.wikipedia.org/wiki/Declarative_programming

  7. https://en.wikipedia.org/wiki/Procedural_programming

  8. https://en.wikipedia.org/wiki/Code_reuse

  9. https://en.wikipedia.org/wiki/Don%27t_repeat_yourself

  10. https://en.wikipedia.org/wiki/Tacit_programming

  11. https://en.wikipedia.org/wiki/Higher-order_function

  12. https://en.wikipedia.org/wiki/Pure_function

  13. https://en.wikipedia.org/wiki/Deterministic_algorithm

  14. https://en.wikipedia.org/wiki/Side_effect_(computer_science)

  15. https://en.wikipedia.org/wiki/Nondeterministic_algorithm

  16. https://en.wikipedia.org/wiki/Referential_transparency

  17. https://en.wikipedia.org/wiki/Compiler

  18. https://en.wikipedia.org/wiki/Program_optimization

  19. https://en.wikipedia.org/wiki/Recursion_(computer_science)

  20. https://en.wikipedia.org/wiki/Memoization

  21. https://en.wikipedia.org/wiki/Syntactic_noise

  22. https://en.wikipedia.org/wiki/Function_composition_(computer_science)

  23. https://en.wikipedia.org/wiki/Nested_function

  24. https://en.wikipedia.org/wiki/Execution_(computing)

  25. https://en.wikipedia.org/wiki/Currying


Share