Взгляните на следующие фрагменты кода, решающие одну и ту же задачу, и подумайте о том, какой из них вам больше нравится.
Вот первый: | Вот второй: |
|
|
«Готов поспорить, что второй вариант отличается гораздо лучшей читабельностью, чем первый», — говорит автор материала, перевод которого мы сегодня публикуем. По его словам — всё дело в аргументах методов filter()
и map()
.
Сегодня мы поговорим о том, как перерабатывать код, подобный первому примеру, так, чтобы он выглядел как код из второго. Автор статьи обещает, что после того, как вы поймёте, как это работает, вы будете относиться к своим программам по-новому и не сможете не обращать внимания на то, что раньше могло показаться вполне нормальным и не требующим улучшения.
Простая функция
Рассмотрим простую функцию sum()
, которая складывает переданные ей числа:
const sum = (a, b) => a + b
sum(1, 2)
// 3
Перепишем её, дав новой функции имя csum()
:
const csum = a => b => a + b
csum(1)(2)
// 3
Работает её новый вариант точно так же, как и исходный, единственная разница заключается в том, как вызывают эту новую функцию. А именно, функция sum()
принимает сразу два параметра, а csum()
принимает те же параметры по одному. Фактически, при обращении к csum()
производится вызов двух функций. В частности, рассмотрим ситуацию, когда csum()
вызывают, передав ей число 1 и больше ничего:
csum(1)
// b => 1 + b
Такой вызов csum()
приводит к тому, что она возвращает функцию, которая может принять второй числовой аргумент, передаваемый csum()
при её обычном вызове, и возвращает результат прибавления единицы к этому аргументу. Назовём эту функцию plusOne()
:
const plusOne = csum(1)
plusOne(2)
// 3
Работа с массивами
В JavaScript с массивами можно работать с помощью множества специальных методов. Скажем, метод map()
используется для применения переданной ему функции к каждому элементу массива.
Например, для того, чтобы увеличить на 1 каждый элемент целочисленного массива (точнее — сформировать новый массив, содержащий элементы исходного, увеличенные на 1), можно воспользоваться следующей конструкцией:
[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
(обратите внимание — в данной ситуации после имени функции нет скобок) эквивалентны. Для того чтобы лучше с этим разобраться, рассмотрим функцию otherPlusOne()
:
const otherPlusOne = x => plusOne(x)
otherPlusOne(1)
// 2
Результатом работы этой функции будет то же самое, что получается при простом вызове уже известной нам plusOne()
:
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)
Это позволяет переписать нашу конструкцию с map()
следующим образом:
[1, 2, 3].map(csum(1))
// [2, 3, 4]
Создадим теперь, используя ту же методику, функцию isBiggerThan()
. Если хотите, попробуйте сделать это самостоятельно, а потом продолжайте читать. Это позволит отказаться от использования ненужных конструкций при использовании метода filter()
. Сначала приведём код к такому виду:
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()
так, чтобы её можно было бы использовать в методе filter()
и не прибегать к конструкции int=>
:
const isBiggerThan = threshold => int => int > threshold
[…].map(isBiggerThan(3))
Упражнение
Предположим, у нас есть следующий фрагмент кода:
const keepGreatestChar =
(char1, char2) => char1 > char2 ? char1 : char2
keepGreatestChar('b', 'f')
// 'f'
// так как 'f' идёт после 'b'
Теперь, на основе функции keepGreatestChar()
, создайте функцию keepGreatestCharBetweenBAnd()
. Нам нужно, чтобы, вызывая её, можно было бы передавать ей лишь один аргумент, при этом она будет сравнивать переданный ей символ с символом b
. Выглядеть эта функция может так:
const keepGreatestChar =
(char1, char2) => char1 > char2 ? char1 : char2
const keepGreatestCharBetweenBAnd = char =>
keepGreatestChar('b', char)
keepGreatestCharBetweenBAnd('a')
// 'b'
// так как 'b' идёт после 'a'
Теперь напишите функцию greatestCharInArray()
, которая, с использованием функции keepGreatestChar()
в методе массива 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()
, что позволит, при практическом применении этой функции, не передавать ей ничего кроме массива, в котором надо найти символ с наибольшим кодом.
Функция creduce()
должна быть достаточно универсальной для того, чтобы её можно было применять для решения любой задачи, в которой требуется использовать возможности стандартного метода массивов reduce()
. Другими словами, функция должна принимать коллбэк, начальное значение и массив, с которым нужно работать. В результате у вас должна получиться функция, с применением которой заработает следующий фрагмент кода:
const greatestCharInArray = creduce(keepGreatestChar, 'a')
greatestCharInArray(['a', 'b', 'c', 'd'])
// 'd'
Итоги
Возможно, сейчас у вас возник вопрос о том, почему методы, переработанные в соответствии с представленной здесь методикой, имеют имена, начинающиеся с символа c
. Символ c
— это сокращение от слова curried (каррированный) — и выше мы говорили о том, как каррированные функции помогают улучшить читабельность кода. Надо отметить, что мы тут не стремились к строгому соблюдению принципов функционального программирования, но, полагаем, что практическое применение того, о чём здесь шла речь, позволяет улучшить код. Если тема каррирования в JavaScript вам интересна — рекомендуется почитать 4 главу этой книги о функциональном программировании, а, в общем-то, раз уж вы дошли до этого места — прочтите всю эту книгу. Кроме того, если вы — новичок в функциональном программировании, обратите внимание на этот материал для начинающих.
Уважаемые читатели! Пользуетесь ли вы каррированием функций в JavaScript-разработке?
Автор: ru_vds