4

I am learning functional javascript and I came across two different implementations of the curry function. I am trying to understand the difference between the two they seem similar yet one works incorrectly for some cases and correctly for other cases.

I have tried interchanging the functions the one defined using es6 'const' works for simple cases but when using 'filter' to filter strings the results are incorrect, but with integers it produces the desired results.


//es6 
//Does not work well with filter when filtering strings
//but works correctly with numbers
const curry = (fn, initialArgs=[]) => (
    (...args) => (
        a => a.length === fn.length ? fn(...a) : curry(fn, a)
    )([...initialArgs, ...args])
);

//Regular js
//Works well for all cases
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 match = curry((pattern, s) => s.match(pattern));
const filter = curry((f, xs) => xs.filter(f));

const hasQs = match(/q/i);

const filterWithQs = filter(hasQs);
console.log(filterWithQs(["hello", "quick", "sand", "qwerty", "quack"]));
//Output:
  //es6:
    [ 'hello', 'quick', 'sand', 'qwerty', 'quack' ]
 //regular:
    [ 'quick', 'qwerty', 'quack' ]

Kinyugo
  • 429
  • 1
  • 4
  • 11
  • 1
    btw, regular (non ES6) does not contain spread/rest `...`. – Nina Scholz Aug 15 '19 at 07:54
  • Interesting question but my brain currently refuses to work on this problem. I will try to look at it later but right now I saw that if you do `const match = curry((pattern, s) => (console.log("match", s), s.match(pattern)));` - just adding a print to the `match` function, that doesn't actually work with the first implementation. So, the match function is not called at all, which suggests that the when `.filter` is called, it's not executing a function as predicate but just getting a truthy object, and thus lets everything through. – VLAZ Aug 15 '19 at 08:18
  • The obvious difference is that one uses `==` to check the number of arguments while the other uses `<`. – Bergi Aug 15 '19 at 09:23
  • Try using `map` instead of `filter` (so that you see what the calls returns), and `console.log` the `a`/`args` arrays and `fn.length`. – Bergi Aug 15 '19 at 09:33

2 Answers2

1

If you change filter to use xs.filter(x => f(x)) instead of xs.filter(f) it will work -

const filter = curry((f, xs) => xs.filter(x => f(x)))

// ...

console.log(filterWithQs(["hello", "quick", "sand", "qwerty", "quack"]))
// => [ 'quick', 'qwerty', 'quack' ]

The reason for this is because Array.prototype.filter passes three (3) arguments to the "callback" function,

  • callback - Function is a predicate, to test each element of the array. Return true to keep the element, false otherwise. It accepts three arguments:
    • element - The current element being processed in the array.
    • index (Optional) - The index of the current element being processed in the array.
    • array (Optional) - The array filter was called upon.

The f you are using in filter is match(/q/i), and so when it is called by Array.prototype.filter, you are getting three (3) extra arguments instead of the expected one (1). In the context of curry, that means a.length will be four (4), and since 4 === fn.length is false (where fn.length is 2), the returned value is curry(fn, a), which is another function. Since all functions are considered truthy values in JavaScript, the filter call returns all of the input strings.

// your original code:
xs.filter(f)

// is equivalent to:
xs.filter((elem, index, arr) => f(elem, index, arr))

By changing filter to use ...filter(x => f(x)), we only allow one (1) argument to be passed to the callback, and so curry will evaluate 2 === 2, which is true, and the return value is the result of evaluating match, which returns the expected true or false.

// the updated code:
xs.filter(x => f(x))

// is equivalent to:
xs.filter((elem, index, arr) => f(elem))

An alternative, and probably better option, is to change the === to >= in your "es6" curry -

const curry = (fn, initialArgs=[]) => (
    (...args) => (
        a => a.length >= fn.length ? fn(...a) : curry(fn, a)
    )([...initialArgs, ...args])
)

// ...

console.log(filterWithQs(["hello", "quick", "sand", "qwerty", "quack"]))
// => [ 'quick', 'qwerty', 'quack' ]

This allows you to "overflow" function parameters "normally", which JavaScript has no problem with -

const foo = (a, b, c) => // has only three (3) parameters
  console.log(a + b + c)
  
foo(1,2,3,4,5) // called with five (5) args

// still works
// => 6

Lastly here's some other ways I've written curry over the past. I've tested that each of them produce the correct output for your problem -

by auxiliary loop -

const curry = f => {
  const aux = (n, xs) =>
    n === 0 ? f (...xs) : x => aux (n - 1, [...xs, x])
  return aux (f.length, [])
}

versatile curryN, works with variadic functions -

const curryN = n => f => {
  const aux = (n, xs) =>
    n === 0 ? f (...xs) : x => aux (n - 1, [...xs, x])
  return aux (n, [])
};

// curry derived from curryN
const curry = f => curryN (f.length) (f)

spreads for days -

const curry = (f, ...xs) => (...ys) =>
  f.length > xs.length + ys.length 
    ? curry (f, ...xs, ...ys)
    : f (...xs, ...ys)

homage to the lambda calculus and Howard Curry's fixed-point Y-combinator -

const U =
  f => f (f)

const Y =
  U (h => f => f (x => U (h) (f) (x)))

const curryN =
  Y (h => xs => n => f =>
    n === 0
      ? f (...xs)
      : x => h ([...xs, x]) (n - 1) (f)
  ) ([])

const curry = f =>
  curryN (f.length) (f)

and my personal favourites -

// for binary (2-arity) functions
const curry2 = f => x => y => f (x, y)

// for ternary (3-arity) functions
const curry3 = f => x => y => z => f (x, y, z)

// for arbitrary arity
const partial = (f, ...xs) => (...ys) => f (...xs, ...ys)

Lastly a fun twist on @Donat's answer that enables anonymous recursion -

const U =
  f => f (f)

const curry = fn =>
  U (r => (...args) =>
    args.length < fn.length 
      ? U (r) .bind (null, ...args)
      : fn (...args)
  )
Mulan
  • 129,518
  • 31
  • 228
  • 259
0

The main difference here is not the es6 syntax but how the arguments are partially applied to the function.

First version: curry(fn, a)

Second versison: $curry.bind(null, ...args)

It works for only one step of currying (as needed in your example) if you change first version (es6) to fn.bind(null, ...args)

The representation of the "Regular js" version in es6 syntax would look like this (you need the constant to have a name for the function in the recursive call):

    curry = (fn) => {
        const c = (...args) => (
            args.length < fn.length ? c.bind(null, ...args) : fn(...args)
        );
        return c;
    }
Donat
  • 4,157
  • 3
  • 11
  • 26
  • `args` is already defined since its part of the body of the function, since the function is an Immediately invoked function expression(IIFE). `const iife = a => ( b => console.log(b) )(a)` in this case the return of the function `iife` is the return of the inner function `b => ...` that is immediately invoked, hence `a` is already in scope. – Kinyugo Aug 16 '19 at 08:11
  • @Kinyugo yes, you are right. I was just confused with the parantheses. – Donat Aug 16 '19 at 08:40