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.
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
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");
});
});