Practical Functional Programming with JavaScript

Tal Joffe
Talking Software
Published in
7 min readJul 24, 2022
Source: XKCD

Functional programming might sound intimidating, theoretical, or overly-complex (and to some all of the above).
I think that its core principles are not only pretty easy to explain but are also very powerful in creating clean code.

So what are these mystical core principles, you ask?

Well, if you want to find out, you came to the right place!

Let’s start with a paragraph from the source of all truth (A.K.A. Wikipedia)

“[Functinal 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. It is a declarative programming paradigm, which means programming is done with expressions or declarations instead of statements. In functional code, the output value of a function depends only on the arguments that are passed to the function, so calling a function f twice with the same value for an argument x produces the same result f(x) each time…”

Which brings us to the core principles — Immutable data, pure functions, and declarative style using function composition.

Now that we have the core principles, we can explore each one (with day-to-day JS examples) and finish by understanding how they all come together and help us write much better code.

Immutable data

An immutable variable is one that cannot be modified after initialization or to put differently, can only be assigned a value once.

Let’s see some examples:

//mutable
let num = 8 // can re-assign, num = 9
const arr = [1,2] //can mutate the array, arr.push(3)
const user = {id:1, name: 'x'} //can mutate the object, user.id = 2//Immutable
const num = 8
Immutable.List.of(1,2) // using ImmutableJS
const user = Object.freeze({id:1, name: 'x'})

— Why is this part of functional programming🤔?

— Because functional programming has a mathematical foundation🎓!

In math, you cannot write the following statement: x = x + 5 because it is the same as writing 0 = 5. Unlike other programming paradigms, functional programming patterns are proven to be mathematically correct (you will have to believe me, I’m not going to try and show it), and one of the assumptions for the math to work is the single assignment.

— OK, Math. I got it. But what’s in it for me 🤨?

Consider the following code

let object = {}
doSemothingToObject(object) //probably mutating object
let data = await getData(object) //possibly mutating object
let extras = await getAdditionalData(data) //possibly mutating data
someCalculation(object, data, extras) // mutating all? just object?
console.log(object) //who knows what will be here

Can you quickly tell at any given moment what the value of the object is?
Can you test each part of the logic on its own?

Mutation is very hard to follow when reading the code, especially if the mutated object has many attributes and is being passed around a lot.

If you ever wrote enough AngularJS code using $scope, for example, you probably had a tough time avoiding race conditions at some point since many parts of the code are mutating the scope.

So the benefits of avoiding data mutation are that it makes your code:

  • Easier to read
  • Easier to test and debug

Here is an excellent quote on why readable and debuggable code is essential:

“Indeed, the ratio of time spent reading versus writing is well over 10 to 1. We are constantly reading old code as part of the effort to write new code. …[Therefore,] making it easy to read makes it easier to write.”
- Uncle Bob

Pure Functions

Pure function follow two rules

  • Return value is the same for the same arguments
  • has no side effects

Here are some examples:

const add = (a, b) => a + b // purearray.forEach(...) // impure (no return value)const incrementAge = prsn => prsn.setAge(prsn.getAge() + 1) //impureconst move = (cur) => cur.copy(x=cur.x + Math.random()) //impure

Another way to understand the strength of pure functions is with the “Referential Transparency” principle:
If a function is pure, we should be able to replace it with its return value

//pure function 
const add = (a, b) => a + b//this
const calculation = add(3,4)
//can be replaced with this
const calculation = 7
//without changing anything in the application

So, whenever you are not sure if a function is pure, ask yourself, can I replace it with its outcome without changing nothing in the behavior?

Benefits:

  • Easy to test
  • Easy to debug
  • Usually leads to smaller, single-responsibility functions
  • Easy to compose (will explain later)

Impure functions

Let’s assume you are convinced that pure-functions are awesome, and you want to write everything using pure-functions.
But can we do that?
Let’s take a look at a few common impure functions:

  • Fetching data from an API call
  • Writing console.log()
  • Mutating global state
  • Get current system time (e.g. using moment())

— So can we use only pure functions 😃?

— No 😢

Impure functions are, unfortunately, necessary to create production applications. When writing functional code, we would try and limit impurity to particular tasks in a particular place, and they should contain as little logic as possible.

A good example for dealing well with impurity would be Redux — the framework handles all of the side effects affecting the main store, and the logic comprised of pure functions (e.g., reducers are returning a new state)

Dan Abramov — the creator of Redux, in the early days of the framework

Declarative

Declarative code focuses on what we are trying to do and not how. Imperative code describes what we are trying to do and how to do it

Functional programming is not the only way to write declarative code consider the following SQL example:

SELECT *
FROM orders
JOIN products
ON orders.productId = products.productId
WHERE products.productName = 'Holy grail'
LIMIT 10;

We are using SQL to tell the DB what we want it to do without telling it how to do it ( We are not describing how to fetch the rows from storage, how to merge the two tables in memory, how to limit the results, etc.).

If it is still not clear, you can think about this way — you know you are writing declarative code when reading it, and explaining what it does is almost the same.

And now for a declarative, functional example.

Let’s imagine we need to write a function that gets products from an API and tries to find the top 10 selling products that are toys (for simplicity, we assume that a toy product is a product containing “toy” or “play” in its name).

Imperative approach:

getTopSellingToys(products) {  const topToys = []
const limit = 10 if (products.size === 0 ){
return topToys
} for (let i=0; i < products.size ; i++ ){
const product = products[i]
if (product.name){
name = product.name.toLowerCase()
if (name.includes("toy")|| name.includes("play")){
if ((topToys.size < limit) || (product.purchasesCount >
getMinimalPurchases(topProducts)){
topToys.push(product)
}
}
}
return topToys
}

Declarative approach:

getTopSellingToys(products) {
return products
.filter(product => product.name !== null)
.map(product => [product.name.toLowerCase(), product])
.filter(nameAndProduct => nameAndProduct[0].includes('toy') ||
nameAndProduct[0].includes('play'))
.sort((a,b) => a[1].purchasesCount - b[1].purchasesCount)
.map(nameAndProduct => nameAndProduct[1])
.slice(0,10)
}

Slightly better:

getTopSellingToys(products) {
return products
.filter(product => isToy(product)
.sort(compareByPurchaseCount)
.slice(0,10)
}

In this example, we can see that:

  1. We need much less code
  2. We focus on what we are trying to do
  3. We have a clear separation of concerns (filtering, sorting, limiting)

The separation of concerns is important because it makes the code much more scalable for changes

— But what if I don’t have an array 🤔?

I used an array here because it allows me to compose functions using chaining, but there are other ways to compose functions in JS. For example:

  1. Promise chaining (_.getPromise().then(...).then(...).catch(...))
  2. Compose (_.compose(foo, bar, biz))
  3. pipeline operator* (5 |> double |> double |> increment |> double)
  4. High order functions

The idea is always the same; We use (pure) functions as building blocks we put together to create our logic.

Summary

In this post, I laid out the fundamentals of functional programming:

  1. Pure functions
  2. Immutable data
  3. Declarative code (using functional composition)

The motivation for following the above principles is simple — It is easier to test, debug, and scale, and it is easy to do in JavaScript. But If that doesn’t convince you, maybe this quote will:

“The bottom line is this. Functional programming is important.
You should learn it“
- Uncle Bob

If you want to learn functional programming in-depth, you will probably need more theory and more tools that I didn’t talk about here. But I’m a big believer in the 20–80 rule, and I think these principles are the 20% you need to learn to make your JavaScript code 80% better.

I hope you liked my post. I’ll probably follow up with slightly more advanced posts about functional programming in JavaScript in the future, so stay tuned!

Additional Information

I use quotes from “uncle Bob” (Robert C. Martin) twice. If you don’t know who that his, I strongly recommend reading “Clean Code.”

If you are interested in functional programming in JavaScript, I suggest following Eric Elliot. He has many excellent articles.

--

--

Tal Joffe
Talking Software

Interested in software, people, and how to bring the best of them both @TalJoffe