javascript

Переосмысливая JavaScript: break и функциональный подход

  • воскресенье, 4 июня 2017 г. в 03:15:02
https://habrahabr.ru/post/330082/
  • JavaScript


Привет Хабр! Предлагаю вам статьи Rethinking JavaScript: Replace break by going functional.


image


В моей предыдущей статье 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 и неструктурированного программирования


image


"Но break никому не мешает, почему бы не оставить возможность его использовать?"


Почему следует ограничивать себя при разработке ПО?


Это может звучать нелогично, но ограничения это хорошая вещь. Запрет GOTO прекрасный тому пример. Мы также с удовольствием ограничиваем себя директивой "use strict", а иногда даже осуждаем игнорирующих её.


"Ограничения могут сделать вещи лучше. Намного лучше" — Чарльз Скалфани


Ограничения заставляют нас писать лучше.


Why Programmers Need Limits


Какие альтернативы у 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


Прим. переводчика: выражаю благодарность Глебу Фокину и Богдану Добровольскому в написании перевода, а также Джо Томсу, без которого перевод был бы невозможен.