📜 ⬆️ ⬇️

Ramda Thinking: Combining Functions

This post is the second part of a series of articles on functional programming called " Ramda Thinking ".

1. First steps
2. We combine functions
3. Partial application (currying)
4. Declarative programming
5. Ruleless Notation
6. Immutability and objects
7. Immutability and arrays
8. Lenses
9. Conclusion

In the first part, I introduced Ramda and some fundamental ideas from functional programming, such as functions, pure functions, and immobility. I further suggested that iterative functions such as forEach, map, select, and their friends are a good place to start.
')

Simple combinations


Once we have mastered the idea of ​​passing functions into other functions, we can begin to look for situations in which we wish to combine several functions together.

Ramda provides several functions to perform simple combinations. Let's take a look at a couple of them:

Complement


(comment of the lane: if someone knows - please write in the comments, where is the word “complement” when it comes to analogue! (expr) from imperative programming?).

In the last post we used find to find the first even number in the list:

const isEven = x => x % 2 === 0 find(isEven, [1, 2, 3, 4]) // --> 2 

If we wanted to find the first odd number, we could write the isOdd function and use it. But we also know that any even number is not odd. Let's reuse the isEven function.

Ramda provides a complement, a higher-order function that takes another function and returns a new function that returns true when the original function returns a false value, and false when the original function returns a true value.

 const isEven = x => x % 2 === 0 find(complement(isEven), [1, 2, 3, 4]) // --> 1 

Better yet, give the complement function its own name in order to be able to reuse it:

 const isEven = x => x % 2 === 0 const isOdd = complement(isEven) find(isOdd, [1, 2, 3, 4]) // --> 1 

Note that the complement implements the same idea as the operator! for values ​​in imperative programming languages.

Both / Either


Imagine that we are working on a voting system. Having a person, we would like to be able to determine whether that person has the right to vote. Based on our current knowledge, a person must be at least 18 years old and must be a citizen of the country to be able to vote. Someone is a citizen of this country from birth, and someone became them as a result of naturalization.

 const wasBornInCountry = person => person.birthCountry === OUR_COUNTRY const wasNaturalized = person => Boolean(person.naturalizationDate) const isOver18 = person => person.age >= 18 const isCitizen = person => wasBornInCountry(person) || wasNaturalized(person) const isEligibleToVote = person => isOver18(person) && isCitizen(person) 

What we wrote above works, but Ramda offers several handy features that will help make our code a bit cleaner.

The both function takes the other two functions and returns a new function that returns true if both functions return a true value when the arguments are applied to it, and false otherwise.

either takes two other functions and returns a new function that returns true if any function returns a true value with arguments applied to it, and false otherwise.

Using these two functions, we can simplify the isCitizen and isEligibleToVote functions:

 const isCitizen = either(wasBornInCountry, wasNaturalized) const isEligibleToVote = both(isOver18, isCitizen) 

Notice that both essentially implements the same idea as the && (and) operator for values, and both implements the same idea for functions as the operator || (or) for values.

Ramda also provides methods such as allPass and anyPass , which take an array with any number of functions. As their names suggest, allPass works like both, and anyPass like both.

Conveyor


Sometimes we want to apply several functions to some data in a conveyor style. Let's say we could take two numbers, multiply them together, add one and square the result. You can write something like this:

 const multiply = (a, b) => a * b const addOne = x => x + 1 const square = x => x * x const operate = (x, y) => { const product = multiply(x, y) const incremented = addOne(product) const squared = square(incremented) return squared } operate(3, 4) // => ((3 * 4) + 1)^2 => (12 + 1)^2 => 13^2 => 169 

Note that each operation applies to the result of a previous operation in the pipeline.

Ramda provides a pipe function that takes a list of one or more functions and returns a new function.

The new function takes the same number of arguments as the first. Further, it “passes over the pipeline” these arguments through each function in the list. It applies the received arguments to the first function, passes its result to the second function, and so on. The result of the last function is the result of passing the entire conveyor.

Please note that all functions after the first should take only one argument.

Knowing this, we can use the pipe function to simplify our conveyor.

 const operate = pipe( multiply, addOne, square ) 

When we call operate (3, 4), the pipe function passes the 3 and 4 functions to multiply, resulting in 12. Next, it passes 12 to addOne, which returns 13. And then it passes 13 to the square function, which returns 169, and this will be the final result of the entire conveyor.

Compose


Another way we could write our original conveyor function is to write it in one line:

 const operate = (x, y) => square(addOne(multiply(x, y))) 

It is much more compact, but somewhat more difficult to read. In this form, however, it can be rewritten using the compose function from Ramda.

compose works just like pipe, except that it applies functions from right to left and not from left to right. We write our function to operate using compose:

 const operate = compose( square, addOne, multiply ) 

This is exactly the same conveyor as the above, but its functions are in reverse order. In fact, the compose function from Ramda is written on the principles of a conveyor.

I always think of compose like this: compose (f, g) (value) is equivalent to f (g (value)).

Note that as with pipe, all functions, except for the last, should take only one argument.

Compose or pipe?


I think that pipe is probably easier to understand when you come from a more imperative field of activity, since you are used to reading functions from left to right. But compose, on the other hand, is much more like calling a few nested functions, as I wrote above.

I have not yet developed a good rule, when I prefer compose, and when - pipe. Since they are essentially equivalent in Ramda, it probably does not matter which one you choose. Just use what suits your situation best.

Conclusion


Combining several functions in a certain way, we can start writing other more powerful functions.

You may have noticed that we basically ignored arguments when we combined functions. We pass arguments only when we finally call the resulting function pipeline.

This is one of the basics of functional programming and we will talk a lot more about this in the next article in this series, “Partial Application (Currying).” We will also talk about how to combine functions that take more than one argument.

Source: https://habr.com/ru/post/348976/


All Articles