After a long time learning and working with object-oriented programming, I took a step back to think about system complexity.
“Complexity is anything that makes software hard to understand or to modify.“?—?John Outerhout
Doing some research, I found functional programming concepts like immutability and pure function. Those concepts are big advantages to build side-effect-free functions, so it is easier to maintain systems?—?with some other benefits.
What is functional programming?
Functional programming is a programming paradigm?—?a style of building the structure and elements of computer programs?—?that treats computation as the evaluation of mathematical functions and avoids changing-state and mutable data?—?
The first fundamental concept we learn when we want to understand functional programming is pure functions. But what does that really mean? What makes a function pure?
So how do we know if a function is
pure or not? Here is a very strict definition of purity:
- It returns the same result if given the same arguments (it is also referred as
- It does not cause any observable side effects
It returns the same result if given the same arguments
Imagine we want to implement a function that calculates the area of a circle. An impure function would receive
radius as the parameter, and then calculate
radius * radius * PI:
Why is this an impure function? Simply because it uses a global object that was not passed as a parameter to the function.
Now imagine some mathematicians argue that the
PI value is actually
42and change the value of the global object.
Our impure function will now result in
10 * 10 * 42 =
4200. For the same parameter (
radius = 10), we have a different result. Let’s fix it!
TA-DA ?! Now we’ll always pass the
PI value as a parameter to the function. So now we are just accessing parameters passed to the function. No
- For the parameters
radius = 10&
PI = 3.14, we will always have the same the result:
- For the parameters
radius = 10&
PI = 42, we will always have the same the result:
If our function reads external files, it’s not a pure function?—?the file’s contents can change.
Random number generation
Any function that relies on a random number generator cannot be pure.
It does not cause any observable side effects
Examples of observable side effects include modifying a global object or a parameter passed by reference.
Now we want to implement a function to receive an integer value and return the value increased by 1.
We have the
counter value. Our impure function receives that value and re-assigns the counter with the value increased by 1.
Observation: mutability is discouraged in functional programming.
We are modifying the global object. But how would we make it
pure? Just return the value increased by 1. Simple as that.
See that our pure function
increaseCounter returns 2, but the
counter value is still the same. The function returns the incremented value without altering the value of the variable.
If we follow these two simple rules, it gets easier to understand our programs. Now every function is isolated and unable to impact other parts of our system.
Pure functions are stable, consistent, and predictable. Given the same parameters, pure functions will always return the same result. We don’t need to think of situations when the same parameter has different results?—?because it will never happen.
Pure functions benefits
The code’s definitely easier to test. We don’t need to mock anything. So we can unit test pure functions with different contexts:
- Given a parameter
A? expect the function to return value
- Given a parameter
C? expect the function to return value
A simple example would be a function to receive a collection of numbers and expect it to increment each element of this collection.
We receive the
numbers array, use
map incrementing each number, and return a new list of incremented numbers.
[1, 2, 3, 4, 5], the expected
output would be
[2, 3, 4, 5, 6].
Unchanging over time or unable to be changed.
When data is immutable, its state cannot change after it’s created. If you want to change an immutable object, you can’t. Instead, you create a new object with the new value.
for loop. This next
for statement has some mutable variables.
For each iteration, we are changing the
i and the
sumOfValue state. But how do we handle mutability in iteration? Recursion!
So here we have the
sum function that receives a vector of numerical values. The function calls itself until we get the list empty (our recursion
base case). For each “iteration” we will add the value to the
With recursion, we keep our variables immutable. The
list and the
accumulator variables are not changed. It keeps the same value.
Observation: Yes! We can use
reduce to implement this function. We will cover this in the
Higher Order Functions topic.
It is also very common to build up the final state of an object. Imagine we have a string, and we want to transform this string into a
In OOP in Ruby, we would create a class, let’s say,
UrlSlugify. And this class will have a
slugify! method to transform the string input into a
Beautiful! It’s implemented! Here we have imperative programming saying exactly what we want to do in each
slugify process?—?first lower case, then remove useless white spaces and, finally, replace remaining white spaces with hyphens.
But we are mutating the input state in this process.
We can handle this mutation by doing function composition, or function chaining. In other words, the result of a function will be used as an input for the next function, without modifying the original input string.
Here we have:
toLowerCase: converts the string to all lower case
trim: removes whitespace from both ends of a string
join: replaces all instances of match with replacement in a given string
We combine all these 4 functions and we can
"slugify" our string.
Let’s implement a
This pure function will always have the same output, given the same input.
2 as a parameter of the
square function will always returns 4. So now we can replace the
square(2) with 4. That’s it! Our function is
Basically, if a function consistently yields the same result for the same input, it is referentially transparent.
pure functions + immutable data = referential transparency
With this concept, a cool thing we can do is to memoize the function. Imagine we have this function:
And we call it with these parameters:
sum(5, 8) equals
13. This function will always result in
13. So we can do this:
And this expression will always result in
16. We can replace the entire expression with a numerical constant and memoize it.
Functions as first-class entities
The idea of functions as first-class entities is that functions are also treated as values and used as data.
Functions as first-class entities can:
- refer to it from constants and variables
- pass it as a parameter to other functions
- return it as result from other functions
The idea is to treat functions as values and pass functions like data. This way we can combine different functions to create new functions with new behavior.
Imagine we have a function that sums two values and then doubles the value. Something like this:
Now a function that subtracts values and the returns the double:
These functions have similar logic, but the difference is the operators functions. If we can treat functions as values and pass these as arguments, we can build a function that receives the operator function and use it inside our function. Let’s build it!
Done! Now we have an
f argument, and use it to process
b. We passed the
subtraction functions to compose with the
doubleOperator function and create a new behavior.
When we talk about higher-order functions, we mean a function that either:
- takes one or more functions as arguments, or
- returns a function as its result
doubleOperator function we implemented above is a higher-order function because it takes an operator function as an argument and uses it.
You’ve probably already heard about
reduce. Let’s take a look at these.
Given a collection, we want to filter by an attribute. The filter function expects a
false value to determine if the element should or should not be included in the result collection. Basically, if the callback expression is
true, the filter function will include the element in the result collection. Otherwise, it will not.
A simple example is when we have a collection of integers and we want only the even numbers.
- create an empty array
- iterate over the
- push the even numbers to the
We can also use the
filter higher order function to receive the
even function, and return a list of even numbers:
One interesting problem I solved on Hacker Rank FP Path was the Filter Array problem. The problem idea is to filter a given array of integers and output only those values that are less than a specified value
We say exactly what our function needs to do?—?iterate over the collection, compare the collection current item with
x, and push this element to the
resultArray if it pass the condition.
But we want a more declarative way to solve this problem, and using the
filter higher order function as well.
this in the
smaller function seems a bit strange in the first place, but is easy to understand.
this will be the second parameter in the
filter function. In this case,
x) is represented by
this. That’s it.