image

Цикл for хорошо послужил нам, но он устарел и должен уступить место более новым, функциональным техникам программирования.

К счастью, этот факт не требует от вас быть мастером функционального программирования, более того, это то, что вы можете использовать в своих текущих проектах прямо сегодня!

Так в чем проблема цикла for в JavaScript?

Дизайн цикла for подталкивает к мутациям состояния (англ. mutation of state — изменение состояния — прим. переводчика) и применению сайд эффектов (англ. side effects — побочные эффекты — прим. переводчика), которые являются потенциальными источниками багов и непредсказуемого поведения кода.

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

Однажды, используя мутабельное состояние (англ. mutable state — изменяемое состояние — прим. переводчика), значение случайной переменной изменится по неизвестной причине, и вы потратите часы на отладку и поиск причины изменения. У меня волосы на голове встают дыбом, только от одной мысли об этом.

Я бы хотел немного поговорить о сайд эффектах. Эти слова даже звучат ужасно, сайд эффекты. Дрянь. Вы хотите чтобы в ваших программах были сайд эффекты? Нет, я не хочу сайд эффектов в моих программах!

Но что такое сайд эффекты?

Считается, что у функции есть сайд эффекты если она модифицирует что-то за пределами своей области видимости. Это может быть изменение переменной, пользовательский ввод, запрос к api, запись данных на диск, лог в консоль и т.д.

Сайд эффекты — действительно мощный инструмент, но с большой силой приходит большая ответственность.

Сайд эффекты, по возможности, должны быть устранены или хотя бы инкапсулированы так, чтобы мы могли их контролировать. Функции с сайд эффектами тяжелее читать и тестировать, избегайте их всегда, когда это возможно. К счастью, в рамках данной статьи, мы не будем беспокоиться о побочных эффектах.

Меньше слов, больше кода. Давайте посмотрим на типичный цикл for, который вы вероятно видели сотни раз.

const cats = [
 { name: 'Mojo',    months: 84 },
 { name: 'Mao-Mao', months: 34 },
 { name: 'Waffles', months: 4 },
 { name: 'Pickles', months: 6 }
]
var kittens = []
// Типичный, плохо написанный  цикл for
for (var i = 0; i < cats.length; i++) {
 if (cats[i].months < 7) {
   kittens.push(cats[i].name)
 }
}
console.log(kittens)

Я собираюсь отрефакторить этот код шаг за шагом, чтобы вы смогли пронаблюдать как легко превратить ваш собственный код в нечто более прекрасное.

Во-первых, я извлеку условное выражение в отдельную функцию:

const isKitten = cat => cat.months < 7
var kittens = []
for (var i = 0; i < cats.length; i++) {
 if (isKitten(cats[i])) {
   kittens.push(cats[i].name)
 }
}

Выносить условия — это в целом хорошая практика. Изменение фильтрации с “меньше чем 7 месяцев” на “является ли котенком” — большой шаг вперед. Теперь код передает наши намерения гораздо лучше. Почему мы берем котов до 7 месяцев? Не совсем понятно. Мы хотим найти котят, так пусть код говорит об этом!

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

Следующее изменение — извлечь преобразование (или отображение) кота его в имя. Это изменение будет понятнее позднее, а сейчас просто доверьтесь мне.

const isKitten = cat => cat.months < 7 
const getName = cat => cat.name
var kittens = []
for (var i = 0; i < cats.length; i++) {
 if (isKitten(cats[i])) {
   kittens.push(getName(cats[i]))
 }
}

Я собирался написать несколько абзацев для описания механики работы filter и map, но вместо этого, я покажу вам как легко читать и понимать этот код, даже увидев их (filter и map — прим. переводчика) впервые. Это лучшая демонстрация того, насколько читабельным может стать ваш код.

const isKitten = cat => cat.months < 7
const getName = cat => cat.name
const kittens =
 cats.filter(isKitten)
     .map(getName)

Обратите внимание, мы избавились от kittens.push(...). Никаких больше мутаций состояния и никаких var.

Код отдающий предпочтение const перед var и let выглядит чертовски привлекательно


Конечно, мы могли использовать const с самого начала, так как он не делает сам объект иммутабельным (об этом больше в другой раз), но это придуманный пример, не наседайте!

И последнее изменение, я бы предложил также извлечь фильтрацию и отображение в функцию (для полного переиспользования кода).

И все вместе:

const isKitten = cat => cat.months < 7
const getName = cat => cat.name
const getKittenNames = cats =>
 cats.filter(isKitten)
     .map(getName)
const cats = [
 { name: 'Mojo',    months: 84 },
 { name: 'Mao-Mao', months: 34 },
 { name: 'Waffles', months: 4 },
 { name: 'Pickles', months: 6 }
]
const kittens = getKittenNames(cats)
console.log(kittens)

Домашнее задание


Изучить извлечение методов filter и map из их объектов. Задание со звездочкой: исследовать композицию функций.


Что насчет break


Многие из вас спрашивали, “Что насчет break”, посмотрите часть вторую серии “Переосмысление JavaScript: break это GOTO циклов”.


Заключение


Пишите что вы думаете по этому поводу в комментариях. Умер ли для вас цикл for?

Для вас это мелочь, но меня очень радует когда кто-то подписывается на меня на медиуме или в твиттере (@joelnet), а если вы считаете мой рассказ бесполезным, то расскажите об этом в комментариях.

Спасибо!
Поделиться с друзьями
-->

Комментарии (157)


  1. token
    18.05.2017 16:58
    +7

    «Пишите что вы думаете по этому поводу в комментариях. Умер ли для вас цикл for?» — конечно уже давно умер, мы уже одними точками с запятыми пишем.


    1. justboris
      18.05.2017 18:12
      -3

      Более того, точки с запятыми тоже писать не надо. Видали?


  1. k12th
    18.05.2017 16:59
    +14

    Я сомневаюсь, что внутри несомненно любимого автором функциональненького React найдется так много map/reduce вместо циклов. Проблема в том, что безблагодатные императивные циклы существенно быстрее красивых трансдьюсеров, а, значит, в узких местах будут применяться они.


    break это не goto, он не отправляет тебя в неизвестное произвольное место.


    1. GlebkaF
      18.05.2017 17:08
      -2

      В узких местах — возможно, но мы каждый день пишем огромное количество кода, который не является узким местом. Интерфейс, скорее всего, не будет лагать если мы напишем map вместо for.
      Конечно, глубоко внутри все сплошная императивщина, но ее уже написали для нас, давайте этим пользоваться и писать красивый, декларативный код :)


      1. k12th
        18.05.2017 17:11
        +8

        Конечно, я тоже люблю красивый код. Просто писать "смерть for" как-то тупо.


    1. Politura
      19.05.2017 02:16

      Вот интересно, почему map/reduce медленнее циклов? Ведь любой map можно разложить в эквивалентный ему цикл for, так почему его реализация столь медленна?
      Теоретически можно транслировать map в for на лету, при загрузке модуля, не затратив практически ничего, если вдруг нет возможности изменить реализацию map.


      1. k12th
        19.05.2017 02:32
        +1

        Вот интересно, почему map/reduce медленнее циклов?

        Да просто вызов функции это относительно дорого.
        А еще все эти map/reduce/forEach делают кучу дополнительных проверок на каждом шагу, потому что должны работать с разреженными массивами, с массивами, в которых кто-то напихал кастомных свойства и прочими граничными случаями. Я не помню, как называлась либа, но там были на порядок более быстрые реализации всего этого дела — но которые работали только с «нормальными» массивами.


        Ведь любой map можно разложить в эквивалентный ему цикл for
        Теоретически можно транслировать map в for на лету

        Мне кажется, это возможно только если итератор — чистая функция...


        Ну ладно, map мы разложили в for автоматом, а если гений типа автора написал filter().map()? В общем, по хорошему, нужна поддержка частых ФП-паттернов, всяких там трансдьюсеров, и, возможно, у авторов движков дойдут до этого руки, если это станет достаточно частым сценарием.


        1. GlebkaF
          19.05.2017 04:33
          -2

          В Ramda map имеет одинаковую с for производительность.


          1. raveclassic
            19.05.2017 10:27
            +1

            А замеряли как? jsperf?


            1. GlebkaF
              19.05.2017 18:13

              Вот такой получился наивный тест :)


              1. raveclassic
                19.05.2017 18:17

                Вопрос вообще-то был риторический


      1. lekzd
        19.05.2017 10:14

        эти функции в движке написаны на JS, все дело в этом, цикл for, к слову, тоже можно затормозить неожиданными вещами, например в v8 недавно пофиксили let в цикле


      1. Aingis
        19.05.2017 13:18
        +2

        Вот интересно, почему map/reduce медленнее циклов? Ведь любой map можно разложить в эквивалентный ему цикл for, так почему его реализация столь медленна?

        Всё дело в спецификации, которая не идентична циклу for. Если сделать именно через цикл, как, например, в fast.js или lodash.js, то будет примерно так же быстро (остаются ещё накладные расходы на вызовы функции).


        А спецификация говорит, что map/foreach и аналоги должны пропускать так называемые «дырки». «Дырки» — это такие ключи, которые не инициализированы.? Например, new Array(10) не содержит ни одного ключа, и поэтому не вызовет обработчик ни разу. Из-за этого реализация методов в браузерах должна на каждом шаге проверять, а есть ли такой ключ.


        ? Если к ним обратиться, то будет undefined, но не стоит путать с тем случаем, когда значение установлено в undefined.


  1. SirEdvin
    18.05.2017 17:06
    +11

    1. Плохо не глобальное состояние, а глобальные переменные. Глобальным состоянием можно управлять, держать в одном месте и оно в целом может быть неизменяемым или мало изменяемым. С переменными так не получится.
    2. Причем тут ФП к циклу for? Side эффект от цикла for — это только переменная цикла, которая окажется снаружи.
    3. Первый пример — чем он лучше?

    const isKitten = cat => cat.months < 7
    const getName = cat => cat.name
    const kittens =
     cats.filter(isKitten)
         .map(getName)

    У вас тут:


    1. Две лишних lambda функции
    2. Вместо одного цикла, у вас их тут два (в зависимости от реализации, но скорее всего).
      И в чем выгода?

    Пропаганда ФП это круто, но ФП далеко не везде и не всегда влазит.


    Also, goto так не плох, плохо его неконтроллирумое использование в коде. Просто он позволяет слишком много.
    Ну а break плох только в запутанных внутренних циклах с кучей логики, но такие циклы всегда выглядят или плохо, или не оптимизировано.


    1. GlebkaF
      18.05.2017 17:18
      -1

      В ФП for либо не используется, либо используется крайне редко, ведь что угодно можно сделать через reduce.

      Лишние лямбды кушать не просят, а если вас волнуют лишние циклы используйте трансдьюсеры.

      ФП — инструмент, выбирайте инструмент исходя из задачи. Вопрос применимости конкретных инструментов обсуждался уже тысячу раз :)


      1. SirEdvin
        18.05.2017 17:26
        +2

        Лишние лямбды кушать не просят, а если вас волнуют лишние циклы используйте трансдьюсеры.

        Очень зависит от. Но в целом лишние лямбды всегда едят больше, чем просто условия и циклы. И они не поддаются оптимизации.


        В Android появилась эта странная мода на retrolambda, которые еще очень как тормозят. Они проигрывают где-то в 20 раз по скорости обработки вызовов.


        1. GlebkaF
          18.05.2017 17:36
          -2

          Конечно зависит, сильно зависит. Но я, в своем фронтенде, в 99% случаев готов идти на такие компромиссы. Я пока не сталкивался с вещами, где несколько лишних функций сыграли бы хоть какую-то роль, хотя не отрицаю того что такие вещи где-то есть.


          1. SirEdvin
            18.05.2017 17:39
            +4

            А потом мы получаем сайты, которые поедают все CPU и грузятся по пять минут.
            Дело не в нескольких лишних функций, дело в подходе.
            "Несколько" лишних функций в большом проекте превращаются в 500-1000 и это уже заметно везде.


            1. GlebkaF
              18.05.2017 17:50
              -4

              Остановитесь, сайты жрут все cpu и грузятся долго не потому что ФП, map, filter или reduce, а потому что разработчик раздолбай. Говно код можно писать в любой парадигме и на любом языке.
              А вот что бы на количестве функций экономить, это я от вас первый раз услышал рекомендацию, спасибо :)


              1. SirEdvin
                18.05.2017 18:17
                +3

                Остановитесь, сайты жрут все cpu и грузятся долго не потому что ФП, map, filter или reduce, а потому что разработчик раздолбай. Говно код можно писать в любой парадигме и на любом языке.

                Говнокод обычно заметно, и с ним можно боротся. А вот боротся с выбранным уровнем абстракции нельзя.
                Каждый дополнительный уровень сверху — это оверхед. ФП такой же оверхед, который может привести к увеличению времени работы от 2 до 100 раз, если вычисления не ленивые, что вполне возможно.


                1. justboris
                  18.05.2017 18:27

                  Иногда бывает и наоборот. Допустим, есть какая-то операция со списком. Через некоторое время нужно сделать еще одну почти такую же, но с некоторыми изменениями.


                  В случае цепочки map/filter переиспользовать часть коллбеков намного проще, чем в императивном коде с for, где все написано единым куском кода.


                  1. SirEdvin
                    18.05.2017 20:05
                    +1

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


                    Другое дело, что ФП штуки работают гораздо лучше и проще в случае работы с данными, но, в языках, в которых они для этого нужны.
                    Если у вас работа с данными не основная часть (а для сайта это врядли так), то я сомневаюсь, что ФП покажет себя лучше.


                    1. di-strange
                      19.05.2017 10:43

                      Пример в статье слишком надуманный, чтобы отразить необходимость использования ФП. И пока никто не привел реального примера ФП где он бы подошел. И странно как то выглядит, что сначала обругали jQuery с его $(selector).each(...).otherfunc(...), а потом пришли к .filter(...).map(...)


              1. vanxant
                18.05.2017 19:43
                +4

                ФП сам по себе может быть быстр, очень быстр. Но вот ФП в браузерном JS, особенно когда куча функционала реализована в либах, а не в браузере…
                Представьте, что у вас на входе 10 000 котят. Миллион котят. Для цикла for нет вообще никакой проблемы вывести не всех, а только текущих видимых, в позициях с 12345-ого по 12435-го.
                Что там у вас внутри filter/map и как это оптимизировать — а крен его знает.


                1. Mycolaos
                  19.05.2017 02:16

                  Array.slice?

                  http://stackoverflow.com/questions/3978492/javascript-fastest-way-to-duplicate-an-array-slice-vs-for-loop

                  Кстати, специально пробовал заменить while на for — в 3 раза медленней.


                1. raveclassic
                  19.05.2017 10:48

                  А если попробовать за-map/reduce-ить котят в вебворкеры? Штук по 10к-100к для миллиона? For-то все-равно быстрей будет, но так для интереса? Хотя, наверное большую часть времени займет доставка котят до воркера и обратно.


      1. JSmitty
        20.05.2017 07:19

        Классически замена циклов — не reduce, а рекурсия. И отсутствие TCO в JS этому приему не дает возможности эффективно применяться.



    1. vasIvas
      18.05.2017 18:10
      +1

      Я на днях статью увидел где на картинке фп стоит выше чем ооп и так же автор уверял что фп это новая парадигма 90 года. То есть фп пропаганда идет ну очень сильно и если раньше казалось что это откидывает людей назад, то сейчас мне кажется что это даже лучше, ведь как ещё научить людей писать правильный ооп, который по факту впитал фп и сделал его ещё лучше. Любой грамотный ооп-шник скажет что в ооп все тоже самое, но вот люди начали это осваивать только тогда, когда им начали подавать это на чистом подносе-фп. Возможно так им легче.
      Только мне не понятно почему до сих пор не начали говорить что код в статье это не фп, ведь в фп вообще нет переменных, а то фп где есть переменные, это ооп. В фп не может быть объявлений const, var, или let.


      1. GlebkaF
        18.05.2017 18:15
        -1

        Код статьи написан в функциональном стиле, т.е. не мутирует переменных и использует чистые функции.
        Действительно, основополагающие принципы у обеих парадигм одинаковые, но их реализация имхо ортогональна, поэтому говорить что ООП впитал ФП не совсем корректно
        p.s. А как бы вы переписали код из статьи на JS?


      1. SirEdvin
        18.05.2017 18:19

        Переменная объявленная как неизменяемая вряд ли переменной остается.


        Так же лисп функциональный, но переменные в нем все равно есть


        1. Ciiol
          19.05.2017 02:17

          Не назвал бы лисп прямо таким функциональным (мы же о Common Lisp?). Да, присваиваний в нормальном коде не увидишь, но и функциями типа map/reduce пользоваться там не слишком удобно.

          Зато, возвращаясь к теме статьи, в нём самый шикарный цикл for из всех языков, что я видел (точнее конструкция loop и библиотека iter как развитие этой идеи). Там решение задачи типа «найти такой элемент массива A, что функция F от следующего за ним элемента примет максимальное значение» будет выглядеть примерно как сам текст этой задачи. В чистом ФП же тут будет жонглирование zip, map, filter, etc. А в условном C или императивном js будет куча вспомогательных переменных и манипуляций с ними. Оба варианта как-то так себе.


      1. wheercool
        18.05.2017 18:24

        Можно поинтересоваться почему в ФП не может быть const и let?
        Const в них используются неявно, а let есть ничто иное как псевдоним, для некоторого выражения, наподобии того как в математики вводят дополнительные переменные


        1. raveclassic
          18.05.2017 20:10

          а то фп где есть переменные, это ооп
          в математики вводят дополнительные переменные
          Видимо, математика — это ооп. =/


  1. Akon32
    18.05.2017 17:10
    +3

    const isKitten = cat => cat.months < 7
    const getName = cat => cat.name
    const kittens = cats.filter(isKitten).map(getName)

    Красиво, но, вероятно, раза в 2 медленнее. Критичные части приходится переписывать в императивщину.


    1. TheShock
      18.05.2017 17:16
      +2

      На самом деле медленнее значительно больше, чем в два раза, но все-равно линейно, потому в большинстве программ, которые пишут на ФП (а на нем все-равно ничего сложного написать нельзя) — это не критично.


    1. saluev
      18.05.2017 18:25

      В написанном хорошо коде переписать узкие места более эффективно можно в любой момент (особенно заменить мапы и фильтры на циклы). Преждевременная оптимизация — корень всех зол.


      1. Regis
        18.05.2017 19:08
        +6

        Преждевременная оптимизация — корень всех зол.

        Эх, как же надоела эта вырванная из контекста цитата.


        1. YourDesire
          23.05.2017 14:12
          +2

          Прошу прошения. Не подскажете, из какого контекста эта фраза?


          1. staticlab
            23.05.2017 16:07
            +1

            There is no doubt that the grail of efficiency leads to abuse. Programmers waste enormous amounts of time thinking about, or worrying about, the speed of noncritical parts of their programs, and these attempts at efficiency actually have a strong negative impact when debugging and maintenance are considered. We should forget about small efficiencies, say about 97% of the time: premature optimization is the root of all evil.

            Yet we should not pass up our opportunities in that critical 3%. A good programmer will not be lulled into complacency by such reasoning, he will be wise to look carefully at the critical code; but only after that code has been identified. It is often a mistake to make a priori judgments about what parts of a program are really critical, since the universal experience of programmers who have been using measurement tools has been that their intuitive guesses fail. After working with such tools for seven years, I've become convinced that all compilers written from now on should be designed to provide all programmers with feedback indicating what parts of their programs are costing the most; indeed, this feedback should be supplied automatically unless it has been specifically turned off.

            https://www.cs.sjsu.edu/~mak/CS185C/KnuthStructuredProgrammingGoTo.pdf


            1. Regis
              26.05.2017 04:08

              Подчеркну, что здесь Кнут говорит не "забейте на заботу о производительности до тех пор, пока не окажется, что у вас всё тормозит" (как это обычно подразумевают цитирующие), а "заботьтесь о производительности там, где это важно и даст наибольший эффект".


      1. SirEdvin
        18.05.2017 20:06

        Поэтому, вместо вызова у списка метода sort вы каждый раз пишите два цикла сортировки вставкой?


        1. saluev
          19.05.2017 09:44

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


          1. SirEdvin
            19.05.2017 10:46
            +1

            Но ведь вы не пишите сначала свою сортировку пузырьком, что бы потом менять ее в хорошо написанном коде, правильно?


            Если мы знаем, что у нас каждая такая цепочка порождает еще один цикл и мы получаем переизбыток циклов и не особо то и выигрываем в читаемости — зачем?


            1. saluev
              19.05.2017 11:22

              Ну, на мой вкус, в читаемости мы выигрываем весьма ощутимо.


              1. SirEdvin
                19.05.2017 11:25

                Уже приводили контрпример немного ниже.
                Как только названия фильтров начинают превращатся в "isValid" или что-то такое, то все превращается в кашу. Далеко не всегда по фильтру можно понять, что конкретно он делает, а в случае с циклом for + if практически всегда.


                А если так не делать, то получается гиганская цепочка фильтров, которую тоже сложно читать.


      1. Akon32
        19.05.2017 14:22

        Константирую тот факт, что в иных проектах узкие места встречаются столь часто, что даже написание сразу циклов не будет преждевременной оптимизацией.
        Но, естественно, если котят в списке значительно меньше десятков миллионов, пара лишних созданных в map-filter массивов не сильно повлияют на скорость.


  1. TheShock
    18.05.2017 17:14
    +3

    Хоронили if, порвали три редакса.

    cats.filter(isKitten).map(getName)
    


    Ну сколько можно то? Еще примитивней пример нельзя привести? Или только для таких основ и годятся все эти тренды? Да, на однострочниках ФП выигрывает. Ну вот на однострочниках его и можно применять. А чем длинеее — тем более трудноподдерживаемым код становится


  1. Drag13
    18.05.2017 17:17
    +7

    Да да, For умер, While разложился А Do While вообще не рождался. For имеет свою область применения. И никуда оттуда не денется.


    1. quwy
      19.05.2017 01:12
      +2

      Спокойно, просто хипстера несут «истину» в массы. В перерывах между походами в барбершоп и покатушками на моноколесах.


  1. Drag13
    18.05.2017 17:25
    +4

    Вообще проблема .map и прочей функциональности (как по мне конечно) что код исполняется «где то там». И вместо того что бы сходу посмотреть что происходит в цикле, нам нужно прыгать по анонимным методам и искать там… Для однозначных вещей это ок, но когда задачи идут менее тривиальные все становится банально не удобным.


    1. wheercool
      18.05.2017 17:47

      А я вот противоположной точки зрения
      Глядя на композицию map, filter, reduce можно сразу выделить структуру даже не вникая в детали (банально если заканчивается reduce — значит результат одиночное значение, иначе — массив). С циклом же нужно полностью изучить весь код, чтобы выявить какую-то структуру.


      1. Drag13
        18.05.2017 18:06

        А зачем вам структура? Структура описана или классом или интерфейсом. И посмотреть ее можно и так. А вот что там происходит это нужно дебажить… А дебажить чистые циклы проще, лично мне. Но тут разговор не об этом. .filter((cat)=>cat.isBlack ) мне гораздо больше нравится чем for(){}. Но при этом я не рискну утверждать что for умер.


        1. wheercool
          18.05.2017 18:13

          Да я ничего и не имею против for :)
          На самом деле он вполне себе декларативен и если присмотреться то можно в нем увидеть монвду list в do нотации (особенно в for of)


          1. Drag13
            18.05.2017 18:25

            Вот и сошлись :)


  1. Aingis
    18.05.2017 17:55
    +10

    Я рад, что автор открыл для себя .map() из ES5, он действительно удобнее for, но это 2011 год. Если он изучил ещё и .reduce(), то смог бы обойтись одним проходом вместо двух.


    Но зачем публиковать это сейчас, в 2017, на Хабре? Да ещё с таким ужасным переводом. Есть русский термин «побочный эффект». Он не «применяется», а появляется.


    Тем временем в ES2015 появилась разновидность цикла for-of, которая гораздо элегантнее .foreach() со вложенной функцией (когда отдельная функция не нужна). С учётом этого говорить о смерти for может только невежда.


    апофеоз перевода
    мутабельное состояние (англ. mutable state — изменяемое состояние — прим. переводчика)

    Что мешало сразу написать «изменяемое состояние»?


  1. AlexZaharow
    18.05.2017 17:57
    +1

    Очень интересно посмотреть на выход из map по break?


    1. wheercool
      18.05.2017 18:05
      +2

      Смысл map в том что производится трансформация над каждым элементом (можете считать что это групповая операция) и кол-во элементов на входе и на выходе обязано быть одинаковым


    1. SirEdvin
      18.05.2017 20:07

      По идее, нужно просто фильтр перед map сделать для такого.


      1. TheShock
        18.05.2017 20:10
        +2

        А если брейк по результатам вычисления, а не до результатов?


        1. SirEdvin
          18.05.2017 20:12

          Сформулируйте задачу детальнее, пожалуйста.
          В случае, если ФП сделано правильно, через ленивые вычисления, то можно сделать фильтр после. Что-то в духе:


          map(тут ваши вычисления).filter(тут ваше условие выхода).any()


          Должно помочь получить нужные данные.


          Это если брать задачу "нужно найти первый такой элемент в цикле". Если задача другая, то надо будет выкручиватся по другому.


          1. TheShock
            18.05.2017 20:14

            Хорошо, давайте обсудим ваше условие — «нужно найти первый такой элемент в цикле». Ну или «найти максимум 5 элементов, удовлетворяющих результатам поиска».

            В случае, если ФП сделано правильно

            А если ФП у нас JS?


            1. gearbox
              18.05.2017 20:21

              Да и в случае правильного fp есть ряд задач плохо решаемых редьюсерами. 21 очко на колоде карт?


              1. TheShock
                18.05.2017 20:24

                21 очко на колоде карт?

                Объясните


                1. gearbox
                  18.05.2017 20:45
                  +1

                  колода карт, перетасована, надо выбирать карты пока не выпадет 21 очко или больше. Редьюсер пойдет по всему массиву а нужен выход. Сделать фильтр не получится — такой же полный перебор.


                  1. Large
                    23.05.2017 02:36
                    -2

                    для этого есть трансдьюсер


                    1. gearbox
                      23.05.2017 15:11
                      -3

                      вы просто новое слово выучили или реально понимаете о чем говорите? Трансдьюсер — это грубо говоря генератор редьюсеров. Какой редьюсер он должен дать что бы решить эту задачу ОПТИМАЛЬНО без выхода? С учетом того что редьюсер не должен выходить, если он выходит — это уже обычный цикл в функциональной обертке. То есть изначально вы не решите ОПТИМАЛЬНО задачу редьюсером если она ОПТИМАЛЬНО решается неполным проходом. Редьюсер по своей природе подразумевает полный проход. Если для решения задачи полный проход не нужен — значит для решения этой задачи не нужен редьюсер.


                      1. Large
                        23.05.2017 15:17
                        -2

                        фу как грубо! а не грубо трансдьюсер это мидлвар который может выходить когда нужно и никакого полного прохода не требуется.


                        1. gearbox
                          23.05.2017 15:35
                          -3

                          >это мидлвар который может выходить когда нужно и никакого полного прохода не требуется.

                          В этом момент мы прекращаем назвать его или результат его работы редьюсером и не нарываемся на грубости на хабре. Если вам нужен фолдинг — делаем редьюсер и делаем полный проход. Если полный проход не нужен — делаем рекурсию или итерацию но не называем это фолдингом. Либо называем фолдингом  но тогда меняем структуру и берем не массив а список, в котором после получения результата не применяем редьюсер к остатку. На массиве редьюсер подразумевает полный проход, изначально мы говорили за массив. Для преобразования его в нужную структуру нужно применить логику в которую и перекочует логика выхода (трансформированная но тем не менее) которую вы пытаетесь запихнуть в редьюсер.


                          1. Large
                            23.05.2017 15:41
                            -3

                            Грубости они не от хабра, а от воспитания. Вас так учили, нас по другому. Я про редьюсер вам вообще ничего не говорил.


                            1. gearbox
                              23.05.2017 16:58
                              -3

                              >Я про редьюсер вам вообще ничего не говорил.

                              Вы говорили про трансдьюсер. Трудно упомянуть трансдьюсер и не ввести при этом в контекст обсуждения редьюсеры.

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

                              То есть имеем:

                              f(a) {
                              return a == 0? 0: a + f(a-1);
                              }

                              это нехвостовая рекурсия, не соптимизируешь. Выносим вычисления в передачу аргумента:

                              f(a, prev){
                              return a == 0? prev: f(a-1, a+prev);
                              }

                              внезапно рекурсия стала хвостовой а функция — редьюсером. Для этого финта надо иметь нулевой элемент определенный на последней операции, для сложения это ноль. Значит первый вызов будет
                              func(a, 0);
                              Все, теперь можно делать фолдинг на ком угодно, это нормально оптимизится.


                              1. Large
                                23.05.2017 17:35
                                -4

                                в немутабельном мире наверное, а так это всего лишь способ сделать свертку на полугруппе.


                                1. gearbox
                                  23.05.2017 17:56
                                  -3

                                  >в немутабельном мире наверное

                                  ? У вас мир меняется пока вы его сворачиваете? ) (это риторический вопрос)


                      1. Large
                        23.05.2017 15:23
                        -3

                        вот пример из рамды http://ramdajs.com/docs/#transduce take(2) выходит после 2х элементов.


                        1. TheShock
                          23.05.2017 15:34
                          -3

                          А пример из Рамбды, когда выходит после N элементов, ума которых больше 20?


                          1. Large
                            23.05.2017 15:42
                            -2

                            Паша, зачем тебе пример из Рамды? Я думаю ты сам можешь написать функцию, которая вместо проверки длинны массива результатов выходит при другом условии.


                            1. TheShock
                              23.05.2017 15:52
                              -2

                              Та я ж тут чисто достаю всех, а сам даже FizzBuzz не напишу)

                              Ты предлагаешь такую функцию написать в императивном стиле с умершим for?


                              1. Large
                                23.05.2017 16:00
                                -2

                                Да, ты тот еще… кодер ))

                                Да нет, ее можно в функциональном написать, просто нужно еще состояние добавить аргументом (текущую суму), а реализацию трансдьюсера через фор делать, так быстрее.

                                Статьи эти смешные конечно, они на самом деле не о функ программировании а как-то вообще не о чем. Функ программирование по моему это скорее про структуры данных, тут ни слова о функторах и фолдебл, зато много пафоса про фор мертв.

                                Из реальных примеров все что генерируется компилятором удобнее писать в функ стиле так как оно тупо менее многословно и подчиняется простым правилам. А вот реализация этого функ стиля уже делается этими самыми мертвыми фор =)


              1. GlebkaF
                19.05.2017 04:38

                В правильном fp функция reduce умеет проверять у себя флаг reduced и выходить когда ей потребуется.


                1. raveclassic
                  19.05.2017 10:27
                  +1

                  Покажите, как вы из reduce выходите. И объясните, что значит выйти из reduce, если идеологически reduce подразумевает проход по всем элементам?


                  1. faiwer
                    19.05.2017 11:36
                    -2

                    Да вариантов много, зависит от взглядов автора библиотеки. К примеру в lodash есть transform. Это такой мутабельный reduce, которые помимо мутабельности итогового значения умеет выходить из цикла за счёт return false.


                    1. raveclassic
                      19.05.2017 11:38
                      +2

                      Но, тем не менее, это ж не reduce, я к этому подводил :)


                  1. GlebkaF
                    19.05.2017 12:23

                    Откуда информация что reduce должен проходить обязательно по всем элементам коллекции?
                    Вот map — да, подразумевает обход всех элементов.
                    reduced в clojureScript, по большому счету — дополнительный внутренний флаг isReduced по которому происходит выход из reduce.


                    1. raveclassic
                      19.05.2017 13:01

                      Ну мы же на JS пишем. Плюс, reduced в кложе выглядит как in-place оптимизация, в других языках я такого не припомню (вы можете меня поправить).
                      Из MapReduceconsists of folding all available b:B to a single value.
                      Ну и reduce иначе зовется foldat each node


            1. SirEdvin
              18.05.2017 20:21
              +2

              А если ФП у нас JS?

              Я думаю, вам сюда. Как я понял, ленивые вычисления в фп называются трансдьюсеры и точно что-то такое там есть. К сожалению, я знаком с такой штукой для Java, которую они добавили в восьмой версии.


              В случае, если использовать их синтаксис, задачи будут выглядит так:


              stream.filter(x => x.state == "ok").findAny()

              Второе будет выглядить так:


              stream.filter(x => x.state == "ok").limit(5)


              1. TheShock
                18.05.2017 20:24
                -1

                Симпатично.

                К сожалению, я знаком с такой штукой для Java, которую они добавили в восьмой версии.

                Почему «К сожалению»?


                1. SirEdvin
                  18.05.2017 20:25

                  Это относить к предыдущей фразе, в которой я говорил, что не знаю такие штуки для JavaScript)


                  1. someone328
                    19.05.2017 16:19

                    RxJS


              1. staticlab
                18.05.2017 23:13

                А как выглядел бы трансдьюсер limitBySum?



      1. AlexZaharow
        18.05.2017 23:06

        т.е. в for фильтр был не нужен, теперь в статье предлагается похоронить for и использовать вместо него map, но вот загвоздка — как жить без фильтра, которого в for не было?
        Думаю, что вполне можно было бы ограничиться сведениями, в которых map хорош и удобнее, чем for, но в задачах определённого класса, в которых конструкция for достаточно громоздка.
        Мне вот не очень понятно, зачем вводить map в стандарт, когда эту функцию можно написать в прототип массива и без всяких стандартов и выглядеть она будет точно так же?


    1. JSmitty
      20.05.2017 06:50

      У автора оригинальной статьи есть продолжение — Rethinking JavaScript: Replace break by going functional


      1. TheShock
        20.05.2017 07:03
        +3

        У автора талант из нормального кода писать неподдерживаемое говнище. Кто еще не заходил — вам загадка. Что делает эта функция и как она могла бы быть названа? Только засеките время на разгадывание этого ребуса.

        const FUNC_NAME = (limit, predicate, list, i = 0, newList = []) => {
            const isDone = limit <= 0 || i >= list.length
            const isMatch = isDone ? undefined : predicate(list[i])
          
            return isDone  ? newList :
                   isMatch ? FUNC_NAME(limit - 1, predicate, list, i + 1, [...newList, list[i]])
                           : FUNC_NAME(limit, predicate, list, i + 1, newList)
        }
        


        А потом сравните с промышленным, а не хипстерским кодом:

        Скрытый текст
        const FUNC_NAME = (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
        }
        


        1. faiwer
          20.05.2017 08:51
          +2

          Боже ж ты мой. Главное чтобы это потом не приснилось. Второй кусок кода предельно очевиден: отфильтровать из list только то, что проходит predicate, но не более limit элементов. А первое?


          • Рекурсия… Представим что limit около 10'000… и всё это в стек. Кошмар
          • [...newList, list[i]] в рамках рекурсии для аккумулируемого значения. Какая там асимптотика будет у такого решения? O(n^2)?
          • Даёшь тернарный оператор внутри тернарного оператора, чтобы ты мог написать терна...

          Я вот не могу понять. Это издержки фанатизма? Или человек действительно не понимаешь, что за дичь он пишет? А ведь потом это ещё и переводят, т.е. это "популярно" и "востребовано".


          1. MikailBag
            25.05.2017 22:03

            Именно рекурсия не проблема, т.к. соптимизируется.
            Но это, конечно, не отменяет превышение нормы СЭС по показателю hipsterity/line)


            1. faiwer
              26.05.2017 06:49
              -2

              const a = new Array(10000); 
              for(let q = 0; q < 10000; ++ q) a[q] = Math.random()
              const predicate = a => a > 0.5;
              FUNC_NAME(10000, predicate, a)
              // Uncaught RangeError: Maximum call stack size exceeded


              1. MikailBag
                26.05.2017 20:38
                +1

                В ES2015 ввели правильную хвостовую рекурсию.
                Только что проверил на массиве из 10000 элементов.
                node 7.7.2


                1. faiwer
                  26.05.2017 21:17
                  -2

                  Хм, странно, но я запустил тот же самый код на v7.7.4 и снова получил RangeError: Maximum call stack size exceeded. Может быть я неправильно понимаю суть хвостовой рекурсии?


      1. AlexZaharow
        22.05.2017 14:55

        Но в этом продолжении нет выхода из map в любой момент времени. Я легко выдумываю условие выхода — если текущее время больше 14:00:00 (часы: мин: сек), то выйти из цикла. Все фильтры летят в мусорную корзину, потому что если в работе обход массива занимает 10 сек и я запущу фильтр в 13:59:45, то в конечный массив попадут все элементы массива. Затем я запускаю перебор в 13:59:59 и получаю полный перебор массива до 14:00:09, хотя он должен был остановиться в 14:00:00 по break.


        1. raveclassic
          22.05.2017 17:02

          нет выхода из map в любой момент времени
          А это идеологически неверно. Map — проекция всех элементов, и выхода из нее нет. Если нужен выход, значит не нужен map.


          1. AlexZaharow
            22.05.2017 22:57
            +1

            Бинго. Я же раньше и написал, что map решает свои задачи, которые только частично перекрываются с for. Поэтому вроде как смерть for откладывается? )))


            1. raveclassic
              22.05.2017 23:13

              Упс, только сейчас заметил


  1. YuryZakharov
    18.05.2017 18:01
    +1

    Пост — отличная лакмусовая бумажка!
    Очень показательно, спасибо.


  1. Maiami
    18.05.2017 18:09
    +2

    О каком отсутствии сайд эффекта можно говорить, если я могу случайно сделать вот так:

    const isKitten = cat => cat.months = 7
    

    И никакой const не поможет

    Поэтому в условиях невозможности гарантировать отсутвие сайд-эффекта, можно только договорится, что его не будет. Но в таком случае разница между
    const getKittenNames = cats =>
        cats.filter(isKitten)
            .map(getName)
    

    И
    const getKittenNames = cats => {
        const kittens = []
        for (let cat of cats) {
            if (isKitten(cat)) {
                kittens.push(getName(cat))
            }
        }
        return kittens
    }
    


    Только в количестве строк и скорости выполнения. Или в читаемости, кому-то больше filter-map/reduce нравится, а кому-то for-of


    1. JSmitty
      20.05.2017 06:45
      -2

      Код существует не в вакууме обычно, поломка такого вида выявляется юнит-тестом сразу же (из isKitten() возвращается уже не boolean). Если его нет — существуют Immutable структуры данных (одно решение тянет за собой цепочку других).


  1. cjbars
    18.05.2017 18:33

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

    Если я хочу число пи, то ИМХО должно быть const pi = 3.14; и все это константа!

    Может я чего то не понимаю?


    1. Drag13
      18.05.2017 18:35
      +1

      В JS функции объекты первого порядка. Т.е. грубо говоря такие же как int или string в других языках. Так что const function вполне себе ничего.


      1. cjbars
        18.05.2017 18:55
        +2

        Ну ок, с этим более менее согласен.

        Попробую побухтеть про читабельность.

        Вариант с циклом: пройтись по всем котам, если возраст кота меньше заданного (вот тут бы константу) — запомнить имя кота.

        Вариант без цикла: пройтись по всем котам, и если кот удовлетворяет условию(сходить узнать условие) то пометить кота. Затем взять помеченных котов и сделать с ними что-то ( надо сходить куда-то за действием). Результатом будет список котят.

        Чот как то много бегать придется, и выше справедливо заметили про сложность прерывания данного процесса.

        Но это я так бухчу ;-)


        1. Drag13
          18.05.2017 18:59
          +2

          Почему «бухчу»? Абсолютно справделивые замечания. Поэтому вся функциональная парадигма должна быть использована в правильном месте и будет все прекрасно.
          Но нельзя не отдать должного что она приучает к Single Responsibility в функциях, что, как по мне, просто прекрасно.


          1. cjbars
            18.05.2017 19:01
            +1

            Вот прям полностью с вами согласен :-)


        1. GlebkaF
          18.05.2017 19:14

          Вся прелесть в том, что при правильном именовании функций и переменных вам не нужно идти и изучать детали реализации. Вот же, прямо в коде написано, отфильтруй по признаку isKitten, а затем получи имя.
          Это скорее непривычно поначалу, записи типа

          const sum = a => b => a + b
          console.log(sum(2)(3))
          

          мне первое время давались с трудом, а теперь жить без них не могу :)


          1. cjbars
            18.05.2017 19:24
            +3

            Но тут внезапно всплывает известная проблема — как назвать эту чертову функцию? :-)


          1. TheShock
            18.05.2017 19:39
            +2

            Это только вначале, когда у вас примеры уровня isKitten. А потом вы делаете

            var validUsers = users.map(isValid);
            


            Но isValid, оказывается, проверяет user.name !== 'Vasek', а не user.access.contains(ADMIN). Но вы слишком горды, чтобы пользоваться типизацией, вы ведь модны и молодежны, а типизация — для старперов и теперь только Васек и не имеет доступ к вашей админ-панели.

            А отвратительные названия — это одно из проклятий ФП в ЖС. Ведь canUserAccessAdminPanel пишут только джависты, по-молодежному надо написать is_ok.

            Вот некоторые названия функций из модных и молодежных библиотек:
            - pipe
            - it
            - connect
            - put
            - dispatch


            Да-да, это именно те «емкие и понятные» названия, в которые не нужно заходить, чтобы понять, что они делают. А ведь это библиотечные. А еще сколько будет локальных для приложения.


  1. Nicklasos
    18.05.2017 18:41
    +7

    Как использовать map/forEach/filter вместе с async/await?


    for (const foo of bar) {
       const result = await doSomethins(foo);
    }


    1. JSmitty
      18.05.2017 21:29
      -2

      return bar.reduce((r, v) => r.then(doSomething.bind(null, v)), Promise.resolve());
      


      PS после транспайлинга это будет выглядеть (и работать) лучше, чем async/await. Прямая противоположность ситуации с for :)


      1. JSmitty
        20.05.2017 06:28

        Пример для последовательного выполнения. Напоминаю, что в JS async/await — всего лишь сахар поверх промисов. И да, если кто не в курсе, генераторы (через который выражается async функция в Babel) порождают очень странный код в конечном ES5.


    1. IIIEII
      19.05.2017 00:40

      Примерно так:

      const result = await Promise.all(bar.map(doSomething)
      

      В качестве бонуса — параллельное выполнение. Если нужно именно последовательное — пригодится reduce


    1. n7olkachev
      19.05.2017 02:15

      const results = await Promise.all(bar.map(doSomething))
      


  1. muxa_ru
    18.05.2017 18:42
    +3

    Я дико извиняюсь, но почему ничего не сказано про скорость работы этих вариантов?


    1. GlebkaF
      18.05.2017 18:52
      -4

      По большому счету, в js map — это сахар на while, посмотрел здесь: map Polyfill.
      Код в примере использует map и filter, вместо одного for, вероятно, что он работает в 2 раза медленнее.


      1. TheShock
        18.05.2017 19:11
        +1

        С вызовом функции, которая значительно более дорогостоящая операция. Два цикла — это еще фигня в сравнии с вызовом функции на каждую операцию, потому там не в 2 раза медленнее, а на порядок.


        1. TheShock
          18.05.2017 19:53
          +8

          Да, вы правы, кто меня минусует. Я ошибся с порядком. Не на порядок, а на два порядка, то есть в сто раз. Функциональный вариант ровно в сто раз медленнее, чем классический:


          1. faiwer
            18.05.2017 19:59
            -4

            Не стоит серьёзно относится к подобным тестам на jsperf. jsperf не чурается модифицировать предоставленный код, изменяя его порой, весьма существенно. Бенчмарки сами по себе редко бывают объективны, но если уж хочется с ними поиграть, то лучше запускать их за пределами таких площадок (скажем в node или просто используя профилирование/console.time браузера).


            1. TheShock
              18.05.2017 20:05
              +2

              Как скажете. Получилось… разница в 100 раз. Увеличил число, чтобы было видно, что количество цифр одинаковое и уж слишком мало получалось в классическом варианте:


              1. raveclassic
                18.05.2017 20:15

                Ну так вы ж лукавите :) Сколько у вас в fp прогонов, а сколько в classic? Не, я не спорю, все верно, map/reduce и компания банально увеличивают количество прогонов. Но можно изловчиться и написать на трансдьюсерах. Другое дело, зачем?..


                1. TheShock
                  18.05.2017 20:19
                  +2

                  Простите, в fp-варианте количество прогонов в три раза больше, чем в классическом варианте, а не в сто раз.

                  Я лишь стараюсь убедить, что утверждение ложно:

                  Код в примере использует map и filter, вместо одного for, вероятно, что он работает в 2 раза медленнее.

                  Основной источник увеличения нагрузки не три цикла for вместо одного (это как раз совершенно не страшно), а «три тяжеловесных вызова функции + три операции» вместо «три операции» на каждую итерацию. То есть у нас сам вызов функции занимает в сто раз больше времени, чем операция, которую эта функция выполняет.

                  И это особенность именно JS, а не FP в целом.


                  1. maeris
                    18.05.2017 22:53

                    Отдельно стоит заметить, что если даже написать свои собственные map и filter, они зачастую будут работать быстрее. Потому что встроенные методы реализованы на сишке, и вызываются через FFI, с соответствующими затратами на маршаллинг аргументов и возвращаемого значения.


                  1. raveclassic
                    22.05.2017 10:57

                    Где я мог облажаться?

                    const numbers = [];
                    for (let i = 0; i < 1000000; i++) {
                        numbers.push(i);
                    }
                    
                    function classic() {
                        console.time('classic');
                        let result = 0;
                        const mapped = [];
                        for (let i = 0; i < numbers.length; i++) {
                            mapped.push(numbers[i] + 1);
                        }
                        const filtered = [];
                        for (let i = 0; i < mapped.length; i++) {
                            const value = mapped[i];
                            if (value % 3 === 0) {
                                filtered.push(value);
                            }
                        }
                        for (let i = 0; i < filtered.length; i++) {
                            result += filtered[i];
                        }
                        console.timeEnd('classic');
                        return result;
                    }
                    
                    function fp() {
                        console.time('fp');
                        let result = numbers.map(n => n + 1).filter(n => n % 3 === 0).reduce((acc, n) => acc + n, 0);
                        console.timeEnd('fp');
                        return result;
                    }
                    
                    console.log(classic());
                    console.log(fp());
                    
                    console.log('---');
                    console.log(fp());
                    console.log(classic());
                    
                    console.log('---');
                    console.log(classic());
                    console.log(fp());
                    
                    console.log('---');
                    console.log(fp())
                    console.log(classic());
                    
                    console.log('---');
                    console.log(classic());
                    console.log(fp());
                    
                    console.log('---');
                    console.log(fp())
                    console.log(classic());
                    
                    console.log('---');
                    console.log(classic());
                    console.log(fp());
                    
                    /*
                    VM3953:30 fp: 336.918701171875ms
                    VM3953:34 166666833333
                    VM3953:23 classic: 259.02001953125ms
                    VM3953:35 166666833333
                    VM3953:37 ---
                    VM3953:30 fp: 294.345947265625ms
                    VM3953:38 166666833333
                    VM3953:23 classic: 188.5771484375ms
                    VM3953:39 166666833333
                    VM3953:41 ---
                    VM3953:23 classic: 233.133056640625ms
                    VM3953:42 166666833333
                    VM3953:30 fp: 303.2099609375ms
                    VM3953:43 166666833333
                    VM3953:45 ---
                    VM3953:30 fp: 273.15283203125ms
                    VM3953:46 166666833333
                    VM3953:23 classic: 232.2529296875ms
                    VM3953:47 166666833333
                    VM3953:49 ---
                    VM3953:23 classic: 224.306884765625ms
                    VM3953:50 166666833333
                    VM3953:30 fp: 272.4609375ms
                    VM3953:51 166666833333
                    VM3953:53 ---
                    VM3953:30 fp: 266.1357421875ms
                    VM3953:54 166666833333
                    VM3953:23 classic: 377.88623046875ms
                    VM3953:55 166666833333
                    VM3953:57 ---
                    VM3953:23 classic: 176.39013671875ms
                    VM3953:58 166666833333
                    VM3953:30 fp: 294.071044921875ms
                    VM3953:59 166666833333
                    */
                    


                    1. TheShock
                      22.05.2017 16:37
                      -1

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


          1. skiedr
            22.05.2017 02:17

            В ramda и в lodash очень наивная реализация. Думаю честно будет добавить в тест Rx.JS реализацию для сравнения. Как никак еще одна хипстерская технология


            1. TheShock
              22.05.2017 02:29

              Это нативный код без фреймворков. Но если вы хотите — можете протестировать и фреймворки)


  1. Aries_ua
    18.05.2017 18:50
    +3

    Хочу добавить, что с async / await такие циклы работать не будут. Возможно не к месту мое замечание. Но когда переходили на async / await я был удивлен, что это перестает работать для forEach (в доке потом выяснил что к чему). Так что старый добрый цикл выручил.


  1. faiwer
    18.05.2017 19:40
    +3

    Какой-то оголтелый фанатизм. И с каждым днём таких статей всё больше. Главное заголовок сделать как можно более пафосным (смерть for). В следующий раз накал надо ещё пуще нагнать. Как насчёт хтонических исчадий из преисподней?


    Фунциональное программирование интересная и полезная штука, если ваш язык позволяет из него взять что-то полезное. Императивное программирование напротив тоже очень интересная и полезная штука. Мир не делится на белое и чёрное. И задачи можно решать выбирая наиболее удобные и привычные для себя инструменты. Выбирая один набор преимуществ, вы неизбежно приобретаете к нему такой же набор недостатков.


    Скажем мутабельность и иммутабельность. И то и другое может быть весьма удобным и к месту. Важно только понимать, где чего стоит избегать (умоляю — избегайте избыточных аллокаций в .reduce).


    Вот скажем мои 5 копеек по сабжу:


    • map & reduce в качестве замены for удобная штука, т.к. код получается несколько нагляднее, ибо синтаксис конструкции for не позволяет красиво возвращать новый объект как результат операций над итерациями
    • forEach вместо for-of по мне так, на мой вкус, выглядит очень… неопрятно, требует лишнего метода, фигурных скобок. В случае одиночного пробега по какой-либо коллекции я прибегаю к for-of как раз в рамках читаемости. Особенно это удобно в случае 2-3 вложенных for.
    • цепочки трансформаций из ФП — очень удобная и полезная штука. Однако мы тут сталкиваемся с двумя весьма пренепреятнейшими проблемами:
      • в JS нет pipe-оператора. В итоге если цепочка какой-либо метод не умеет, то приходится либо оборачивать её всю целиком (читаемость катастрофически падает), или делить цепочку на несколько, добавляя какие-нибудь вветвления, либо делать какие-нибудь .tap методы. Тоже касается и композиции методов, без pipe оператора выглядит это ну прямо безобразно.
      • в JS из коробки нет никаких удобств для работы с callback-ами в async & * контекстах, а это бывает ну очень актуально
    • ФП подразумевает очень хорошее понимание работы и методов своей ФП библиотеки. Для нового человека ваш код может напоминать набор слов. А если учесть что ряд из либ ещё постоянно меняет свои сигнатуры и названия методов (камень в сторону lodash)...

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


    1. maeris
      18.05.2017 22:50

      А если ещё на jsperf сравнить for и map, то станет понятно, почему в каждом angular и react внутри только for.


  1. dom1n1k
    18.05.2017 19:41
    +1

    Недавно сравнивал for и forEach на реальной задаче — оказалось, что в ноде (последняя LTS версия) forEach на порядок медленнее. Я без понятия откуда такая огромная разница, в браузерах обычно отличия невелики.


    1. TheShock
      18.05.2017 19:54

      Потому что кроме самой операции (каждая итерации очень дешевая) допольнительно происходит вызов функции, который очень тяжелый. В браузерах это проявляется точно так же, см. выше.


      1. dom1n1k
        18.05.2017 22:15

        Про вызов функции я в курсе. Но как-то раньше опыт показывал, что в отличие от классических языков, в JS дело с этим обстоит не так однозначно. Что-то внутри JIT такое происходит, что эффект смазывается и разница во многих случаях не получается очень уж огромной.


        1. TheShock
          18.05.2017 22:24

          Предполагаю, что вы где-то прочитали и запомнили некорректную информацию. Я выше показал, что в JS все очень однозначно.


          1. dom1n1k
            18.05.2017 22:30
            +1

            Не читал, а из собственных тестирований.
            Правда, припоминаю, что я частенько их делал в консоли браузеров (как выше), а известный Вячеслав Егоров говорит, что в бенчмарки в консоли это плохо, потому что там оптимизации могут отключаться.


            1. TheShock
              18.05.2017 23:44
              -1

              Ну выше есть и в консоли и в jsperf. И там, и там одинаковый результат. Можете еще на jsfiddle попробовать. Тот же результат будет


    1. dom1n1k
      18.05.2017 23:01
      +6

      И отвечая на вопрос в конце статьи — нет, for живее всех живых.
      Я понимаю, что

      const kittens = cats.filter(isKitten).map(getName);
      
      выглядит лаконично и красиво. Но меня ломает, что:
      а) два прохода по массиву вместо одного (а может и более, если цепочку наращивать);
      б) filter возвращает промежуточный массив, то есть лишняя память, лишнее инстанцирование, чаще приход GC;
      в) вызов функций отнюдь не бесплатен.
      Применяю такой подход только к малым массивам. Видимо, первичное обучение программированию в те времена, когда 640 кб хватало всем, наложило неизгладимый отпечаток на мою психику.


    1. Maiami
      19.05.2017 01:19
      +1

      оказалось, что в ноде (последняя LTS версия) forEach на порядок медленнее.

      forEach хорошо оптимизируется на реальном коде, разницы с for может и не быть, но LTS версия имеет старый оптимизатор, последний турбофан умеет намного больше

      Но даже это не столь важно. Проблема микробенчмарков на js в том, что сравнивать нужно в изолированной среде. Вот например типичный микробенчмарк:
      Заголовок спойлера
      'use strict';
      
      const iter = 1000000
      const items = []
      for (let i = 0; i < iter; i++) {
          const obj = {}
          obj.a = 'a' + i
          obj.num = '1' + i
          obj.random = Math.random() * 1000 | 0
          items.push(obj)
      }
      
      function doSomething(item) {
          if (item.random % 2 === 0) {
              result += item.num | 0
          }
          else {
              str += item.a
          }
      }
      
      console.time('forEach')
      var result = 0
      var str = ''
      items.forEach(function (item) {
          if (item.random % 2 === 0) {
              result += item.num | 0
          }
          else {
              str += item.a
          }
      })
      console.log(`result: ${result}, str len: ${str.length}`)
      console.timeEnd('forEach')
      
      console.time('for')
      var result = 0
      var str = ''
      for (let i = 0; i < items.length; i++) {
          const item = items[i]
          doSomething(item)
      }
      console.log(`result: ${result}, str len: ${str.length}`)
      console.timeEnd('for')
      


      1. raveclassic
        19.05.2017 01:34
        +1

        Побольше бы таких дельных замечаний, а то от бенчей с jsperf уже изжога, честное слово.


      1. TheShock
        19.05.2017 02:17

        Две физически разных страницы для фп-стиля и классического, результат — тот же, разница в сто раз, проверьте сами.


      1. dom1n1k
        19.05.2017 03:07

        Про порядок тестов я в курсе — JIT «разогревается» поначалу. Я это учитывал, менял и так и сяк — принципиально расстановка сил не менялась.
        Код так сходу сейчас не предоставлю, но могу сказать, что задачка была про вычисление convex hull на большом массиве географических координат (сотни тысяч элементов * многократные проходы).


  1. amaksr
    18.05.2017 19:41
    +3

    Хайп насчет const сильно преувеличен, так как он не запрещает изменять внутренности объекта. Т.е. не совсем получается и const.

    filter и map хороши на циклах с совсем уж простенькой логикой. Если нужно что-то сложное и/или быстрое, то for может быть более к месту.


    1. gearbox
      18.05.2017 20:14
      +2

      Хайп насчет const сильно преувеличен, так как он не запрещает изменять внутренности объекта.

      хайп вокруг const не более чем непонимание того что при ссылке на объект константой является ссылка а не объект. При const на скаляр — все ровно так как ожидалось. Но кого это сейчас интересует, 2017 год на дворе как ни как.


  1. Error1024
    18.05.2017 21:00
    +2

    Далек от js — но то, что у автора получилось в итоге вызывает боль, везде…


  1. maeris
    18.05.2017 22:49
    +1

    А как быть с yield и await внутри for? Заменять нативный синтаксис на библиотеки с npm?


  1. shuhray
    19.05.2017 02:17
    -3

    Javascript — это куча компоста, куда каждый кидает вонючие объедки. Давно пора перевести браузеры на Lua.


    1. TheShock
      19.05.2017 02:23

      Давно пора перевести браузеры на Lua.

      Это полумера ;)


  1. DeLuxis
    19.05.2017 07:07

    Браузеры функции будут оптимизировать.
    Поэтому при применении нужно исходить из контекста.


    1. MikailBag
      19.05.2017 22:28

      но map все равно не станет быстрее for.
      Максимум что близко приблизится.


  1. nullc0de
    19.05.2017 12:40

    Перед тем как делать громкие заявления, советую глубже автору разобраться в вопросе. Где-то синтаксический сахар транслируется в цикл for хочет этого автор или нет, где-то запускается как есть и реализация синтаксического сахара имеет определенный оверхед и работает медленее for, особенно на старых платформах. Советую поиграться с правильными бенчмарками. Если автор не пишет качественный код и не заботится о производительности, то это только его недальновидность. Синтаксический сахар удобен только в некритических участках кода.


  1. lxsmkv
    20.05.2017 01:10
    +3

    Почитал комментарии. Офигел, как из-за куска кода в 10 строк можно устроить Ледовое Побоище? Понял почему код ревью никогда не проводят командно, а по-одиночке — просто этот бой никогда не закончится :) Программисты все-таки они такие программисты ;)
    Это отличный пример того, что все зависит от того под каким углом посмотреть. Нужна производительность — лучше так, нужна удобочитаемость — лучше эдак.

    От себя добавлю что если бы мне пришлось тестировать этот кусок кода, то функции проще тестировать чем цикл. Взял фунцкию сунул в нее значения проверил результат. Цикл надо сперва во что-то обернуть, если обертка(метод) без возвратного значения, то ага, уже посложнее будет, возможно даже код рефакторить придется. Так что есть свои преимущества и в том и в этом подходе.

    Я просто почитал, порадовался чего нового умеет яваскрипт, чего я еще не видел, подумал, что надо будет попробовать надосуге. Как и практически все комментаторы оригинала. Тут уже на хабре как-то упоминали, мол. наша аудиотория совсем материал по другому воспринимает чем на западе. У нас в принципе любой намек на императив в стиле повествования обречен быть принятым в штыки. (Это замечание переводчику)


  1. Pilat
    20.05.2017 03:40
    +1

    Когда читаю подобные статьи, всё время думаю: и ведь это те же самые люди ругают Perl :)


  1. Mycolaos
    27.05.2017 00:45

    Ёмаё, эту тему уже более недели обсуждают :D

    А не думали делать компиляцию из комментариев в новую статью? Есть же переводы, а будут еще компиляции.