Array.prototype.filMap (Filter and Map function in a single iteration)

I would like to propose a new Array.prototype function that does a filter and map in a single iteration ("filMap"), I'm aware of .reduce function, however this would allow the user work in a more direct way with the familiarity of both .map and .filter functions

// Typescript
Array.prototype.filMap = function <T, K>(
  filterFn: (item: T, index: number, array: T[]) => boolean | undefined,
  mapperFn: (item: T, index: number, array: T[]) => K,
  thisArg?: any
) {
  const result: K[] = [];
  for (let i = 0; i < this.length; i++) {
    if (filterFn.call(thisArg, this[i], i, this)) {
      result.push(mapperFn.call(thisArg, this[i], i, this));
    }
  }
  return result;
};

// Polyfill
Array.prototype.filMap = function (filterFn, mapperFn) {
  if (this == null)
    throw TypeError("this is null or undefined");
  if (typeof filterFn !== "function")
    throw TypeError("filter callback is not a function");
  if (typeof mapperFn !== "function")
    throw TypeError("mapper callback is not a function");

  const O = Object(this);
  const len = O.length >>> 0;
  let T;

  if (arguments.length > 2) {
    T = arguments[2];
  }
  const A = new Array(len);
  let k = 0;
  let to = 0;
  while (k < len) {
    if (k in O) {
      const kValue = O[k];
      if (filterFn.call(T, kValue, k, O)) {
        const kResult = mapperFn.call(T, kValue, k, O);
        A[to] = kResult;
        to += 1;
      }
    }
    k += 1;
  }
  A.length = to;
  return A;
};

// Typings
declare global {
  interface Array<T> {
    /**
     * Calls a filter function on each element of the array and
     * then run a mapper function for elements that passed the criteria
     *
     * @param filterFn A Function that returns true for qualifying elements,
     * receives 3 elements as arguments
     * (current element, index of current element and the array being iterated)
     * @param mapperFn A function that returns the desired mapped element,
     * this will run only for elements that passed filter criteria
     * @param thisArg An object to which the this keyword can refer in the callbackfn function.
     *  If thisArg is omitted, undefined is used as the this value
     */
    filMap<K>(
      filterFn: (element: T, index: number, array: T[]) => boolean | undefined,
      mapperFn: (element: T, index: number, array: T[]) => K,
      thisArg?: any
    ): K[];
  }
}

Examples:

const numbers = [1,2,3,4,5,6];

// Get odds numbers multiplied by 3
// with .filMap
numbers.filMap((n) => n % 2 !== 0, (n) => n * 3);

// with .map and .filter
numbers.filter((n) => n % 2 !== 0).map((n) => n * 3);

// with reduce
numbers.reduce((p, c) => {
  if (c % 2 !== 0) p.push(c * 3);
  return p;
}, []);
1 Like

The solution with filter().map() is simple and intuitive, I don't see why we would need an extra function with a new name (that has to be learned first).
If you care about not creating the intermediate array, I'd recommend using the methods from the iterator helpers proposal:

numbers.values().filter(n => n%2==1).map(n => n*3).toArray();

I agree that .filter().map() is intuitive, but it has an issue, it needs 2 iterations instead of one, and combined with a complex and large array the difference in time to .reduce / .forEach or even this proposed function is considerable. The issue is not about an intermediate array but more of a performant alternative with a similar syntax.

You could also use flatMap for similar effect (it only iterates once), and it's hardly any larger in practice:

// Your proposal
array.filMap(x => isOdd(x), x => x / 2)

// `flatMap`
array.flatMap(x => isOdd(x) ? [x / 2] : [])

// `filter` + `map`
array.filter(x => isOdd(x)).map(x => x / 2)

If performance is a concern, by that point you should be considering writing a manual for loop directly and looking to explicitly elide intermediate arrays. I also mentioned in this V8 bug that it's possible to elide intermediate arrays. (It's difficult, but it'd sharply reduce GC churn in functional programs.)

I've used flatMap in the past as workaround, but it's way slower than .filter().map() (and needs to wrap results into an array), also according to MDN "It is identical to a map() followed by a flat() of depth 1" so it's indeed doing a single iteration? because it looks like it needs 2

@ejose19 MDN's technically incorrect - it observably flattens as it goes. This isn't observable from the source array, but it's observable by when the value's entries are accessed.

As for performance, I'll just reiterate:

Don't prematurely optimize. Leave it up to the engine to optimize the baseline, and only optimize on your end after you've determined from profiling that 1. you have performance problems at all and 2. where your performance problems lie.

1 Like

@isiahmeadows Thanks for the reference, indeed seems that summary may not be the best.

Agree on the premature optimization, just wanted to propose a more direct alternative than .filter().map().

Personally, I'd rather stick with the explicit .filter(...).map(...) anyways absent perf issues - it's also easier to add in a .filter or .map between the two calls and it's all around more explicit.