Вот первый: | Вот второй: |
|
|
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-разработке?
Комментарии (15)
saggid
25.06.2018 10:20Как раз недавно писал перевод статьи о каррировании в рамде. С нею всё становится ещё удобнее.
maxzh83
25.06.2018 10:58+1В первом примере есть лукавство, в правой части не показано, сколько нужно написать, чтобы добиться красоты. И понятно почему. С кучей дополнительных функций ответ уже будет не такой очевидный.
JustDont
25.06.2018 13:04Добро пожаловать в мир фронтэнда, тут все фреймворки, например, себя всегда точно так же пиарят :-)
А потом проходит время, и вот ты уже недоуменно читаешь чьи-то дифирамбы про то, как же просто и быстро проходит обновление данных модели в ангуляре (просто — да, для пользователя фреймворка, но вообще-то нет; быстро — гкхммммм).
Вообще автор транслирует разумную мысль (если можно улучшить читаемость нетривиального кода, не теряя других важных качеств — то её стоит улучшить), только конечно заданная тема статьи этому мешает. Во-первых, не всегда читаемость повышается именно через каррирование. Во-вторых, демонстрировать это стоит все же не на функциях а-ля plusOne(), ну серьезно, тут все взрослые люди.
Desprit
25.06.2018 13:01Сеньоры, по вашему опыту ведения крупных проектов, вы действительно поощряете переходы подобные этому:
.filter(int => isEven(int))
->.filter(isEven)
? Разве это не усложняет разработку? Да, выглядит более емко, но как быть сconst csum
, вот это что вообще? Какие-то неоднозначные нейминги, опускание типов, где они, по сути, вообще не мешают, а лишь экономят драгоценные секунды чтения кода. Ради чего?JustDont
25.06.2018 13:10Именно так, как в статье — конечно нет, это же детсад какой-то.
Но если есть подходящий контекст, где подобные цепочки преобразований ожидаются часто и обильно, то вполне можно заняться делом уменьшения повторов. То есть, у нас в проекте, например, объектная обёртка над canvas написана, естественно, с возвратом самой себя везде (это не каррирование как таковое, но тоже уменьшает повторы и увеличивает читаемость), чтоб можно было писать canvas.то().сё().иЕщеВотЭто().
shaukote
25.06.2018 13:35+2Готов поспорить, что второй вариант отличается гораздо лучшей читабельностью, чем первый.
Нет, ни капли не лучшей. В первом сразу понятно, что происходит, а во втором приходится вникать в эту "магию".
Непонятно, зачем вообще тратить столько сил ради экономии считанных символов.
Правило №1
Две следующих конструкции эквивалентны:
Вообще-то нет, по крайней мере в общем случае.
['1','2','3'].map(parseInt)
отличный пример того, как можно напороться (и потратить долгие часы в отладке).
P.S.
Очень хочется верить, что однажды люди всехвероисповеданийязыков узнают про The Zen of Python. В данном случае он весьма уместен, к примеру:
Явное лучше, чем неявное.
Простое лучше, чем сложное.
Если реализацию сложно объяснить — идея плоха.nickolaym
25.06.2018 15:15Кстати о питоне.
Лямбды и генераторы связывают переменные по имени, что в сочетании с изменяемостью данных означает "штирлиц знал, что запоминается последнее".
На это можно очень больно наступить:
plusators = [(lambda x: x+d) for d in range(10)] print(plusators[5](100)) # 109, wtf?! # сразу выполнил тета-подстановку, чтобы не путаться между одноимёнными переменными plusators = [(lambda c: (lambda x: x+c))(d) for d in range(10)] print(plusators[5](100)) # 105 # вышло страшненько, не правда ли? # вот так выглядит чуть менее страшно plusator = lambda c: lambda x: x+c plusators = [plusator(d) for d in range(10)] # а вот так выглядит в соответствие с PEP8 # где сказано "даёшь функции вместо переменных с присвоенными лямбдами" def plusator(c): return lambda x: x+c plusators = [plusator(d) for d in range(10)] # четыре строчки вместо одной, больше читаемости богу читаемости
argonavtt
25.06.2018 15:15Готов поспорить, что второй вариант отличается гораздо лучшей читабельностью, чем первый», — говорит автор материала, перевод которого мы сегодня публикуем. По его словам — всё дело в аргументах методов filter() и map().
В каком это мире данный код выглядит более читабельнее? Сейчас бы элементарные (даже элементарными назвать с натяжкой можно) арифметические выражения запихивать в отдельные функции. С какой целью это делается? Читабельнее код становится? Т.е. если человек посмотрит и увидит + 1 он не поймёт, а если прочитает plusOne ему всё сразу станет ясно? Где то логика умерла.
kemsky
25.06.2018 15:34Точно такой же подход я использовал в своей библиотечке для ActionScript, но причина была только в том, что там не было arrow-functions:
// 1. using custom callback var result:Stream = s.filter(function(item:Item):Boolean{ return item.price > 1; }); // 2. using provided global functions(can compare Boolean, Number, Date, XML, String types) // supports nested properties i.e. member("prop.prop.prop") var result:Stream = s.filter(gt(member("price"), 1)); // 3. using Proxy magick and global functions var result:Stream = s.price(gt(_, 1));
Имеет смысл так делать только для каких-нибудь сложных агрегатов или типовых операций над строками.
Скажем
.filter(not(isVowel))
лучше все же переписать как.filter(x => !isVowel(x))
.argonavtt
25.06.2018 16:06Со строками как правило и кода больше всегда, если мы конечно говорим не про очередное выражение вроде (string) => string + ", ".
shybovycha
26.06.2018 01:52Я приверженец первого варианта в плане "явности". Но есть два случая, когда второй вариант более уместен:
- в каждой функции-колбеке замешано большое количество логики
- вся вот эта цепочка вызывается тысячи раз — в таком случае банально экономится память, выделяемая под анонимные функции (как получается в первом варианте)
Как можно понять, не существует серебряной пули — каждый вариант имеет право на существование; все дело в контексте.
jt3k
26.06.2018 11:21+1У меня в одном проекте подрядчики напилили код как в правом столбике. Только для методов массива они также юзали свои функции.
Я долго привыкал и до сих пор не уверен что это полезный подход по части поддерживаемости и читабельности.
Akuma
С одной стороны вроде все хорошо, с другой кто-то и правда может подумать, что для x > 3 нужно писать отдельную функцию и затачивать ее под все случаи жизни.
.filter(x => x > 3), все, конец. Просто и понятно.
Я это к тому, что может быть будет лучше в конце статьи указывать более реальное применение всему сказанному? Найдутся ведь те, кто будет весь свой код переписывать на isBiggerThan(3)
Vadem
github.com/jezen/is-thirteen
Akuma
Стоп. И что, вы хотите сказать, что для этого не нужно подключать jQuery и Angular?
К тому же у этой библиотеки 163 открытых Issue. Сразу видно, что это нестабильное нечто, сделанное на коленке.