gpbaculio
gpbaculio

Reputation: 5968

I need help understanding the rest and spread operator

This is the code:

const Pipe = (...fns) => fns.reduce((f,g) => (...args) => g(f(...args)));

So by (...fns) the fns arguments becomes an array right? in this part:

 (f,g) => (...args)

where did args came from? is there a default args parameter? and I cannot read this part:

(...args) => g(f(...args))

I just cannot wrap my head with this nesting and what reduce does here is so confusing.

Upvotes: 2

Views: 941

Answers (3)

NiRUS
NiRUS

Reputation: 4259

For better understanding I have translated the code to block level as below trying to explain the code in question for a novice developer which helps. For better implementation @naomik has solution.

ES6

const Pipe = (...fns) => {
    return fns.reduce((f, g) => {
        return (...args) => {
            return g(f(...args))
        }
    })
};

Equivalent ES5 implementation:

var Pipe = function () {
    var fns = [];
    for (var _i = 0; _i < arguments.length; _i++) {
        fns[_i] = arguments[_i];
    }
    return fns.reduce(function (f, g) {
        return function () {
            var args = [];
            for (var _i = 0; _i < arguments.length; _i++) {
                args[_i] = arguments[_i];
            }
            return g(f.apply(void 0/*Undefined or can use 'null'*/, args));
        };
    });
};

Part by part breakdown:(read the code comments for better understanding)

const inc = (num) => num+1 //Function to increment the number
const dbl = (num) => num*2 //Function double's the number
const sqr = (num) => num*num //Function Square's the number

/*Function breakdown*/
const _pipe = (f, g) => (...args) => g(f(...args)); //Second part
const Pipe = (...fns) => fns.reduce(_pipe); //First part

const incDblSqr = Pipe(inc, dbl, sqr) //Piping all the functions
const result = incDblSqr(2) // Piped function
console.log(result) // ((2+1)*2) ^ 2  = 36

Explaination:

The code in question helps to pipe the result from one function to another function.

Step by step process of above code:

  • First number is incremented by 1
  • The result after increment is accumulated and piped to another function where the number gets multiplied by 2 (doubling it)
  • Finally the doubled number is squared next function.

All these are achieved using closures which has access to the reducing arguments (Read the article linked below for clarity).

Conclusion: The code in question helps to pipe the n-number of functions that operates on the result emitted by previous function.


Best article by Andrew L. Van Slaars: https://vanslaars.io/post/create-pipe-function/

Please read the above article for clarity and also @naomik solution

Upvotes: 0

Mulan
Mulan

Reputation: 135277

Your first problem is you're dealing with a bad implementation of pipe – the second problem is there's a variety of spread syntaxes in new JavaScript and it's not always clear (to beginners) which one is being used where

rest parameter

a rest parameter collects the supplied arguments to a function in an array. This replaces the old arguments object of JavaScript's yesteryear

const f = (...xs) =>
  xs
  
console.log(f())    // []
console.log(f(1))   // [1]
console.log(f(1,2)) // [1,2]


spread arguments

spread arguments allows you to spread an array (or any iterable) as arguments to a function call. This replaces (almost all) instances of Function.prototype.apply

const g = (a,b,c) =>
  a + b + c
  
const args = [1,2,3]

console.log(g(...args)) // 6


why that pipe is bad

It's not a total function – pipes domain is [Function] (array of functions), but this implementation will produce an error if an empty array of functions is used (TypeError: Reduce of empty array with no initial value)

It might not be immediately apparent how this would happen, but it could come up in a variety of ways. Most notably, when the list of functions to apply is an array that was created elsewhere in your program and ends up being empty, Pipe fails catastrophically

const foo = Pipe()
foo(1)
// TypeError: Reduce of empty array with no initial value

const funcs = []
Pipe(...funcs) (1)
// TypeError: Reduce of empty array with no initial value

Pipe.apply(null, funcs) (1)
// TypeError: Reduce of empty array with no initial value

Pipe.call(null) (1)
// TypeError: Reduce of empty array with no initial value

reimplementing pipe

This is one of countless implementations, but it should be a lot easier to understand. We have one use of a rest parameter, and one use of a spread argument. Most importantly, pipe always returns a function

const pipe = (f,...fs) => x =>
  f === undefined ? x : pipe(...fs) (f(x))
  
const foo = pipe(
  x => x + 1,
  x => x * 2,
  x => x * x,
  console.log
)

foo(0) // 4
foo(1) // 16
foo(2) // 36

// empty pipe is ok
const bar = pipe()
console.log(bar(2)) // 2


"but i heard recursion is bad"

OK, so if you're going to pipe thousands of functions, you might run into a stack overflow. In such a case, you can use the stack-safe Array.prototype.reduce (or reduceRight) like in your original post.

This time instead of doing everything within pipe, I'm going to decompose the problem into smaller parts. Each part has a distinct purpose, and pipe is now only concerned with how the parts fit together.

const comp = (f,g) => x =>
  f(g(x))

const identity = x =>
  x
  
const pipe = (...fs) =>
  fs.reduceRight(comp, identity)

const foo = pipe(
  x => x + 1,
  x => x * 2,
  x => x * x,
  console.log
)

foo(0) // 4
foo(1) // 16
foo(2) // 36

// empty pipe is ok
const bar = pipe()
console.log(bar(2)) // 2


"I really just want to understand the code in my post though"

OK, let's step thru your pipe function and see what's happening. Because reduce will call the reducing function multiple times, I'm going to use a unique renaming for args each time

// given
const Pipe = (...fns) => fns.reduce((f,g) => (...args) => g(f(...args)));

// evaluate
Pipe(a,b,c,d)

// environment:
fns = [a,b,c,d]

// reduce iteration 1 (renamed `args` to `x`)
(...x) => b(a(...x))

// reduce iteration 2 (renamed `args` to `y`)
(...y) => c((...x) => b(a(...x))(...y))

// reduce iteration 3 (renamed `args` to `z`)
(...z) => d((...y) => c((...x) => b(a(...x))(...y))(...z))

So what happens then when that function is applied? Let's have a look when we apply the result of Pipe(a,b,c,d) with some argument Q

// return value of Pipe(a,b,c,d) applied to `Q`
(...z) => d((...y) => c((...x) => b(a(...x))(...y))(...z)) (Q)

// substitute ...z for [Q]
d((...y) => c((...x) => b(a(...x))(...y))(...[Q]))

// spread [Q]
d((...y) => c((...x) => b(a(...x))(...y))(Q))

// substitute ...y for [Q]
d(c((...x) => b(a(...x))(...[Q]))

// spread [Q]
d(c((...x) => b(a(...x))(Q))

// substitute ...x for [Q]
d(c(b(a(...[Q])))

// spread [Q]
d(c(b(a(Q)))

So, just as we expected

// run
Pipe(a,b,c,d)(Q)

// evalutes to
d(c(b(a(Q))))

additional reading

I've done a lot of writing on the topic of function composition. I encourage you to explore some of these related questions/I've done a lot of writing on the topic of function composition. I encourage you to explore some of these related questions/answers

If anything, you'll probably see a different implementation of compose (or pipe, flow, et al) in each answer. Maybe one of them will speak to your higher conscience!

Upvotes: 5

Rico Kahler
Rico Kahler

Reputation: 19242

Well @naomik beat me to just about the same answer but I thought I'd still share what I had to help you explain how the function works in a possibly less cryptic way. (Maybe)

I think you already know how the "..." works (and in case you didn't then naomik's answer should help with that :D)

Here is another version of that same pipe function expect re-written to better explain what's going on using assignments just to explain the point.

Array.prototype.reduce calls the "reducer", toASingleFunction, multiple times--one call for each function in functionsToPipe. The currentFunctionToPipe is first x => x + 1 then x => x * 2 and so on...

The first value of newFunction is theIdentityFunction and the reducer returns another function nextNewFunction. As the name suggests, it becomes the next newFunction in the next call to the "reducer" (toASingleFunction).

Once all the items in functionsToPipe have been reduced, the final newFunction is returned as finalPipeFunction.

/**
 * `x` goes to itself
 */
const theIdentityFunction = x => x;

/**
 * the `reducer` that reduces the array of functions into a single function
 * using the value from the last function as the input to the next function
 */
const toASingleFunction = (newFunction, currentFunctionToPipe) => {
  const nextNewFunction = function(value) { // i'm not using arrow functions here just to explicitly show that `nextNewFunction` is, in fact, a function
    const valueFromLastFunction = newFunction(value);
    return currentFunctionToPipe(valueFromLastFunction);
  }
  return nextNewFunction;
};

const pipe = (...functionsToPipe) => {
  const finalPipeFunction = functionsToPipe.reduce(toASingleFunction, /* start with */ theIdentityFunction);

  return finalPipeFunction;
}

const f = pipe(
  x => x + 1,
  x => x * 2,
  x => x * x
);

console.log(f(2)) // ((2 + 1) * 2) ^ 2 === 36

Maybe this helps?

good luck!

Upvotes: 0

Related Questions