Ramda provides probably pretty much the same set of features as does Lodash, but it does it in a purer functional programming style manner. This includes that all the functions are curried and their parameters are arranged consistently, and they're side effect free and don't mutate any state.
Functional programming paradigms might not so easy to grasp if one is used to program in Object Oriented style, so I'd like to provide a hands-on example where we build a table containing data and construct filtering and sorting around it.
All the important tenets of Ramda or, as a matter of fact, functional programming have been covered more extensively elsewhere, such as Currying and we won't go into that in this article but instead focus on having a practical example while keeping it short and crips.
Filtering and sorting a table
(All the following examples can be seen in their full implementation on Codepen)
For our example, we use a list of NASA facilities, which they luckily provide as open data. The goal is to make the table sortable and filterable.
I won't go into detail about writing the markup around displaying the data and the filters but instead provide direct examples of the Ramda code used to achieve these goals, the rest can be viewed in the Codepen.
Data for the dropdowns
One of the features we want to provide is the possibility to a filter by state. For that we need a list of all available states in all the datasets. To extract those, what we basically want to do is: go through all the records in the dataset, extract all the states that are available, make the list unique and order it alphabetically.
Our implementation with Ramda looks like this (lines 98 - 104 in Codepen):
const extract = R.compose(
R.sort((a, b) => a > b),
R.uniq,
R.reject(R.isNil),
R.map(R.prop('state'))
)
return extract(this.data || [])
So what happens here is that we compose a function with several other functions. All those functions are applied from right to left, after each other, to the result of the previous function, starting with the list this.data
.
Notice that this is heavily taking advantage of currying of functions. For instance R.reject
takes two parameter: a function and a list. We provide only the first parameter, the second parameter will be the return value of R.map
.
Explained in plain english, those functions in the composition do as follows
R.map(R.prop('state'))
extract the property 'state' from the objects in the listR.reject(R.isNil))
reject/filter out those properties that are NilR.uniq
make the list of states uniqueR.sort((a, b) => a > b))
sort the list alphabetically
Isn't that beautiful? The rest of the examples will more or less be in a similar fashion: we compose a function of several other functions that apply the necessary logic to our list of data.
Sort the table
Next we want the table to be able to be sorted. Assuming we have a property orderBy
, which tells us by which property to order by, and orderDirection
which is either "asc" or "desc". Since this is a bit more of a complex example, we construct this step by step.
The core of this endavour it is of course R.sort
, which takes a compare function and the data:
sortFunc() {
const dataSorter = R.sort(R.comparator(R.gte))
return dataSorter;
}
We need R.comparator
to convert the true/false
results of R.get
to 1/0/-1
results.
Since we want to flip the compare function from "greater than" to "less than" and vice versa based on the property orderDirection
, we change this to
sortFunc() {
const comparator = R.comparator(this.orderDirection == 'asc' ? R.gte : R.lte);
const dataSorter = R.sort(comparator);
return dataSorter;
}
Now we have little problem: if we were to apply datSorter
to our list of data, it would pass the entire objects to R.gte
and R.lte
. Instead we want comparator
to work with only a certain property of the objects in the list. In order to do that, we can use useWith
to apply functions to the parameters before they get passed to comparator
.
Putting it all together, it looks like this:
sortFunc() {
const extractor = R.prop(this.orderBy);
const comparator = R.comparator(this.orderDirection == 'asc' ? R.gte : R.lte);
// apply extractor to the parameters of comparator
const dataSorter = R.sort(R.useWith(comparator, [extractor, extractor]));
return dataSorter;
}
Filter the data
Next on the list is to filter the data. This is a pretty straightforward task, as we can use R.filter
very similar to Array.prototype.filter
:
const filter = R.filter((obj) => obj['state'] == this.stateFilterValue))
To shorten this, Ramda provides the function R.whereEq
, which tests an object against the given spec object:
const filter = R.filter(R.whereEq({'state': this.stateFilterValue}))
But since this is a bit boring, let's make a method that creates a filter function based on given parameters, since we want more than just one filtering option. We also make it to be more dynamic by having the possibility to provide the actual comparison function:
createFilter(filterValue, propertyName, comparisonFunc) {
if (filterValue) {
// filter the datasets where propertyName meets the comparisonFunc
return R.filter(R.where({[propertyName]: comparisonFunc}))
} else {
return R.identity
}
},
So if the filterValue
is falsy (i.e. the user has not chosen anything), we return R.identity
which does nothing but return the provided value. When filterValue
does contain something, we run comparisonFunc
against the propertyName
in the objects.
The power of all this unleashes in the next section, where we...
Put it all together
We now have a computed property sortFunc
which returns a sorting function and a method createFilter
with which we can create filter functions. It's time to put it all together and apply it to our data. Once again, R.compose
comes into play:
const filterByStateFunc = this.createFilter(
this.stateFilter, 'state', R.equals(this.stateFilter))
const filterByCityFunc = this.createFilter(
this.cityFilter, 'city', R.equals(this.cityFilter))
return R.compose(
this.sortFunc,
filterByStateFunc,
filterByCityFunc
)(this.data || []);
Again, the beauty of listing functions into a composition that will run the data through each function. Maybe this does not seem like much, but imagine we needed more filters (there are more in the Codepen example), we could just throw more functions as parameters into R.compose
. Or we could start creating filters programmatically and spread a list of them to R.compose
(R.compose(...listOfFilters)
). Maybe we want to stuff complex filtering logic into the vuex store and only expose the ready made functions to the components.
Conclusion
Now I hope that you'll have a easier time taking your first steps with ramda. I'm pretty sure that some of those example could be solved even more elegantly in Ramda. But maybe that's missing the point. I believe tools should be there to solve problems and not for their own sake. Also, using more complex functional programming constructs to solve problems doesn't really increase the readability for beginners.
The beauty of Ramda is that it does not force you into using all that complex stuff and even provides R.curry
to turn any function into a curried one while still offering those helpful functions that we're used to from Lodash, like for instance to deep clone objects or access deep properties with a fallback.
For further information about all these functional programming concepts, I highly recommend checking out the links under 'introductions on the Ramda homepage.
Featured image by Roman Mager on unsplash