Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Consider changing the curry implementation in Appendix A #588

Open
dotnetCarpenter opened this issue Dec 28, 2020 · 2 comments
Open

Consider changing the curry implementation in Appendix A #588

dotnetCarpenter opened this issue Dec 28, 2020 · 2 comments

Comments

@dotnetCarpenter
Copy link
Contributor

dotnetCarpenter commented Dec 28, 2020

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
Copy link
Contributor Author

dotnetCarpenter commented Jan 4, 2021

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
Copy link
Contributor Author

dotnetCarpenter commented Jan 4, 2021

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);
  };
}
// 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)
}
// 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
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant