jeanpaul62
jeanpaul62

Reputation: 10581

JavaScript map & find at the same time: findMap?

How would you rewrite this without using a for loop?

const a = [2, 5, 78, 4];
const expensiveFunction = n => 2 * n;

let result;

// Find the first number 
for (let i = 0; i < a.length; i++) {
    const r = expensiveFunction(a[i]);

    if (r > 100) {
        result = r;
        break;
    }
}

console.log(result);

My naive approach:

const result = a.map(expensiveFunction).find(x => x > 100);
console.log(result);

But this runs expensiveFunction on all the elements, which I would like to avoid. In the above case, we should avoid running expensiveFunction(4).

Some languages have find_map (e.g, Rust), I didn't find it in lodash nor in underscore.

Upvotes: 17

Views: 41816

Answers (12)

trincot
trincot

Reputation: 351029

As of ECMAScript 2025, we've iterator helpers. This allows for a lazy evaluation using iterators instead of intermediate arrays:

const expensiveFunction = n => 2 * n;
const a = [2, 5, 78, 4];
const result = a.values().map(expensiveFunction).find(r => r > 100);

console.log(result);

The only difference with the "greedy" version you provided, is that here we first call .values() on the input array, so that we get to work with an iterator instead of an array. The subsequent calls of .map() and .find() calls are now not executed array methods, but the (newer) iterator methods. This has the desired behaviour: find is responsible for consuming the values it needs, and this results in as many consumptions of the iterator returned by map. By consequence, the expensiveFunction is only called for the elements up to the one that returns the value that is to be found.

Upvotes: 0

georg
georg

Reputation: 215029

Built-in map is greedy so you have to write your own, lazy version:

const a = [2, 5, 78, 4];
const expensiveFunction = n => {
     console.log('expensiveFunction for', n); 
     return 2 * n 
};


function *map(a, fn) {
    for(let x of a)
        yield fn(x);
}

function find(a, fn) {
    for(let x of a)
        if (fn(x))
            return x;
}



r = find(map(a, expensiveFunction), x => x > 100)
console.log('result', r)

Unlike the stock map, this map is a generator and returns (yields) results on demand rather than processing the whole array at once. find and map in this example are "coroutines" and play some kind of a ping-pong game where find asks for results and map delivers them when asked. As soon as find is satisfied with what it's got, it quits and so does map, because nobody is asking for its results anymore.

You can also add map, find and friends to the IteratorPrototype to make them available for all iterators and be able to use dot notation:

const IteratorPrototype = Object.getPrototypeOf(Object.getPrototypeOf([][Symbol.iterator]()));

Object.defineProperties(IteratorPrototype, {
    map: {
        value: function* (fn) {
            for (let x of this) {
                yield fn(x);
            }
        },
        enumerable: false
    },

    find: {
        value: function (fn) {
            for (let x of this) {
                if (fn(x))
                    return x;
            }
        },
        enumerable: false
    },

});

//

const a = [2, 5, 78, 4];
const expensiveFunction = n => {
    console.log('expensiveFunction', n);
    return 2 * n
};


let r = a.values().map(expensiveFunction).find(x => x > 100);

console.log(r)

Here's a small library based on this technique: https://github.com/gebrkn/armita


TypeScript Implementation and Tests

find-map.ts

function* map<T, U>(a: T[], fn: (x: T) => U) {
  for (let x of a) yield fn(x);
}

function find<T>(a: Generator<T, void, unknown>, fn: (x: T) => boolean) {
  for (let x of a) if (fn(x)) return x;
}

export function mapFind<T, U>(
  collection: T[],
  mapper: (item: T) => U,
  finder: (item: U) => boolean
): U | undefined {
  const mapperGenerator = map(collection, mapper);

  return find(mapperGenerator, finder);
}

map-find.spec.ts

Note: these tests utilize Bun, but shouldn't be far off from Jest.

import { describe, expect, it, mock } from "bun:test";
import { mapFind } from "./map-find";

describe("findMap", () => {
  const collection = [2, 5, 78, 4];
  const expensiveFunction = mock((n: number) => {
    console.log("expensiveFunction for", n);
    return 2 * n + ""; // Wanting to test with the change of types
  });
  const condition = (x: string) => x.length > 2;
  const result = mapFind(collection, expensiveFunction, condition);

  it("only calls the expensive function 3 times", () => {
    expect(expensiveFunction).toHaveBeenCalledTimes(3);
  });

  it("returns the expected result", () => {
    expect(result).toEqual("156");
  });
});

Upvotes: 20

vitaly-t
vitaly-t

Reputation: 25930

You can execute multiple operations on an iterable, while iterating through values only once.

Below is an example of doing this, with the help of my iter-ops library:

import {pipe, map, skipWhile} from 'iter-ops';

const a = [2, 5, 78, 4];

const res = pipe(
    a,
    map(m => 2 * m), // your expensive function
    skipWhile(r => r <= 100)
);

console.log('result:', res.first);

Above, it will do the same number of steps as in your for-loop example.

Upvotes: 0

Brandon McConnell
Brandon McConnell

Reputation: 6129

Mapping essentially runs each value through a function and returns its result as the value at that index in a new array. With this in mind, just move your expensiveFunction inside your find() method, like this:

Before:

const result = a.map(expensiveFunction).find(x => x > 100);

After:

const result = a.find(x => {
  const expensiveResult = expensiveFunction(x);
  return expensiveResult > 100;
});

Now it will only check the result of expensiveFunction for each element as the array is iterated over, and stop once the value is found.

A light abstraction to make things easier and more reusable

** Note, because you are not actually mapping over the array, the result of the find() method will be the original value at that index, not the value of expensiveResult.

If you want the value of expensiveResult to be returned and without re-running the expensiveFunction on the found element again needlessly, you can just move this logic into a light and reusable abstraction, like this:

// Function definition

function findMap(arr, fn, condition) {
  for (const elem of arr) {
    const result = fn(elem);
    if (condition(result)) {
      return result;
    }
  }
}

// Usage

const result = findMap(a, expensiveFunction, x => x > 100);

The same light abstraction, but with no loops 😉

…and now that same abstraction again, but this time with no loops per the original question's requirements:

// Function definition

function findMap(arr, fn, condition) {
  let result;

  arr.find(x => {
    const fnResult = fn(x);
    const conditionMet = condition(fnResult);
    if (conditionMet) {
      result = fnResult;
    }
    return conditionMet;
  })

  return result;
}

// Usage (same as above)

const result = findMap(a, expensiveFunction, x => x > 100);

Upvotes: 1

kajkal
kajkal

Reputation: 594

for loops have some interesting properties that offset the ugly-looking code with their usefulness. The findMap functionality described in this question can be achieved by a simple function like this:

function mapFind(array, mapFn, findFn) {
    for (const value of array) {
        const mapResult = mapFn(value);
        if (findFn(mapResult)) {
            return mapResult;
        }
    }
}

const result = mapFind([ 2, 5, 78, 4, 100 ], n => 2 * n, r => r > 100);

console.log(`Result: ${result}`);

TS version:

function mapFind<T, U>(array: T[], mapFn: (value: T) => U, findFn: (value: U) => unknown): U | undefined {
    for (const value of array) {
        const mapResult = mapFn(value);
        if (findFn(mapResult)) {
            return mapResult;
        }
    }
}

const result = mapFind([ 2, 5, 78, 4, 100 ], n => 2 * n, r => r > 100);

console.log(`Result: ${result}`);

Upvotes: 0

David Tran
David Tran

Reputation: 10644

Why not use a smarter function for find?

let desiredValue;
const result = a.find( x =>{
      desiredValue = expensiveFunction(x);
      return desiredValue > 100;
});
console.log(desiredValue);

It will quit the expensive loop immediately after finding out the first result.

Upvotes: 3

Ronald C
Ronald C

Reputation: 367

Here's a terse, functional version of Titus's .reduce answer:

const arr = [2, 5, 78, 100]
const result = arr.reduce((a,v) => (a > 100 && a) || expensiveFunction(v), null)
console.log(result)

It iterates through the whole array, but stops executing the expensive function once the the condition is met.

Here's what I'm using personally, in case it helps anyone:

const result = arr.reduce((a,v) => a || expensiveFunction(v), null) 

Upvotes: 3

aviral garg
aviral garg

Reputation: 31

You can go through a shortest way by using ternary operator to simplify the condition and filter() to remove Boolean(null) values of an Array.

const a = [2, 5, 78, 100];
const result  = a.map((n)=>  2*n > 100 ? 2*n : null ).filter(Boolean)[0];
console.log(result);

Upvotes: -2

Addis
Addis

Reputation: 2530

The approach I followed is to decrease the possibility of calling the 'expensiveFunction' function to the least possible number of times. For this purpose I used the 'Divide and conquer algorithms'. You divide the array into half parts and call the expensive function on the dividing element to decide which half to proceed. Do this step recursively until you find the smallest element above 100. Particularly for a very large sized array this method will reduce the expensive function call to a significantly smaller number. So the 'expensiveFunCaller' function will call your 'expensiveFunction' economically. The array should also be sorted first.

const a = [2, 5,78, 80].sort((a,b) => a-b);
const expensiveFunction = n => 2 * n;

const expensiveFunCaller= ([...arr]) =>{
  if(arr.length<2){
    let r = expensiveFunction(arr[0]);
    if(r>100) return r;
    return;
  }
  else if(arr.length === 2){
    let r = expensiveFunction(arr[0]);
    if(r>100) return r;
    r = expensiveFunction(arr[1]);
    if(r>100) return r;
    return;
  }
  let idx = Math.floor(arr.length / 2);
  let r = expensiveFunction(arr[idx]);

  return (r<100)?expensiveFunCaller(arr.slice(idx+1, arr.length)):expensiveFunCaller(arr.slice(0, idx+1));
}
console.log(expensiveFunCaller(a));

Upvotes: 0

ecc521
ecc521

Reputation: 657

If you are willing to accept that the first matching element in your array is modified, you can do this:

a[a.findIndex((value, index) => {
    value = expensiveFunction(value); 
    return (value > 100 && (a[index] = value))
})] //Returns 156

Otherwise, you will need to use a placeholder variable to make this work - quite possibly making a for-loop the cleanest option.

Upvotes: 2

Code Maniac
Code Maniac

Reputation: 37755

Something like this

const a = [2, 5, 78, 4];
const expensiveFunction = n => 2 * n;
let findMap = arr => {
  let found = arr.find(a => expensiveFunction(a) > 100)
  return found !== undefined ? expensiveFunction(found) : found
}

console.log(findMap(a));


Alert:- JUST out of curiosity , But hacky or you can call it misuse of find

const a = [2, 5, 78, 4];
const expensiveFunction = n => 2 * n;
let findMap = arr => {
  let returnValue;
  let found = arr.find(a => {
    returnValue = expensiveFunction(a)
    return returnValue > 100
  })
  return returnValue
}

console.log(findMap(a));

Upvotes: 4

Titus
Titus

Reputation: 22484

You can use .reduce, the only down side is that you can't stop once a value is found but you won't have to run expensiveFunction for each value.

Here is an example:

const a = [2, 5, 78, 4];
const expensiveFunction = n => 2 * n;

const result = a.reduce((acc, cur) => {
  if (!acc) {
    const r = expensiveFunction(cur);
    if (r > 100) {
      acc = r;
    }
  }
  return acc;
}, null);



console.log(result);

Upvotes: 5

Related Questions