Functional Programming in JavaScript

Uriel Rodriguez
7 min readFeb 21, 2021

When we think of programming paradigms, the one that receives the most coverage is Object-Oriented Programming. This is to say, a programming paradigm is how code is implemented and executed, and in the case of OOP, it is via the use of classes and objects. However, aside from the OOP paradigm, the functional programming paradigm is one that is gaining popularity, as it is seen extensively within React and other libraries. JavaScript enables such a style because functions can be used as data, and passed around within a program. In order to implement a functional approach, certain key concepts must be followed, so let’s explore them.

The most important aspect of a functional programming approach is the ability for functions to be versatile, and JavaScript accomplishes that by making functions first-class members. This means that functions can be used as data, and passed around within a program, such as when stored within variables, data structures like array or objects, passed as arguments to other functions, or returned from function calls.

// non-functional approach
let message;
console.log(message);
// functional approach
const log = message => console.log(message);
message = "log me"
console.log(message)
log(message)

In the example above, the console.log() call utilizes an element of OOP by invoking the log method on the console object. The functional equivalence is by strictly using functions and so a function object is stored within the log variable and simply invoked by passing a “message” argument to it. Another functional programming concept that the example above is implementing is the use of a declarative approach versus an imperative one. The first way a message is logged details how it is done, by invoking the log method on the console object. Albeit not many steps are involved, this form of logging a message is imperative or descriptive of the process. The functional approach is declarative, where the details for logging a message are not important. The actual implementation of invoking the log method on the console object is abstracted away within the log() function.

Another key concept in functional programming is the immutability of data, or that data should not be mutated. In practice, any argument passed to a function should not be transformed, but rather a changed copy should be returned.

const initialState = () => {};
const updateState = (updates, state) => {...state, ...updates}
const state = initialState()
const updatedState = updateState({ prop: "someProp" }, state)
console.log(state) // => {}
console.log(updatedState) // => { prop: "someProp" }

This example demonstrates the idea of utilizing functions for every step in a process, in this case, invoking the initialState() function to return an empty object. Afterward, we add properties to the initial state object by passing it as an argument to the updateState() function along with the updates and return a new object containing the added properties. Again this example demonstrates a declarative approach, abstracting away the method of obtaining an object, and updating it.

In the previous example, the updateState() function maintained the immutability of its arguments. It also demonstrated another key concept: pure functions which state that functions should compute a return value based solely on the arguments it receives and should not cause side effects.

// functional approach
const log = message => console.log(message);

Taking a look back at the first example, the log() function is not a pure function. It causes the side effect of logging to the console while returning a value, undefined, that is not computed by its “message” argument. And because pure functions return values are based on their arguments, they are predictable and easy to implement tests for.

const increaseByOne = num => num + 1const num = 1
const newNum = increaseByOne(num) // => 2
console.log(num) // => 1

Because the increaseByOne() function is pure, returning a value that is base on the computation made its “num” argument and does produce any side effects, it can easily be tested. A test can be written where an assertion is made that when a number of 5 is passed into the increaseByOne() function, a value of 6 is expected. The test will also consistently prove this assertion because the function follows the guidelines of a pure function.

Now, going back to the idea of functions being first-class members, JavaScript enables functions to operate on other functions by passing them as arguments and even returning functions from function calls. Functions that accept functions as arguments or return functions are known as higher-order functions and are another key concept of functional programming.

const sum = (a,b) => a + b
const average = (fn, ...nums) => nums.reduce(fn) / nums.length
const numsAvg = average(sum, 1, 2, 3) // => 2

In this example, two pure functions are defined sum() and average(), which take in some arguments, and do not mutate the arguments but compute a return value based on them. The average() function is a higher-order function that takes in a function, sum(), as an argument and computes a return value by passing it into another higher-order function, reduce(), as the reducer. Next, an arbitrary amount of numbers are passed to the rest parameter and aggregated into a “nums” array. Finally, the array of numbers are reduced and divided by the total amount of numbers passed into the average() function to return the average.

So far, we’ve seen how functional programming truly relies on functions to execute any and all processes. In keeping with this approach, iterations are another process that can also be tackled with functions with recursion. For example:

// non-functional approachconst nums = [1, 2, 3]
let total = 0
for (let i = 0; i < nums.length; i++) {
total += nums[i]
}
console.log(total) // => 6// functional recursive approachfunction addRecursively(nums) {
return !nums.length ? 0 : nums[0] + addRecursively(nums.slice(1))
}
console.log(addRecursively(nums)) // => 6

The first approach utilizes a for-loop which again is not entirely functional so it is replaced with the recursive function addRecursively(). Like the for-loop which operates on an array, the recursive function takes in an array of numbers and begins adding the first to the subsequent one. It also establishes a base case of returning 0 if all the numbers have been added, which is checked by assessing if the array of numbers length is 0.

Finally, because functional programming involves implementing various highly specialized functions, in order to carry out larger processes they need to be assembled. Functions can be composed, or aggregated in a certain order within another larger function to execute in sequence to carry out an overall process. Composition is another key concept of functional programming and there are many variations or patterns all of which involve assembling pure functions to execute a larger process.

const add = (a,b) => a + b
const compare = num => num % 2 === 0
const sum = add(1, 2)
const isEven = compare(sum)
// using compositionconst add = (a,b) => a + b
const compare = num => num % 2 === 0
const operation = fn => (...nums) => nums.reduce(fn)
const compose = (...funcs) => {
return arg => {
return funcs.reduce((composedArg, func) => {
return func(composedArg)
}, arg)
}
}
const addNums = operation(add)const createComposition = compose(addNums, compare)
const isEven = createComposition(1, 2, 3, 4, 5)

The first part of this example shows a simple approach to calculate if a number is even, by invoking two separate functions. Another approach to this problem is by aggregating every function call into one function call to simplify the process. First, a compose() function is defined in which it takes any amount of functions as arguments and returns a function that takes a single argument to be used as a starting point in a series of calculations. The arbitrary amount of functions are aggregated into an array and then iterated over, reducing and transforming the single argument by passing it to each function in the aggregated function array. Next, a custom function is created by invoking the higher-order operation() function, passing in the add() function. After this, a composition is created in which a series of function calls are established by passing in the functions to compose(). The compose() function call returns a function that receives the starting point argument which will be passed to each function argument, in the order that the functions are passed into the compose() functions. The numbers are first added via addNums(), then the return value is passed to the compare() function returning a Boolean value. When processes become large enough to warrant multiple steps and function calls, composition is a powerful way to compute an otherwise modular process.

In conclusion, these key concepts, the immutability of data, pure functions, higher-order functions, recursion, and composition, create the foundation for functional programming. By relying on the power of functions, programs can be written in a manner that enables the testing of discreet components in a reliable manner. And as popularity continues to grow, more and more libraries will begin incorporating this programming approach. To learn more about functional programming, the article below provides great information.

--

--

Uriel Rodriguez

Flatiron School alumni and Full Stack web developer.