6

New to lodash and playing around with it to gain more understanding. I don't understand the behavior of the following code.

After learning about the arity argument to _.curry, I have a code snippet that produces results that seems strange to me.

const words = ['jim', 'john'];
const pad10 = words =>
    _.map(words, word => _.pad(word, 10));

console.log(pad10(words)); // [ '   jim    ', '   john   ' ]

const flipMap = _.flip(_.map);
const flipPad = _.flip(_.pad);

const curriedFlipMap = _.curry(flipMap, 2);

const pad10v2 = curriedFlipMap(word => flipPad(' ', 10, word));

console.log(pad10v2(words)); // [ '   jim    ', '   john   ' ]

const curriedFlipPad = _.curry(flipPad, 3);
const padWord10 = curriedFlipPad(' ', 10);
const pad10v3 = curriedFlipMap(word => padWord10(word));

console.log(pad10v3(words)); // [ '   jim    ', '   john   ' ]

const pad10v4 = curriedFlipMap(padWord10);
console.log(pad10v4(words)); // [ 'jim,john', 'jim,john' ]
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.min.js"></script>

I don't understand the output of the last console.log. Looks to me like I'm just replacing a => f(a) with f when a one arg function is expected.

imz -- Ivan Zakharyaschev
  • 4,921
  • 6
  • 53
  • 104
bbarrington
  • 115
  • 7
  • This is called eta conversion. It assumes curried functions. So consider `arr.map(f)` compared to `arr.map(a => f(a))`. Can you tell the difference? Check out what sort of functions `Array.prototype.map` is able to process. This is the only difference between both forms in Javascript. –  May 06 '19 at 08:52
  • 1
    I know what eta conversion is. That's not my question. That's what I'm attempting to use to simplify the code. However, in this case, it gives a surprising (wrong) result. Note that if you change the declaration of 'flipPad' to `const flipPad = (padding, length, text) => _.pad(text, length, padding);` (IOW - just do the flip yourself), then the above code works as expected. This leads me to believe that there is something going on with the use of '_.flip' that I'm not aware of. – bbarrington May 06 '19 at 12:18
  • I believe arrow function syntax in JavaScript implicitly binds the function’s `this` to the call-site, whereas passing a function won’t (neither does the full `function(...) { ... }` syntax which requires an explicit `.bind(this)` call). I don’t know if that applies to this situation though. – Dai May 06 '19 at 12:34
  • 1
    You say you know what eta conversion is and still your question's title is `When is a => f(a) not equivalent to f?`. I told you when it is not equivalent: In the context of Javascript's multi argument functions. Btw., Strings like `jim,john` are often constructed when there is an implicit `toString` cast of an `Array` value. –  May 06 '19 at 13:11
  • @bbarrington if you're using lodash for functional programming, look at `lodash/fp` – Mulan May 06 '19 at 16:47

1 Answers1

2

Yes, there is a difference between f and a => f(a) in JavaScript. Consider the following example:

const array = (...args) => args;

const arrayEta = a => array(a);

console.log(array(1, 2, 3)); // [1, 2, 3]

console.log(arrayEta(1, 2, 3)); // [1]

Do you see the problem? When I call arrayEta(1, 2, 3) it expands to (a => array(a))(1, 2, 3) which beta reduces to array(1) because the 2 and 3 are never used. However, the non-eta expanded version is array(1, 2, 3). This is the problem with your code:

const words = ["jim", "john"];

const flipMap = _.flip(_.map);
const flipPad = _.flip(_.pad);

const curriedFlipMap = _.curry(flipMap, 2);
const curriedFlipPad = _.curry(flipPad, 3);

const padWord10 = curriedFlipPad(" ", 10);

const pad10v4 = curriedFlipMap((...args) => {
    console.log(args); // args is an array of 3 arguments
    return padWord10(...args);
});

console.log(pad10v4(words)); // ["jim,john", "jim,john"]
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.min.js"></script>

Notice that args is an array of three arguments, word, index, and array. Hence, curriedFlipMap(padWord10) is actually eta equivalent to curriedFlipMap((word, index, array) => padWord10(word, index, array)). It is not eta equivalent to curriedFlipMap(word => padWord10(word)).

Hence, your function call is reduced as follows:

  padWord10("jim", 0, ["jim", "john"])
= curriedFlipPad(" ", 10)("jim", 0, ["jim", "john"])
= _curry(flipPad, 3)(" ", 10)("jim", 0, ["jim", "john"])
= _.pad(["jim", "john"], 0, "jim", 10, " ")
= _.pad(["jim", "john"], 0, "jim")

As you can see, you're providing the function _.pad 5 arguments out of which it ignores the last 2. Hence, it converts ["jim", "john"] to a string and then adds padding to it.

Anyway, the solution is to not do eta-conversion in this case. By the way, if you want to use Lodash for functional programming then use lodash/fp instead.

Aadit M Shah
  • 72,912
  • 30
  • 168
  • 299
  • Thanks so much for the very detailed examination. If I can ask your indulgence one more time, explain to me why the function `add2` below is different from the function `padWord10` in the original. They are both 3 arg curried functions with the first 2 args supplied. The example below works as expected. `const sum = (x, y, z) => x + y + z; const curriedSum = _.curry(sum); const add2 = curriedSum(1)(1); const map = (f, array) => array.map(f); const curriedMap = _.curry(map); const mapAdd2 = curriedMap(add2); const r = mapAdd2([1, 2, 3, 4]); console.log(r); // [ 3, 4, 5, 6 ] ` – bbarrington May 06 '19 at 17:03
  • You didn't `flip` the `sum` function. – Aadit M Shah May 06 '19 at 18:00
  • Okay. Back to my original premise - I'm not understanding what `_.flip` is really doing. `_curry(_.flip(f))` creates a varargs function (NOT curried) that takes its arguments from right to left. So in the code below I can invoke `curriedFlippedSum` with a variable number of arguments, but only the last three will be evaluated. I'm sorry if this is obvious to everyone else. It certainly is not to me. I appreciate the help. `const sum = (x, y, z) => x + y + z; const flippedSum = _.flip(sum); const curriedFlippedSum = _.curry(flippedSum); console.log(curriedFlippedSum(1, 2, 3, 4, 5)); // 12` – bbarrington May 06 '19 at 20:21
  • Using `flip` reverses the order of arguments. Hence, `curriedFlippedSum(1, 2, 3, 4, 5)` is the same as `sum(5, 4, 3, 2, 1)` which beta reduces to `5 + 4 + 3` which is equal to `12`. – Aadit M Shah May 07 '19 at 02:56
  • Yep, but the issue is that you can't use it as a curried function: `const sum = (x, y, z) => x + y + z; const flippedSum = _.flip(sum); const curriedFlippedSum = _.curry(flippedSum); const add2 = curriedFlippedSum(1)(1); // TypeError: curriedFlippedSum(...) is not a function const four = add2(2); ` My original goal was to move data arguments to the end of the argument list and then via currying, simplify use of `_.map`: `_.map(f)(data)`. But clearly this doesn't work using this approach. Anyway, I appreciate your help. – bbarrington May 07 '19 at 12:01
  • It doesn't work because you haven't provided an arity to `curry`. Try `const curriedFlippedSum = _.curry(flippedSum, 3);`. The arity of `flippedSum` is `0` because `_.flip` doesn't know what the arity of the result is supposed to be. Hence, your `curriedFlippedSum` is actually defined as `const curriedFlippedSum = _.curry(flippedSum, 0);`. Therefore, when you define `const add2 = curriedFlippedSum(1)(1);` it gives a type error. – Aadit M Shah May 07 '19 at 12:31
  • Maybe you are finally penetrating my thick skull. Still curious about the following, though. ;-) 'padWord10' is a unary function since it is the result of applying 2 args to 'curriedFlipPad'. When passed to '_.map' like so: `_.map(['word1', 'word2'], padWord10)` lodash is unable to detect that it is a unary function and thus passes all arguments as an array ('[value, index, collection]') via the single arg. Normally, lodash is able to detect that you have declared your iteratee with only one arg, say, and in that case it only passes the value. So what is different in this case? Thanks. – bbarrington May 09 '19 at 17:31
  • Lodash doesn't do any function `length` detection. It always passes all the arguments to your callbacks, whether or not you actually require them. Another problem with Lodash, and every other functional JavaScript library that I know of, is that they [implement the `curry` function incorrectly](https://stackoverflow.com/q/27996544/783743). As described in [this paper](http://webyrd.net/scheme-2013/papers/HemannCurrying2013.pdf) by my professors, Jason Hemann and Dan Friedman, the reasonable behaviour when a curried function is oversupplied with arguments is to supply the excess to the result. – Aadit M Shah May 10 '19 at 04:25
  • A correct implementation of `curry` should be able to both `curry` and `uncurry`. For example, both `const f = curry((a, b, c) => a + b + c)` and `const f = curry(a => b => c => a + b + c)` should be callable as `f(1, 2, 3)`, `f(1)(2)(3)`, `f(1)(2, 3)`, and `f(1, 2)(3)`. However, all the functional JavaScript libraries that I know of currently implement what Lodash does – when oversupplied with arguments they supply the excess arguments to the function instead of to the curried result of the function. This behaviour is why your program has incorrect results. – Aadit M Shah May 10 '19 at 04:34
  • Pretty sure in the below case 'doubler' is only passed 'value', not the entire array. In any case, your tenacity has helped my understanding. `const doubler = x => x * 2; _.map([1, 2], doubler);` – bbarrington May 10 '19 at 17:54
  • No, it's passed all three arguments. You are just ignoring the other two. – Aadit M Shah May 11 '19 at 01:57
  • Yeah, OK. Because they are not being passed as an array. `doubler(value, index, array)` vs. `doubler([value, index, array])`. The latter case is what 'padWord10' is getting. Bottom line for me is I just won't do this. – bbarrington May 11 '19 at 20:51