DEV Community

Discussion on: Human Readable JavaScript

Collapse
 
gypsydave5 profile image
David Wickes • Edited

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.

Collapse
 
laurieontech profile image
Laurie

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

Collapse
 
gypsydave5 profile image
David Wickes

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

Collapse
 
robocel profile image
Rob Ocel • Edited

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.

Collapse
 
salembeats profile image
Cuyler Stuwe

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

Collapse
 
laurieontech profile image
Laurie

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

Collapse
 
puiutucutu profile image
puiu • Edited

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]
Thread Thread
 
crosseye profile image
Scott Sauyet

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!)

Thread Thread
 
puiutucutu profile image
puiu

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