This is the code:
const Pipe = (...fns) => fns.reduce((f,g) => (...args) => g(f(...args)));
So by (...fns) the fns arguments beco
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 – pipe
s 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!