How to correctly curry a function in JavaScript?

前端 未结 4 1350
陌清茗
陌清茗 2020-11-29 05:37

I wrote a simple curry function in JavaScript which works correctly for most cases:

4条回答
  •  遥遥无期
    2020-11-29 06:08

    Apart from its mathematical definition

    currying is the transformation of a function with n parameters into a sequence of n functions, which each accept a single parameter. The arity is thus transformed from n-ary to n * 1-ary

    what impact has currying on programming? Abstraction over arity!

    const comp = f => g => x => f(g(x));
    const inc = x => x + 1;
    const mul = y => x => x * y;
    const sqr = x => mul(x)(x);
    
    comp(sqr)(inc)(1); // 4
    comp(mul)(inc)(1)(2); // 4
    

    comp expects two functions f and g and a single arbitrary argument x. Consequently g must be an unary function (a function with exactly one formal parameter) and f too, since it is fed with the return value of g. It won't surprise anyone that comp(sqr)(inc)(1) works. sqr and inc are both unary.

    But mul is obviously a binary function. How on earth is that going to work? Because currying abstracted the arity of mul. You can now probably imagine what a powerful feature currying is.

    In ES2015 we can pre-curry our functions with arrow functions succinctly:

    const map = (f, acc = []) => xs => xs.length > 0
     ? map(f, [...acc, f(xs[0])])(xs.slice(1))
     : acc;
    
    map(x => x + 1)([1,2,3]); // [2,3,4]
    

    Nevertheless, we need a programmatic curry function for all functions out of our control. Since we learned that currying primarily means abstraction over arity, our implementation must not depend on Function.length:

    const curryN = (n, acc = []) => f => x => n > 1
     ? curryN(n - 1, [...acc, x])(f)
     : f(...acc, x);
    
    const map = (f, xs) => xs.map(x => f(x));
    curryN(2)(map)(x => x + 1)([1,2,3]); // [2,3,4]
    

    Passing the arity explicitly to curryN has the nice side effect that we can curry variadic functions as well:

    const sum = (...args) => args.reduce((acc, x) => acc + x, 0);
    curryN(3)(sum)(1)(2)(3); // 6
    

    One problem remains: Our curry solution can't deal with methods. OK, we can easily redefine methods that we need:

    const concat = ys => xs => xs.concat(ys);
    const append = x => concat([x]);
    
    concat([4])([1,2,3]); // [1,2,3,4]
    append([4])([1,2,3]); // [1,2,3,[4]]
    

    An alternative is to adapt curryN in a manner that it can handle both multi-argument functions and methods:

    const curryN = (n, acc = []) => f => x => n > 1 
     ? curryN(n - 1, [...acc, x])(f)
     : typeof f === "function"
      ? f(...acc, x)
      : x[f](...acc);
    
    curryN(2)("concat")(4)([1,2,3]); // [1,2,3,4]
    

    I don't know if this is the correct way to curry functions (and methods) in Javascript though. It is rather one possible way.

    EDIT:

    naomik pointed out that by using a default value the internal API of the curry function is partially exposed. The achieved simplification of the curry function comes thus at the expense of its stability. To avoid API leaking we need a wrapper function. We can utilize the U combinator (similar to naomik's solution with Y):

    const U = f => f(f);
    const curryN = U(h => acc => n => f => x => n > 1
     ? h(h)([...acc, x])(n-1)(f)
     : f(...acc, x))([]);
    

    Drawback: The implementation is harder to read and has a performance penalty.

提交回复
热议问题