Use Array.prototype.map in a functional, covariant way

Viewed 65

Say I have the following input (to be used with Node, but the problem is more general, and not Node related):

  • Absolute path to a directory, call it dirPathAbs
  • An array of basenames (call it namesSeq) of some JS files that exist inside that folder

For example:

I might have namesSeq = ['a', 'b', 'c'] which corresponds to some a.js, b.js, c.js inside dirPathAbs.

Problem:

How do I resolve the paths of the files, in a purely functional way, but also in a covariant way? (i.e. without having to ever talk about a variable that iterates the array. Covariant might not be the word for this, sorry).

What I don't want:

namesSeq.map(base => path.join(dirPathAbs, `${base}.js`));

Nor

namesSeq.map(base => require.resolve(path.join(dirPathAbs, base)));  

Nor

namesSeq.map(base => path.resolve.bind(dirPathAbs)(base));

Nor

const cb = base => path.resolve.bind(dirPathAbs)(base);
namesSeq.map(cb);

I was expecting this one to work

namesSeq.map(path.resolve.bind(dirPathAbs))

but it doesn't. I think path.resolve.bind(dirPathAbs) receives as input namesSeq, which is the 3rd param of the callback provided to Array.prototype.map, because the error I see is

TypeError: Path must be a string. Received [ 'a', 'b', 'c' ]

This is just one exercise of this kind that frustrates me, but a whole category of analogous exercises gave me headaches since learning JS. I am still missing something when it comes to how this binds, and how all these Function.prototype, Array.prototype & friends should be used.

3 Answers

You could add another function in the middle to eat up those extra variables:

 const take = (fn, n) => (...args) => fn(...args.slice(0, n));
 const bind = fn => (...args) => (...args2) => fn(...args, ...args2);

 namesSeq.map(take(bind(path.resolve)(dirPathAbs), 1));

but I don't see any advatange over a named parameter.

I think there might be an assumption about namesSeq that is partially at fault here. Here's a very simple example that more or less follows what you're doing. You can see when you run the snippet that it works as expected:

function mockResolve(input) {
  console.log(input)
  return input * 2;
}

var scopeObject = {
  sequence: [1, 2, 3]
};

var accessSequence = scopeObject.sequence;
var mapping = accessSequence.map(mockResolve.bind(scopeObject));
console.log(mapping);

Is namesSeq just an array containing strings of those files? Without knowing for sure I'm guessing that it might be an array containing an array of file name strings, hence the error for an array of strings when it expected a string.

You are not paying attention to the callback expected by Array.map.

Upon reading the docs, you will discover that this is the syntax of the method:

var new_array = arr.map(function callback(currentValue[, index[, array]]) {
    // Return element for new_array
}[, thisArg])

As you can see, it will pass the entire array to the callback. This is why the solution you wanted does not work.

Your best solution without seeking the help of external libraries (like fp/lodash/) is your second to last option:

namesSeq.map(base => path.resolve.bind(dirPathAbs)(base));
Related