MostlyAdequate / mostly-adequate-guide

Mostly adequate guide to FP (in javascript)
Other
23.39k stars 1.86k forks source link

Consider changing the curry implementation in Appendix A #588

Open dotnetCarpenter opened 3 years ago

dotnetCarpenter commented 3 years ago

While I appreciate the simplicity of the curry implementation it does go against the general definition of Currying. I suggest an implementation that via a co-recursive accumulator function will apply each argument to a new function as in the wikipedia definition1.

Currying is the technique of converting a function that takes multiple arguments into a sequence of functions that each take a single argument.

My main issue with the current implementation is that it silently ignore extra arguments which makes it rather difficult to trace extraneous arguments when function composition goes wrong. This also seem more inline with how Lisp is implemented (does not allow too many arguments).

Consider the following, with the current implementation:

// curry :: ((a, b, ...) -> c) -> a -> b -> ... -> c
function curry(fn) {
  const arity = fn.length;

  return function $curry(...args) {
    if (args.length < arity) {
      return $curry.bind(null, ...args);
    }

    return fn.call(null, ...args);
  };
}

const add = (x, y, z) => x + y + z
console.log(
  curry(add)(1,2,3),
  curry(add)(1,2)(3),
  curry(add)(1)(2)(3),

  curry(add)(1,2,3,4), // should be an error but 4 is ignored
  curry(add)(1,2)(3,4), // should be an error but 4 is ignored
  curry(add)(1)(2)(3)(4), // is correctly an error since 6 is not a function
)

Wrong usage of the add function, silently ignores data, leading to subtle and hard to find bugs.

Now consider the following implementation and results:

export default function curry (f) {
  if (f.length === 0) return f()
  return (a, ...rest) => curryAccumulator(f.bind(null, a), rest)
}
function curryAccumulator (f, rest) {
  return rest.length === 0
    ? curry(f)
    : curry(f)(...rest)
}

const add = (x, y, z) => x + y + z
console.log(
  curry(add)(1,2,3),
  curry(add)(1,2)(3),
  curry(add)(1)(2)(3),

  curry(add).length === 1, // true
  curry(add)(1).length === 1, // true
  curry(add)(1, 1).length === 1, // true
  curry(add)(1)(1).length === 1, // true

  curry(add)(1,2,3,4), // is correctly an error since 6 is not a function
  curry(add)(1,2)(3,4), // is correctly an error since 6 is not a function
  curry(add)(1)(2)(3)(4), // is correctly an error since 6 is not a function
)

_It is not permitted to supply too many arguments to a curried function. Doing so will throw a TypeError._

One limitation of my suggestion is that zero parameter functions will be invoked immediately which is never desirable.

curry(() => console.log('zero parameter function'))()

Will fail with TypeError: curry(...) is not a function, with my implementation.

But currying zero parameter function also seem like a very edge case, adding an unnecessary layer of execution, that is better solved by having a partial for one-off function wrapping.

const partial (f, ...args) => f.bind(null, ...args)

partial implementation.


Lastly, my implementation does not let the user specify an arity which might be needed for variadic functions but neither does the one in Appendix A.

My implementation is meant as a mean to start a discussion about popularise a curry function that actually turns a function into a sequence of functions that each take a single argument.

I apologise if this has been debated to death. It's not my intention to nag.

1: I have yet to find a formal definition of currying other than the wikipedia article.

dotnetCarpenter commented 3 years ago

Actually after looking at the function for while I decided an easier to read curry function might be one that is recursive only with itself.

function curry (f) {
  return isEmpty(f)
    ? f()
    : (a, ...rest) => {
      let curriedF = f.bind(null, a)

      return isEmpty(rest)
        ? curry(curriedF)
        : curry(curriedF)(...rest)
    }
}

function isEmpty (l) {
  return l.length === 0
}

It also dawned on me that curryAccumulator might be a confusing name since the accumulation was happening in the anonymous function parameters which does the job of separating the first argument with the rests of the arguments, (a, ...rest) =>.

The same tests are of course still valid:

const add = (x, y, z) => x + y + z
console.log(
  curry(add)(1,2,3),   // 6
  curry(add)(1,2)(3),  // 6
  curry(add)(1)(2)(3), // 6

  curry(add).length === 1, // true
  curry(add)(1).length === 1, // true
  curry(add)(1, 1).length === 1, // true
  curry(add)(1)(1).length === 1, // true

  // curry(add)(1,2,3,4),    // TypeError
  // curry(add)(1,2)(3,4),   // TypeError
  // curry(add)(1)(2)(3)(4), // TypeError
)
dotnetCarpenter commented 3 years ago

a quick post to line up the 3 curry functions against each other

  1. Silently ignores extra arguments and does not always return a function take a single argument (e.i. cf.length !== 1).

    // curry :: ((a, b, ...) -> c) -> a -> b -> ... -> c
    function curry(fn) {
    const arity = fn.length;
    
    return function $curry(...args) {
    if (args.length < arity) {
      return $curry.bind(null, ...args);
    }
    
    return fn(...args); // used to be: fn.call(null, ...args);
    };
    }
  2. // curry :: ((a, b, ...) -> c) -> a -> b -> ... -> c
    function curry (f) {
    if (f.length === 0) return f()
    return (a, ...rest) => continuation(f.bind(null, a), rest)
    }
    function continuation(f, rest) {
    return rest.length === 0
    ? curry(f)
    : curry(f)(...rest)
    }
  3. 
    // curry :: ((a, b, ...) -> c) -> a -> b -> ... -> c
    function curry (f) {
    return isEmpty(f)
    ? f()
    : (a, ...rest) => {
      let curriedF = f.bind(null, a)
    
      return isEmpty(rest)
        ? curry(curriedF)
        : curry(curriedF)(...rest)
    }
    }

function isEmpty (l) { return l.length === 0 }