I just want to know how reduce works in the case of code below(which was provided by a stackoverflow user in my previous question, i’m asking this question as his code snippet led to me having more questions that weren’t cleared up and are too long to fit in a comment section). An array of functions is passed into a reducer. There is a compose function which runs on the array of functions. From my understanding of this f is the accumulator and g is the next item in the array. What is returned each cycle of the reduce becomes the accumulator for the next cycle. If there is no initalValue parameter passed into reduce then the first item in the array will be used as the initial accumulator value.
const compose = (f, g, i) => (...args) => { console.log(i, g); console.log(i, f); return f(g(...args)); } const f_xe = (x) => x + 'e', f_xd = (x) => x + 'd', f_xc = (x) => x + 'c', f_xy = (x, y) => x + y; console.log([f_xe, f_xd, f_xc, f_xy].reduce(compose)('a','b')); // 3 [Function: f_xy] // 3 [Function] // 2 [Function: f_xc] // 2 [Function] // 1 [Function: f_xd] // 1 [Function: f_xe] // abcde
I visualize it like this:
cycle #1: f = f_xe g = f_xd return f(g(...args)) ^ which is f_xe(f_xd('a', 'b')) cycle #2: f = what was returned previously ^^ which will be f_xe(f_xd('a', 'b')) g = f_xc return f(g(...args)) ^^ which is f_xe(f_xd('a', 'b'))(f_xc('a', 'b'))
I already know this line of thinking is wrong the way the flow works it works in an encapsulating manner, like so: f_xe(f_xd((f_xc(f_xy('a', 'b')))))
but why is this the case. If someone can intricately explain why it wraps this way and break down each cycle of the reduce step by step it would be immensely appreciated. Another thing I was wondering is, why doesn’t f just try to evaluate immediately on the first cycle? f_xe(f_xd('a', 'b'))
when this piece of code is returned wouldn’t it try to evaluate that and produce an error instead of proceeding to the next item in the array? Instead the code starts evaluating from the last item in the array even though the compose function is instructed to be applied from the beginning. Which I do understand as with a composition function the last item will be ran first and then so on, however shouldn’t the console log statements be ran in the order of first to last?
Again, I know my line of thinking is completely off with this one, but I was hoping if I shared my train of thought someone could push it in the right direction. Thank you to anyone who can shed some light on this.
Advertisement
Answer
Forget about the 'a'
and 'b'
arguments first. The important part is
const f = [f_xe, f_xd, f_xc, f_xy].reduce(compose);
This is what we need to look at, and where we can apply our definition of reduce
for. The call of f('a','b')
comes later.
When expanding the reduce
call, we find
const f = compose(compose(compose(f_xe, f_xd, 1), f_xc, 2), f_xy, 3);
(This is a bit weird actually. I’d recommend using reduceRight
for composing functions. Also pass the identify function as the initial value for the accumulator.)
Now we can expand the compose
calls:
const f1 = (...args) => { console.log(1, f_xe); console.log(1, f_xd); return f_xe(f_xd(...args)); } const f2 = (...args) => { console.log(2, f1); console.log(2, f_xc); return f1(f_xc(...args)); } const f3 = (...args) => { console.log(3, f2); console.log(3, f_xy); return f2(f_xy(...args)); } const f = f3;
Now when you call f3('a', 'b')
, you can see why the logs happen “backwards”.
shouldn’t the console log statements be ran in the order of first to last?
If you want that, you maybe better put them in the compose
function and not in the closure that it returns. Try with
const compose = (f, g, i) => { console.log(i, g); console.log(i, f); return (...args) => f(g(...args)); }