Igor Konovalov
JS developer with a passion. Currently working as a senior software engineer at Epam
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() и пр., найти в них общую идею, и научиться совмещать вместе несколько операций для дальнейшего переиспользования
Через свертку можно определить любую операцию над массивами (filter, map)
Разобрав, как работают базовые операции над массивами через reduce мы сможем выделить общее и понять принцип
начнем с абстрактного: из чего состоит reduce?
перебор
трансформация
создание новой коллекции
Чистая функция, принимающая 2 аргумента (аккумулятор и значение) и возвращающая один (аккумулятор)
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
}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
}, [])
}
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
}
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 (ENG): https://medium.com/@roman01la/understanding-transducers-in-javascript-3500d3bd9624
Примеры кода этой лекции: https://github.com/IgorKonovalov/Transducer-Talk-Examples
CSP and transducers: https://tgvashworth.com/2014/08/31/csp-and-transducers.html
Transducers.JS: https://jlongster.com/Transducers.js--A-JavaScript-Library-for-Transformation-of-Data
Transducers.JS lib: https://github.com/jlongster/transducers.js
Transducers + Ramda: http://simplectic.com/blog/2015/ramda-transducers-logs/
Rich Hickey - transducers (Closure): https://www.youtube.com/watch?v=6mTbuzafcII
By Igor Konovalov
Функциональные паттерны разработки в JS. Построение простых, прозрачных и переиспользуемых алгоритмов для преобразования данных. Эффективность и возможность работы с любым перечисляемым типом данных как отличительная черта трансдюсеров.
JS developer with a passion. Currently working as a senior software engineer at Epam