Секреты JavaScript-кухни: специи

в 7:00, , рубрики: javascript, Блог компании RUVDS.com, разработка, Разработка веб-сайтов

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

Вот первый: Вот второй:
[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'

«Готов поспорить, что второй вариант отличается гораздо лучшей читабельностью, чем первый», — говорит автор материала, перевод которого мы сегодня публикуем. По его словам — всё дело в аргументах методов filter() и map().

Секреты JavaScript-кухни: специи - 1

Сегодня мы поговорим о том, как перерабатывать код, подобный первому примеру, так, чтобы он выглядел как код из второго. Автор статьи обещает, что после того, как вы поймёте, как это работает, вы будете относиться к своим программам по-новому и не сможете не обращать внимания на то, что раньше могло показаться вполне нормальным и не требующим улучшения.

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

Рассмотрим простую функцию 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-разработке?

Секреты JavaScript-кухни: специи - 2

Автор: ru_vds

Источник

Поделиться

* - обязательные к заполнению поля