I recently started learning functional programming and found this awesome library: Ramda. In this post I will show how easy handling objects is with Ramda.
What is Ramda anyway?
Ramda is a functional programming library that emphasises pure function style, immutability and side-effect free functions. It is build of a bunch of small functions that can work effective together.
All these functions are automatically curried, it other words they can called with less argument than they expect. In that case they return a function that holds the argument already passed and expects the rest.
prop, propOr
Let's say we want to log the maths grade of each student or log 'No grade' if they don't have maths grade.
import * as R from 'ramda';
const student1 = {
name: 'Alice',
grades: {
english: 'B',
history: 'C',
biology: 'D',
},
};
const student2 = {
name: 'Bob',
grades: {
maths: 'A',
english: 'B',
history: 'C',
},
};
const student3 = {
name: 'Cecile',
};
const students = [student1, student2, student3];
students.forEach(student => {
const grade =
student.grades && student.grades.maths ? student.grades.maths : 'No grade';
console.log(`${student.name} \t ${grade}`);
});
// Alice No grade
// Bob A
// Cecile No grade
I can't say it is easy to read and if we had more deeply nested objects the code would become very ugly very quickly. With Ramda the same code would look like this.
import * as R from 'ramda';
const gimmeTheGrades = R.prop('grades');
students.forEach(student => {
const grade = R.propOr('No grade', 'maths', gimmeTheGrades(student));
console.log(`${student.name} \t ${grade}`);
});
// Alice No grade
// Bob A
// Cecile No grade
gimmeTheGrades is a function that returns the grades property of the object I pass to it, if no grades then it simply returns undefined. propOr takes an extra argument - default value. If the result is falsey, it returns the default value.
If I needed a grade with default value somewhere else later in my app, I would have done this.
import * as R from 'ramda';
const gimmeTheGrades = R.prop('grades');
const gradeWithDefault = R.propOr('No grade');
students.forEach(student => {
const grade = gradeWithDefault('maths', gimmeTheGrades(student));
console.log(`${student.name} \t ${grade}`);
});
// Alice No grade
// Bob A
// Cecile No grade
path, pathOr
What if we need a value of a deeply nested property? We can use path or pathOr. It works the same as prop and propOr, but takes an array of stings, instead of one single string.
import * as R from 'ramda';
const gradeWithDefault = R.pathOr('No grade');
const gimmeTheMathGrade = gradeWithDefault(['grades', 'maths']);
students.forEach(student => {
const grade = gimmeTheMathGrade(student);
console.log(`${student.name} \t ${grade}`);
});
// Alice No grade
// Bob A
// Cecile No grade
getter/setter
It is very easy to define getter and setter functions for a property with lens function. The first argument a function that gets the property, the second is the one that sets it. The setter should not change the data structure.
import * as R from 'ramda';
const gradeWithDefault = R.pathOr('No grade');
const mathsLens = R.lens(
gradeWithDefault(['grades', 'maths']),
R.assocPath(['grades', 'maths']),
);
console.log(R.view(mathsLens, student1)); // No grade
console.log(R.view(mathsLens, student2)); // A
const newStudent1 = R.set(mathsLens, 'F', student1);
const newStudent2 = R.set(mathsLens, undefined, student2);
console.log(R.view(mathsLens, newStudent1)); // F
console.log(R.view(mathsLens, newStudent2)); // No grade
console.log(newStudent2);
// {
// name: 'Bob',
// grades: { maths: undefined, english: 'B', history: 'C' }
// }
Note: new Student has grades.maths property, but it is undefined.
objOf, mergeLeft
objOf creates an object with a single key:value pair, which can be merged with another object, so if we want to create an array with student objects we can do it like below.
import * as R from 'ramda';
const names = ['Alice', 'Bob', 'Cecile'];
const defaultStudent = {
grades: {
english: null,
history: null,
biology: null,
},
};
const createSudents = R.pipe(
R.map(R.objOf('name')),
R.map(R.mergeLeft(defaultStudent)),
);
const students = createSudents(names);
console.log(students);
pipe takes functions as arguments and call them in order, passing the result of each function to the next. First we map through the names and create objects with a name property.
[ { name: 'Alice' }, { name: 'Bob' }, { name: 'Cecile' } ]
Then we feed this to the next map and merge each one with defaultGrades.
[
{
name: 'Alice',
grades: { english: null, history: null, biology: null }
},
{
name: 'Bob',
grades: { english: null, history: null, biology: null }
},
{
name: 'Cecile',
grades: { english: null, history: null, biology: null }
}
]
Thanks for reading it. Happy coding. ❤
Top comments (3)
What I like about Ramda functions for objects is that they act like an interface to your objects. The object structure can change in the future but your code doesn't care, still uses the same interface.
Hmm... I did not think about them this way, but it makes sense. :) Thanks.
can you use pathOr to protect against bad api calls?