I have been reading up on transducers and playing around trying to grasp the concept. I now understand them a bit, but during my fiddling, I came across something very peculiar that has me dumbfounded. I'm hoping someone can explain what I'm missing.
I have 2 transducers that have the signature: reducer -> reducer
I also have a simple compose function: const compose = (f, g) => x => f(g(x))
When I compose the 2 transducers:
const filterLessThanThreeAndMultiply = compose(
filteringReducer(lessThanThreePredicate),
mappingReducer(multiplyTransform)
)
I would expect the evaluation to be from right to left, in this case applying the mapping transform before the filtering. Instead, the filtering is applied first (which gives the expected answer).
But f(g(x)) runs f of the result of g(x), so my result should reflect:
filteringReducer(lessThanThreePredicate)(mappingReducer(multiplyTransform)
(concatTransducer))
But instead it reflects (correctly):
mappingReducer(multiplyTransform)(filteringReducer(lessThanThreePredicate)
(concatTransducer))
(See code below)
Why??!! (I suspect I'll make a quantum leap in understanding once someone explains to me what's happening here).
const filteringReducer = predicate => transducer => (result, input) =>
predicate(input) ? transducer(result, input) : result
const mappingReducer = transform => transducer => (result, input) =>
transducer(result, transform(input))
const compose = (f, g) => x => f(g(x))
const concatTransducer = (a, b) => a.concat([b])
const lessThanThreePredicate = x => x < 3
const multiplyTransform = x => x * 100
const filterLessThanThreeAndMultiply = compose(
filteringReducer(lessThanThreePredicate),
mappingReducer(multiplyTransform)
)
const result = [-2, -1, 0, 1, 2, 3, 4].reduce(
filterLessThanThreeAndMultiply(concatTransducer),
[]
)
console.log('result ', result) // [-200, -100, 0, 100, 200]
You are seeing the
filterhappen (correctly) before themapeven though they are both insidecomposebecause of an implementation detail of transducers. I will explain by breaking down transducers in implementation and then showing you how to use them.Consider the following variants of
mapandfiltermapArraytakes a functionfnand an array and returns another array withfnapplied to each element.filterArraytakes a functionfnand an array and return another array filtered byfnmapReducertakes a functionfnand returnsreducer => (y, xi) => {...}filterReducertakes a functionfnand returnsreducer => (y, xi) => {...}Now, consider the transducer signature
Given
(y, xi) => {...}is just another reducer, this meansmapReducer(multiplyTransform)andfilterReducer(lessThanThreePredicate)are both transducers.Great! So now we know what transducers are, but how do we use them?
Exhibit A (no compose)
In order to set up our transducers so that we first filter
x => x < 3and then mapx => x * 100, we must compose our transducerslessThanThreeTransducerandx100Transduceras we have done above. Now, if we toss incompose, you will have your answer as to why everything seems backwards.Exhibit B (with compose)
Indeed,
finalComposedReducerandfinalReducerLessThanThreeThenX100ThenConcatare algorithmically equivalent. So then,It's an implementation detail of transducers. If you're still curious about transducers, I write more about them here.