This post is the third part of a series of articles on functional programming called “Thinking in the Ramda Style”.
1. First steps2. We combine functions3. Partial application (currying)4. Declarative programming5. Ruleless Notation6. Immutability and objects7. Immutability and arrays8. Lenses9. ConclusionIn the
second part, we talked about the combination of functions in different ways, ending on the functions
compose and
pipe , which allow the use of a series of functions in the conveyor mode.
')
In that post, we looked at simple functions that take only one argument. But what if we want to use functions that take more than one argument?
For example, let's say that we have a collection of book objects and we want to find the names of all the books published in a particular year. Let's solve this problem using only the iterative Ramda functions:
const publishedInYear = (book, year) => book.year === year const titlesForYear = (books, year) => { const selected = filter(book => publishedInYear(book, year), books) return map(book => book.title, selected) }
It will be good if we combine
filter and
map in a pipeline, but we don’t know how to do this, because filter and map take two arguments.
It will also be good if we do not need to use the arrow functions in filter. Let's solve this problem first, as it will let us know some things that we can use when creating pipelines:
Higher order functions
In the
first part of this series, we talked about functions as first-class constructions. Similar functions can be passed as parameters to other functions and returned as a result from other functions. We have done a lot of the first, but have not yet applied the last.
Functions that accept or return other functions are also called “higher order functions.
In the example above, we pass the arrow function to filter: book => publishedInYear (book, year), and it would be nice to get rid of it. In order to do this, we need a function that accepts a book and returns true if the book was published in the desired year. But we also need to pass the number of the year to make it flexible.
The way we can solve this problem is to create a function that will return another function. I will write it in the usual syntax of functions, so that you understand what is happening here, but then we move on to a shorter version with a switch syntax:
This is the new version of the function publishedInYear, we can rewrite the call to filter, excluding the arrow function:
const publishedInYear = year => book => book.year === year const titlesForYear = (books, year) => { const selected = filter(publishedInYear(year), books) return map(book => book.title, selected) }
Now that we call filter, publishedInYear (year) is immediately called and returns a function that takes the book, which is exactly what is needed for filter.
Partially applied functions
We can rewrite a function with several arguments in a similar way, if we wish, but not all of our functions should work this way. Also, we may wish to use functions with several arguments in the usual way.
For example, if we had some other code that just wanted to check that the book was published in a certain year, we would like to write like this: publishedInYear (book, 2012), but we cannot write in this way. Instead, we have to write a little differently: publishedInYear (2012) (book). It is less readable and more annoying.
Fortunately, Ramda provides two functions to help us with this:
partial and
partialRight .
These two functions allow you to call another function with a smaller number of arguments that it needs. They both return a new function that will accept the remaining arguments and call the original function already when all the arguments have been provided.
The difference between partial and partialRight is that the arguments we supply will be substituted with the leftmost or rightmost arguments necessary for the original function.
Let's go back to our original example and try to use these functions instead of rewriting publishedInYear. Since we only need to provide a year, and this is the rightmost argument, we need to use partialRight:
const publishedInYear = (book, year) => book.year === year const titlesForYear = (books, year) => { const selected = filter(partialRight(publishedInYear, [year]), books) return map(book => book.title, selected) }
If we wrote publishedInYear, the host (year, book) instead of (book, year), we would use partial instead of partialRight.
Note that the arguments that we pass to partial and partialRight should always be in an array, even if you pass only one of them there. I can’t say how many times I’ve forgotten about it and got a confusing error message:
First argument to _arity must be a non-negative integer no greater than ten
Currying
The need to use partial and partialRight everywhere leads to verbosity and fatigue. But the need to call functions with many arguments as a series of single-argument functions has long been badly bad.
Fortunately, Ramda provides us with a solution: currying.
curry is another basic concept of functional programming. Technically, a curried function is always a series of single-argument functions that I have just complained about. In pure functional languages, the syntax looks like in such a way that it has no difference from calling functions with several arguments.
But because Ramda is a JavaScript library, and JavaScript does not have a good syntax for calling a series of single-argument functions, the authors have somewhat softened the traditional definition of currying.
In Ramda, a curried function can be called with a subset of its arguments, and it will return a new function that will wait for the remaining arguments. If you call a curried function with all its arguments, it will trigger a normal function call.
You can think of curried functions as the best of two worlds: you can call them with all the arguments, and they will just work. Or you can call them with part of the arguments, and they will work in partial application mode.
Please note that this flexibility has little effect on performance, because for currying it is necessary to determine how the function was called and then determine what needs to be done. In general, I use currying functions when I see that I need to use partial binding in more than one place.
Let's apply the currying capabilities to our publishedInYear function. Note that currying always works as if we were using the partial function, and there is no way to use a version like partialRight. Next, we'll talk a little more on this topic, but for now we will simply change the arguments to reverse order in publishedInYear, so that the year begins to go first.
const publishedInYear = curry((year, book) => book.year === year) const titlesForYear = (books, year) => { const selected = filter(publishedInYear(year), books) return map(book => book.title, selected) }
Now we can call publishedInYear once a year only and get back the function that will take the book and call our original function. However, we can still call the usual publishedInYear (2012, book) without annoying) (syntax. The best of two worlds!
Argument order
Please note that in order to make currying work for us, we need to reverse the order of the arguments. This is extremely common in functional programming, so almost every Ramda function is written so that the data it ultimately works with is the most recent in the argument list.
You can think of the first parameters as a configuration for the operation. So, for publishedInYear, the year parameter is a configuration (what are we looking for?), And a book parameter is data (where are we looking for?).
We have already seen examples of this with iterative functions. They all used the collection as a final argument, because it makes this programming style simpler.
Arguments in incorrect order
What if we leave the order of the publishedInYear function arguments unchanged? How can we still benefit from the nature of currying?
Ramda provides several options.
The first option is flip. Flip takes a function with two or more arguments and returns a new function that takes the same arguments, but reverses the first two arguments. In most cases, it is used with two-argument functions, but is more general in use.
Using flip, we can return to the original order of the arguments for publishedInYear:
const publishedInYear = curry((book, year) => book.year === year) const titlesForYear = (books, year) => { const selected = filter(flip(publishedInYear)(year), books) return map(book => book.title, selected) }
In most cases, I would prefer to use a more convenient order of arguments, but, for example, if you need to use a function that you do not control, then flip is a useful option.
Aggregate
A more general option is the placeholder argument
(__) .
What if we have a curried function with three arguments, and we want to pass the first and last arguments, leaving the one in the middle - for the future? We can use the placeholder for the middle argument:
const threeArgs = curry((a, b, c) => { }) const middleArgumentLater = threeArgs('value for a', __, 'value for c')
We can also use a substitute more than once in a call. For example, what if we want to pass only the middle argument?
const threeArgs = curry((a, b, c) => { }) const middleArgumentOnly = threeArgs(__, 'value for b', __)
We can use the placeholder style instead of the flip if we want:
const publishedInYear = curry((book, year) => book.year === year) const titlesForYear = (books, year) => { const selected = filter(publishedInYear(__, year), books) return map(book => book.title, selected) }
I find this version more readable, but if I need to reuse the “inverted” version of publishedInYear, I can define an additional function using flip and then use it everywhere. You may see some examples in future posts.
Note that __ works only with curried functions, when partial, partialRight and flip work with any function. If you need to use __ with a normal function, you can always wrap it with a curry call before that.
Let's do the conveyor
Let's see how we can move our filter and map calls to the inside of the pipeline. This is the current state of the code, with a convenient order of arguments for publishedInYear:
const publishedInYear = curry((year, book) => book.year === year) const titlesForYear = (books, year) => { const selected = filter(publishedInYear(year), books) return map(book => book.title, selected) }
We learned about pipe and compose in the last post, but we need to learn another piece of information to get the full benefit from this study.
The last piece of information is as follows: almost every Ramda function is curried by default. This includes filter and map. So filter (publishedInYear (year)) fits perfectly and returns a new function that just waits for books to be transferred later, as well as map (book => book.title).
And now we can write a pipeline:
const publishedInYear = curry((year, book) => book.year === year) const titlesForYear = (books, year) => pipe( filter(publishedInYear(year)), map(book => book.title) )(books)
Let's take a step forward and invert the arguments for titlesForYear to match the Ramda data agreements last. We can also curry a function to allow it to be used in subsequent pipelines.
const publishedInYear = curry((year, book) => book.year === year) const titlesForYear = curry((year, books) => pipe( filter(publishedInYear(year)), map(book => book.title) )(books) )
Conclusion
This post may be the deepest part of this series of articles. Partial application and currying may take some time and effort to keep within your head. But when you “get” them once, they will introduce you to a very powerful way to convert data in a functional way.
They force us to make transformations on the basis of pipelines consisting of small simple building blocks.
Further
In order to start writing in a functional style, we need to start thinking “declaratively” instead of “imperatively”. To do this, we will have to find ways to express imperative constructions in a functional style.
An article on declarative programming will discuss these ideas.