Having trouble stepping through function that reduces an array of functions

120 views Asked by At

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?

3

There are 3 answers

4
Mulan On BEST ANSWER

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.

0
T.J. Crowder On

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.)

0
Drakes On

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