Exploring RamdaJs functions

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:

  1. A function that will receive the current "object accumulator" that for the first run will be {} (the initial value)
  2. 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 and lastName 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!