Reputation: 956
When using the reduce method on an array of functions I am having difficulty tracing through how reduce works on the array exactly.
comboFunc(num, functionsArr) {
return functionsArr.reduce(function (last, current) {
return current(last);
}, input);
}
so with functionsArr = [add, multi]
and the functions add
and multi
being
function add(num) {
return num + 1;
}
and
function multi(num) {
return num * 30;
}
the function comboFunc
would:
comboFunc(2, functionsArr); //returns 90;
I understand how the reduce method works on an array of numbers folding the array into one value as it were, by applying a function to each element of the array. This example with comboFunc taking an array of functions as a parameter uses simple enough math that I can understand how reduce produces the value of 90 mathematically but I don't understand what is happening line by line and on each iteration from a functional level. I'm unable to articulate in my mind what exactly is happening when---how the input of 2 is getting passed from function to function vis-a-vis closure and the sequential firing off of each function, etc.
If the reduce method applies the function you pass in to each element of an array, and when each element in the array is a function...how can I write out for my own understanding syntactically how that looks?
The function passed into the reduce method returns current(last)
so that's a function returning a function. So that would make last
equal to the add
function and current
equal to multi
? In other words returning multi(add)
? If my logic is correct, this means what is happening is that the input then gets passed into add
and computed and then that returned value is passed into multi
and computed to ultimately return 90?
Upvotes: 1
Views: 119
Reputation: 135357
It looks like you're trying to create a composition function that takes an array of functions to apply to an input, num
Let's first cover a couple syntax errors. First you're missing the function
keyword
// your code
comboFunc(num, functionsArr) {
// should be
function comboFunc(num, functionsArr) {
Next, you've named your parameter num
but then you reference it as input
in the function body
// your code
}, input);
// should be
}, num);
So after you fix this, your function works, but what if we approach the problem a little differently?
Disclaimer: this answer does not walk you through the function you've provided. Rather, it sympathizes with your difficulty by demonstrating
comboFunc
was written in an overly complex way. In turn, I give you an alternate definition of your function that operates identically but is a lot easier to read/understand.
To begin, I think we should go back to the basics of function composition.
I like picturing function composition as a little map.
f(x)=y g(y)=z
+--------> y -------+
| |
| |
| v
x ----------------> z
?
In the example above, we could easily replace f with your add
function, and g with your multi
function. Defining ? should also be fairly obvious now.
add(2) multi(3)
+--------> 3 -------+
| |
| |
| v
2 ----------------> 90
h(x) = g(f(x)) = z
h(x) = (g∘f)(x) = z
h(2) = 90
(g∘f) can be said "g of f" and it just means apply x to f first, then apply that result to g.
Notice y isn't even present. We've defined h in terms of an x that gives us a direct "map" to a z, skipping over y entirely.
Composing two functions together is a very basic operation, and we could define a function to do that very easily
function comp(g,f) {
return function(x) {
return g(f(x));
}
}
var h = comp(multi,add);
h(2); // 90
// h(2) = multi(add(2)) = 90
"Yeah, but I'm trying to work with N functions. Not just 2."
OK, comp
works for two functions, but I've over simplified the problem. You're probably wondering how this would work if we wanted to compose 3, 4, or even more functions together.
Well let's look at it again
a(w) = x
b(x) = y
c(y) = z
d(w) = c(b(a(w))) = z
d(w) = (c∘b∘a)(w) = z
Look very closely at the ∘
. We know this to be our comp
function and we can see it appear between each of our functions, a, b, c.
Before we go further, let's say we have an array of numbers
var xs = [1,2,3];
If we want to sum the numbers, we would need to call
var sum = 1 + 2 + 3
See how the +
appears between each number? With our array of functions, it's no different!
var fs = [c,b,a];
var d = (c ∘ b ∘ a)
We just need to make that valid JavaScript. And it'll be easy too because we've already defined ∘
as comp
.
This calling of a function between terms in a list is called a linear fold and JavaScript has a function that does this called reduce
.
Alright, so let's put it use! We'll fold ("reduce") our list of functions using our comp
function
function compN(fs) {
return fs.reduce(comp);
}
var h = compN([multi, add]);
h(2); // 90
// h(2) = multi(add(2)) = 90
// h(2) = (multi ∘ add)(2) = 90
OK, so it works for our original two functions, but let's make sure it works with an even longer list
var i = compN([multi, add, add, add]);
i(2); // 150
// i(2) = multi(add(add(add(2))) = 150
// i(2) = (multi ∘ add ∘ add ∘ add)(2) = 150
Ok, so let's recap
function add(num) {
return num + 1;
}
function multi(num) {
return num * 30;
}
function comp(g,f) {
return function(x) {
return g(f(x));
}
}
function compN(fs) {
return fs.reduce(comp);
}
compN([multi, add])(2); // 90
"But why is this better?"
Just as we cut out y
in h(x) = (g∘f)(x) = z
, notice how our compN
function doesn't concern itself with num
, last
, or current
. We've dropped 3 variables for our brain to think and it's much clearer what our function is doing.
comp
takes f and g and composes them together to make g ∘ f
.
compN
calls comp
between each term in a list of functions
To me, this is very straight forward and easy to understand. Just looking at the function bodies for each comp
and compN
, I can identify the intent immediately.
Compare that to
comboFunc(num, functionsArr) {
return functionsArr.reduce(function (last, current) {
return current(last);
}, input);
}
It's no wonder you had difficulty trying to follow it. comboFunc
was concerning itself with too much from the very onset because it's trying to be two operations instead of just one.
So, I know I didn't step you through your original function, but I hope after reading this answer you have an even greater understanding of function composition and building your own functions with separated concerns.
EDIT
According to some discussion below, it would be valuable for our compN
function to work on an empty array, []
.
var f = compN([]);
// Uncaught TypeError: Reduce of empty array with no initial value
Yuck! The intended behavior here will be for compN
to create a function that returns an unmodified input.
function id(x) {
return x;
}
Using this as the starting point for the reduce will guarantee that our compN
function will work even when an empty array is given
// revised compN
function compN(fs) {
return fs.reduce(comp, id);
}
Check it out
compN([])(2); // 2
compN([multi, add])(2); // 90
Now compN
will work for an array of 0 or more functions.
Upvotes: 2
Reputation: 23670
The easiest way to understand what is happening is to add console.log()
to your code so you can see what is happening step-by-step:
function comboFunc(num, functionsArr) {
return functionsArr.reduce(function (last, current) {
console.log(current + " " + last + " ==> " + current(last));
return current(last);
}, num);
}
function add(num) {
return num + 1;
}
function multi(num) {
return num * 30;
}
var functionsArr = [add, multi];
console.log( comboFunc(2, functionsArr) );
Output:
"function add(num) {
return num + 1;
} 2 ==> 3"
"function multi(num) {
return num * 30;
} 3 ==> 90"
90
This essentially is
multi(add(2)); // (2+1)*30 = 90
Upvotes: 1
Reputation: 1074969
The way you've called it (with two arguments), Array#reduce
calls your callback once for each entry in the array.
On the first call, the last
argument is the value you gave at the end (input
, in your case) and the current
argument is the first entry in the array. Your callback is calling that entry (your first function) passing in last
, and so the first function is called with 2
. Since that's add
, the result of calling it is 3
, which you then return from your callback.
On the second call, the last
argument is what you returned on the previous call; so it's 3
. The current
argument is the next function in the array. In your case, that's multi
, and so you call it with 3
. multi
returns 90
, which you return.
The result of the reduce
is the last value the callback returned. So, 90
, in your example.
You can think of this form of reduce
like this (this is not literally what it does, reduce
is more complicated; this is a simplified version):
// Again, this is a *simplified* example
function pseudoReduce(array, callback, initialValue) {
var index, last;
last = initialValue;
for (index = 0; index < array.length; ++index) {
last = callback(last, array[index], index, array);
}
return last;
}
(Array#reduce
would behave slightly differently on the first pass if you didn't give it that second argument; that's not reflected above.)
Upvotes: 1