Introduction to Functional Programming

The first programming paradigm that most programmers in the 21st century learn is probably Object Oriented Programming (or OOP for short). Object Oriented Programming allows us to transform real-world entities into highly abstract models with descriptive properties and executable methods to simulate their behaviors in the real world. Despite its numerous advantages, such as readability, reusability, and maintainability, it also introduces immense complexity to state management. The difficulty in keeping track of the states of objects has led head-scratching exhausted programmers to churn out error-prone code and cause bugs that would take hours, or even days, to locate and fix. “Is there a way out of this undesirable situation,” you might ask. Here is the good news: a lesser-known yet widely-used programming paradigm called Functional Programming (or FP for short) can help turn the tide around and decrease the number of bugs we cause. In this article, I will explain what Functional Programming is and why it can help us avoid bugs.

Functional Programming: Pure as Gold, Buggy no More

Functional programming is a programming paradigm that seeks to achieve the following two goals:

  1. To ensure inputs and outputs always have a deterministic relation
  2. To eliminate side effects including state mutations and I/O streams

The ultimate motivation behind this deterministic and side-effect-free approach to programming is to make it easier to reason about code and harder to introduce bugs. No unpleasant surprises will ever come our way if the same inputs always generate the same outputs. We also do not have to keep track of how each state changes in value at different stages if we rid our codebase of side effects.

Functional programming makes heavy use of pure functions to achieve the goals mentioned above. Pure functions have the following two properties:

1. Pure functions always return the same values for the same arguments

Having such a deterministic mapping between inputs and outputs brings about an obvious advantage: we can always count on pure functions to return our expected results. Such certainty prevents us from causing bugs that result from functions returning unexpected results.

2. Pure functions do not produce any side effects

The no-side-effect rule stipulates that each pure function should not cause changes outside its scope. This rule helps eliminate external state modifications made by non-pure functions, which constitute another great source of bugs because the modifications non-pure functions make are less conspicuous and can easily escape our attention.

Now let’s look at some examples of non-pure functions and pure functions.

Non-Pure Functions

num1 = 5
def add(num2: int) -> int:
    global num1
    return num1 + num2

# First execution
print(add(3)) # returns 8
num1 = 3
# Second execution
print(add(3)) # returns 6

The add function in the above code example is not pure because it does not consistently produce the same outputs given the same inputs.

product = 0
def multiply_by_two(num: int) -> int:
    global product
    product = num * 2
    return product

# First execution
print(multiply_by_two(3)) # returns 6
print(product)
# Second execution
print(multiply_by_two(5)) # returns 10
print(product)
# Third execution
print(multiply_by_two(3)) # returns 6
print(product)

The multiply_by_two function in this code example does return the same outputs given the same inputs. However, it is still not a pure function because it modifies the product variable in each invocation, which violates the no side-effect rule.

Pure Functions

def add_five(num2: int) -> int:
    return num2 + 5

# First execution
print(add(3)) # returns 8
# Second execution
print(add(3)) # returns 8

Unlike the previous add function, the add_five function here is pure because the mapping between inputs and outputs is deterministic.

def multiply_by_two(num: int) -> int:
    return num * 2

# First execution
print(multiply_by_two(3)) # returns 6
print(product)
# Second execution
print(multiply_by_two(3)) # returns 6
print(product)

In this code example, the multiply_by_two function is pure. It always returns the same outputs given the same inputs. Also, it does not have any side effects.

Disclaimer:

One thing to note: you do not have to give up using other programming paradigms, such as Objective Oriented Programming, to take advantage of Functional Programming. Please feel free to combine functional programming with other paradigms however you see fit!

Summary

In this article, I briefly introduced what Functional Programming is and how it makes use of pure functions. I also explained why using pure functions in Functional Programming can help us avoid creating bugs unknowingly. I hope this article has helped you kick-start your functional programming.