re: Human Readable JavaScript VIEW POST

FULL DISCUSSION
 

The refactor from

const arr = [1,2,3]

const timesTwo = (el) => el*2

let multipliedByTwo = arr.map((el) => timesTwo(el))

to

const arr = [1,2,3]

const timesTwoPlusIndex = (el, index) => (el*2) + index

let multipliedByTwo = arr.map(timesTwoPlusIndex)

is one of my favourites.

Interestingly (well, interesting to me anyway), it's an example of η-conversion (eta-conversion), one of the three basic reduction steps in the Lambda calculus.

 

Haha it is! I don't think I'd made that connection before.

 

I know! I think it's maybe the only place I've ever found a practical use for lambda calculus!

 

The most common pitfall though with this approach is when, for example, converting an array of strings into integers.

["1", "2", "3"].map(parseInt) = [1, NaN, NaN]

["1", "2", "3"].map(x => parseInt(x)) = [1, 2, 3]

The reason for this is that parseInt actually takes 2 arguments (string and radix). Map accepts methods that take up to 3 arguments (value, index, and array). So, when passed to map directly, string is getting value (what we expect!), but index is being passed as the radix. Meaning, you try to parse the 0th element in a natural way (base 10), then you try to parse the 1st element as base one (which it's not a valid base 1 number, so NaN), parse the 2nd element as base two (again, 3 is not valid base 2, so NaN), and so on...

I've been bitten by this bug quite a few times. When using map, bypassing the anonymous function and passing a named function should generally only be used if the function takes a single argument.

 

Though it might be a distraction from the example... You could put Number there in place of parseInt.

 

That's a good one! Thanks for pointing it out.

 

The default behaviour of Array.map is unintuitive given that it returns the index and the original array as the second and third arguments to the callback function respectively.

I would approach the parseInt problem by writing a map function that takes two args, supplied one at a time (to facilitate partial application).

  • the first arg, a function f that will be supplied only one value at a time, that is, the current iterated value
  • the second arg, a 1-dimensional array of values to apply the function f on

It may appear complicated seeing it for the first time, but come back to the example and mull it over and it will start to click.

/* 
// alternative map implementation
const mapAlt = f => xs => Array.prototype.map.call(xs, currentValue => f (currentValue));


// unterse
function map(f) {
  return function(xs) {
    return xs.map(function(currentValue, index, originalArray) {
      return f(currentValue);
    });
  };
}
*/

const map = f => xs => xs.map(function(currentValue, index, originalArray) {
  return f (currentValue);
});


const xs = [1, 2, 3, 4];
const multiplier = x => x * 6;
const multiplied = map (multiplier) (xs);

const ys = ["1", "2", "3", "4"];
const parser = x => parseInt(x);
const parsed = map (parser) (ys);

console.log("xs:", xs); //=> [1, 2, 3, 4]
console.log("xs multiplied:", multiplied); //=> [6, 12, 18, 24]
console.log("ys:", ys); //=> ["1", "2", "3", "4"]
console.log("ys parsed:", parsed); //=> [1, 2, 3, 4]

But why include index and originalArray parameters when you don't use them?

For that matter, why not make this point-free?

const map = (f) => (xs) => xs .map ((x) => f(x))

(... and around in circles we go!)

That's true, they are superfluous - I left those other args there to make it clearer how the args are moving around.

code of conduct - report abuse