JavaScript 地图和同时查找:findMap?

问题描述 投票:0回答:11

如果不使用 for 循环,你会如何重写它?

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);

我天真的做法:

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

但是这会在所有元素上运行

expensiveFunction
,我想避免这种情况。在上述情况下,我们应该避免跑步
expensiveFunction(4)

有些语言有

find_map
(例如,Rust),我在 lodash 或下划线中都没有找到它。

javascript lazy-evaluation
11个回答
20
投票

内置

map
是贪婪的,所以你必须编写自己的惰性版本:

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)

与库存

map
不同,这个
map
是一个生成器,可以根据需要返回(产生)结果,而不是立即处理整个数组。此示例中的
find
map
是“协程”,它们会玩某种乒乓球游戏,其中
find
询问结果,而
map
在询问时提供结果。一旦
find
对它所得到的感到满意,它就会退出,
map
也会退出,因为没有人再询问它的结果了。

您还可以将

map
find
和朋友添加到
IteratorPrototype
,使它们可用于所有迭代器并能够使用点表示法:

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)

这是一个基于此技术的小型库:https://github.com/gebrkn/armita


TypeScript 实现和测试

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

注意:这些测试使用 Bun,但应该与 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");
  });
});


4
投票

您可以使用

.reduce
,唯一的缺点是一旦找到值就无法停止,但您不必为每个值运行
expensiveFunction

这是一个例子:

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);


4
投票

类似这样的事情

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));


警告:- 只是出于好奇,但是 hacky 或者你可以称之为滥用

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));


3
投票

这是 Titus

.reduce
答案的简洁实用版本:

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

它迭代整个数组,但一旦满足条件就停止执行昂贵的函数。

这是我个人使用的,以防对任何人有帮助:

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

3
投票

为什么不使用更智能的查找功能?

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

找到第一个结果后,它会立即退出昂贵的循环。


2
投票

如果您愿意接受数组中第一个匹配元素被修改,您可以这样做:

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

否则,您将需要使用占位符变量来完成这项工作 - 很可能使 for 循环成为最干净的选项。


1
投票

映射本质上是通过一个函数运行每个值,并将其结果作为新数组中该索引处的值返回。考虑到这一点,只需将

expensiveFunction
移动到
find()
方法中,如下所示:

之前:

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

之后:

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

现在,它只会在迭代数组时检查每个元素的

expensiveFunction
结果,并在找到值后停止。

轻量级抽象,使事情变得更容易、更可重用

** 注意,由于您实际上并未映射数组,因此

find()
方法的结果将是该索引处的原始值,而不是
expensiveResult
的值。

如果您希望返回

expensiveResult
的值,并且不需要在找到的元素上再次运行
expensiveFunction
,您可以将此逻辑移至轻量且可重用的抽象中,如下所示:

// 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);

相同的光抽象,但没有循环😉

…现在又是同样的抽象,但这次没有按照原始问题的要求进行循环:

// 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);

0
投票

我遵循的方法是将调用“expenseFunction”函数的次数减少到尽可能少的次数。为此,我使用了“分治算法”。您将数组分为两半,并对分割元素调用昂贵的函数来决定继续进行哪一半。递归地执行此步骤,直到找到 100 以上的最小元素。特别是对于非常大的数组,此方法会将昂贵的函数调用减少到明显更小的数量。因此,“expenseFunCaller”函数将经济地调用您的“expenseFunction”。数组也应该先排序。

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));

0
投票

for
循环有一些有趣的属性,用它们的实用性抵消了丑陋的代码。 本问题中描述的
findMap
功能可以通过如下简单函数来实现:

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版本:

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}`);


0
投票

您可以对可迭代对象执行多个操作,而仅迭代一次值。

下面是在我的 iter-ops 库的帮助下执行此操作的示例:

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);

在上面,它将执行与您的

for-loop
示例中相同数量的步骤。


-2
投票

您可以通过使用三元运算符来简化条件和使用filter()来删除数组的布尔(null)值来通过最短的方法。

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

© www.soinside.com 2019 - 2024. All rights reserved.