Вы знаете, что такое трансдьюсеры

    Трансдьюсеры были анонсированы еще в далеком 2014, с тех пор по ним было написано немалое количество статей (раз, два), но ни после одной статьи я не мог сказать, что понимаю трансдьюсеры кристально ясно. После каждой статьи у меня возникало ощущение, что я приблизительно понимаю что-то сложное, но оно все равно оставалось сложным. А потом однажды в голове что-то щелкнуло: "я ведь уже видел этот паттерн, только он назывался иначе!"


    Задача:


    Есть массив scores, содержащий результаты проведенных мною игр в футбол в виде объекта с полями:


    • gameID — номер игры;
    • my — количество очков у меня;
    • others — количество очков у моего противника.

    Необходимо найти номера первых двух выигранных мною игр.


    Исходные данные для задачи (ответ для таких данных — [1, 3]):


    const scores = [
      { gameID: 0, my: 1, others: 2 },
      { gameID: 1, my: 2, others: 1 },
      { gameID: 2, my: 0, others: 3 },
      { gameID: 3, my: 3, others: 2 },
      { gameID: 4, my: 3, others: 1 },
      { gameID: 5, my: 0, others: 0 },
      { gameID: 6, my: 4, others: 1 },  
    ]

    Решение №1. Императивный цикл


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


    const result = []
    
    // Пробегаемся по всем играм, пока количество результатов не достигнет двух.
    for (let i = 0; i < scores.length && result.length < 2; i++) {
      const { gameID, my, others } = scores[i]
    
      if (my > others) {
        result.push(gameID)
      }
    }

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


    Решение №2. Array#map и Array#filter


    Это уже решение иммутабельное, более выразительное, "современное".


    const result =
      scores
        .filter(({ my, others }) => my > others) // выбираем выигрышные
        .map(({ gameID }) => gameID)             // получаем их номер
        .slice(0, 2)                             // берем первые два

    Именно так решило бы большинство js-разработчиков в наши дни, полагаю. Но у этого решения есть одна важная проблема: если в предыдущем решении мы делали всего один проход, да и тот не до конца, то сейчас у нас уже аж два полных прохода. Если бы у нас scores содержал миллион элементов, то предикат в filter вызывался бы миллион раз, функция в map применилась бы меньшее, но все равно большое число раз, а в конце мы все равно берем всего лишь два первых элемента. Конечно, преждевременная оптимизация — зло, но это уже ни в какие ворота.


    Решение №3. Свертка


    Через свертки можно определить [почти] любую операцию над массивами. В этом решении у нас всего один проход:


    const result = scores.reduce((result, { my, others, gameID }) => {
      // Если игра выиграна и количество результатов меньше двух,
      // то добавляем номер в результаты.
      if (my > others && result.length < 2) {
        return result.concat([gameID])
      }
    
      return result
    }, [])

    Это чем-то напоминает решение с циклами, но тут мы передаем промежуточное состояние явно и иммутабельно. Но проблема осталась — вместо двух проходов у нас теперь один, но он все равно полный, то есть при миллионе элементов мы пройдемся по всему миллиону, даже если нужное число результатов мы уже получили, ведь у стандартного reduce нет возможности выйти из цикла через break. Давайте тогда напишем свой reduce, из которого можно выйти, если вернуть reduced-значение (идею позаимствовал из clojure).


    // Класс-обертка для итогового значения аккумулятора.
    class Reduced {
      constructor(value) {
        this.value = value
      }
    }
    
    const isReduced = value =>
      value instanceof Reduced
    
    const reduced = value =>
      isReduced(value) ? value : new Reduced(value)
    
    // Рекурсивная реализация reduce с проверкой аккумулятора на reduced-значение.
    const reduce = (reducer, state, array) => {
      // Если аккумулятор - reduced-контейнер со значением,
      // то распаковываем и возвращаем значение, завершая цикл.
      if (isReduced(state)) {
        return state.value
      }
    
      if (array.length === 0) {
        return state
      }
    
      // Рекурсивно вызовем reduce для хвоста массива,
      // применяя первый элемент к состоянию.
      const [x, ...xs] = array
      return reduce(reducer, reducer(state, x), xs)
    }
    
    const result = reduce((result, { my, others, }) => {
      if (my > others) {
        if (result.length < 2) {
          result = result.concat(gameID)
        }
    
        // Завершаем цикл, если набрано нужно число результатов.
        if (result.length >= 2) {
          result = reduced(result)
        }
      }
    
      return result
    }, [], scores)

    Ух, теперь-то у нас все быстро (один проход и ровно до тех пор, пока мы не получим нужное количество элементов), но красиво ли? Я считаю, что код выше — страшный: в нем слишком много кода, который не относится к задаче.


    Решение №4. Декомпозиция редьюсера, трансдьюсеры


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


    1. filterWins фильтрует игры по статусу, то есть пропускает дальше по цепочке только выигранные игры.
    2. mapGameID достает из игры ее номер.
    3. firstTwo проверяет количество полученных результатов. Если их меньше двух, то вызывает следующую функцию, получает новые результаты, а потом завершает цикл, если набрано нужное количество.
    4. appendToResult добавляет номер игры в массив с результатами и возвращает его.

    // next - следующая функция в цепочке
    const filterWins = next => (result, score) =>
      // Если игра выигрышная, то передаем ее дальше по цепочке
      score.my > score.others
        ? next(result, score)
        : result
    
    const mapGameID = next => (result, { gameID }) =>
      // Передаем дальше номер игры
      next(result, gameID)
    
    // Эта функция позволяет пропустить через себя ограниченное число элементов.
    const firstTwo = next => {
      // Мы используем замыкания для добавления локального состояния к нашим редьюсерам.
      // n - количество элементов, которые нужно пропустить дальше для добавления в
      // массив результатов.
      let n = 2
    
      return (result, score) => {
        // Если есть свободные места, пропускаем игру дальше и уменьшаем их количество.
        if (n > 0) {
          result = next(result, score)
          n -= 1
        }
    
        // Если свободных мест нет, завершаем цикл.
        if (n <= 0) {
          result = reduced(result)
        }
    
        return result
      }
    }
    
    const appendToResult = (result, gameID) =>
      result.concat([gameID])
    
    const result = reduce(
      // Последовательно трансформируем редьюсер, применяя наши функции к
      // редьюсеру, создавая тем самым цепочку редьюсеров.
      filterWins(mapGameID(firstTwo(appendToResult))),
      [],
      scores,
    )

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


    Паттерн вам может показаться знакомым, ведь именно так работают middleware. Это и есть middleware. И это и есть трансдьюсеры, потому что трансдьюсеры это и есть middleware. Трансдьюсер — функция, которая принимает один редьюсер и возвращает новый (с дополнительной логикой перед или после вызова редьюсера).


    Те, кто слышит понятие middleware впервые, могут ознакомится с ним тут: express.js, laravel, а так же я пробовал вчера объяснить его своими словами: мой пост


    В данном решении у нас три трансдьюсера: filterWins, mapGameID и firstTwo, и мы их по последовательно применяем к редьюсеру appendToResult, создавая все более и более сложный редьюсер.


    А теперь давайте унифицируем наши трансдьюсеры:


    // pred - предикат - функция, которая принимает значение и возвращает true или false.
    // Если предикат возвращает true, то пропускаем значение по цепочке дальше, иначе
    // возвращаем неизмененный аккумулятор.
    const filter = pred => next => (acc, x) =>
      pred(x) ? next(acc, x) : acc
    
    // mapper - функция, которая преобразует один элемент в другой.
    const map = mapper => next => (acc, x) =>
      next(acc, mapper(x))
    
    // Принимает редьюсер и создает новый, который выполнится только n раз.
    // После n-ого выполнения цикл завершится.
    const take = n => next => (acc, x) => {
      if (n > 0) {
        acc = next(acc, x)
        n -= 1
      }
    
      if (n <= 0) {
        acc = reduced(acc)
      }
    
      return acc
    }

    Заодно переименуем редьюсер:


    const append = (xs, x) =>
      xs.concat([x])

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


    const compose = (...fns) => x =>
      fns.reduceRight((x, fn) => fn(x), x)

    Добавим хэлпер, который применит транcдьюсер к редьюсеру и вызовет reduce с получившимся редьюсером:


    const transduce = (transducer, reducer, init, array) =>
      reduce(transducer(reducer), init, array)

    Ну и наконец решим задачу с помощью всех этих функций:


    const firstTwoWins = compose(
      filter(({ my, others }) => my > others),
      map(({ gameID }) => gameID),
      take(2),
    )
    
    const result = transduce(firstTwoWins, append, [], scores)

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


    Вывод:


    Трансдьюсеры — очень простой паттерн, который является частным случаем паттерна middleware, который известен намного шире (отсюда и название статьи), основа которого — создание сложных редьюсеров с помощью композиции. А полученные редьюсеры в свою очередь очень универсальны, их можно использовать с обработкой коллекций, стримов, Redux.


    Писать весь код для работы с трансдьюсерами каждый раз вручную не придется, ведь трансдьюсеры уже давно реализованы во многих библиотеках для обработки данных (например в ramda.js или стандартной библиотеке clojure)


    Ramda:


    const scores = [
      { gameID: 0, my: 1, others: 2 },
      { gameID: 1, my: 2, others: 1 },
      { gameID: 2, my: 0, others: 3 },
      { gameID: 3, my: 3, others: 2 },
      { gameID: 4, my: 3, others: 1 },
      { gameID: 5, my: 0, others: 0 },
      { gameID: 6, my: 4, others: 1 },  
    ]
    
    {
      const firstTwoWins = compose(
        filter(({ my, others }) => my > others),
        pluck("gameID"),
        take(2),
      )
    
      const result = transduce(firstTwoWins, flip(append), [], scores)
    }
    
    // Или еще лучше
    {
      const firstTwoWins = into([], compose(
        filter(({ my, others }) => my > others),
        pluck("gameID"),
        take(2),
      ))
    
      const result = firstTwoWins(scores)
    }

    Clojure:


    (def scores [{:game-id 0, :my 1, :others 2}
                 {:game-id 1, :my 2, :others 1}
                 {:game-id 2, :my 0, :others 3}
                 {:game-id 3, :my 3, :others 2}
                 {:game-id 4, :my 3, :others 1}
                 {:game-id 5, :my 0, :others 0}
                 {:game-id 6, :my 4, :others 1}])
    
    (defn win? [{:keys [my others]}]
      (> my others))
    
    (def first-two-wins
      (comp (filter win?)
            (map :game-id)
            (take 2)))
    
    (def result (transduce first-two-wins conj [] scores)) ;; [1 3]
    Поделиться публикацией
    Ой, у вас баннер убежал!

    Ну. И что?
    Реклама
    Комментарии 98
    • НЛО прилетело и опубликовало эту надпись здесь
      • +2
        "… Вы пишете на этом языке лисп, а потом собственно программу" Грэм кажется.
        Или сразу пишите на лиспе. :)
      • 0

        Условие выхода в for некорректно, запятая это не && и, если побед в массиве нет, цикл не остановится никогда.

      • +1
        Честно говоря, не встречался раньше с таким названием паттерна, как middleware. Возможно, это ограниченнойсть моих познаний, но наверное ссылочка бы не помешала.
        • +1

          Добавил ссылки на описание с документации express.js, laravel, а так же на свое приблизительно объяснение.

          • +3
            Я теперь понял, о чем вы. Пожалуй, в классическом понимании, это описание ближе к паттерну chain of responsibility, например.

            Просто у термина middleware есть и другое, значительно более широко известное употребление, когда этим словом называют нечно (сервер или сервис), между backend и frontend, базой и web, ну и так далее. И там это не паттерн вообще.
            • +2

              Под паттерном я тут понимаю не паттерн проектирования, а просто какой-то повторяющийся кусок кода/повторяющаяся сигнатура. В данном случае под middleware я понимаю что-то вроде обертки, которая принимает функцию и возвращает новую с такой же сигнатурой, но с дополнительной логикой.

              • +1
                Да нет, трансдьюсер это вполне себе паттерн проектирования, просто в применении к функциональной парадигме. А в слове middleware меня смущает только то, что у него есть уж очень употребимый и совершенно другой смысл.

                >обертки, которая принимает функцию и возвращает новую с такой же сигнатурой, но с дополнительной логикой.

                Это просто функция высшего порядка, ничего больше. Как правило — просто композиция. И для этого тоже есть готовая куча терминов, уже, как правило из теории категорий.
                • 0
                  Это просто функция высшего порядка, ничего больше.

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

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

                    А то определение, что вы написали — это и есть формулировка термина «функция высшего порядка». Один в один.
                    • 0

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

                      • +1

                        На входе всегда один редьсер. А вот с помощью операции композиции функций (не трандьюсеров) мы можем скомбинировать несколько, но мы можем сделать это и без нее по цеопчке t3(t2(t1(r)))

                        • 0
                          Хм. Ну да, согласен, теоретически трансдьюсер может принимать один редьюсер, и возвращать другой. Но как правило речь все-таки об их композиции.
              • +2
                Еще мне нравится, как middleware объясняют на сайте Slim Там очень хорошая схема, сразу все понятно.

                Middleware
                image
            • +3
              <zanuda_mode>

              Очевидно, что Решение №2 следует записать в виде:
              const result =
                scores
                  .filter(({ my, others }) => my > others) // выбираем выигрышные
                  .slice(0, 2)                             // берем первые два
                  .map(({ gameID }) => gameID)             // получаем их номер
              

              чтобы убрать «лишний» полный проход для получения gameID.

              </zanuda_mode>
              • +1
                тут первый проход фильтром по всей коллекции тоже лишний.
                • +1

                  А вот можно по-подробнее? Если сделать сперва .slice(0, 2), то мы получим первые две игры независимо от того, выигранные они или нет.

                  • +1
                    Если в списке 100500 элементов и 2 выигрышных находятся в самом вначале, то неленивый filter все равно пройдет всю коллекцию, прежде чем отдаст результат в map, а тот в slice.
                    const result =
                      scores
                        .filter(({ my, others }) => my > others)
                        .map(({ gameID }) => gameID)
                        .slice(0, 2)
                    

                    Если поменять местами map и slice, выигрыш будет. Но это все равно не самый оптимальный вариант. Реализация на базе композиции функций, описанная в статье, переберет лишь необходимое количество элементов.
                    const result = transduce(compose(
                      filter(({ my, others }) => my > others),
                      map(({ gameID }) => gameID),
                      take(2)
                    ), append, [], scores)
                    

                    Чисто внешне код отличается не сильно.
                • +1

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

                • +2
                  Есть какое-то смутное подозрение, что если эту штуковину (функцию-трансдьсер с аргументом next) вывернуть наизнанку, должен получиться знакомый всем yield, с тем же самым next(), только уже внутри (объекта-генератора) %)
                  • +2

                    Ещё бы. Если переписать это на Хаскеле (ленивость же!) — внезапно окажется, что тут фигурирует единственный тип сущностей: функция, делающая из одного ленивого списка другой. И генераторы — наиболее понятный аналог в некоторых языках...

                    • 0

                      Собственно, я переписывал это на хаскеле, но без нормальной реализации take.


                      Заголовок спойлера

                      image

                      • 0

                        Ужас какой. По моим представлениям — должно быть в точности решение 2.

                        • +1

                          Из-за ленивости на хаскеле действительно эти трансдьсеры не нужны (если не нужно переиспользовать трансформации) и на js такое действительно можно написать только через генераторы, вы правы.


                          x |> f = f x
                          
                          main =
                              [1..]
                                  |> filter even
                                  |> map (* 2)
                                  |> take 3
                                  |> print -- [4, 8, 12]

                          Несколько недель назад я такое тоже писал:
                          image

                          • 0
                            Интересно, от цикла for в каждой функции в варианте с yield можно как-нибудь избавится?
                            • 0

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

                  • +5
                    Теперь вопрос — в чём смысл этого всего? Ну допустим вы героически преодолели функциональщину чтобы она не убивала кхерам производительность, добившись результата идентичного (наверное) изначальному циклу. А ради чего? Увеличение количества уровней абстракции?
                    • 0
                      Сравните решение 1 и решение 2 – решение 2 и есть «функциональщина» и оно намного более читаемо. При условии, что вы знаете набор очень простых примитивов(.map, .filter, .reduce и производные)

                      Касательно всех остальных решений – вполне согласем с вашим мнением
                      • +3

                        Увеличение уровня абстракции в данном случае помогает использовать трансдьюсеры со всеми сворачиваемыми типами (массивы, стримы в rxjs, множества, словари) независимо от того, реализовали ли они функции map/filter/take. Чисто теоретически, если бы трансдьюсеры бы встроили в js, то из всех библиотек типа rxjs/xstream все эти трансформации можно было бы выпилить, оставив только reduce, потому что универсальные трансдьюсеры будут работать однообразно везде.

                        • +1

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

                        • +1

                          К слову со своей колокольни: в scala можно у коллекций вызвать метод view — вернётся обёртка над коллекцией и операции типа map, filter и т.п. начнут работать лениво.


                          scores
                              .view                                // благодаря этой строчке появляется "ленивость"
                              .filter(x => x.my > x.others)        // выбираем выигрышные
                              .map(_.gameId)                       // получаем их номер
                              .take(2)                             // берем первые два

                          причём результат — тоже view, "содержащий" 2 элемента и вычисляющий их в самый последний момент.


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

                          • +1

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


                            Но работа с трансформациями как с объектом первого класса — фишка трансдьюсеров, которая позволяет
                            а) переиспользовать их
                            б) использовать одни и те же трансдьсеры для любых типов данных, которые поддерживают свертки (Foldable), скажем, стримы в rxjs (те же самые трансдьюсеры будут прекрасно работать и там), а значит из этих библиотек можно было бы спокойно выпилить все трансформации, если бы трансдьюсеры были бы нативными и общеиспользуемыми (маловероятное будущее, но в clojure стремятся к этому)


                            image

                            • 0
                              На C#, кстати, большинство методов для работы с коллекциями ленивые, так что такой код будет работать «из коробки»:
                              scores
                                .Where(score => score.my > score.others)
                                .Select(score => score.gameID)
                                .Take(2);
                              

                            • +4
                              Если использовать lodash (а его очень много людей использует), то лишнего прохода не будет благодаря Lazy Evaluation
                                _(scores)
                                  .filter(({ my, others }) => my > others) // выбираем выигрышные
                                  .map(({ gameID }) => gameID)             // получаем их номер
                                  .take(2)
                                  .value()
                              


                              Но главное, ваш пример выглядит так будто вы очень простой(чем проще – тем надежнее) и короткий код (пусть даже не оптимальный, хотя lodash это исправляет) который работает, заменили на какой-то «матан», который в придачу еще и длинее. Зачем?
                              • +5

                                Как я уже сказал, писать все эти трансдьюсеры с нуля не придется. Почти во всех своих проектах я использую ramda (как аналог lodash, только функциональный), который уже имеет реализацию трансдьюсеров (пример в статье есть). Мало того, трансдьюсеры — паттерн намного проще, чем ленивые вычисления (в lodash). Да и и сама ramda намного проще и немагичнее, чем lodash. Так же трансдьюсеры позволяют переиспользовать код, потому что позволяют работать с трансформациями как с объектом первого класса, а потом применять их.


                                Еще важно то, что трансдьюсеры работают где угодно, где есть reduce, то есть эти трансьдеюсеры будут работать что с lodash, что с ramda, что со стримами (при условии конечно, что они не зависят от реализации reduce, как функция take). Из этого следует вывод, что всякие xstream/rxjs можно было упросить, убрав оттуда filter/map/прочее и оставив только reduce.


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


                                Ну и для сравнения с циклами: читать цепочку трансформация (будь то просто Array#map, или трансдьюсеры) проще, чем циклы с внутренней логикой, так как это декларативно.

                                • 0
                                  Вы не обижайтесь, но начните обосновывать свои утверждения – а то церковь свидетелей Трансдюсера выходит
                                  >паттерн намного проще, чем ленивые вычисления (в lodash)
                                  вас же не просят их имплементировать

                                  >Да и и сама ramda намного проще и немагичнее, чем lodash
                                  ни чем не обоснованое утверждение

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

                                  >убрав оттуда filter/map/прочее и оставив только reduce.
                                  я либо что-то не понимаю – либо вы что-то не договариваете.
                                  возьмем простой кусок
                                  const H = require('highland')
                                  
                                  H(process.stdin)
                                    .map(x => 'something' + b)
                                    .pipe(process.stdout)
                                  


                                  из вашего утверждения выходит что я .map(и любые другие преобразования) могу выразить через .reduce, но ведь .reduce возраващает нам результа когда «пройдет» по всей коллекции – а тут конца коллекции как бы нет.

                                  Я не поленился и проверил:

                                  const H = require('highland')
                                  
                                  const xf = require('transducers-js').map(H.add('b'));
                                  
                                  H(process.stdin)
                                    .transduce(xf)
                                    .pipe(process.stdout)
                                  


                                  И что интересно – он работает абсолютно аналогично первому варианту. Я что-то не понимаю про reduce или вы плохо объяснили?

                                  И самое главное: вы так и не объяснили – зачем все это? вы говорите: простой паттерн, который позволяет добится большой эффективности. Обоснуйте это утверждение,

                                  пока что как в вашем примере так и в моем – мы эффективно увеличили количество строк кода и количество зависимостей
                                  • +2
                                    вас же не просят их имплементировать

                                    они в понимании проще, чем ленивые коллекции, ведь это просто вызов функций по цепочке. Или вы к тому, что не обязательно понимать то, что используешь?


                                    ни чем не обоснованое утверждение

                                    да, это субъективное мнение после чтения исходного кода обеих библиотек


                                    это круто, но зачем? единственное здравое объяснение, которое приходит в голову – тестирование. Но это не проблема

                                    уменьшение повторения кода?


                                    Про последний пункт насчет того, зачем это нужно: трансдьсеры частично решают ту же проблему, что и ленивые коллекции в lodash. Но решение через трансдьюсеры проще хотя бы потому, что тут больше нет довольно сложных ленивых коллекций, нет лишний преобразований с _ и value, нет методов, только функции, и это самый обычный reduce. Вроде как обосновал, нет? (простой паттерн — ведь это просто последовательное выполнение функций, большая эффективность — не выполняются лишние трансформации).


                                    Ramda:


                                    const result = into([], compose(
                                      filter(({ my, others }) => my > others),
                                      pluck("gameID"),
                                      take(2),
                                    ), scores)

                                    Lodash:


                                      _(scores)
                                        .filter(({ my, others }) => my > others)
                                        .map("gameID")
                                        .take(2)
                                        .value()

                                    Да, обычный цикл конечно проще, но обычный цикл еще и мутабелен, и императивен, ну а (субъективно) читать императивный код сложнее, чем декларативный.


                                    Я мало что знаю про highload, но в xstream свертка вместо возвращения значение при завершении стрим возвращает новый, состоящий из результатов сверток после каждого элемента, то есть


                                    ------1-----1--2----1----1------
                                      fold((acc, x) => acc + x, 3)
                                    3-----4-----5--7----8----9------

                                    const map = fn => next => (acc, x) => next(acc, fn(x))
                                    const filter = pred => next => (acc, x) => pred(x) ? next(acc, x) : acc
                                    const compose = (f, g) => x => f(g(x))
                                    const append = (arr, x) => arr.concat([x])
                                    const isOdd = x => x % 2 === 1
                                    const inc = x => x + 1
                                    const double = x => x * 2
                                    
                                    const xs = xstream.default
                                    
                                    const button = document.querySelector("button")
                                    
                                    const clicks$ = xs.create({
                                      start(listener) {
                                        button.addEventListener("click", () => listener.next())
                                      },
                                      stop() {
                                        button.removeEventListener("click", next)
                                      }
                                    }).fold(inc, 0)
                                    
                                    const doubledOdds$ = clicks$.fold(compose(
                                      filter(isOdd),
                                      map(double),
                                    )(append), [])
                                    
                                    doubledOdds$.addListener({
                                      next(x) { console.log(x) }
                                    }) // [2, 6, 10, ...]

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


                                    Пока писал это, понял, что я недостаточно компетентен в этой области. Все, что могу сделать — дать ссылку на статью про трансдьюсеры от Рича Хикки — их создателя — https://clojure.org/reference/transducers. Там же есть ссылка на пост в блоге и видео.

                              • +1

                                На правах изврата:


                                const win = ({ my, others }) => my > others
                                const firstResult = scores.findIndex(win)
                                const secondResult = firstResult + 1 + scores.slice(firstResult + 1).findIndex(win)

                                В реальном проекте я бы конечно использовал первый или второй вариант.

                                • +1
                                  Что скажете про этот вариант?

                                  function archaeological(arr, {filter, map, take}) {
                                      let acc = 0,
                                          res = []
                                  
                                      for (let i = 0; i < arr.length; i++) {
                                        if (filter(arr[i])) {acc += 1; res.push(map(arr[i]))}
                                        if (take(acc)) return res
                                      }
                                    }
                                  
                                    const result = archaeological(scores, {
                                      filter({my, others}) {return my > others},
                                      take(x) {return x == 2},
                                      map({gameID, my}) {return {gameID, my}} })
                                  
                                  • +3

                                    Нууу, это выглядит странно

                                    • +1
                                      в смысле странно? безопасный, легкий вариант для продакшена в 6 строчек кода..)
                                      немного сахарка для вызова функции
                                      const result = archaeological(scores, {
                                        filter: ({my, others}) => {return my > others},
                                        take  : 2,
                                        map   : 'gameID' })
                                      
                                      
                                  • +3
                                    Отличная статья, кратко и со смыслом!

                                    Поделюсь своим велосипедом:
                                    Телеграм боты на трансдьюсерах
                                    • 0

                                      Немного смутила постановка задачи.


                                      Необходимо найти номера первых двух выигранных мною игр

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

                                      • +1
                                        const take
                                        const take = n => next => (acc, x) => {
                                          if (n > 0) {
                                            acc = next(acc, x)
                                            n -= 1
                                          }
                                        
                                          if (n <= 0) {
                                            acc = reduced(acc)
                                          }
                                        
                                          return acc
                                        }
                                        


                                        Кажется такая реализация `take` не композится сама с собой. Если вызвать `take(1)(take(1))`, то результат будет обернут в Reduced дважды.

                                        Как реализовать пустое перечисление, которое не вернет ни одно элемента?
                                        const eof = next => (acc, x) => {
                                            ???
                                        }
                                        

                                        Работать со значением в самом трансдьюсере тоже становится не просто:
                                        const last = next => (acc, x) => reduced(x);
                                        
                                        const inc = next => (acc, x) => { 
                                           acc = next(acc, x); 
                                           // return acc + 1;
                                           return acc instanceof Reduced ? reduced(acc.value + 1) : acc;
                                        });
                                        
                                        inc(last);

                                        Можно-ли как-то иначе реализовать протокол для трансдьюсеров, чтобы таких проблем не возникало?
                                        • +2
                                          Как реализовать пустое перечисление, которое не вернет ни одно элемента?

                                          Нужно просто возвращать acc. reduce требует начальное состояние, именно ого и вернётся для пустого перечисления. При отстуствии начального состояния он берет первый элемент и при пустом массиве Array#reduce кидает исключение.


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


                                          • init — не принимает аргументов и должна вернуть начальное состояние. Зачастую просто вызывает next, то есть делегирует init на следуюущий трансдьюсер.
                                          • step — это то, что в статье — два аргумента: аккумулятор и элемент, возвращает аккумулятор
                                          • result — принимает один аргумент — аккумулятор, срабатывает после завершения reduce.

                                          Кажется такая реализация take не композится сама с собой. Если вызвать take(1)(take(1)), то результат будет обернут в Reduced дважды.

                                          Да, это действительно так, спасибо за замечание. Кложурная версия вместо reduced использует ensure-reduced, которая работает как identity, если значение уже обернуто. Исправлю.

                                          • 0
                                            `take(0)` это допустимая операция для редьюсера? из реализации следует, что технически вызвать можно, но результат будет такой же как у `take(1)`. т.е. вернется не начальное значение, а результат вызова предыдущей функции. поэтому не понятно баг или фича.
                                            • +1

                                              take(0) вернет начальное состояние, потому что не пройдет первая проверка if (n > 0) и take вернет аккумулятор (а это и есть начальное состояние, если take до этого не выполнялся) вместо отправки аргументов по цепочке дальше.


                                              Другое дело, что все предыдущие редьюсеры выполнятся впустую.

                                          • +1
                                            Работать со значением в самом трансдьюсере тоже становится не просто:

                                            Да, это действительно проблема. Но проблема скорее в моей реализации функции reduce, можно попробовать придумать другой способ завершать её выполнение, но у меня идей нет. Можно сделать Reduced каким-нибудь прокси.

                                          • 0
                                            Отличная статья. Теперь когда кто-то предложит мне использовать трансдьюсеры, сразу будет послан лесом.
                                            • +2

                                              Рад, что статья хоть как-то оказалась вам полезна )

                                            • –1
                                              Вместо пяти строк элементарного цикла, который будет работать очень шустро, вы предлагаете использать prev => next => (a,b,c) => (d,e,f) ад и 100500 строчек кода? :)

                                              На более сложных и однотипных задачах оно может и прокатит. Но на примере из статьи — это маразм :)
                                              Хотя бы укажите в самой статье, что не стоит использать все это чтобы просто выбрать два элемента из массива.
                                              • +3

                                                а) писать всё это не придется, все это уже реализовано в библиотеках для работы с данными (ramda, clojure.core)
                                                б) использовать императивные циклы я в принципе не считаю хорошей идей, а трансдьюсеры будут намного быстрее, чем .map/.fiter на массивах с большим количеством данных. Ленивые коллекции (как в хаскеле, кложе, lo-dash) или трандьюсеры тут лучший вариант.


                                                Но естественно использовать это для мелких задач поиска по массиву в 10 элементов — оверхед.

                                                • 0
                                                  использовать императивные циклы я в принципе не считаю хорошей идей

                                                  Не завидую я пользователям вашего ПО.

                                                  • +3

                                                    К счастью, на языках, на которых я пишу, циклов в принципе нет (Clojure, для развлечения Elm и Haskell)

                                                    • –2

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


                                                      Мне, если честно и самому нравятся функциональные решения, но(!) нужно понимать, что твоим кодом в первую очередь будут пользоваться и только во вторую — поддерживать. В связи с этим стоит всё же заменять все эти map(), reduce(), filter() и прочее на циклы во всех случаях, кроме тех, где это серьёзно ударит по читаемости и возможности повторно использовать код. Нет, не везде, но в JS точно.

                                                      • +1

                                                        Простенький оптимизатор заинлайнит вызовы функций в цикле. И если функциональные решения улучшают читаемость, то надо ставить их, пока профайлер не скажет, что тормозит или вот-вот начнёт тормозить из-за них.

                                                        • +1
                                                          В связи с этим стоит всё же заменять все эти map(), reduce(), filter() и прочее на циклы во всех случаях, кроме тех ...

                                                          Может всё-таки строго наоборот? Писать понятный удобный код пока не упрёшься в производительность. А вот уже затем профилирование и оптимизация. И скорее всего там не замена .map на for ;; будет, а что-нибудь уровня организации кода.


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


                                                          Правда назвать вот такой вот код:


                                                          const result = reduce(
                                                            // Последовательно трансформируем редьюсер, применяя наши функции к
                                                            // редьюсеру, создавая тем самым цепочку редьюсеров.
                                                            filterWins(mapGameID(firstTwo(appendToResult))),
                                                            [],
                                                            scores,
                                                          )

                                                          Понятным и красивым у меня язык не поворачивается. Та изначальная for-простыня была куда нагляднее:


                                                          const result = [];
                                                          for(const { gameID, my, others } of scores)
                                                              if(result.length === 2)
                                                                  break;
                                                              else if(my > others)
                                                                  result.push(gameID);

                                                          На мой взгляд JS в текущем виде с текущими тенденциями сильно не хватает |>-оператора. Все эти композиции функций с оборачиванием всего написанного до этого скобочками изрядно раздражают. Их, в большинстве случаев, ещё и "из глубины" читать надо, а не слева-направо и сверху-вниз.


                                                          Касательно производительности. Тут мне кажется стоило бы избегать return arr.concat(el) и return Object.assign(obj, {...}) в reducer-ах в пользу мутабельных вариантов, т.к. разница может быть более чем тысячекратной (проверял на object.assign в reduce-цикле). Таки JS пока не Haskell :)

                                                          • 0

                                                            Вот небольшой пример про 2600-кратное ускорение. Проверил его на свежей сборке хрома:


                                                            mutable: 8.73486328125ms
                                                            reduce: 19150.360107421875ms
                                                            • +1
                                                              Ваш код:
                                                              const keys = []; for(let i = 0; i < 100000; i += 10) keys.push(i); 
                                                              
                                                              console.time('mutable');
                                                              const obj1 = {};
                                                              for(let key of keys) obj1[key] = key;
                                                              console.timeEnd('mutable');
                                                              
                                                              console.time('reduce');
                                                              const obj2 = keys.reduce((o, key) => Object.assign({}, o, </b> [key]: key }), {});
                                                              console.timeEnd('reduce');
                                                              

                                                              Результат:
                                                              mutable: 3.034ms
                                                              reduce: 21646.400ms
                                                              


                                                              Исправленный код, в котором исправлена «опечатка»:
                                                              const keys = []; for(let i = 0; i < 100000; i += 10) keys.push(i); 
                                                              
                                                              console.time('mutable');
                                                              const obj1 = {};
                                                              for(let key of keys) obj1[key] = key;
                                                              console.timeEnd('mutable');
                                                              
                                                              console.time('reduce');
                                                              const obj2 = keys.reduce((o, key) => Object.assign(o || {}, { [key]: key }), {});
                                                              console.timeEnd('reduce');
                                                              

                                                              Результат:
                                                              mutable: 3.084ms
                                                              reduce: 11.763ms
                                                              


                                                              Ускорение всего лишь в 4 раза, поэтому можно не отказываться от Object.assign, если кому-то с ним удобнее/нагляднее.

                                                              Почему так получилось?
                                                              Вот эта строка:
                                                              Object.assign({}, o, { [key]: key })

                                                              Исправлена на такую:
                                                              Object.assign(o || {}, { [key]: key })

                                                              Из документации:
                                                              Метод Object.assign() копирует из исходных объектов в целевой объект

                                                              Поэтому хоть первый вариант тоже корректный, но он заставляет бесконечно клонировать объект «o» в новый пустой (каждый раз который увеличивается в размерах), вместо того, чтобы просто добавить новый ключ в уже существующий «o».
                                                              • 0

                                                                А почему вы решили, что там опечатка? В этом то суть была. Иммутабельность во имя бога иммутабельности. Аналог .concat, но для объектов. Правда я предполагаю, что concat должен быть таки оптимизированным и на деле мутировать объект (либо иметь хитрую структуру, позволяющую ему иметь часть данных общими в разных переменных), пусть хоть и за кадром, а вот с Object.assign такой фокус не прокатил.

                                                                • 0
                                                                  Эээ…
                                                                  Клонировать внутренний объект 10000 раз, только лишь для того, чтобы в результате забрать финальный клон — с трудом верю, что это именно то, что вы хотели сделать.
                                                                  Именно это и делал ваш код (хотя ваш вариант с for делает другое).

                                                                  Но если опечатки нет, и это действительно была ваша цель, то что-то вы не то делаете, к производительности js это ни как не относится, и речи про 2600-кратное ускорение тем более не идет.
                                                                  • –2
                                                                    с трудом верю, что это именно то, что вы хотели сделать

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


                                                                    Примерно также я думаю и про return arr.concat(el) в reduce-ах. Это выглядит несколько дико, не находите? Но упорно в каждой первой статье про reduce и иже с ним я вижу такие куски кода.

                                                                    • +3
                                                                      Вот ваши слова:
                                                                      Касательно производительности. Тут мне кажется стоило бы избегать return arr.concat(el) и return Object.assign(obj, {...}) в reducer-ах в пользу мутабельных вариантов, т.к. разница может быть более чем тысячекратной (проверял на object.assign в reduce-цикле). ...2600-кратное ускорение.

                                                                      И вы приводите пример кода, который специально делает много лишней работы (раз вы говорите что это не опечатка).

                                                                      Потом я привожу исправленный код, и выясняется, что никаких проблем с производительностью нет, Object.assign прекрасно работает в reduce-цикле без потери производительности.
                                                                      • +1

                                                                        Правильный выход — использовать персистентные структуры данных.

                                                                        • –2

                                                                          @kana-desu, Насколько я понимаю, их в JS пока нет. Скажем персистентный hash-map. Да даже set. А на уровне языка их не воссоздать.

                                                                          • +2
                                                                            Э, почему? Ну т.е. я готов поверить, что может получиться медленно и плохо, но по-идее ничего не должно мешать реализации.
                                                                            • +2

                                                                              Да, скажем, ClojureScript вполне спокойно реализовал всякие персистентные иммутабельные типы типа векторов, списков, хэш-мапов, множеств на js. Все это прогоняется через google closure compiler и реакт-приложения с иммутабельным стейтом на кложовских типах данных работает быстрее, чем react + redux на pure js-объектах.

                                                                              • 0

                                                                                @kana-desu, спасибо за наводку. Посмотрю как оно там внутри устроено.

                                                                          • 0

                                                                            Моя вина, ок. В процитированной вами цитате я хотел указать Object.assign({}, obj, т.к. речь вёл про true иммутабельность и ФП, но поторопился. Правда я думал, что это очевидно. Но судя по хейтеру набежавшему на мои комментарии (lol), таки не очевидно. Ок поясню мысль детальнее: в рамках ФП большое значение имеет immutable значения. Reduce в ФП предполагает проброс в него чистой функции. В рамках чистой функции Object.assign(obj... это грубейшая ошибка. Но в рамках здравого смысла это практически единственный допустимый вариант в циклах приличной длины.

                                                                            • 0
                                                                              Именно поэтому надо избегать Object.assign в reduce, потому что это не тру вэй ФП/чистые_функции, хотя производительность такая же как с вариантом с for-ом?
                                                                              … так бы я написала, если бы то что вы написали имело смысл.

                                                                              Какая разница с точки зрения иммутабельности в коде:
                                                                              Object.assign({}, o, [key]: key }) (медленный вариант)
                                                                              Object.assign(o || {}, [key]: key }) (быстрый вариант)
                                                                              Если в обоих случаях «o» это внутренний объект reduce?

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

                                                                              Еще говорят, что у меня женская логика…

                                                                              Оффтоп
                                                                              Я уже кажется с вами раньше дискутировала и уже приходила к выводу, что это бесполезно.
                                                                              Наверняка найдется тот, кто поймет ваши хитросплетения мыслей, но у меня на это банально нет желания.
                                                                              • –1
                                                                                Какая разница с точки зрения иммутабельности в коде

                                                                                Разница исключительно в подходе. Баланс между разумностью и фанатичностью. Мы с вами говорим об одном и том же, но разными словами. Даже выводы одни и те же.

                                                                                • +2
                                                                                  То есть это не вы рекомендовали не использовать Object.assign, потому что якобы он в тысячи раз медленнее в reduce?

                                                                                  Мы с вами говорим об одном и том же, но разными словами. Даже выводы одни и те же.
                                                                                  Ну да, получается мы об «одном и том же», я привожу данные что падения производительности нет, а вы утверждаете что в 2600 раз медленнее. Ну да, видно что вывод один в один.
                                                                                  • 0

                                                                                    Maiami, предлагаю продолжить в личку.

                                                                                    • 0
                                                                                      Продолжать особо не о чем, потому что я написала изначальный комментарий только чтобы мимо проходящие посетители не ужаснулись вашими псевдо-огромными цифрами и не послушали вашего совета.
                                                                                      Особенно на фоне того, что, как выяснилось позже, вы осмысленно их пытались ввести в заблуждение.
                                                                                      При этом совет про «профилирование» вполне себе разумный.
                                                                                      • 0

                                                                                        Поясню, я имел, и имею ввиду ровно следующее: Object.assign({}, o, diff это медленно, очень медленно. И в цикле так поступать нельзя. Примерно та же ситуация с arr.concat(...items) (но тут допускаю наличие оптимизаций). Отсюда вывод, если возникает желание расширять объект в reduce, то лучше либо мутировать объект напрямую, либо через тот же Object.assign, но задав ему первым аргументом сам объект. Таким образом мы нарушаем принцип иммутабельности, но это разумный компромисс, в особенности, учитывая то, что за пределами цельного вызова reduce ни один объект не модифицируется. Насколько я знаю, примерно также себя ведёт монада St в Haskell. Это совпадает с вашими словами? Да.


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


                                                                                        Если даже после ^ ничего непонятно, то поясню совсем уж тезисно:


                                                                                        • Object.assign({}, в цикле ― плохо
                                                                                        • arr.concat(newEl) в цикле, вероятно, тоже плохо
                                                                                        • Object.assign(el, diff — нормально
                                                                                        • точечная мутабельность в функциональном JS — приемлемый компромисс
                                                                                        • +2
                                                                                          Нельзя просто написать «Не используйте, это медленно, вот цифры показывающие что в 2600 раз медленнее» и надеяться, что все молча это схавают.

                                                                                          Надоело смотреть на то, как никто не хочет отвечать за свои слова «ой, я не то имел ввиду, ой, меня не так поняли, ой я говорил про сферическое фп в вакуме».
                                                                                          • 0

                                                                                            Ну вот откуда в вас столько желчи? :(

                                                                    • –1

                                                                      К слову, в lodash один из моих любимых методов — _.transform, это примерно то же, что и reduce, но с явным уклоном на мутабельность. Не требуется return. В таком случае там было бы


                                                                      .transform((o, key) => { o[key] = key; }, {})

                                                                      К тому же он поддерживает break (return false).

                                                      • 0

                                                        Я честно говоря так и не понял, чем это лучше Iterable, который гораздо понятней и для реализации поддержки в коллекции, и для построения полностью ленивых трансформаций поверх такой ленивой последовательности — в т.ч. reduce.

                                                        • 0
                                                          Iterable слишком расплывчатый термин. Можете пояснить? Тут речь идет не просто о reduce, а о композиции нескольких редьюсеров, разве iterable это про композицию?
                                                          • 0

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


                                                            • требуют нетривиальную логику во вспомогательном коде
                                                            • требуют более нетривиальной работы с пробросом состояния в рекурсивной манере
                                                            • применяются к коллекциям точно также через обёртки

                                                            Или весь вопрос исключительно в получении функционального иммутабельного ленивого аналога итераторов без ленивых коллекций?

                                                            • 0
                                                              Да, это вот все изначально — про reduce и их композицию.

                                                              Все-таки редьюсер это не совсем итератор. Это свертка, и у него в сигнатуре есть элемент коллекции и нечто, куда мы сворачиваем. Т.е. для простоты, текущая сумма, к которой прибавляется очередной элемент. Чем это лучше простого прохода — видимо тем, что это все-таки не совсем простой проход?

                                                              Что касается остального, то кратко ответ нет, это не совсем так. Нетривиальной логики там нет никакой, как впрочем и состояния. Его и в reduce нет, вообще говоря. Редьюсер это чистая функция.

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

                                                              >Или весь вопрос исключительно в получении функционального иммутабельного ленивого аналога итераторов без ленивых коллекций?

                                                              Вообще на моей памяти трансдьюсеры появились в кложе, и все-таки не совсем для этого. Другое дело что в javascript все может быть иначе.

                                                              Собственно, вот оригинальный анонс: http://blog.cognitect.com/blog/2014/8/6/transducers-are-coming
                                                        • 0
                                                          Ух, теперь-то у нас все быстро (один проход и ровно до тех пор, пока мы не получим нужное количество элементов), но красиво ли?

                                                          JS коварен, тут не все так быстро как хотелось бы. Все в одном фор цикле будет пожалуй быстрее.


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

                                                          • 0
                                                            Спасибо за статью, было интересно. Вот тесты:

                                                            императив
                                                            const result = []
                                                            
                                                            for (let i = 0; i < scores.length && result.length < 2; i++) {
                                                              const { gameID, my, others } = scores[i]
                                                            
                                                              if (my > others) {
                                                                result.push(gameID)
                                                              }
                                                            }
                                                            


                                                            трансдьюсер
                                                            class Reduced {
                                                              constructor(value) {
                                                                this.value = value
                                                              }
                                                            }
                                                            
                                                            const isReduced = value => value instanceof Reduced
                                                            
                                                            const reduced = value => isReduced(value) ? value : new Reduced(value)
                                                            
                                                            const reduce = (reducer, state, array) => {
                                                              if (isReduced(state)) {
                                                                return state.value
                                                              }
                                                              if (array.length === 0) {
                                                                return state
                                                              }
                                                            
                                                              const [x, ...xs] = array
                                                              return reduce(reducer, reducer(state, x), xs)
                                                            }
                                                            
                                                            const filter = pred => next => (acc, x) =>
                                                              pred(x) ? next(acc, x) : acc
                                                            
                                                            const map = mapper => next => (acc, x) =>
                                                              next(acc, mapper(x))
                                                            
                                                            const take = n => next => (acc, x) => {
                                                              if (n > 0) {
                                                                acc = next(acc, x)
                                                                n -= 1
                                                              }
                                                              if (n <= 0) {
                                                                acc = reduced(acc)
                                                              }
                                                            
                                                              return acc
                                                            }
                                                            
                                                            const append = (xs, x) =>
                                                              xs.concat([x])
                                                            
                                                            const compose = (...fns) => x =>
                                                              fns.reduceRight((x, fn) => fn(x), x)
                                                            
                                                            const transduce = (transducer, reducer, init, array) =>
                                                              reduce(transducer(reducer), init, array)
                                                            
                                                            const firstTwoWins = compose(
                                                              filter(({ my, others }) => my > others),
                                                              map(({ gameID }) => gameID),
                                                              take(2)
                                                            )
                                                            
                                                            const result = transduce(firstTwoWins, append, [], scores)
                                                            


                                                            объекты
                                                            function takeFrom(arr, {filter, take, max}) {
                                                              let counter = 0,
                                                                  result = []
                                                            
                                                              for (let i = 0; i < arr.length; i++) {
                                                                if (filter(arr[i])) {counter++; result.push(arr[i][take])}
                                                                if (max && counter == max) return result
                                                              }
                                                              return result
                                                            }
                                                            
                                                            const firstTwoWins = {
                                                              filter: ({my, others}) => my > others,
                                                              take  : 'gameID',
                                                              max   : 2
                                                            }
                                                            
                                                            takeFrom(scores, firstTwoWins)
                                                            


                                                            на правах изврата
                                                              const win = ({my, others}) => my > others
                                                              const firstResult = scores.findIndex(win)
                                                              const secondResult = firstResult + 1 + scores.slice(firstResult + 1).findIndex(win)
                                                            


                                                            повседневный
                                                            const result =
                                                              scores
                                                                .filter(({ my, others }) => my > others)
                                                                .map(({ gameID }) => gameID)
                                                                .slice(0, 2)
                                                            



                                                            Результаты:
                                                            количество операций за 1 секунду, время выполнения операции
                                                            
                                                            элементов в массиве    10          100        100 000    1 000 000
                                                            
                                                              императив          1 360 836   1 347 020   1 454 109   1 426 568
                                                                                    0           0           0           0
                                                            
                                                              трансдюсер         361 359      63 541       47           5 (1 если элементы конечные)
                                                                                    0           0          28          247 (1 066 если элементы конечные)
                                                            
                                                              обьекты            1 194 086   1 151 443   1 128 703   1 160 694
                                                                                    0           0           0           0
                                                            
                                                              на правах изврата  904 147     809 852      1 436        139
                                                                                    0           0           1           5
                                                            
                                                              повседневный       281 325      47 045       49           5
                                                                                    0           1          26          224
                                                            


                                                            • 0

                                                              Всё-таки в повседневном нужно сначала лимитировать коллекцию кол-вом, а потом брать нужные данные, а не наоборот, как это в статье.

                                                            • 0
                                                              По ходу статьи 10 строчек плавно превратились в стек абстракций, который с наскоку не смогут понять 90% знакомых разрабов. А победу в цифрах можно выразить?
                                                              • +1

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


                                                                То есть разница между [1...100000].filter(..).map(..).take(5) и аналогичным решением на трансдьюсерах будет различаться на N порядков.

                                                                • 0
                                                                  Тогда benchmark. Когда видишь пример и разницу во времени для разных реализаций, это мотивирует.
                                                                    • 0
                                                                      На 6.3.1 у меня даже лучше.

                                                                          Name: Array methods
                                                                          HZ: 4603.449432980031
                                                                          Count: 241
                                                                        
                                                                      
                                                                          Name: Transducers
                                                                          HZ: 138288.02637186056
                                                                          Count: 7398
                                                                      


                                                                      Простой цикл не участвует, потому что он безнадежен?
                                                                      • 0
                                                                            Name: Array methods
                                                                            HZ: 9575.634258372818
                                                                            Count: 491
                                                                        
                                                                            Name: Transducers
                                                                            HZ: 133520.15607709397
                                                                            Count: 6952
                                                                        
                                                                            Name: For
                                                                            HZ: 2598682.4299278776
                                                                            Count: 134571

                                                                        code:


                                                                        Код
                                                                        .add("For", () => {
                                                                            const result = [];
                                                                            for(const { my, others, gameID } of scores)
                                                                              if(my > others)
                                                                              {
                                                                                result.push(gameID);
                                                                                if(result.length >= 2)
                                                                                  break;
                                                                              }
                                                                          })

                                                                        Полная версия. Что-то уж сильно они разнятся, наверное я что-то напутал.

                                                                        • 0
                                                                          Или для такого случая трансдьюсер не нужен.

                                                                              Name: Array methods
                                                                              HZ: 4728.839488752196
                                                                              Count: 240
                                                                            
                                                                          
                                                                              Name: Transducers
                                                                              HZ: 125970.59052681188
                                                                              Count: 7208
                                                                            
                                                                          
                                                                              Name: Loops
                                                                              HZ: 1856034.4085212706
                                                                              Count: 94904
                                                                          
                                                                          
                                                                        • 0

                                                                          Нет, циклы заведомо быстрее и нет смысла с ними даже сравнивать)

                                                                          • +1
                                                                            Тогда возвращаясь к началу, как в цифрах выразить победу трансдьюсера над циклом? =)
                                                                            • 0

                                                                              Я процитирую себя же:


                                                                              Трансдьюсеры — апгрейд для функциональных трансформаций

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


                                                                              В гисте ниже я выполнил пару оптимизаций:


                                                                              • трансдьюсеры/редьюсеры хороши тем, что мы можем работать с транфсормацией как с объъектом, это их сильная сторона (реюзабельность), поэтому я вынес их (по сути как aot-компиляция, которая проводится для циклов).
                                                                              • как тут советовали где-то выше в комментах, нет смысла делать concat, потому что стейт нигде больше не используется, адекватно будет заменить на push. Как оказалось, существенной роли это не сыграло, concat видимо как-то оптимизируется.

                                                                              Name: Array methods
                                                                              HZ: 4642.041341511592
                                                                              Count: 240
                                                                              Name: Transducer
                                                                              HZ: 161841.1974395176
                                                                              Count: 8669
                                                                              Name: For
                                                                              HZ: 786935.9631453991
                                                                              Count: 40472

                                                                              Функциональные трансформации медленнее циклов на 3 порядка, но трандьюсеры быстрее их на два.


                                                                              https://gist.github.com/kana-sama/d0d5eb8362dda32841cc58302fa1c191

                                                                              • +2
                                                                                Т.е. если переводить в цифры, мы ожидаем сокращения количества баг-репортов, например.
                                                                                • –1
                                                                                  Функциональные трансформации медленнее циклов на 3 порядка, но трандьюсеры быстрее их на два.

                                                                                  Если я правильно понял суть, то функ. трансформации медленнее трандьюсеров настолько, насколько душе угодно. Ведь основной принцип: не считать лишнее. Так что можно подобрать такой пример, что там и N порядков будет :)

                                                                  Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                                                  Самое читаемое