Functional Programming in JavaScript with Ramda.js
Functional programming is a way of writing code where ideally everything is a…function. A pure function. Don’t worry if you don’t understand pure functions yet, we will explore that in this article. Although JavaScript is not a fully functional language, it does provide awesome features that help us write functional styles of code.
Ramda.js is one of the most popular functional libraries for JavaScript. In this and upcoming articles, I will use Ramda.js to explore examples of various functional programming concepts in JavaScript. Ramda.js can be installed using a simple command line:
npm install ramda
or
yarn install ramda
and import and use using:
import * as R from "ramda";
In this article, we will explore the basics of the functional style of programming, why functional programming at all, and how to use Ramda.js utilities to write functional code in JavaScript.
Before writing this article, I pondered on how to write this so that a programmer who has never dabbled in the functional world, falls in love with it. Or maybe play around. Then, I came to the conclusion that no one is going to change their style of coding unless it offers something. And thus, there is no better way than to start by explaining side-effects.
Side effects
Consider the two functions in this piece of code:
let age = 0;
function adder(a, b) {
age++;
return age + a + b;
}
function subtractor(a, b) {
age--;
return age - a + b;
}
We have seen code like this, we might even write it daily. What is so special about the above functions that need to be discussed?
We are discussing the functions based on what variables they are touching. Both functions are manipulating the age
variable. The age
variable that’s not theirs. It’s outside of their function scope. It is accessible by them, but not theirs.
The arguments a
and b
are theirs. And in functional programming, you don’t touch something that’s not yours. The functions can’t be touching things outside of them. And when they do, it’s called side effects. Just like side effects in medicine, when you take a pill for a headache and get gas, you tell the pill showed side effects because it operated on something it shouldn’t have: the stomach.
What is the immediate consequence of side effects?
When you pass the same value for arguments for functions with side effects ( a and b
in the above example), they can return different results. For eg: in the above functions, the return value depends on the age
. And these types of functions that show side-effects and evaluate differently on the same values of arguments are called impure functions.
Consider another function:
function add(a, b) {
return a + b;
}
Now this is the type of function that functional programming loves. It just meddles with its own arguments. It is enough and contained within itself. When you pass in the same value of a
and b
it’s always going to return the same value. Never is 3+5 going to be 8 once and 9 the next time. It is a beautiful function. Hence, it’s called a pure function. Thus, pure functions are those functions that don’t have side effects and that return the same value if you pass in the same arguments.
And why are side-effects bad for a programmer?
Consider that the first piece of code is in a project that you have to now take over because the old developer left(because he stopped understanding his own code). Say you are given a feature to implement, based on their age, recommend toys for people.
Just take a glance at the code. Can you tell what the value of age
is? It seems to be 0 because it’s initialized as 0. But then the adder
function is incrementing the age
. And then the subtractor
is decrementing it. So what actually is age
at any point in the program? To figure that out you need to figure out how many times are adder
and subtractor
being called. You now need to keep track of the other two functions just to tell a bit about the age
variable. If age
were just what it was, it was easy. But we have functions showing side effects that are doing things to age
and that’s what causing age
to be all over the place. This is disadvantageous because:
-
Debugging takes time: You have to check each and every places where
age
is being called. -
Testing it is tedious : Even with the same value of
a
andb
, the value might differ due to being dependent onage
. So you are getting different results for the same value of the arguments. -
Reading the code is hard: You cannot tell what’s happening in the code at all by just a single glance. And you have to start logging every place to get hold of just what
age
has become.
And in functional programming, we hate side effects. Maybe hate is the strong word. We try to use it less. Functional programmers are not stupid though. They know side effects are not totally avoidable.
Can you just do things with pure functions itself? What about calling an API and writing to the database? What about changing the color of your screen with the click of a button? Yes, those side effects are unavoidable and we do use them. The goal is to minimize side effects as much as possible.
Now when you keep this in mind and try to minimize side effects, you actually start writing code differently. The whole consequence of writing things differently is what functional programming is. And functional programming is good because it provides you with ideas on how to write code in a way that reduces side effects. It’s got concepts that are helpful to write code differently. Uncle Bob tried to tell it with a whole book (He did succeed though).
Now that we have seen, why side effects are not so good and why it’s better to write fewer functions with side effects, we proceed on the other concepts of functional programming which helps us achieve our goal.
Higher-order functions
Even though it’s a weird name, higher-order functions are not scary. In fact, you are probably already using them.
Consider the below code:
const sayHello = () => {
return "Hello";
};
This is a normal piece of code. At least in JavaScript(looking at you Java). Here, a function is getting assigned to a variable. A variable can hold the function. This feature is called first-class. And thus functions in javascript are first-class citizens (I know they went overboard with namings).
Why is this significant?
Because that gives rise to another feature in JavaScript: you can write a function that can return a variable and that variable can be a function. You can write a function that takes in an argument and that argument can be a function. So the return types and the argument of the function can be a function.
For eg:
function hello() {
return function () {
return a;
};
}
function hello(func) {
return func();
}
And either of these types of functions is called a higher-order function. But why are higher-order functions important?
Consider we want to have two functions: one that doubles the elements of an array and returns them and the next that triples the elements of an array and returns them. Let’s implement it:
const arr = [1, 2, 3];
function doubleMe() {
const returnedArray = [];
for (let i = 0; i < arr.length; i++) {
returnedArray.push(arr[i] * 2);
}
return returnedArray;
}
function tripleMe() {
const returnedArray = [];
for (let i = 0; i < arr.length; i++) {
returnedArray.push(arr[i] * 3);
}
return returnedArray;
}
doubleMe(); //Output: [2,4,6]
tripleMe(); //Output: [3,6,9]
While both these functions do the job and there’s nothing wrong with them, can you notice that all that’s really different about these functions are their names and what they are doing to the elements before pushing it in the returedArray
? The first one is doubling it and the second one is just tripling it. Everything else is the same. Can we make this doubling it and tripling a variable so that we could later even quadruple it or add 2 to it or whatever we want to do? Turns out yes, because JavaScript functions are first-class right? And the implementation would be similar to:
function computeArray(arr, func) {
let returnedArray = [];
for (let i = 0; i < arr.length; i++) {
returnedArray.push(func(arr[i]));
}
return returnedArray;
}
We passed the func
as a function and the func
has access to the current array element. Meaning now we can perform the doubling and tripling this way:
console.log(
"add",
computeArray(arr, (arrayElement) => {
return arrayElement * 2; //or arrayElement* 3
})
);
This is all possible because computeArray
is now a higher-order function because it can take in a function as an argument. And that function can be anything and it has access to the current array element.
Now, whenever you want to do things with the array you don’t have to write the ugly looking for
loop every time and instead just assume that the array is getting looped, and write a self-explanatory name for what you are doing to the array’s elements i.e, the function parameter of the computeArray
.
And JavaScript is cool enough to provide you with important higher-order functions and you are probably using them. For eg: map.
And notice how clean the map
looks compared to the for
loops.
arr.map((element) => {
return element * 2;
});
The above computeArray
is just a simplified custom implementation of the map
.
The others are under Array.prototype
for eg: reduce, sort, and filter.
Thus, higher-order functions provide abstraction and let us reuse the same function. They hide the inner workings of the function. The computeArray
or the map function basically just wants a function and an array to work with. Since they abstract the control flow, the code becomes easier to read, test, and understand.
Currying
Currying is the process of converting a single function that takes multiple arguments to multiple functions that take a single argument.
For eg: consider the function:
function sum(a, b) {
return a + b;
}
sum(3, 5); //Output: 8
This function takes in two arguments at a time and returns a value by adding them. This is not a curried function. Let’s implement the current version of the function. We need to make functions take in a single argument:
function currySum(a) {
return function (b) {
return a + b;
};
}
How do you call this function?
Let’s see how this works. Since we are making functions that take single arguments, let’s pass a single number to currySum
.
addThree=currySum(3)
This evaluates as:
function currySum(3) {
return function (b) {
return 3 + b;
};
}
and returns :
function (b){
// because a=3
return 3+b;
}
Thus,
addThree = function (b) {
return 3 + b;
};
This means we have another function that is ready to accept another param b
:addThree
.
so, addThree()
is a function. So, add three(5)
or replacing addThree
with currySum(3)
as currySum(3)(5)
:
function (b){
return 3+b
}
evaluates to:
function (5){
return 3+5
}
and returns 8.
Ramda.js provides functions and helpers that curry your function, so you don’t have to implement them on your own. Let’s look at an example of using R.curry
.
Say we have the following function that gets the logs of a particular date. The log type can be error, warning, info:
function getLogs(date, type, message) {
const logMessage = `The logs for ${date} is of type ${type} with the message as: ${message}`;
console.log(logMessage);
return logMessage;
}
Now, say you want to get today’s warning and error messages. One obvious way is to call getLogs
two times with different error types:
getLogs("2023-05-03", "warn", "message")
getLogs("2023-05-03", "error", "message")
But with currying, we can make smaller functions that can be reused. For example, the date parameter is the same so we can reuse it in the following way:
function getLogs(date, type, message) {
const logMessage = `The logs for ${date} is of type ${type} with the message as: ${message}`;
console.log(logMessage);
return logMessage;
}
const curryGetLogs = R.curry(getLogs);
const todaysLogs = curryGetLogs("2023-05-03");
const todaysLogWithWarning = todaysLogs("warn")("This is a warn");
const todaysLogWithError = todaysLogs("warn")("This is an error");
console.log("todays log", todaysLogWithWarning, todaysLogWithError);
And we are able to reuse the todaysLogs
function wherever we need it without having to call whole getLogs
. This is a very useful and important because sometimes todaysLogs
could be a very computationally expensive calculation and you could do it only once.
Conclusion
I hope I convinced you a little about the usefulness of functional programming in writing clean and maintainable code. There are more concepts in functional programming that are interesting to explore like closures, function compositions, monads, functors, and lazy evaluation. I will try to cover each topic in the coming articles. If you want to dive deeper yourself, Haskell has awesome resources to delve into functional programming.
If you want to be informed about articles like in your email, subscribe to the newsletter! Thanks!