The code you have there is about as far from functional as could possibly be. The entire function is built around a side-effecting loop which mutates a value.
Here's the trivial functional implementation of a map
operation:
Array.prototype.map = function (fn) {
const [first, ...rest] = this;
return this.length === 0 ? [] : [fn(first)].concat(rest.map(fn));
};
This will, however, blow the stack for a sufficiently large array. To avoid that, we need to make the function tail-recursive. In the version above, the tail-call is to the concat
method. That call will be optimized away, but that doesn't help us much: we want the recursive call to map
to be the tail-call, so that it gets optimized away.
The standard trick to make a function tail-recursive is to introduce an accumulator that gets passed along and then reverse the result.
Array.prototype.map = function (fn) {
const mapTailrec = (ary, acc) =>
ary.length === 0 ?
acc :
mapTailrec(ary.slice(0, -1), acc.concat([fn(ary[ary.length-1])]));
return mapTailrec(this, []).reverse();
};
[Note: I'm actually cheating here, because Array.prototype.reverse
is unfortunately not referentially transparent. It is however, pretty simple to write a tail-recursive referentially-transparent reverse
, so I'll let that slide.]
As it turns out, fold
is a general method of iteration: everything that can be done with loops can be done with folds, and so every collection operation is actually just a special case of fold
:
Array.prototype.map = function (fn) {
return this.reduce([], (acc, el) => acc.concat([fn(el)]));
};