[Transducer] Step by Step to build a simple transducer

Transducers are composable algorithmic transformations. They are independent from the context of their input and output sources and specify only the essence of the transformation in terms of an individual element. Because transducers are decoupled from input or output sources, they can be used in many different processes - collections, streams, channels, observables, etc. Transducers compose directly, without awareness of input or creation of intermediate aggregates.

OK, short in description... let's see why we need it

  1. Normal Javascript `map`,`filter` create inter variable and loop though array everytime we call `map` or `filter` function, `transducer` can loop thought the array only ONCE and apply all the transofrmations necessary.
const data = [1,2,3];
const inc = x => x + 1;
const double = x => 2 * x;
const lessThanThree = x => x < 3;
////////////////////
/**
 * Problem: We loop over array 3 times! We want to loop over only once
 * in order to improve the profermance.
 */
const res1 = data
    .filter(lessThanThree)
    .map(double)
    .map(inc)

console.log(res1)    // [3,5]

 

  2. We don't want to introduce any mutation or impure function such as `forEach` does, transducer are mutation free.

/**
 * Problem: it is not pure function and we do mutation. But it is faster
 * than we do .filter.map.map style, because it only loop the array once.
 */
let res2 = [];
data.forEach((x) => {
    let item;
    if (lessThanThree(x)) {
        item = inc(double(x))
        res2.push(item);
    }
})
console.log(res2)    // [3,5]

 

  3. We want to style functional style to keep the code more readable, in the meanwhile improve the proferemance:

/**
 * Good: We avoid the mutation and can be write as pure function and it only loop once!
 * Problem: But we lose our function composion style! We still want .filter.map.map styling.
 * Meanwhile it should be profermance wise.
 */
const res3 = data.reduce((acc, curr) => {
    if (lessThanThree(curr)) {
        acc.push(inc(double(curr)));
    }
    return acc;
}, []);
console.log(res3);    // [3,5]

 

OK, until now, we have some idea, what kind of code we want. Basiclly it should be composable and efficient.

The question is how to make composable code?

As we might know about, in OOP; if we want to chain multi function calls, from each function, we need to return `this`:

Class Bot {
    ...
    sayName() {
        console.log(this,name)
        return this;
     }

    sayHello() {
        console.log("Hello")
        return this;
    }

}

const b = new Bot('Petter')

b.sayName().sayHello() 

For Array, the reason we can chain calls together is because each call return Array type. The same as String, number...

 

The key is we need to keep the input and output as the same type!

Therefore for function, we need to keep input function and output function have the same function signature! 

//data.reduce(reducer, seed), reducer is something we can compose!
//Because reducer :: (acc, curr) => acc
//For every reducer functions' signature are the same.
//If the function sinature are the same, then we can compose function together!
const _mapReducer = (xf, array) => 
    array.reduce((acc, curr) => {
        acc.push(xf(curr))
        return acc;
    }, []);
const _filterReducer = (xf, array) => 
    array.reduce((acc, curr) => {
        if (xf(curr)) acc.push(curr);
        return acc;
    }, []);
// To make fns easy to compose, we extract 'array' data & init value
const mapReducer = (xf) => ((acc, curr) => {
    acc.push(xf(curr))
    return acc;
});
const filterReducer = pred => ((acc, curr) => {
    if (pred(curr)) acc.push(curr);
    return acc;
});
// now mapReducer and filterReducer both have the same function signature.
console.log(data.reduce(mapReducer(double), [])); // [2,4,6]
console.log(data.reduce(mapReducer(inc), [])); // [2,3,4]
console.log(data.reduce(filterReducer(lessThanThree), []));  // [1,2]

 

In order to compose reudcers together we need to make mapReducer and filterReducer as high order functions to take reducer as arguement, take a reducer as input and return a reducer signature as output is the key to do composion!
// In order to compose reudcers together we need to make mapReducer and filterReducer as high order functions to take reducer as arguement
// Take a reducer as input and return a reducer signature as output is the key to do composion!
const map = xf => reducer => ((acc, curr) => {
    acc = reducer(acc, xf(curr))
    return acc;
});
const filter = pred => reducer => ((acc, curr)=> {
    if (pred(curr)) acc = reducer(acc, curr) 
    return acc;
})
// For mapReducer and filterReducer, we both do acc.push()
// therefore we can extrat this as base reducer
const pushReducer = (acc, value) => {
    acc.push(value);
    return acc;
};

 

Now we are able to use functional style and loop the array only once!
const doulbeLessThanThree = compose(
    map(inc),
    map(double),
    filter(lessThanThree)
)
const res5 = data.reduce(doulbeLessThanThree(pushReducer),  []);
console.log(res5); // [3,5]

 

Define our transducer!

/**
 * transducer :: ((a -> b -> a), (a -> b -> a), [a], [a]) -> [a]
 * @param {*} xf: base reducer 
 * @param {*} reducer: the composion redcuer signature
 * @param {*} seed : init value
 * @param {*} collection : data
 */
const transducer = (xf, reducer, seed, collection) => {
    return collection.reduce(reducer(xf), seed);
}
const res6 = transducer(pushReducer, doulbeLessThanThree, [], data);
console.log(res6); // [3,5]

 

posted @ 2019-02-20 20:54  Zhentiw  阅读(273)  评论(0编辑  收藏  举报