Map through an array while filtering - `Array.prototype{.filterAndMap, .mapIf}`

Motivation

Often an array needs to be filtered and the remaining values need to be mapped.

This can be done with filter and map, but this 1) iterates through the array twice and 2) can be repetitive if the filter depends on what the mapped value would be.

This can also be done with flatMap, but this is not the intention of flatMap and (in my opinion) is not very readable.

Filter And Map (.filterAndMap, .mapIf or .fmap)

Array.prototype.filterAndMap(func)

Examples

const example1 = [1, 2, 3, 4, 5];
// returning `undefined` means that the value is not desired
const result2 = example1.filterAndMap(item => item > 2 ? item * 2 : undefined);

const example2 = [{ id: 1, isValid: true }, { id: 2, isValid: false }, { id: 3, isValid: true}];
const result2 = example2.filterAndMap(item => {
  if (item.isValid) return item.id;
});

Implementation

I have implemented the examples above in this npm package, along with the TypeScript for it.

I chose this version of a few I came up with (which I can share if needed); the main issue with it right now would be needing to insert undefined into the resulting array, which is currently impossible... However this version seemed to be the cleanest.

3 Likes

For undefined to output, I think the callback should return something else, like {output: trueOrFalse, value: anyValue}. It just gets a bit more verbose.

Specially with the tuple and record types proposal, this return could be a costless #{} object passed as value.

In my dialect (whose compiler is still in development), you would be able to do:

function filterAndMap.<R>(f:(a:T, i:Int, o:[T])->{output:Boolean, value:R?}):[R] {
    // procedure
}

In my dialect, {output:Boolean, value:R?} is a record type, which is passed as value. You can initialize it with an object initializer without # punctuator.

const result = example.filterAndMap(item -> item.isValid
    ? {output: true, value: item.name}
    : {output: false, value: undefined});

First, your example can be simplified:

const result = example.filterAndMap(item => ({
  included: item.isValid,
  value: item.name
}));

But beyond that - I considered an object but that seems clunky to me... had the idea for something like:

const result = example.filterAndMap(item => item.isValid ? () => item.name : undefined);
// or even
const result = example.filterAndMap(item => item.isValid && () => item.name);

with a dummy function that returns the mapped value; this would allow you to insert whatever value you wanted into the array.

However, I feel that this form of writing is a bit too verbose, and I find that the cases to include undefined in a filtered and mapped array are basically nonexistent, and would probably be served better by some other solution...

Would love to hear what you think

Well, yeah, I think undefined into an array isn't very useful. Someone might use null for emulating holes, but... not sure of a case.

This is exactly why flatMap - returning an empty array to omit and an array wrapper to keep - is likely to be the simplest, cleanest solution.

2 Likes

flatMap does look more readable, but there is a cost of creating an Array, possibly several times in one iteration. I'm not sure if this is a peformance concern though regarding garbage collector.

I agree that filterMap is useful for these situations, but it's unintuitive in my opinion, and there shouldn't be the need to map and then flatten into the array...

MDN says that

(flatMap()) is identical to a map() followed by a flat() of depth 1 (arr.map(...args).flat()), but slightly more efficient than calling those two methods separately.

And the ECMAScript spec for flatMap is significantly more complex then what filterAndMap would need to be.

I think it would require an implementer to say so, or else a lot of benchmarks, to demonstrate that creation of throwaway arrays (a very very common operation) is actually a performance issue in practice.

2 Likes

Or return Array.prototype, a built-in, 0-length array you don't have to create each (or any) iteration :stuck_out_tongue_winking_eye:

1 Like

Another approach to potentially optimising a series of array methods, avoiding the intermediate arrays, is the stage 3 iterator helpers proposal.

arr.values().filter(f1).map(f2).toArray()
1 Like

Again - this makes sense, but I still think it makes more sense for arrays to have a built-in way to both filter and map, without having to iterate twice or flattening from different arrays, or having to go to iterators...

I had a look if there were examples of other languages with a method like this, so far have just found:

Do you know if there are other examples?

There are several:

1 Like

It strikes me that this could be solved with an array-like object that lazy-evaluates map, filter, etc...

Thoughts?

that’s basically what an iterator, with the helpers, is.

2 Likes

I do this all the time and am confused why we need more off shoot methods on Array when we could generators:

function *filterAndMap(iterable) {
  for (let item of iterable)
    if (filter(item))
      yield map(item);
}

let result = Array.from(filterAndMap(items));

Since we now have them on iterators, you can Iterator.from(iterable).filter(filter).map(map) and have a nice lazy iterator for exactly what you want.

This is amazing! I love it! I wish more than just Chrome had it though.