Привет! Предлагаю вам статьи Rethinking JavaScript: Replace break by going functional.
В моей предыдущей статье Rethinking JavaScript: Death of the For Loop (есть перевод: Переосмысление JavaScript: Смерть for) я пытался убедить вас отказаться от for в пользу функционального подхода. И вы задали хороший вопрос "Что на счет break?".
break это GOTO циклов и его следует избегать
Нам следует отказаться от break
также, как мы когда-то отказались от GOTO
.
Вы можете думать, "Да ладно, Джо, ты преувеличиваешь. Как это break это GOTO
?"
// плохой код. не копируй!
outer:
for (var i in outerList) {
inner:
for (var j in innerList) {
break outer;
}
}
Рассмотрим метки (прим. labels) для доказательства утверждения. В других языках метки работают в паре с GOTO
. В JavaScript'e же метки работают вместе с break
и continue
, что сближает последних с GOTO
.
JavaScript'вые метка, break и continue это пережиток GOTO и неструктурированного программирования
"Но break никому не мешает, почему бы не оставить возможность его использовать?"
Почему следует ограничивать себя при разработке ПО?
Это может звучать нелогично, но ограничения это хорошая вещь. Запрет GOTO
прекрасный тому пример. Мы также с удовольствием ограничиваем себя директивой "use strict", а иногда даже осуждаем игнорирующих её.
"Ограничения могут сделать вещи лучше. Намного лучше" — Чарльз Скалфани
Ограничения заставляют нас писать лучше.
Какие альтернативы у break?
Я не буду врать. Не существует простого и быстрого способа заменить break. Здесь нужен совершенно иной стиль программирования. Совершенно иной стиль
Хорошая новость в том, что существует много библиотек и инструментов, которые могут нам помочь, такие как Lodash, Ramda, lazy.js, рекурсия и другие.
Например, у нас есть коллекция котов и функция isKitten
:
const cats = [
{ name: 'Mojo', months: 84 },
{ name: 'Mao-Mao', months: 34 },
{ name: 'Waffles', months: 4 },
{ name: 'Pickles', months: 6 }
]
const isKitten = cat => cat.months < 7
Начнем со старого доброго цикла for
. Мы проитерируем наших котов и выйдем из цикла, когда найдем первого котенка.
var firstKitten
for (var i = 0; i < cats.length; i++) {
if (isKitten(cats[i])) {
firstKitten = cats[i]
break
}
}
Сравним с аналогичным lodash вариантом
const firstKitten = _.find(cats, isKitten)
Этот был довольно простой пример, давайте попробуем что-нибудь по-серьезнее. Будем перебирать наших котов пока не найдем 5 котят.
var first5Kittens = []
// старый добрый for
for (var i = 0; i < cats.length; i++) {
if (isKitten(cats[i])) {
first5Kittens.push(cats[i])
if (first5Kittens.length >= 5) {
break
}
}
}
Легкий путь
Прим. переводчика: позволил себе немного вольности и дополнил размышления о легком пути недостающими, по моему мнению, частями.
Мы можем использовать стандартные методы массива JavaScript.
const result = cats.filter(isKitten)
.slice(0, 5);
Но это не очень функционально. Мы можем воспользоваться Lodash'ем.
const result = _.take(_.filter(cats, isKitten), 5)
Это достаточно хорошее решение пока вы ищете котят в небольшой коллекции котов.
Lodash великолепен и умеет делать массу хороших вещей, но сейчас нам нужно что-то более специфичное. Тут нам поможет lazy.js. Он "Как underscore, но ленивый". Его ленивость нам и нужна.
const result = Lazy(cats)
.filter(isKitten)
.take(5)
Дело в том, что ленивые последовательности (которые предоставляет lazy.js) сделают ровно столько преобразований (filter, map и тд) сколько элементов вы хотите получить в конце.
Сложный путь
Библиотеки это весело, но иногда по настоящему весело сделать что-то самому!
Как на счет того, чтобы создать обобщенную (прим. generic) функцию, которая будет работать как filter
, но вдобавок будет уметь останавливаться при нахождении определенного количества элементов?
Сначала обернем наш старый добрый цикл в функцию.
const get5Kittens = () => {
const newList = []
// старый добрый for
for (var i = 0; i < cats.length; i++) {
if (isKitten(cats[i])) {
newList.push(cats[i])
if (newList.length >= 5) {
break
}
}
}
return newList
}
Теперь давайте обобщим функцию и вынесем всё котоспецифичное. Заменим 5
на limit
, isKitten
на predicate
и cats
на list
и вынесем их в параметры функции.
const takeFirst = (limit, predicate, list) => {
const newList = []
for (var i = 0; i < list.length; i++) {
if (predicate(list[i])) {
newList.push(list[i])
if (newList.length >= limit) {
break
}
}
}
return newList
}
В итоге у нас получилась готовая для повторного использования функция takeFirst
, которая полностью отделена от нашей кошачьей бизнес логики!
takeFirst
— чистая функция. Результат ее выполнения определяется только входными параметрами. Функция гарантированно вернет тот же результат получив те же параметры.
Функция до сих пор содержит противный for
, так что продолжим рефакторинг. Следующим шагом переместим i
и newList
в параметры функции.
const takeFirst = (limit, predicate, list, i = 0, newList = []) => {
// ...
}
Мы хотим закончить рекурсию (isDone
) когда limit
достигнет 0
(limit
будет уменьшаться во время рекурсии) или когда закончится list
.
Если мы не закончили, мы выполняем predicate
. Если результат predicate
истинен, мы вызываем takeFirst
, уменьшаем limit
и присоединяем элемент к newList
.
Иначе берем следующий элемент списка.
const takeFirst = (limit, predicate, list, i = 0, newList = []) => {
const isDone = limit <= 0 || i >= list.length
const isMatch = isDone ? undefined : predicate(list[i])
if (isDone) {
return newList
} else if (isMatch) {
return takeFirst(limit - 1, predicate, list, i + 1, [...newList, list[i]])
} else {
return takeFirst(limit, predicate, list, i + 1, newList)
}
}
Последний наш шаг замены if
на тернарный оператор объяснен в моей статье Rethinking Javascript: the If Statement.
/*
* takeFirst работает как `filter`, но поддерживает ограничение.
*
* @param {number} limit - Максимальное количество возвращаемых соответствий
* @param {function} predicate - Функция соответствия, принимает item и возвращает true или false
* @param {array} list - Список, который будет отфильтрован
* @param {number} [i] - Индекс, с которого начать фильтрацию (по умолчанию 0)
*/
const takeFirst = (limit, predicate, list, i = 0, newList = []) => {
const isDone = limit <= 0 || i >= list.length
const isMatch = isDone ? undefined : predicate(list[i])
return isDone ? newList :
isMatch ? takeFirst(limit - 1, predicate, list, i + 1, [...newList, list[i]])
: takeFirst(limit, predicate, list, i + 1, newList)
}
Теперь вызовем наш новый метод:
const first5Kittens = takeFirst(5, isKitten, cats)
Чтобы сделать takeFirst
ещё полезнее мы могли бы её каррировать (прим. currying) и использовать для создания других функций. (больше о карировании в другой статье)
const first5 = takeFirst(5)
const getFirst5Kittens = first5(isKitten)
const first5Kittens = getFirst5Kittens(cats)
Итоги
Есть много хороших библиотек (например lodash, ramda, lazy.js), но будучи достаточно смелыми, мы можем воспользоваться силой рекурсии чтобы создавать собственные решения!
Я должен предупредить, что хотя takeFirst
невероятно крутая, с рекурсией приходит великая сила, но также и большая ответственность. Рекурсия в мире JavaScript может быть очень опасной и легко привести к ошибке переполнения стека Maximum call stack size exceeded
.
Я расскажу о рекурсии в JavaScript в следующей статьей.
Я знаю что это мелочь, но меня очень радует когда кто-то подписывается на меня на Медиуме и Твиттере @joelnet. Если же вы думаете что я дурак, скажите это мне в комментах ниже.
Связанные статьи
→ Functional JavaScript: Functional Composition For Every Day Use.
→ Rethinking JavaScript: Death of the For Loop
(есть перевод: Переосмысление JavaScript: Смерть for)
→ Rethinking JavaScript: Elliminate the switch statement for better code
→ Functional JavaScript: Resolving Promises Sequentially
Прим. переводчика: выражаю благодарность Глебу Фокину и Богдану Добровольскому в написании перевода, а также Джо Томсу, без которого перевод был бы невозможен.
Автор: crazymax11