Трансдюсеры в JS

О себе:

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

Twitter: igor_dlinni

Github: IKonovalov

Instagram: some_strange

Codepen: Igor_Konovalov

Оглавление:

  • проблемы подходов к перебору коллекций - перформанс против декларативности
  • что такое трансдюсеры и зачем они нам нужны
  • иплементация map и filter через reduce
  • к трансдюсерам через декомпозицию map и filter
  • blue bird комбинатор
  • функция - хелпер transduce и ее друзья
  • немного экспериментов
  • перформанс замеры 2
  • вопросы

YO DAWG I HEARD YOU LIKE FUNCTIONAL PROGRAMMING SO I PUT FUNCTIONS IN YOUR FUNCTIONS

SO YOU CAN FUNCTION WHILE YOU CALL FUNCTIONS

arrayOfMillion // 613 ms
    .map(xform1)
    .map(xform2)
    .map(xform3)
    .filter(predicate)

(() => { // 147 ms
  let el
  const res = []
  const { length } = arrayOfMillion
  for (let i = 0; i < length; i++) {
    el = xform3(xform2(xform1(arrayOfMillion[i])))
    if (predicate(el)) {
      res.push(el)
    }
  }
  return res
})()

Перформанс против читаемости:

Декларативно - говорим функции что нам нужно сделать

Императивно - говорим функции как нам нужно сделать

при каждом проходе map или filter создается дополнительный массив

Старый добрый итеративный метод (и трансдюсеры) не создает дополнительных массивов

Проблема:

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

Что мы хотим?

const transform = compose(
    map(xform1),
    map(xform2),
    map(xform3),
    filter(predicate)
)

applyTransform(collection, transform)
  • только один проход по коллекции
  • возможность композиции map и filter
  • читабельность!

Выход есть!

Трансдюсеры

это

мощный и компонуемый способ построения алгоритмических преобразований

или

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

[].reduce

Через свертку можно определить любую операцию над массивами (filter, map)

Разобрав, как работают базовые операции над массивами через reduce мы сможем выделить общее и понять принцип

начнем с абстрактного: из чего состоит reduce?

перебор

трансформация

создание новой коллекции

Редюсер

Чистая функция, принимающая 2 аргумента (аккумулятор и значение) и возвращающая один (аккумулятор)

  • не привязан к типу коллекции
  • не обязательно является составной частью метода reduce
  • обязан только вернуть аккумулятор - значение вообще может не использоваться (если предикат вернул false)

Несколько примеров редюсеров:


const sumReducer = (accumulation, value) => 
    accumulation + value


const objReducer = (accumulation, value) => ({
  ...accumulation,
  ...value
})

const setReducer = (accumulation, value) =>
    accumulation.add(value)

const pushReducer = (accumulation, value) => {
    accumulation.push(value)
    return accumulation
}

map и filter через reduce

const map = (xf, array) => {
  return array.reduce((accumulation, value) => {
    accumulation.push(xf(value))
    return accumulation
  }, [])
}

const filter = (predicate, array) => {
  return array.reduce((accumulation, value) => {
    if (predicate(value)) accumulation.push(value)
    return accumulation
  }, [])
}

BEHOLD!

Что общего в полученных map и filter?

const map = (xf, array) => {
  return array.reduce((accumulation, value) => {
    accumulation.push(xf(value))
    return accumulation
  }, [])
}

const filter = (predicate, array) => {
  return array.reduce((accumulation, value) => {
    if (predicate(value)) accumulation.push(value)
    return accumulation
  }, [])
}

 

const map = (xf, array) => {
  return array.reduce((accumulation, value) => {
    accumulation.push(xf(value))
    return accumulation
  }, [])
}

map(func, [])
const mapDecoupled = xf => (accumulation, value) => {
  accumulation.push(xf(value))
  return accumulation
}

[].reduce(mapDecoupled(func), [])
const mapTransducer = xf => reducer => (accumulation, value) => {
  return reducer(accumulation, xf(value))
}

const pushReducer = (accumulation, value) => {
    accumulation.push(value)
    return accumulation 
}

[].reduce(mapTransducer(func)(pushReducer), [])

И еще раз, всё вместе:

Трансдюсер - декоратор редюсера

В случае нашего map и filter он принимает функцию трансформации или предикат и возвращает функцию, принимающую редюсер и возвращающую редюсер

const map = xf => reducer => (accumulation, value) => {
  return reducer(accumulation, xf(value))
}

const filter = predicate => reducer => (accumulation, value) => {
  if (predicate(value)) return reducer(accumulation, value)
  return accumulation
}

Схема работы Трансдюсера

Редюсер

Редюсер

Трансдюсер

Редюсер

Трансдюсер

Редюсер

Таким образом, трансдюсер возвращает функцию, принимающую и возвращающую редюсер (сигнатуры этих функций одинаковы).

Композиция - возможна!

const isEvenOnlyFilter = filter(x => x % 2 === 0)

const isNot2Filter = filter(x => x !== 2)

const doubleTheMap = map(x => x * 2)

const cleanNumbers = isEvenOnlyFilter(isNot2Filter(doubleTheMap)))

Избавимся от пугающего количества скобок с b-combinator

g(...(f(x))) === compose(g, ..., f)(x)

const compose = (...functions) =>
  functions.reduce((accumulation, fn) =>
    (...args) => accumulation(fn(...args), x => x))

уже есть в Ramda и Lodash

foo(bar(baz))(x) === compose(foo, bar, baz)(x)
const cleanNumbersXf = compose(isNot2Filter, isEvenOnlyFilter, doubleTheMap)

И, наконец, избавляемся от привязки к типу коллекции при помощи хелпера transduce

const transduce = (xf, reducer, seed, collection) => {
  const transformerReducer = xf(reducer)
  
  let accumulation = seed
  
  for (const value of collection) {
    accumulation = transformerReducer(accumulation, value)
  }

  return accumulation
}

Функции, частично применяющие transduce

Partial Application

// into([], xf, collection) 

const into = (to, xf, collection) => {
    if (Array.isArray(to)) return transduce(xf, pushReducer, to, collection)
    else if (isPlainObject(to)) return transduce(xf, objReducer, to, collection)
    else throw new Error('choose another type')
}

// sequence(xf, collection)

const sequence = (xf, collection) => {
    return into([], xf, collection)
}

etc...

Эксперименты и замеры производительности

Вывод:

Трансдьюсеры - простой паттерн, основанный на декорировании и композиции редьюсеров, позволяющий эффективно обрабатывать большие объемы данных.

 

 

 

 

Трансдьюсер — функция, которая принимает один редьюсер и возвращает новый

Трансдьюсеры не привязаны к типу коллекции, поддерживают ленивые вычисления и декларативны

Спасибо за внимание!

Трансдюсеры в JS

By Igor Konovalov

Трансдюсеры в JS

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

  • 1,170