发布于 2026-01-06 2 阅读
0

使用生成器、Map、Filter 和 Reduce 在 JavaScript 中实现惰性求值 AWS 安全直播!

使用生成器、Map、Filter 和 Reduce 在 JavaScript 中实现惰性求值

AWS 安全直播!

我的朋友edA-qa最近在Twitch上用 Rust 语言进行直播编程。其中出现了一段很有意思的代码:

(1..).filter(|num| num%2 == 0).take(n).sum() 
Enter fullscreen mode Exit fullscreen mode

我们可以看到,某些运算是在无界数字范围内进行的,(1..)也就是说,从 1 开始一直到无穷大。这种代码属于函数式编程范式,并利用了“惰性求值”的特性,即表达式仅在需要时才进行实际计算。

我最近一直在用 JavaScript 编程,于是就好奇这个方法在 JavaScript 里是否也适用。我知道 JavaScript 有像filtermapreduce这样可以处理数组的函数,但我想知道它们是否也能用于生成器

事实证明,它们目前还不支持,至少默认情况下不支持。假设我们有一个生成器,它只生成从 1 开始的整数:

const numbers = function* () {
    let i = 1
    while (true) {
        yield i++ 
    }
}
Enter fullscreen mode Exit fullscreen mode

我们可以直接使用它来进行筛选和映射等操作吗?

let result = numbers.map(num=>num**2).slice(0,3) //doesn't work :(
console.log('result = ' + result)
Enter fullscreen mode Exit fullscreen mode

这将产生:

let result = numbers.map(num=>num**2).slice(0,3) //doesn't work :(
                 ^

TypeError: numbers.map is not a function
    at Object.<anonymous> (C:\dev\lazy.js:66:18)
Enter fullscreen mode Exit fullscreen mode

先尝试启动发电机也不行:

let result = numbers().map(num=>num**2).slice(0,3) //doesn't work :(
console.log('result = ' + result)
Enter fullscreen mode Exit fullscreen mode

这将产生:

TypeError: numbers(...).map is not a function
    at Object.<anonymous> (C:\dev\lazy.js:66:20)
Enter fullscreen mode Exit fullscreen mode

我决定用 JavaScript 编写一个简单的类包装器,以实现类似于 Rust 示例的功能。

下面这个Lazy类作为所需行为的基类。

class Lazy {
    constructor(iterable, callback) {
        this.iterable = iterable
        this.callback = callback
    }

    filter(callback) {
        return new LazyFilter(this, callback)
    }

    map(callback) {
        return new LazyMap(this, callback)
    }

    next() {
        return this.iterable.next()
    }

    take(n) {
        const values = []
        for (let i=0; i<n; i++) {
            values.push(this.next().value)
        }

        return values
    }
}  
Enter fullscreen mode Exit fullscreen mode

这个Lazy类只是简单地封装了一个简单的 JavaScript 可迭代对象(参见迭代协议)。默认情况下,如果你调用它的next方法,它会将该调用委托给被它封装的可迭代对象。

请注意,单独调用 `__init__` 和 `__init__`filter并不会执行太多操作:它们只会实例化一个对象。以下是 ` __init__` 和 `__init__`map的实现LazyFilterLazyMap

class LazyFilter extends Lazy {
    next() {
        while (true) {
            const item = this.iterable.next()

            if (this.callback(item.value)) {
                return item
            }
        }
    }
}

class LazyMap extends Lazy {
    next() {
        const item = this.iterable.next()

        const mappedValue = this.callback(item.value)
        return { value: mappedValue, done: item.done }
    }
}

Enter fullscreen mode Exit fullscreen mode

这两个子类也都只是实现了 JavaScript 的next方法。

现在让我们看看这段代码的实际运行效果!以下是一些运行这段代码的简单示例:


let result = new Lazy(numbers()).map(num=>num*3).take(4).reduce((a,v) => a + v)
console.log('result = ' + result)

result = new Lazy(numbers()).filter(n=>n%2==0).take(4).reduce((a,v) => a + v)
console.log('result = ' + result)

result = new Lazy(numbers()).filter(n=>n%2==0).map(num=>num**2).take(4).reduce((a,v) => a + v)
console.log('result = ' + result)

result = new Lazy(numbers()).map(num=>num**2).filter(n=>n%2==0).take(4).reduce((a,v) => a + v)
console.log('result = ' + result)

Enter fullscreen mode Exit fullscreen mode

以下是使用 Node.js 运行此示例的结果:

C:\dev>node lazy.js
result = 30
result = 20
result = 120
result = 120
Enter fullscreen mode Exit fullscreen mode

如果您不熟悉这种类型的代码,我将尝试解释它的工作原理。我们来看第一个例子:

let result = new Lazy(numbers()).map(num=>num*3).take(4).reduce((a,v) => a + v)
console.log('result = ' + result)
Enter fullscreen mode Exit fullscreen mode

首先,我们来看一下这个take函数。这个函数启动一切。在take它被调用之前,除了创建一些对象之外,不会发生任何事情。

take函数将对`Take` 返回的对象调用next4 次。这反过来又将对 `Take` 返回的生成器调用 4 次生成器生成的每个数字都会传递给回调函数,回调函数会将每个数字乘以 3,然后再将结果传递回 ` Take`。`Take` 返回一个普通的 JavaScript 数组。在本例中,它将包含 `Take` 。现在我们可以调用`collapse` 方法,该方法使用提供的回调函数将数组合并为一个值。在本例中,所有数字相加得到最终结果 '30'。LazyMapmap(num=>num*3)nextnumbers()mapnum=>num*3take[3,6,9,12]Array.reduce

这里有几个需要注意的潜在陷阱:首先,尝试调用reduce一个永不停止的生成器是行不通的,因为该reduce函数永远不会处理完所有值。不过,我们可以编写一个迭代式的 reduce 版本来逐步合并值。此外,使用 `.` 时务必小心filter。`.`会一直运行filter,直到找到与其回调函数匹配的值才会停止。如果我们尝试调用filter一个无限运行的生成器,而它又找不到任何匹配项,那么 `.`filter也会无限运行,导致程序挂起。

map我认为,如果 JavaScript 能够支持任何可迭代对象作为`and`filter和 `push` 等函数的调用目标,而不仅仅是数组,那就更优雅了reduce。或许 Mozilla 会在后续版本中实现这一点,并提供类似 Rust 中无界惰性求值范围的语法糖(1..)

有关的:

文章来源:https://dev.to/nestedsoftware/lazy-evaluation-in-javascript-with-generators-map-filter-and-reduce--36h5