Functional JavaScript
There are many programming paradigms and JavaScript is flexible enough to offer most of them. Programming paradigms are often related to the way we read and modify the state of an application.
The paradigms we have seen so far are:
-
Procedural Programming: Which is specifying steps that a computer should take to reach a desired state.
-
Object-oriented Programming (Often abbreviated OOP): Which is organizing program elements as objects with data/state properties and methods for interactions.
The third programming paradigm is one that has been getting a lot of attention lately: Functional Programming.
Functional Programming
Functional programming is simply programming relying entirely on pure functions and avoiding any shared state. This makes the code easier to understand because functional programming is declarative rather than imperative.
When you need to perform any task, there is always the what and the how aspects of it: what exactly needs to be done and how will it be done.
Imperative programming is about the how. Declarative programming is about the what.
An imperative approach represents a list of steps. Do this first, then do that, and after that do something else. For example: Go over a list of numbers one by one and for every one add its value to a running sum.
A declarative approach represents what we have and what we need. For example: We have a list of numbers and we need the sum of those numbers. The imperative language is closer to the computers of today because they only know how to execute instructions. The declarative language is closer to how we think and command. Get it done, please. Somehow!
The good news is computer languages have evolved to offer us declarative ways to do the needed imperative computer instructions.
Note that coding does not have to be one way or the other. Any non-trivial computer program will most likely have a little bit of both approaches. Also, knowing how to code declaratively is great, but it does not mean that you do not need to learn the imperative ways as well. You should simply be confident using both.
JavaScript can be used to program with any of the three programming paradigms mentioned above. This is one of the many reasons why JavaScript is popular.
In this article, we will focus on three of the most popular functional programming methods: map, filter, and reduce.
However, to understand these basic methods, we need to first understand the two new terms that the functional program definition used above.
Pure Functions
A pure function is a function that returns the exact same output when executed with the same input. This means that a pure function cannot depend on any state and cannot perform any side-effects.
Here is an example of a pure function:
function pure1(a, b) { return a + b; }
Here are a few examples of non-pure functions:
// Output will be different every run function np1(a, b) { return a + b + Date.now(); } // Perform a side effect on the global answer variable function np2() { window.answer = 42; }
A pure function represents a simple map between input values and return values. Replacing a pure function call with the return value it maps to will have no effect on the code.
Programming using pure functions has many advantages, but perhaps the most important one is avoiding shared mutable state.
Shared State
Every time you create a variable you are basically starting a state. This state is shared with everything that is defined in the same scope for that variable.
When multiple sections of the code need to access and change the same state, the program becomes more susceptible to bugs. For example, there can easily be cases of race conditions.
A race condition happens when some shared state is depending on a sequence of asynchronous events. You should write code that avoids race conditions and functional programming is one way to do that very easily.
Functional programming has many other concepts but you can focus on just these for now. To taste the flavor of functional programming, we will explore three new methods that we can use with collections. Even if you are not convinced about functional programming as a paradigm, using these three methods will make your code a bit more concise and readable than any procedural-programming alternative.
Map/Filter/Reduce
The best way to understand these three methods is to compare examples of the same feature written with and without them.
Let’s use simple operations on arrays to compare. We will start with map
.
Let’s say you were asked to write a program that accepts a list of numbers, for example, [4, 12, 9, 5
]. The program should return a matching list of these numbers' square values, which in this case are [16, 124, 81, 25
].
We can implement this with a simple forEach
loop:
function squareNumbersInList(list) { const newList = []; list.forEach(number => newList.push(number * number) ); return newList; }
We can also implement it with a simple map
call:
function squareNumbersInList(list) { return list.map(number => number * number); }
This is a very simple example but it is clear that the map
version is a lot more concise and readable than the forEach
version. In the map version, we did not need to use a different array (newList
) and we did not need to use the push method.
A more important difference between these two solutions is that the first one represents a list of manual instructions (imperative) while the second simply represents a desire (declarative).
When we call the map
method (or the filter/reduce
methods) on a collection, a new collection is created and returned from that call. The map/filter/reduce
functions receive a single argument that we call a handler or a callback function.
This callback function should be a pure one; it should not mutate anything. The callback function we used for the squareNumbersInList
example above was:
number => number * number
The callback function is invoked for each item in the collection. Its input and output are used differently among the three functional methods.
-
For
map
, the callback receives a single argument that gets passed as the current iteration element. The returned value of that callback function determines what goes into the resulting collection in the exact same position as the current iteration element. The result of mapping a collection is always a new collection with the same number of elements. -
For
filter
, the callback receives a single argument that gets passed as the current iteration element. This callback needs to return a Boolean expression. If that expression is false, the current iteration element will be filtered out of the resulting collection. If the expression is true, the current iteration element will be kept in the resulting collection. The result of filtering a collection is always a new connection and the number of elements in the new collection depends on the logic inside the callback function. -
For
reduce
, the callback receives two arguments. The first argument is the callback function and the second argument is aninitialValue
. The callback for reduce receives two arguments as well, the first one we call the accumulator and the second one is the current iteration element. The initial value of the accumulator is set from theinitialValue
argument and subsequent values for the accumulator are set based on what gets returned from the callback function. The result of reducing a collection is always a single object, which is what we will have in the accumulator after the last iteration.
Let’s see an example of both filter
and reduce
used together.
Let’s assume that we have a list of numbers and we want to calculate the sum of only the odd numbers in that list. Here is an array to test with:
const testArr = [4, 7, 7, 9, 11, 1, 3, 6, 12]; // sumOdd(testArr) should be 38
Here is a way to do that with a simple loop:
const sumOdd = (arr) = { let sum = 0; sumOff.forEach(number => { if (number % 2 === 1) { sum += number; } }); return sum; };
This works okay: jscomplete.com/playground/BJNQw0bxG
However, to make the solution above work, we had to define a sum
variable and use a forEach
loop, and use an if-statement in that loop. The code above is considered imperative. The declarative way is closer to how we think about the problem in English. We need to filter the list of numbers to only the odd ones, then sum the new list. Summing is the act of reducing a list into a single number. Here is a possible implementation using the filter/reduce:
const sumOdd = (arr) => { return arr .filter(num => num % 2 === 1) .reduce((acc, curr) => acc + curr, 0); };
I like this solution much better: jscomplete.com/playground/BJNQw0bxG
Note how I chained the call of reduce
onto the result of the filter
operation because filter
returns an array. This code is easier to maintain because it is simpler. There is no shared state. There is initial state, then we call a few functions and get a new state. This code is easier to improve. We can, for example, make a separate function responsible for summing an array and have it replace the .reduce
part. The imperative code, on the other hand, has an if
statement that mixes two responsibilities together. This makes the task of extracting the summing part a bit more difficult.
Note that on today’s computers, imperative code usually runs a bit faster than declarative code, but that will probably change in the near future. Also, you should never prematurely optimize your code. You should invest your time to optimize your code only when:
-
You need to. This means your application is trying to use more resources than available.
-
It is cheaper for you to optimize than adding more resources. *You can measure before and after the optimization.
Learn about other functional programming collection methods like .every()
and .some()
. Find examples written with them and try to come up with the imperative version of these examples.