Hello guys! These days I was remembering how one year ago, maybe more, I decided to look over each function from RamdaJs and try to understand how to use it. So, let me start the story.
Since I’m a big fan of functional programming, when I discovered RamdaJs I gave it a chance, and (obviously) I like the approach and how I could compose my functions with it. I used it in several projects but there were times when I was not so glad that we were using only a few functions from this library like map
, forEach
, reduce
, filter
, and some other similar functions. To be honest, this is a very common thing to happen with utility libraries like RamdaJs or Lodash.
This makes me feel like we have a huge toolbox but we only know how to use the hammer and silver tape.
That’s why I decided to read each function and do some code snippets to figure out how its works and this way, I could use all the potential from this library to compose better functions.
Disclaimer 1
I will not explain too much what functional programming or curry functions are. If you want more information about it, you can read this article: Introduction to Functional Programming with Javascript.
Disclaimer 2
Since there are a lot of functions in RamdaJs I will try to break this article into more than one part. This way, I can explain a few functions in each article and not keep it short and sweet.
Protip
RamdaJs has a Try Ramda page where you can copy and paste all codes in this article to see the result by yourself.
map, filter, reduce
These three functions have the same behavior as the Vanilla JS functions. The main diferrence is since all of then are curry functions and the target object it’s always the last parameter, you can easily compose a function:
With Vanilla JS
const users = [
{ id: 1, name: 'Felipe', age: 31, active: true },
{ id: 2, name: 'Gabriel', age: 20, active: true },
{ id: 3, name: 'João', age: 20, active: false },
{ id: 4, name: 'Marina', age: 40, active: true },
]
const mapUsersNames = (users) => users.map(user => user.name)
const filterActiveUsers = (users) => users.filter(user => user.active)
const groupUsersByAge = (users) =>
users.reduce(
(acc, user) => {
const { age: userAge } = user
const group = acc[userAge] ?? []
return {
...acc,
[userAge]: [...group, user],
}
},
{}
)
With ramda
import R from 'ramda'
const users = [
{ id: 1, name: 'Felipe', age: 31, active: true },
{ id: 2, name: 'Gabriel', age: 20, active: true },
{ id: 3, name: 'João', age: 20, active: false },
{ id: 4, name: 'Marina', age: 40, active: true },
]
const mapUsersNames = R.map(user => user.name)
const filterActiveUsers = R.filter(user => user.active)
const groupUsersByAge = R.reduce(
(acc, user) => {
const { age: userAge } = user
const group = acc[userAge] ?? []
return {
...acc,
[userAge]: [...group, user],
}
},
{}
)
As I mentioned, the last parameter will always be the target object that is not the default in all other libraries, for example:
// Lodash
const map = (list, mapFunction) => { ... }
// Ramda
const map = curry((mapFunction, list) => { ... })
In Lodash or Vanilla JS, the only way to create a mapUsersNames
function will be to create a new function like the Vanilla JS example above. This happens because in Lodash, the list
is the first argument and the function is not curry. And in Vanilla JS you need the list
object to call the .map
function.
But in RamdaJs, you can create the mapUsersNames
function by only passing the mapFunction
. This happens because the R.map
function is curry and the parameters order is the mapFunction
and then the list
.
So, if you execute this code:
R.map(user => user.name)
The curry method will not be executed because the list
parameter was not given, therefore, it will return a new function that will wait for this parameter:
// Following this sample code:
// const map = curry((mapFunction, list) => { ... })
const mapUsersNames = R.map(user => user.name) // (list) => { ... }
You can execute the map
function (or all RamdaJs functions) if you provide all parameters, like:
const usersNames = R.map(user => user.name, users) // ['Felipe', 'Gabriel', 'João', 'Marina']
But this way you don’t take advantage of the compose power that RamdaJs gives to you.
Be aware of mutations
Before I talk about the next RamdaJs function, it’s important to remember to always keep your functions free of mutations.
Consider the following example:
const arrayToObject = R.reduce(
(acc, [key, value]) => {
acc[key] = value
return acc
},
{},
)
This function will receive an array of tuples and create an object.
arrayToObject([
['name', 'Felipe'],
['age', 31],
['active', true]
]) // { name: 'Felipe', age: 31, active: true }
But when I execute this function again, it will have a strange behavior.
arrayToObject([
['anotherName', 'Marina'],
]) // { name: 'Felipe', age: 31, active: true, anotherName: 'Marina' }
This happened because my R.reduce
function is not pure. Analyzing the arrayToObject
function will call the R.reduce
function with two arguments:
- A function that will receive the current "object accumulator" that for the first run will be
{}
(the initial value) - The initial value, that is an object
{}
const arrayToObject = R.reduce(
(acc, [key, value]) => { // first argument
acc[key] = value
return acc
},
{}, // second argument
)
But, since I’m calling R.reduce
in the global context (not inside a function), the reference for the initial value will always be the same. Let me rewrite this function to make it clear.
const initialValue = {}
const arrayToObject = R.reduce(
(acc, [key, value]) => {
acc[key] = value
return acc
},
initialValue,
)
The initialValue
and acc
are the same objects. When we are mutating the acc
, it’s also mutating the initialValue
. Because of that, every time we execute arrayToObject
, the initialValue
will be changed and it will impact this function behavior.
Just to make it clearer, this is what this function is doing:
const initialValue = {}
const arrayToObject = R.reduce(
(acc, [key, value]) => {
initialValue[key] = value
return initialValue
},
initialValue,
)
To fix that, you just have to avoid mutations in your function:
const arrayToObject = R.reduce(
(acc, [key, value]) => {
return { ...acc, [key]: value }
},
{},
)
reject
Similar to filter
but this function will iterate an array and remove the items that match with your validation.
const isPositive = number => number >= 0
const filterNegatives = R.reject(isPositive)
const numbers = [1, -1, 2, -2, 3]
const negatives = filterPositives(numbers)
negatives // [-1, -2]
You can use this function in those cases when you must deny the filter function, like:
const filterInvalidItems = R.filter(item => !isItemValid(item))
// vs
const filterInvalidItems = R.reject(isItemValid)
I know that it may seem silly, but I already saw this in some project filters that was denying a bunch of properties.
In this situation, I prefer the reject
method.
addIndex
By default, all loop functions like map
, reduce
, forEach
, and so on, don’t have the index
parameter in the callback function.
const items = Array(10).fill(null) // [null, null, ...8 more items]
R.forEach(
(item, index) => { console.log(index) },
items,
)
R.map(
(item, index) => { console.log(index) },
items,
)
R.reduce(
(acc, item, index) => { console.log(index) },
[],
items,
)
All these console.log
will log undefined
so, to have the index
value you need to wrap these functions in addIndex
.
const items = Array(10).fill(null) // [null, null, ...8 more items]
const forEachIndexed = R.addIndex(R.forEach)
const mapIndexed = R.addIndex(R.map)
const reduceIndexed = R.addIndex(R.reduce)
forEachIndexed(
(item, index) => { console.log(index) },
items,
)
mapIndexed(
(item, index) => { console.log(index) },
items,
)
reduceIndexed(
(acc, item, index) => { console.log(index) },
[],
items,
)
pipe and compose
The pipe
function is a way to compose one function where you can pass as many functions as you like to parse/change a value. In the end, it will generate a function where you will pass your object.
It may seem confusing so let me explain by showing you guys a snippet code.
Let’s create a greeting
function that will receive a firstName
and lastName
. It will create a fullName
and then put it in a greeting message.
const greeting = R.pipe(
(firstName, lastName) => ${firstName} ${lastName}
,
(fullName) => Hello ${fullName}!
,
)
greeting('Felipe', 'Nolleto') // Hello Felipe Nolleto!
Here, the pipe
function will receive two functions:
- First Function -> It will receive the
firstName
andlastName
and concatenate both to generate a full name. - Second Function -> It will receive the result of the First Function and generate a greeting message.
The result of pipe
will always be a function that when called it will execute all functions, where each function will change the current value and pass to the next function until getting the final value.
It’s important to know that only the first function can receive more than one parameter, since it will return a single value, the next function will receive a single value also.
The compose
function is almost equal to the pipe
function. The only difference is that it will call the functions backward.
const greeting = R.compose(
(fullName) => Hello ${fullName}!
,
(firstName, lastName) => ${firstName} ${lastName}
,
)
greeting('Felipe', 'Nolleto') // Hello Felipe Nolleto!
These are the functions I most use in Ramda to create new functions because you can create small functions by using them in a pipe
or compose
operator to create a complex function.
const users = [
{ id: 1, name: 'Felipe', age: 31, active: true, skills: ['ruby', 'js'] },
{ id: 2, name: 'Gabriel', age: 20, active: true, skills: ['ruby'] },
{ id: 3, name: 'João', age: 20, active: false, skills: ['js'] },
{ id: 4, name: 'Marina', age: 40, active: true, skills: ['c#', 'js'] },
]
const mapUsersNames = R.map(user => user.name)
const filterActiveUsers = R.filter(user => user.active)
const filterJsUsers = R.filter(user => user.skills.includes('js'))
const getActiveJsUsersNames = R.pipe(
filterActiveUsers, // output: [{"active": true, "age": 31, "id": 1, "name": "Felipe", "skills": ["ruby", "js"]}, {"active": true, "age": 20, "id": 2, "name": "Gabriel", "skills": ["ruby"]}, {"active": true, "age": 40, "id": 4, "name": "Marina", "skills": ["c#", "js"]}]
filterJsUsers, // output: [{"active": true, "age": 31, "id": 1, "name": "Felipe", "skills": ["ruby", "js"]}, {"active": true, "age": 40, "id": 4, "name": "Marina", "skills": ["c#", "js"]}]
mapUsersNames, // output: [ 'Felipe', 'Marina' ]
)
getActiveJsUsersNames(users) // [ 'Felipe', 'Marina' ]
prop and propOr
The prop
function will get the property of an object.
const getName = R.prop('name')
getName({ name: 'Matheus' }) // Matheus
But IMHO, I believe this kind of function it’s most useful to use with map
, filter
, pipe
and so on. For example, we can refactor the mapUsersNames
and filterActiveUsers
in the last example with prop
.
const mapUsersNames = R.map(user => user.name)
const filterActiveUsers = R.filter(user => user.active)
// with prop
const getName = R.prop('name')
const isUserActive = R.prop('active')
const mapUsersNames = R.map(getName)
const filterActiveUsers = R.filter(isUserActive)
If you don’t want to spend two extra lines of code, you can simply do:
const mapUsersNames = R.map(R.prop('name'))
const filterActiveUsers = R.filter(R.prop('active'))
But I like to create getName
and isUserActive
functions because this way we will have a meaningful name (getName
vs R.prop('name')
) and if you need to refactor or change the way you get the name, the mapUsersNames
function will not change.
The propOr
it’s just like prop
but with the difference that you can set a default value.
const users = [
{ id: 1, name: 'Mark', active: true },
{ id: 2, name: 'Klaus' },
]
const isUserActive = R.propOr(
false, // default value
'active', // object property
)
const filterActiveUsers = R.filter(isUserActive)
filterActiveUsers(users) // [{ id: 1, name: 'Mark', active: true }]
Conclusion
Well, RamdaJS has over 200 functions, and talking about all of them in one post is too much 😅
So, I will do a series of articles talking and explaining other functions and methods that I think are very useful and we can use more often in your life.
If you have some questions about these methods don’t forget to comment. Also, if you want to know about some functions that I didn’t mention yet, write the function name that I will do my best to put a good explanation in the next article.
See you in the next post!
We want to work with you. Check out our "What We Do" section!