Yuri Delgado
Photo by Meghan Holmes on Unsplash
All posts

Functional programming in JavaScript

November 27 2022Yuri8 min read

When we start programming, it’s usual to start by learning imperative programming, which is a paradigm that describes how your software must behave. Probably you also learned OOP (object-oriented programming), which defines your software in terms of objects and data, and organizes your code with classes and methods.

However there are other paradigms and styles you can follow in order to take leverage of some great benefits. Besides imperative programming and OOP, there’re declarative programming, FP (functional programming), and many more.

What is FP

Like OOP, FP is also a programming paradigm. As the name suggests, it focuses on defining your software around functions. In OOP we use functions inside object definitions, these type of functions we call “methods”. If we can use functions in both, why does FP take “functional” in its name?

The biggest difference is that FP has a restricted set of rules you need to follow to make it purely functional. Some of them are: the use of pure functions, immutability, first-class and higher-order functions, and recursions to name a few.

Among the most known functional languages we have Haskell, Elixir, Lisp, Clojure, and Erlang. I’ll explain some FP principles, but it’s worth noting that many of those languages are not 100% functional. Sticking to the constraints I mentioned before can make the developer’s life simpler, but also harder, decreasing their productivity. Then, some languages started absorbing some imperative/OOP features to be a little more flexible, without giving up their main concepts. Also, I’d say it’s almost impossible to be 100% pure functional because eventually you’ll have to perform some interface with impure languages like C or C++.

In this post I’ll not cover how to write functional code in languages other than JavaScript, mainly because I don’t have enough experience with most of them. But here it’s a naive implementation of the Fibonacci sequence in a few of them.

Haskell: fib 0 = 0 fib 1 = 1 fib n = fib (n-1) + fib (n-2) fibList n = map fib [1..n]

Lisp: (defun fibonacci(n) (cond ((eq n 1) 0) ((eq n 2) 1) ((+ (fibonacci (- n 1)) (fibonacci (- n 2))))))))

Clojure: (defn fib [n] (case n 0 0 1 1 (+ (fib (- n 1)) (fib (- n 2))) ) )

They look a bit different from the good old imperative and OOP, don’t they?

It might be weird at first look, and probably at second and third look too, but after dozens (or checking their docs), it starts making more sense.

It looks so different because we are not used to thinking in the same way when we are coding. Probably you don’t firstly think of recursions instead of iterative loops, or consider using prefix instead of infix notation (+ a b instead of a + b).

As a first step, let’s see how we can code following some FP principles in JavaScript.

Is JavaScript functional?

JavaScript is not a pure functional language. Nevertheless, JavaScript has an important feature that allows us to follow a FP paradigm. JavaScript has first-class functions, i.e. functions are treated like any other variable.

Assigning a function to a variable:

const foo = function(bar) { console.log(bar) }

Returning a function:

const foo = function(bar) { return () => console.log(bar) }

Passing a function as an argument:

const foo = function(callback, ...args) { callback(...args) }

Concepts

Pure functions

Pure functions always return the same output from the given input. It means that they are deterministic. You won’t have surprises when running a pure function over and over again. It might sound obvious, but a classic example of a non-pure function is a function that returns the current date.

const getCurrentDate = () => new Date()

Each time you call this function it’ll return a different value. And so what?

The problem is that you can’t easily test it. Once the return is not deterministic, we can’t tell what to expect in an automated test. By definition, a pure function must be isolated from the rest of the world.

The nature of what we want is not deterministic because it interfaces with something impure (the current date from your browser). One approach, and more difficult, to test that impure function is mocking the environment where it’s called, i.e. the browser instance. A second approach, and more functional, would be mocking the new Date() constructor.

FP relies on pure functions because they are deterministic and don’t change values by reference. It creates a more safe environment for development, avoiding side-effects.

Immutability

Since ES6 (2015) there’re three options to declaring variables in JavaScript: var, let, and const.

It’s very common to see var in old JavaScript codes. The advent of let and const allowed us to handle scopes in a safer way.

Back to ES5, we could reassign a variable without even noticing it:

var foo = 'Hi there' // a lot of code here if (true) { var foo = 'Bye bye' }

Furthermore, we could access foo's value out of the block's scope in which it was declared:

if (true) { var foo = 'Bye bye' } console.log(foo) // Bye bye

The introduction of let made our implementation safer in terms of side-effects:

let foo = 'Hi there' if (true) { let foo = 'Bye bye' } console.log(foo) // Hi there

And:

if (true) { let foo = 'Bye bye' } console.log(foo) // ReferenceError: foo is not defined

let is block scoped, i.e. it can't be used out of the block in which it was declared. let throws a syntax error when trying to re-declare it.

But it’s possible to reassign values to variables declared with let. That's when const comes to play.

const is also block scoped and can't be re-declared, but unlike let, variables declared with const can't be reassigned.

const foo = 'Hi there' foo = 'Bye bye' // TypeError: "foo" is read-only

There’s a difference when it comes to objects. The following code throws an exception as we expect:

const foo = { bar: '1' } foo = {} // TypeError: "foo" is read-only

However, the following code doesn’t:

const foo = { bar: '1' } foo.bar = '2' foo['newkey'] = '3' console.log(foo) // {bar: "2", newkey: "3"}

This happens because reassigning a value changes the object’s reference to a new one, which is not allowed in const declaration, but an update doesn't.

So const is the safest way to avoid side-effects when you don't want to reassign values or when you're trying to follow FP principles, but we still have to be aware of some mutations that can mutate objects by reference, it can be by updating a key's value, adding a key, appending an item to an array, etc.

Map, filter, reduce

for and while are popular ways to iterate over arrays in JavaScript.

Say you want to append only even numbers to an array:

const arr = [1,2,3,4,5,6] const evenNums = [] for (let i=0; i < arr.length; i++) { if (arr[i] % 2 === 0) evenNums.push(arr[i]) } console.log(evenNums) // [2, 4, 6]

This code works and there’s nothing wrong with it. But if we want to follow FP principles, we should note that we are pushing to an array declared out of the block that the push occurs.

A good way to handle arrays in JavaScript is by using map, filter and reduce, which are methods from Array.prototype.

map: applies a function on each element of the array and return a new array with the results. The output array has exactly the same length as the input.

filter: applies a function on each element of the array and returns a new array only with the elements that returned truthy to the function application. The output has a length less or equal than the input’s length.

reduce: applies a reducer function in each element of the array and uses an accumulator that is returned on each call of the reducer. This one is slightly more confusing, so let’s see an example and some comparisons.

Say we want to double each element of an array:

const arr = [1,2,3,4,5,6] const doubled = arr.map(num => num * 2) console.log(doubled) // [2, 4, 6, 8, 10, 12] console.log(arr) // [1, 2, 3, 4, 5, 6]

Notice that arr was not changed.

Back to the previous example, let’s get only the even numbers:

const arr = [1,2,3,4,5,6] const evenNums = arr.filter(num => num % 2 === 0) console.log(evenNums) // [2, 4, 6] console.log(arr) // [1, 2, 3, 4, 5, 6]

I consider this a clearer and more direct approach, don’t you?

Now, let’s sum all the numbers of the array:

const arr = [1,2,3,4,5,6] const sum = arr.reduce((acc, curr) => acc + curr, 0) console.log(sum) // 21 console.log(arr) // [1, 2, 3, 4, 5, 6]

Every callback call must return the new accumulator state, in this case I called it acc. The last argument I provided is the initial value. Since we are summing numbers, it does make sense to start the sum at 0.

Let’s separate the even numbers into an array and the odds into another array. To do that, the initial value will be an array with two empty arrays and we’ll check: if the number is even, it goes to the first array, otherwise, it goes to the second. At the end, we expect to have an output like [[2,4,6], [1,3,5]].

const arr = [1,2,3,4,5,6] const splitNumbers = arr.reduce(([even, odd], curr) => { if (curr % 2 === 0) { return [[...even, curr], odd] } return [even, [...odd, curr]] }, [[], []]) console.log(splitNumbers) // [[2,4,6], [1,3,5]] console.log(arr) // [1, 2, 3, 4, 5, 6]

Remember you always have to return something in your callback.

Reduce is very powerful and brings many possibilities because from an array you can return a number, string, list or any type you wish.

Reduce is also the base of map and filter. Let’s quickly see how to implement a map and filter functions using reduce.

const arr = [1,2,3,4,5,6] const myMap = (arr, callback) => { return arr.reduce((acc, curr) => { return [...acc, callback(curr)] }, []) } const myFilter = (arr, callback) => { return arr.reduce((acc, curr) => { if (callback(curr)) { return [...acc, curr] } return acc }, []) } const mapped = myMap(arr, (n) => n * 2) console.log(mapped) // [2, 4, 6, 8, 10, 12] const filtered = myFilter(arr, (n) => n % 2 === 0) console.log(filtered) // [2, 4, 6]

Map, filter and reduce are very important to FP and they are mainly implemented using a technique called recursion.

Recursion

In simple terms, a recursive function is a function that calls itself.

Say we want to print all the numbers from a given number to zero. An iterative approach could be:

function printUntil(num) { for (let i=num; i>=0; i--) { console.log(i) } } printUntil(10)

In this simple example we are applying the same function to all is, i.e., to all numbers we are interested in.

The function (console.log in this case) that is being applied for i is the same for i-1 and so on and so forth.

A recursive approach could be:

function rec(num) { console.log(num) return rec(num - 1) } rec(10)

We first print 10 and call the rec function for 9, which will also be printed and will call rec for 8. Cool, right?

There’s one problem with this recursive function. It doesn’t stop! It tends to negative infinity. So here it is a crucial concept for recursion: the base case.

The base case is the stop condition. It can be one or many, depending on your problem, but it has to exist, otherwise your recursion will run forever.

The fixed version of the previous code is:

function rec(num) { if (num < 0) { return num } console.log(num) return rec(num - 1) } rec(11)

When num decrements to -1, it'll stop the recursion.

For the sake of this post’s size I won’t detail why recursion works. A TL;DR could be: each call of your recursive function goes to a call stack and is popped when it returns. You can see a detailed explanation in this freeCodeCamp article.

Recursions are useful because you don’t need to mutate data when using them. Your function becomes stateless and deterministic.

It’s possible to code recursive functions as iterations, but the opposite is not always true.

Closures

Closure is the capability of an inner function to remember the scope of its outer function even when it was already returned. Let’s see an example:

const buildInvitation = (from, to) => { return `Dear ${to},\nThis is my wedding invitation.\n\n${from}` } const inv1 = buildInvitation('John and Jane Doe', 'Bob') const inv2 = buildInvitation('John and Jane Doe', 'Chris') // Dear Bob, // This is my wedding invitation. // // John and Jane Doe

John and Jane Doe are inviting some friends and family to their wedding. John and Jane are the only senders of the message, i.e., from will be always the same.

One approach could be removing the param and adding their names directly in the string. But let’s say this is a generic function used in your wedding’s application that will serve thousands of couples.

Thus, to avoid repetition, let’s change to the following:

const buildInvitation = (from) => (to) => `Dear ${to},\nThis is my wedding invitation.\n\n${from}` const janeJohnInv = buildInvitation('John and Jane Doe') const inv1 = janeJohnInv('Bob') const inv2 = janeJohnInv('Chris')

It gives us the exact same output, but our function janeJohnInv can remember the from argument.

JavaScript lets us return functions, and due to its capability of “remembering” previously passed arguments, it can save some time and lines of code.

Final words

Using FP can be a good way to prevent side-effects, keep your code more testable, easy to change and debug. There’s nothing wrong with other approaches and paradigms, each one has its pros and cons. It’s up to you to choose the best one according to your problem.

That was a brief explanation of this topic, there are thousands of materials out there and many cool things to study. What about studying a functional language next?

Author's

Yuri Delgado

I'm a Brazilian full-stack engineer who loves learning new technologies and trying out new tools.