Ramda’s converge
This article is for those who have made a start learning functional programming in Javascript and are getting to know the Ramda.js library. In particular this article is all about one of the Ramda functions, R.converge, which I believe you will find most useful once you get to know it.
After living through some struggles to get it to work in various circumstances, and after staring at the docs many times, I thought it worthwhile to lay out a more detailed explanation. If you are already aquainted with R.converge, (perhaps after reading this article once through), the diagram above may be all you need as a quick recap. If not, read on…
To demonstrate, we’ll use a scenario that is frankly contrived, and to do this we’re going to make some frivolous data to operate on:
const questionableData = {
questionTypes: ['littleQuestion', 'bigQuestion'],
questions: {
littleQuestion: { question: 'What is 6 x 7?', answer: 42 },
bigQuestion: { question: 'What is the answer to life the universe and everything?', answer: 42 }
}
}
The task is to get either the bigQuestion or littleQuestion out of the questionableData, based on a size value, that we will provide. The next couple of functions help us do part of that:
const getQuestionType = (data, size) => size < 0.5 ? data.questionTypes[0] // i.e. 'littleQuestion' : data.questionTypes[1]; // i.e. 'bigQuestion' const getQuestionsFromData = data => data.questions;
- We have a function to get the questionType from the data, choosing either the ‘littleQuestion’ or the ‘bigQuestion’ according to a size value
- We have a function that gets the questions object from the data
Next we’re going to use another Ramda function, R.prop, which allows us to get value from an object. It expects 2 parameters — first the property name and then the object to query. Thus to get the question from the data, we effectively want to do this:
const bigSize = 0.7;
R.prop(
getQuestionType(questionableData, bigSize),
getQuestionsFromData(questionableData)
) // { "question": "What is the answer to life the universe and everything?", "answer": 42 }
The call getQuestionType(questionableData, bigSize) will give us the property name, and getQuestionsFromData(questionableData) will give us the questions object. Notice that both functions need to receive questionableData as the first parameter, and getQuestionType needs an additional, second parameter — bigSize.
You might think that we’ve done what we need to do, in that we’ve retrieved the object with the correct question and answer in it…so why not stop here? But we’re not really in keeping with the spirit of functional programming at this point: we have all kinds of data nested inside our function. It would be more functional-kosher if we had a function that was divorced from the data itself. Such a function would take questionableData and bigSize as parameters and output the appropriate question object.
This is why we need R.converge. It takes the following parameters:
- a function to be invoked with the return values from #2 (below)
- an array of functions. Each of these functions receives all parameters that are fed to R.converge
To see how that in practice, this is how we can make a generalized function from the R.prop construction above:
const getQuestionBySize = R.converge(
R.prop,
[getQuestionType, getQuestionsFromData]
);
//example of use
getQuestionBySize(questionableData, bigSize); // {"question": "What is the answer to life the universe and everything?", "answer": 42}
As with a lot of functional programming functions, getQuestionBySize ends up being pretty compact, but there is a lot going on…
Firstly, note that R.converge returns a function, not a value, hence getQuestionBySize is a function. In order to get an actual value out of said function, you need to feed it some data, which is the last step that we want to take and that is demonstrated in the line at the bottom.
Now let’s think about what is happening with the data we feed in. Here’s a copy of the diagram at the top with our particular function names in place of the generic ones.
R.converge is going to take both parameters and hand them to both functions. In the case of the function getQuestionType, both questionableData and mySize are needed, so we’re all good there.
However getQuestionsFromData only needs one parameter, namely questionableData. This is also fine, because questionableData is the first of the two parameters, so getQuestionsFromData will just ignore the second.
We would have been in a mess if getQuestionType wanted the order of its parameters to be (size, data) instead of (data, size), because then getQuestionsFromData would receive size as the first parameter and everything would break. This is a bit of a confusing point, I’ll admit, so please take a moment to make sure you follow.
Luckily, since you reign over your code like a god, you have the power to arrange your functions’ parameters in whatever order you like. If your omniscient purposes require placing data before size in the parameter list for getQuestionType, then so be it. I wager you’ll find yourself doing this kind of arranging quite a bit in your encounters with R.converge and it’s compatriots.
After all that is done, we end up with one output value from each of the functions in the array. R.converge uses the array’s guaranteed order to feed R.prop with parameters, as follows:
- first parameter to R.prop = the output of first function in array = the output of getQuestionType
- second parameter to R.prop = output of second function in array = the output of getQuestionsFromData
Just as we organized our functions to take parameters in the right order, we have similarly organized our functions to output the right stuff in the right order for R.prop to do its thing with them. (This functional programming gig frequently involves lining up your billiard balls just right, so they all go into the pocket in one stroke.)
As a final note, you can have more, or fewer parameters fed into R.converge, and more functions in the array (if you only had one function in the array you don’t really need R.converge). All functions would receive all parameters, then the output of each arrayed function would be fed, in order, to the “after” function.
Here’s the full code from above, if you want to play with it. I suggest copy-pasting it into the Try Ramda REPL, which is excellent for easy experimentation with Ramda functions.
// The best place to try this code is at https://ramdajs.com/repl?v=0.26.1
import * as R from 'ramda'; // this line not needed in the above REPL
// some mock data
const questionableData = {
questionTypes: ['littleQuestion', 'bigQuestion'],
questions: {
littleQuestion: {
question: 'What is 6 x 7?',
answer: 42
},
bigQuestion: {
question: 'What is the answer to life the universe and everything?',
answer: 42
}
}
}
// 2 functions to get parts of the info out of the data
const getQuestionType = (data, size) =>
size < 0.5
? data.questionTypes[0] // i.e. 'littleQuestion'
: data.questionTypes[1]; // i.e. 'bigQuestion'
const getQuestionsFromData = data => data.questions;
// ...and here is where all the above funcitons converge
const getQuestionBySize = R.converge(
R.prop,
[getQuestionType, getQuestionsFromData]
);
// now we have the function, here's how to use it
const mySize = 0.7; // change this to get a big or little question
getQuestionBySize(questionableData, mySize);