JavaScript kitchen secrets: spices

Original author: Riccardo Odone
  • Transfer
Take a look at the following code snippets that solve the same problem, and think about which one you like best.
Here is the first: Here is the second:
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
 .filter(int => isEven(int))
 .filter(int => isBiggerThan(3, int))
 .map(int => int + 1)
 .map(int => toChar(int))
 .filter(char => !isVowel(char))
 .join('')
// 'fhjl'
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
 .filter(isEven)
 .filter(isBiggerThan(3))
 .map(plus(1))
 .map(toChar)
 .filter(not(isVowel))
 .join('')
// 'fhjl'
“I bet that the second version has a much better readability than the first,” says the author of the material, the translation of which we are publishing today. According to him, it’s all about the arguments of the methods filter()and map().



Today we will talk about how to recycle code like the first example, so that it looks like the code from the second. The author of the article promises that after you understand how it works, you will relate to your programs in a new way and will not be able to ignore what used to seem quite normal and not requiring improvement.

Simple function


Consider a simple function sum()that adds the numbers passed to it:

const sum = (a, b) => a + b
sum(1, 2)
// 3

Rewrite it, giving the new function a name csum():

const csum = a => b => a + b
csum(1)(2)
// 3

It works its new version in the same way as the original one, the only difference is how this new function is called. Namely, the function sum()takes two parameters at once, and csum()takes the same parameters one by one. In fact, when accessing, csum()two functions are called. In particular, consider the situation when they csum()call, passing it the number 1 and nothing else:

csum(1)
// b => 1 + b

Such a call csum()leads to the fact that it returns a function that can take the second numeric argument passed csum()in its usual call, and returns the result of adding one to this argument. Let's call this function plusOne():

const plusOne = csum(1)
plusOne(2)
// 3

Work with arrays


In JavaScript, you can work with arrays using a variety of special methods. Let's say the method is map()used to apply the function passed to it to each element of the array.

For example, in order to increase by 1 each element of an integer array (more precisely, to form a new array containing elements of the original, increased by 1), you can use the following construction:

[1, 2, 3].map(x => x + 1)
// [2, 3, 4]

In other words, what is happening can be described as follows: the function x => x + 1takes an integer and returns the number that follows it in the series of integers. If we use the above function plusOne(), this example can be rewritten as follows:

[1, 2, 3].map(x => plusOne(x))
// [2, 3, 4]

There is a moment to slow down and think about what is happening. If this is done, then it can be noted that in the considered case the constructions x => plusOne(x)and plusOne(note that in this situation there are no brackets after the function name) are equivalent. In order to better deal with this, consider the function otherPlusOne():

const otherPlusOne = x => plusOne(x)
otherPlusOne(1)
// 2

The result of this function will be the same as that obtained by a simple call already known to us plusOne():

plusOne(1)
// 2

For the same reason, we can talk about the equivalence of the following two constructions. Here is the first we have already seen:

[1, 2, 3].map(x => plusOne(x))
// [2, 3, 4]

Here is the second:

[1, 2, 3].map(plusOne)
// [2, 3, 4]

In addition, recall how the function was created plusOne():

const plusOne = csum(1)

This allows us to rewrite our design with the map()following:

[1, 2, 3].map(csum(1))
// [2, 3, 4]

Let's create now using the same method, function isBiggerThan(). If you want, try to do it yourself, and then continue reading. This will eliminate the use of unnecessary constructions when using the method filter(). First, we give the code to this form:

const isBiggerThan = (threshold, int) => int > threshold
[1, 2, 3, 4].filter(int => isBiggerThan(3, int))

Then, getting rid of all the excess, we get the code that you have already seen at the very beginning of this material:

[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
 .filter(isEven)
 .filter(isBiggerThan(3))
 .map(plus(1))
 .map(toChar)
 .filter(not(isVowel))
 .join('')
// 'fhjl'

We now consider two simple rules that allow you to write code in the style considered here.

Rule number 1


The following two constructions are equivalent:

[…].map(x => fnc(x))
[…].map(fnc)

Rule number 2


Callback can always be rewritten to reduce the number of arguments used when it is called:

const fnc = (x, y, z) => …
[…].map(x => fnc(x, y, z))
const fnc = (y, z) => x => …
[…].map(fnc(y, z))

If you wrote the function yourself isBiggerThan(), then you probably already resorted to a similar transformation. Suppose we need to pass through the filter numbers that are greater than 3. This can be done as follows:

const isBiggerThan = (threshold, int) => int > threshold
[].filter(int => isBiggerThan(3, int))

Now we will rewrite the function isBiggerThan()so that it could be used in the method filter()and not resort to the construction int=>:

const isBiggerThan = threshold => int => int > threshold
[].map(isBiggerThan(3))

Exercise


Suppose we have the following code snippet:

const keepGreatestChar =
  (char1, char2) => char1 > char2 ? char1 : char2
keepGreatestChar('b', 'f') 
// 'f'
// так как 'f' идёт после 'b'

Now, based on the function keepGreatestChar(), create a function keepGreatestCharBetweenBAnd(). We need that, calling it, it would be possible to pass to it only one argument, while it will compare the character passed to it with the character b. This function may look like this:

const keepGreatestChar = 
  (char1, char2) => char1 > char2 ? char1 : char2
const keepGreatestCharBetweenBAnd = char =>
  keepGreatestChar('b', char)
keepGreatestCharBetweenBAnd('a')
// 'b'
// так как 'b' идёт после 'a'

Now write a function greatestCharInArray()that, using the function keepGreatestChar()in an array method, reduce()allows you to search for the “largest” character and does not need arguments. Let's start with this code:

const keepGreatestChar =
  (char1, char2) => char1 > char2 ? char1 : char2
const greatestCharInArray =
  array => array.reduce((acc, char) => acc > char ? acc : char, 'a')
greatestCharInArray(['a', 'b', 'c', 'd'])
// 'd'

To solve this problem, implement a function creduce()that can be used in a function greatestCharInArray(), which will allow, in the practical application of this function, not to transfer to it anything other than an array in which you need to find the symbol with the largest code.

The function creduce()must be sufficiently universal so that it can be used to solve any problem in which you want to use the capabilities of the standard array method reduce(). In other words, the function must accept a callback, an initial value, and an array to work with. As a result, you should have a function with which the following code fragment will work:

const greatestCharInArray = creduce(keepGreatestChar, 'a')
greatestCharInArray(['a', 'b', 'c', 'd'])
// 'd'

Results


Perhaps now you have a question about why the methods, processed in accordance with the method presented here, have names starting with a symbol c. A symbol cis an abbreviation for curried — and we’ve talked about how curried functions help improve readability of the code. It should be noted that we did not strive here for strict adherence to the principles of functional programming, but we believe that the practical application of what was discussed here allows us to improve the code. If the topic of currying in JavaScript is interesting to you, it is recommended to read chapter 4 of thisbooks on functional programming, and, in general, since you have reached this place - read the whole book. In addition, if you are new to functional programming, pay attention to this material for beginners.

Dear readers! Do you use currying functions in JavaScript development?


Also popular now: