Какую из следующих эквивалентных реализаций вы предпочитаете?
[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'
Я бы сказал, что второй вариант более читабельный. Секрет заключается в удалении аргументов из filter
s и map
s.
Посмотрим, как это работает. Я обещаю, как только вы увидите, как это работает, вы больше не сможете этого не видеть.
Простая функция
Возьмем функцию суммы
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!