使用生成器、Map、Filter 和 Reduce 在 JavaScript 中实现惰性求值
AWS 安全直播!
我的朋友edA-qa最近在Twitch上用 Rust 语言进行直播编程。其中出现了一段很有意思的代码:
(1..).filter(|num| num%2 == 0).take(n).sum()
我们可以看到,某些运算是在无界数字范围内进行的,(1..)也就是说,从 1 开始一直到无穷大。这种代码属于函数式编程范式,并利用了“惰性求值”的特性,即表达式仅在需要时才进行实际计算。
我最近一直在用 JavaScript 编程,于是就好奇这个方法在 JavaScript 里是否也适用。我知道 JavaScript 有像filter、map和reduce这样可以处理数组的函数,但我想知道它们是否也能用于生成器。
事实证明,它们目前还不支持,至少默认情况下不支持。假设我们有一个生成器,它只生成从 1 开始的整数:
const numbers = function* () {
let i = 1
while (true) {
yield i++
}
}
我们可以直接使用它来进行筛选和映射等操作吗?
let result = numbers.map(num=>num**2).slice(0,3) //doesn't work :(
console.log('result = ' + result)
这将产生:
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)
先尝试启动发电机也不行:
let result = numbers().map(num=>num**2).slice(0,3) //doesn't work :(
console.log('result = ' + result)
这将产生:
TypeError: numbers(...).map is not a function
at Object.<anonymous> (C:\dev\lazy.js:66:20)
我决定用 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
}
}
这个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 }
}
}
这两个子类也都只是实现了 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)
以下是使用 Node.js 运行此示例的结果:
C:\dev>node lazy.js
result = 30
result = 20
result = 120
result = 120
如果您不熟悉这种类型的代码,我将尝试解释它的工作原理。我们来看第一个例子:
let result = new Lazy(numbers()).map(num=>num*3).take(4).reduce((a,v) => a + v)
console.log('result = ' + result)
首先,我们来看一下这个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..)。
有关的:
- 如何在 JavaScript 中序列化并发操作:回调、Promise 和 Async/Await
- 仔细检查 JavaScript 等待
- 迭代器来了!JavaScript 中的 [Symbol.iterator] 和 [Symbol.asyncIterator]
- JavaScript 中的异步生成器和管道