The post contains a translation of the article “ Why Ramda? », Which was prepared by one of the contributors Scott Sayet . The article was published on June 10, 2014 on the site and talks about why you should pay your attention to the Ramda library and functional programming in general.
Translator's Note
Due to the fact that the article was written in 2014, some examples are outdated and did not work with the latest version of the library. Therefore, they were adapted to the latest version of Ramda@0.25.0 .
Once upon a time, buzzdecafe introduced Ramda to the world, at the same time the community was divided into two camps.
The first camp brought together those accustomed to the functional style in JavaScript. They warmly accepted the new library, because they clearly understood why it was needed.
And in the second camp, people gathered who did not respond.
For those who are not used to functional programming, Ramda will be indifferent. Since most of its core capabilities are already covered by libraries such as Underscore and Lodash .
If you are one of those who want to keep your code in an imperative or object-oriented style, then Ramda will not help you.
Nevertheless, Ramda offers a different style of writing code, a style that was borrowed from purely functional programming languages. Here the mechanism for creating complex functional logic is implemented using composition. However, any library provides the ability to implement a functional composition. But unlike others, here it is “done with ease.”
Let's see how to work with Ramda.
As an experimental, we take the “TODO list”, since it is a common comparison method for web frameworks, libraries, and so on. Let's start with the fact that we need to get a list of completed tasks:
Using the built-in methods of the Array
prototype, filtering is done like this:
// Plain JS const incompleteTasks = tasks.filter(task => !task.complete);
With Lodash, it looks a bit simpler:
// Lo-Dash const incompleteTasks = _.filter(tasks, {complete: false});
In any case, a filtered task list is obtained.
In Ramda, filtering can be done like this:
const incomplete = R.filter(R.whereEq({complete: false}));
Noticed that something is missing? Missing task list. The Ramda code returns a function, not data.
To get the data, you need to call this function with a task list, after which it will return a filtered list.
Since we only have a function, we can combine it with any data set, or with other functions. Imagine that we have a groupByUser
function that groups the task list by user. Then you can easily create a new function that takes uncompleted tasks and groups them by users.
const activeByUser = R.compose(groupByUser, incomplete);
It turns out that we combine the functions without data binding and get a new function. If we wanted to write an example above ourselves, it would look like this:
const activeByUser = tasks => groupByUser(incomplete(tasks));
Thanks to the composition, it is possible to build new functions based on others without being tied to the data. Thus, we can conclude that composition is a key technique in functional programming.
Let's go ahead and see what else can be done with our example. Suddenly it occurred to you to sort the list grouped by users by date? Then the decision will not take long to wait:
const sortUserTasks = R.compose(R.map(R.sortBy(R.prop('dueDate'))), activeByUser);
It should be noted that the above examples can be combined, since the compose
function allows using more than two parameters:
const sortUserTasks = R.compose( R.mapObj(R.sortBy(R.prop('dueDate'))), groupByUser, R.filter(R.where({complete: false}) );
However, this makes no sense, since we have intermediate functions activeByUser
and incomplete
. And everything else, debugging will be very complicated, and the code is unreadable.
Therefore, I suggest to go the other way. Break all logic into small functions that could be reused.
const sortByDate = R.sortBy(R.prop('dueDate')); const sortUserTasks = R.compose(R.mapObj(sortByDate), activeByUser);
Now sortByDate
can be used to sort any collection of tasks by date. In fact, this is a more flexible option; it sorts any collection of objects containing the dueDate
property being sorted.
Wait, what if you need to sort the dates in descending order?
const sortByDateDescend = R.compose(R.reverse, sortByDate); const sortUserTasks = R.compose(R.mapObj(sortByDateDescend), activeByUser);
If we knew for sure that we would only sort in descending order of date, then we could combine this sorting into one definition of sortByDateDescend
. But personally, I prefer to keep both options, in case I decide to sort the data in ascending or descending order. But it depends on you.
So far we have no data, but then what is happening here? Data processing without data or what? Be patient a little more and everything will become clear. When you write in a functional style, all that comes out is the functions that make up the pipeline. One function transmits data to the next, which transmits them to the next, and so on, until the desired result is achieved.
At the moment we have the following set of functions:
incomplete: [Task] -> [Task] sortByDate: [Task] -> [Task] sortByDateDescend: [Task] -> [Task] activeByUser: [Task] -> {String: [Task]} sortUserTasks: {String: [Task]} -> {String: [Task]}
To implement sortUserTasks
, we have created the above functions, but despite this, they are also useful separately. Earlier, I asked you to imagine that there is a groupByUser
function, but I did not show how to implement it.
Here is one way:
const groupByUser = R.groupBy(R.prop('username'));
The groupBy
function under the hood uses reduce
from Ramda, which is very similar to Array.prototype.reduce
. So the groupBy
function uses reduce
to group the list by the username
field, that is, the object is obtained where the key is username
and the value is the user's task list.
Well, did I manage to impress you with the flexibility of the Ramda? Notice, I still don’t mention the data. Excuse me, but then I'll show you some more features of this library.
Imagine that you wanted to get the first 5 items from the list. This can be done using the take
function. To get the first 5 tasks of each user from our TODO sheet, it is enough to write like this:
const topFiveUserTasks = R.compose(R.map(R.take(5)), sortUserTasks);
Then you should reduce the size of the returned objects by removing the extra fields, for example, you can leave only the title
and dueDate
. In this data structure, user information is redundant and creates only overheads that we do not need.
This selection can be implemented using the Ramda project
function, which is analogous to select
from SQL:
const importantFields = R.project(['title', 'dueDate']); const topDataAllUsers = R.compose(R.mapObj(importantFields), topFiveUserTasks);
Some of the features we created earlier seem to be really useful and can be used for other purposes within our TODO application. The rest are just placeholders, so they can be combined. If we revise all of our code, it could be refactored as follows:
const incomplete = R.filter(R.where({complete: false})); const sortByDate = R.sortBy(R.prop('dueDate')); const sortByDateDescend = R.compose(R.reverse, sortByDate); const importantFields = R.project(['title', 'dueDate']); const groupByUser = R.partition(R.prop('username')); const activeByUser = R.compose(groupByUser, incomplete); const topDataAllUsers = R.compose(R.mapObj(R.compose(importantFields, R.take(5), sortByDateDescend)), activeByUser);
Yes, now I will show you the data itself.
It's time to transfer them to our functions. But the fact is that all these functions take the same data, it is an array of TODO elements. I did not specifically describe the structure of these elements, but it is clear from the code that they should have at least the following properties:
complete
: BooleandueDate
: String, formatted YYYY-MM-DDtitle
: StringuserName
: StringSo, if we have a task list, how do we use it? Yes, very simple:
const results = topDataAllUsers(tasks);
And it's all? All above described function will fulfill and the necessary result will turn out?
I'm afraid so. The result will be the object:
{ Michael: [ {dueDate: '2014-06-22', title: 'Integrate types with main code'}, {dueDate: '2014-06-15', title: 'Finish algebraic types'}, {dueDate: '2014-06-06', title: 'Types infrastucture'}, {dueDate: '2014-05-24', title: 'Separating generators'}, {dueDate: '2014-05-17', title: 'Add modulo function'} ], Richard: [ {dueDate: '2014-06-22', title: 'API documentation'}, {dueDate: '2014-06-15', title: 'Overview documentation'} ], Scott: [ {dueDate: '2014-06-22', title: 'Complete build system'}, {dueDate: '2014-06-15', title: 'Determine versioning scheme'}, {dueDate: '2014-06-09', title: 'Add `mapObj`'}, {dueDate: '2014-06-05', title: 'Fix `and`/`or`/`not`'}, {dueDate: '2014-06-01', title: 'Fold algebra branch back in'} ] }
But here's an interesting feature. You can pass the same task list to the incompleteTasks
function, which results in a filtered list:
const incompleteTasks = incomplete(tasks);
[ { username: 'Scott', title: 'Add `mapObj`', dueDate: '2014-06-09', complete: false, effort: 'low', priority: 'medium' }, { username: 'Michael', title: 'Finish algebraic types', dueDate: '2014-06-15', complete: true, effort: 'high', priority: 'high' } /*, ... */ ]
And, of course, you can also pass the task list to sortBydate
, sortByDateDescend
, importantFields
, toUser
or activeByUser
. Because they all work with the same type of TODO task list . In this way, you can create a large collection of tools with simple combinations.
After everything was done, you learned that you need to implement another function. It should filter the list of tasks for only one specific user. To do this, you need to select a subset of tasks for only one user, and then perform the same sorting and filtering that you used earlier for the entire group of users.
This logic is currently embedded in topDataAllUsers
... In fact, this is a rather aggressive decision. But reorganizing it is very easy. As it often happens, the most difficult thing is to come up with a good name. "Gloss" is probably not the best name for the function, but that's all I could think of late at night:
const gloss = R.compose(importantFields, R.take(5), sortByDateDescend); const topData = R.compose(gloss, incomplete); const topDataAllUsers = R.compose(R.mapObj(gloss), activeByUser); const byUser = R.useWith(R.filter, [R.propEq('username'), R.identity])
Now, when you need to use this, it will be enough to call the following function:
const results = topData(byUser('Scott', tasks));
“Okay,” you say, “maybe it's cool, but for now I just want to get the data. I don't need the functions that my data will return one day ... Can I use Ramda in this case?”
Sure you can.
Let's return to the very first function:
const incomplete = R.filter(R.whereEq({ complete: false }));
How to turn this function into one that returns data? Very simple:
const incompleteTasks = R.filter(R.whereEq({ complete: false }), tasks);
The same applies to the rest of the functions, just add the tasks
parameter and you will get the data back.
This is another important point in the Ramda. All its functions are automatically curried. If such a function does not pass all the expected arguments, it will return a new function that will wait for the remaining arguments. The function R.filter
, used in incomplete
, takes an array of values, as well as a predicate function to filter them. In the original version, we did not pass an array of values, so the filter simply returned a new function that expects this array. In the second version, the expected array was transmitted immediately and it was used together with the predicate to calculate the answer.
Autocamping of the Ramda functions is combined with the principle of “first function, then data”. Data transfer last makes Ramda a very simple library for working in the style of a functional composition.
More details about currying in Ramda are described in the article: Favoring Curry . At the same time, it is certainly worth reading Hugh Jackson's excellent post: Why Curry Helps .
Here is the code that is discussed in the article.
This code clearly demonstrates why it is worth using Ramda.
Ramda has very good documentation .
The described code is quite applicable and should help you for the first time.
Ramda source code can be taken from the GitHub repository or installed via npm .
To use Node.JS, do the following:
npm install ramda const R = require('ramda')
To use in the browser just add:
<script src="path/to/yourCopyOf/ramda.js"></script>
Or:
<script src="path/to/yourCopyOf/ramda.min.js"></script>
You can also use the CDN:
<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.25.0/ramda.min.js"></script>
Source: https://habr.com/ru/post/349468/