Writing Functional Code

Learn to write functional code in this lesson.

A question that has gone unanswered until now is how we can combine our small, focused functions to build larger applications. In object-oriented programming, we create applications by letting objects (containing state and behavior) interact with each other. What does functional programming offer instead?

Declarative programming style

First, functional programming offers a more declarativeDeclarative programming, also known as high-level programming, is the one in which a program specifies what is to be done rather than how to do it. style of programming. Object-oriented programming has an imperativeImperative programming is the one in which a program specifies how the problem is to be solved. feel because our program consists of a list of instructions to execute (or objects to execute). The illustration below shows the difference between declarative and imperative programming.

Functional programming tries to lift this behavior to a higher level of abstraction. Whenever possible, the low-level behavior is left to the computer. A well-known example of this style is the process where we demonstrate how to execute repeated instructions. In imperative programming, this is often accomplished using loops. Let’s look at an example below.

const exampleArray = [1, 2, 3, 4];
function addOneToEach(arr) {
for(let i = 0; i < arr.length; i++) {
arr[i] = arr[i] + 1;
}
return arr;
}
console.log(addOneToEach(exampleArray));

This is repetitive, error prone, and adds a lot of boilerplate. Functional programming prefers recursion or higher-order functions like map, which is built into JavaScript for arrays. The map function executes a function for each element of an array. Let’s look at an example.

const exampleArray = [1, 2, 3, 4];
function fpAddOneToEach(arr) {
return arr.map(el => el + 1); // add 1 to each element of array
}
console.log(fpAddOneToEach(exampleArray));

Another advantage of higher-order functions is that they often use immutable data structures. The loop in the first example above, on the other hand, mutates the existing array and is error prone.

Note: Modify the second code snippet above and include the loop version (from the first example), followed by the functional programming version (from the second example). Notice how the functional programming version now returns [3, 4, 5, 6] instead of [2, 3, 4, 5]. This is because the exampleArray has been modified by the loop version.

Tacit programming

Recall composition and our addOne function from an earlier example. Using it within map is trivial, as shown below.

const exampleArray = [1, 2, 3, 4];
function addOne(el) {
return el + 1;
}
function fpAddOneToEach(arr) {
return arr.map(addOne);
}
console.log(fpAddOneToEach(exampleArray));

The above is called point-free style or tacit programming. The function we passed in expects one argument, and map produces one. We don’t have to write el => addOne(el) because JavaScript knows it has to pass the incoming value to the function.

So, with higher-order functions like filter and map, we only have to write logic that filters or transforms a single element. The rest is handled by the higher-order function. We know how to increment a single value, and we can now handle arrays without writing any additional code!

Additionally, in functional languages, maps are available for more than just arrays, further extending the usefulness of this function. Because functions like map and filter are more abstract and less bound to specifics, they can be applied in many situations. They work with any kind of array we throw at them. In contrast, the object methods only know how to do things for one specific class. Functional languages like Clojure and Haskell offer the ability to write functions that operate on a broad range of data structures.

The result is also clearer and more readable because functions like filter, map, reduce, and fold all have a clear purpose. We can do anything in a loop. The filter function has one job. That is, we already know what it will be doing just by glancing over the code. Try doing that with a non-trivial loop! Let’s look at a couple of examples provided in the code snippets below. Both examples do the same thing. In the first example, we use loops to filter elements from an array.

const exampleArray = [1, 2, 3, 4, 5, 6, 7];
function inALoop(arr) {
let newArr = [];
for(let i = 0; i < arr.length; i++) {
// filter out any elements smaller than 2 and then increment them by one
if(arr[i] > 2) {
let temporaryResult = arr[i] + 1;
// add the incremented element to a new array if it is greater than four
if(temporaryResult > 4) {
newArr.push(temporaryResult);
}
}
}
// return the new array
return newArr;
}
console.log(inALoop(exampleArray));

The only difference between these two examples is that the first runs in a loop and the second uses the filter and map functions.

const exampleArray = [1, 2, 3, 4, 5, 6, 7];
function withFilterAndMap(arr) {
// return the array after computing defined filter and map operations
return arr
.filter(el => el > 2) // filter elements greater than two
.map(el => el + 1) // add one to filtered elements
.filter(result => result > 4); // from the incremented elements, filter the ones greater than four
}
console.log(withFilterAndMap(exampleArray));

The second example is shorter and much clearer. It can be read, whereas the loop version must be deciphered. A programmer experienced in the first technique can become a capable decipherer, but it will always take more effort than just reading plain English.

Note: Many languages have added functions such as map and filter to imperative loops. Java became one of those languages in version 8, with the Streams API. The corresponding Javadoc mentions the following:

  • Stream pipeline results may be nondeterministic or incorrect if the behavioral parameters to the stream operations are stateful.
  • Side effects in behavioral parameters to stream operations are, in general, discouraged, as they can often lead to unwitting violations of the statelessness requirement, as well as other thread-safety hazards.

In summary, choose pure functions for reliable behavior when using streams. The Javadoc notes in several locations that this is also a requirement for parallelism, for reasons that are no longer a mystery. That is, pure functions behave in a predictable fashion. However, with side effects and mutations, the result might be unpredictable.

Recursion

Recursion is another way that functional programming deals with repetition and avoiding loops. Recursion means solving a problem by breaking it up into smaller problems.

For example, we can calculate the sum of an array using a recursive function just like this:

const exampleArray = [10, 20, 30];
function sum(arr) {
if (arr.length === 0){
console.log("Reached terminal condition");
return 0;
}
const [head, ...tail] = arr;
console.log(`Adding ${head} to the sum`);
return head + sum(tail);
}
console.log(sum(exampleArray));

In the code snippet above:

  • Line 4: This is our terminal condition. There’s nothing left to count if we have an empty array, so we return zero.
  • Line 5: Here, we take the first element of the array and separate it from the remaining elements. We assign the first element to a variable called head and the remaining elements to a variable called tail.
  • Line 6: Here, we add the head to the sum of the remaining elements. Our sum function is called repeatedly until we reach the terminal condition defined in line 4.

We called the sum function with [10, 20, 30]. The function takes the first element, the number 10, and adds it to the sum of the tail, that is, the sum of array [20, 30]. What’s that sum? We enter the function again to determine this. It’s the sum of 20 and the [30] array. Similarly, the sum of [30] is 30 plus an empty array. Finally, the sum of an empty array returns as zero, as defined by our terminal condition. Now 30 (sum of 30 and 0) is returned, which is added to 20, giving 50. Finally, 50 is added to 10, giving 60, which is the correct result.

Recursion often leads to elegant solutions for complicated problems. However, it’s not frequently used in mainstream languages like JavaScript because the repeated function calls cause the stack to grow, possibly resulting in a stack overflow. In this course, we’ll mainly use higher-order functions to solve problems involving repetition.