Какую из следующих эквивалентных реализаций вы предпочитаете?

[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'

vs

[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'

Я бы сказал, что второй вариант более читабельный. Секрет заключается в удалении аргументов из filters и maps.

Посмотрим, как это работает. Я обещаю, как только вы увидите, как это работает, вы больше не сможете этого не видеть.

Простая функция

Возьмем функцию суммы

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

и переписать по-другому

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

Они работают одинаково, разница только в том, как вы их называете: sum принимает два параметра одновременно, csum принимает параметры один за другим. В частности, допустим, вы звоните csum только один раз с термином 1

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

тогда вы вернетесь к функции, которая принимает второй член и возвращает его с увеличением на единицу

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

Работа с массивами

В JavaScript массивами можно манипулировать различными методами. Например, map используется для применения одной и той же функции к каждому элементу массива.

Чтобы увеличить каждое целое число в массиве

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

Другими словами, x => x + 1 принимает целое число и возвращает его преемника. Используя plusOne сверху, функцию можно переписать на

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

Но подождите секунду, x => plusOne(x) и просто plusOne эквивалентны, на самом деле

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

По той же причине

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

эквивалентно

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

и поскольку plusOne был определен выше как const plusOne = csum(1)

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

Теперь ваша очередь применить тот же процесс, что и sum к isBiggerThan, чтобы вам не нужно было указывать аргументы в filter (т.е. int =>)

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

Теперь код из вступления не должен содержать для вас никаких секретов.

[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'

Два простых правила

Правило 1

Следующие два эквивалентны

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

Правило 2

Всегда можно переписать обратный вызов, чтобы удалить аргументы.

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

Вы наверняка применили это преобразование, если работали над isBiggerThan упражнением. Фактически, допустим, мы хотим, чтобы целые числа были больше 3

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

Теперь мы можем переписать isBiggerThan, чтобы удалить часть int => в filter

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

Идти после этого

Допустим, у вас есть

const keepGreatestChar =
  (char1, char2) => char1 > char2 ? char1 : char2
keepGreatestChar('b', 'f') 
// 'f'
// because 'f' comes after 'b'

Перепишите keepGreatestCharBetweenBAnd, чтобы удалить аргумент char.

const keepGreatestChar = 
  (char1, char2) => char1 > char2 ? char1 : char2
const keepGreatestCharBetweenBAnd = char =>
  keepGreatestChar('b', char)
keepGreatestCharBetweenBAnd('a')
// 'b'
// because 'b' comes after 'a'

Перепишите greatestCharInArray, используя keepGreatestChar, чтобы удалить аргументы (т.е. (acc, char)) изнутри reduce

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'

Реализуйте creduce, чтобы greatestCharInArray мог использовать его и не нуждался в аргументах

const creduce = …
const greatestCharInArray =
  array => array.reduce((acc, char) => acc > char ? acc : char, 'a')

creduce должен быть достаточно общим, чтобы его можно было применять к любым проблемам, требующим операции сокращения. Другими словами, он должен получать обратный вызов, значение инициализации и массив, с которым нужно работать.

Подсказка: вы хотите уметь писать

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

Что, черт возьми, это `c` в` csum` и `creduce`?

c означает каррированный, и то, что вы видели выше, - это то, как каррированные функции могут сделать ваш код более читабельным. Если быть точным, в этой статье я не использовал подходящие каррированные функции, но этого достаточно.

Если вы хотите глубже изучить искусство и науку каррированных функций, я рекомендую прочитать главу 4 В основном адекватного руководства по функциональному программированию. И раз уж вы там, поверьте мне, прочтите книгу целиком.

Кроме того, если вы хотите узнать больше о функциональном программировании на JavaScript, взгляните на FP в обычном JavaScript: введение для новичков.

Получайте самую свежую информацию по электронной почте от меня лично. Ответьте своими мыслями. Давайте учиться друг у друга. Подпишитесь на мою PinkLetter!