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


Upcoming events

  • The Test Automation Meetup

    PLEASE RSVP SO THAT WE KNOW HOW MUCH FOOD WE WILL NEED Test automation is a cornerstone of effective software development. It's about creating robust, predictable test suites that enhance quality and reliability. By diving into automation, you're architecting systems that ensure consistency and catch issues early. This expertise not only improves the development process but also broadens your skillset, making you a more versatile team member. Whether you're a developer looking to enhance your testing skills or a QA professional aiming to dive deeper into automation, RSVP for an evening of learning, delicious food, and the fusion of coding and quality assurance! 🚀🚀 18:00 – 🚪 Doors open to the public 18:15 – 🍕 Let’s eat 19:00 – 📢 First round of Talks 19:45 – 🍹 Small break 20:00 – 📢 Second round of Talks 20:45 – 🍻 Drinks 21:00 – 🙋‍♀️ See you next time? First Round of Talks: The Power of Cross-browser Component Testing - Clarke Verdel, SR. Front-end Developer at iO How can you use Component Testing to ensure consistency cross-browser? Second Round of Talks: Omg who wrote this **** code!? - Erwin Heitzman, SR. Test Automation Engineer at Rabobank How can tests help you and your team? Beyond the Unit Test - Christian Würthner, SR. Android Developer at iO How can you do advanced automated testing for, for instance, biometrics? RSVP now to secure your spot, and let's explore the fascinating world of test automation together!

    | Coven of Wisdom - Amsterdam

    Go to page for The Test Automation Meetup
  • Coven of Wisdom - Herentals - Winter `24 edition

    Worstelen jij en je team met automated testing en performance? Kom naar onze meetup waar ervaren sprekers hun inzichten en ervaringen delen over het bouwen van robuuste en efficiënte applicaties. Schrijf je in voor een avond vol kennis, heerlijk eten en een mix van creativiteit en technologie! 🚀 18:00 – 🚪 Deuren open 18:15 – 🍕 Food & drinks 19:00 – 📢 Talk 1 20:00 – 🍹 Kleine pauze 20:15 – 📢 Talk 2 21:00 – 🙋‍♀️ Drinks 22:00 – 🍻 Tot de volgende keer? Tijdens deze meetup gaan we dieper in op automated testing en performance. Onze sprekers delen heel wat praktische inzichten en ervaringen. Ze vertellen je hoe je effectieve geautomatiseerde tests kunt schrijven en onderhouden, en hoe je de prestaties van je applicatie kunt optimaliseren. Houd onze updates in de gaten voor meer informatie over de sprekers en hun specifieke onderwerpen. Over iO Wij zijn iO: een groeiend team van experts die end-to-end-diensten aanbieden voor communicatie en digitale transformatie. We denken groot en werken lokaal. Aan strategie, creatie, content, marketing en technologie. In nauwe samenwerking met onze klanten om hun merken te versterken, hun digitale systemen te verbeteren en hun toekomstbestendige groei veilig te stellen. We helpen klanten niet alleen hun zakelijke doelen te bereiken. Samen verkennen en benutten we de eindeloze mogelijkheden die markten in constante verandering bieden. De springplank voor die visie is talent. Onze campus is onze broedplaats voor innovatie, die een omgeving creëert die talent de ruimte en stimulans geeft die het nodig heeft om te ontkiemen, te ontwikkelen en te floreren. Want werken aan de infinite opportunities van morgen, dat doen we vandaag.

    | Coven of Wisdom Herentals

    Go to page for Coven of Wisdom - Herentals - Winter `24 edition
  • Mastering Event-Driven Design

    PLEASE RSVP SO THAT WE KNOW HOW MUCH FOOD WE WILL NEED Are you and your team struggling with event-driven microservices? Join us for a meetup with Mehmet Akif Tütüncü, a senior software engineer, who has given multiple great talks so far and Allard Buijze founder of CTO and founder of AxonIQ, who built the fundaments of the Axon Framework. RSVP for an evening of learning, delicious food, and the fusion of creativity and tech! 🚀 18:00 – 🚪 Doors open to the public 18:15 – 🍕 Let’s eat 19:00 – 📢 Getting Your Axe On Event Sourcing with Axon Framework 20:00 – 🍹 Small break 20:15 – 📢 Event-Driven Microservices - Beyond the Fairy Tale 21:00 – 🙋‍♀️ drinks 22:00 – 🍻 See you next time? Details: Getting Your Axe On - Event Sourcing with Axon Framework In this presentation, we will explore the basics of event-driven architecture using Axon Framework. We'll start by explaining key concepts such as Event Sourcing and Command Query Responsibility Segregation (CQRS), and how they can improve the scalability and maintainability of modern applications. You will learn what Axon Framework is, how it simplifies implementing these patterns, and see hands-on examples of setting up a project with Axon Framework and Spring Boot. Whether you are new to these concepts or looking to understand them more, this session will provide practical insights and tools to help you build resilient and efficient applications. Event-Driven Microservices - Beyond the Fairy Tale Our applications need to be faster, better, bigger, smarter, and more enjoyable to meet our demanding end-users needs. In recent years, the way we build, run, and operate our software has changed significantly. We use scalable platforms to deploy and manage our applications. Instead of big monolithic deployment applications, we now deploy small, functionally consistent components as microservices. Problem. Solved. Right? Unfortunately, for most of us, microservices, and especially their event-driven variants, do not deliver on the beautiful, fairy-tale-like promises that surround them.In this session, Allard will share a different take on microservices. We will see that not much has changed in how we build software, which is why so many “microservices projects” fail nowadays. What lessons can we learn from concepts like DDD, CQRS, and Event Sourcing to help manage the complexity of our systems? He will also show how message-driven communication allows us to focus on finding the boundaries of functionally cohesive components, which we can evolve into microservices should the need arise.

    | Coven of Wisdom - Utrecht

    Go to page for Mastering Event-Driven Design

Share