I need help understanding the rest and spread operator

后端 未结 3 403
無奈伤痛
無奈伤痛 2021-01-15 09:13

This is the code:

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

So by (...fns) the fns arguments beco

3条回答
  •  慢半拍i
    慢半拍i (楼主)
    2021-01-15 10:04

    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!

    • How to understand curry and function composition using Lodash flow?
    • lodash curry does not work on function returned by flow; lodash FP enough for FP?
    • functional composition of a boolean 'not' function (not a boolean value)
    • How to compose functions of varying arity using Lodash flow?
    • How to reconcile Javascript with currying and function composition

提交回复
热议问题