注册

别再用generator模拟async啦,它还有很酷的用法

前一阵在某个技术群里发现有人在讨论JavaScript的generator,不少人一提generator就会把它跟异步联系在一起,我认为这是一个很深的误解。


generator 究竟跟异步是什么关系?以co为代表的一批早期框架用它来模拟async/await,但是这是否能说明 generator 与异步相关呢?我认为答案是否定的,co中用到的语言特性很多,if、函数、变量……而 generator 只是其中之一罢了。


generator 的确是实现模拟async/await的一个关键语言特性,但是,正确的因果关系是 generator 和 async/await 共用了一个JS的底层设施:函数暂停执行并保留当时执行环境。


在我的观念中,generator 的应用前景远远比 async/await 更为广阔。generator 代表了一种"无穷序列"的抽象,或者说不定长序列的抽象,这个抽象可以为我们带来编程思路上的突破。


在非常前卫的函数式语言Haskell的官网首页,有这样一段代码:


primes = filterPrime [2..]
where filterPrime (p:xs) =
p : filterPrime [x | x <- xs, x `mod` p /= 0]

这是一段 Haskell 程序员引以为傲的代码,它的作用是生成一个质数序列,在多数语言中,都很难复刻这个逻辑结构。其中,最关键的一点就是它应用了延迟计算和无穷列表的概念。


我们试着分析这段 Haskell 代码的逻辑,[2..]表示一个从2开始到无穷的整数序列, filterPrime是一个函数,对这个整数序列做了过滤,函数具体内容则由后面的where指定。所以,能够把整数序列变成质数序列的关键代码就是 filterPrime。那么,它究竟做了什么呢?


这段代码简短得不可思议,首先我们来看参数部分,p:xs 是解构赋值的形参,它表示,把输入中的列表第一个元素赋值为p,剩余部分,赋值为xs。


第一次调用 filterPrime 实参为[2..],此时,p的值就是2,而xs则是无尽列表[3..]


那么,filterPrime 是如何将 p 和 xs 过滤成质数列表的呢?我们来看这段代码:


[x | x <- xs, x `mod\` p /= 0]`

这段大概的意思,可以用一段适合JS程序员理解的伪代码来解释:


xs.filter(x => x % p !== 0)

就是从列表 xs 中,过滤 p 的倍数。当然了,xs 并不是 JavaScript 原生数组,所以它并没有方便的filter方法。


那么,接下来,这个过滤好的数组传递给 filterPrime 递归就很有意思了,此时 xs 中已经被过滤掉了 p 的倍数,剩下的第一个数就必定是质数了,我们继续用 filterPrime 递归过滤其第一个元素的倍数,就可以继续找到下一个质数。


最后,代码p : 表示将 p 拼接到列表的第一个。


那么,在 JavaScript 中,是否能复刻这样的编程思想呢?


答案当然是可以,其关键正是 generator。


首先我们要解决的问题就是[2..],这是一个无尽列表,JavaScript中不支持无尽列表,但是我们可以用 generator 来表示,其代码如下:


function *integerRange(from, to){
for(let i = from; i < to; i++){
yield i;
}
}

接下来,数组的filter并不能够很好地作用于无尽列表,所以我们需要一个针对无尽列表的filter函数,其代码如下:



function *filter(iter, condition) {
for(let v of iter) {
if(condition(v)) {
yield v;
}
}
}

最后是我们的重头戏 filterPrime 啦,只要读懂了Haskell,这算不上困难,实现代码如下:


function* filterPrime(iter) {
let p = iter.next().value;
let rest = iter;

yield p;
for(let v of filterPrime(filter(iter, x => x % p != 0)))
yield v;
}

代码写好了,我们可以用JavaScript中独有的异步能力,来输出这个质数序列看看:


function sleep(d){
return new Promise(resolve => setTimeout(resolve, d));
}
void async function(){
for(let v of filterPrime(integerRange(2, Infinity))){
await sleep(1000);
console.log(v);
}
}();

好啦,虽然语法噪声稍微有点多,但是到此为止我们就实现了跟 Haskell 一样的质数序列算法。


除了无尽列表,generator 也很适合包装一些API,表达“不定项的列表”这样的概念。比如,我们可以对正则表达式的exec做一些包装,使它变成一个 generator。


function* execRegExp(regexp, string) {
let r = null;
while(r = regexp.exec(string)) {
yield r;
}
}

使用的时候,我们就可以用 for...of 结构了。下面代码展示了一个简单的词法分析写法。


let tokens = execRegExp(/let|var|\s+|[a-zA-Z$][0-9a-zA-Z$]*|[1-9][0-9]*|\+|-|\*|\/|;|=/g, "let a = 1 + 2;")
for(let s of tokens){
console.log(s);
}

这样的API设计,是不是比原来更简洁优美呢?


你看 generator 是一个潜力如此之大的语言特性,它为 JavaScripter 们打开了通往"无尽"数学概念的大门。所以,别再想着拿它模拟异步啦,希望看过本文,你能获得一点灵感,把 generator 用到开源项目或者生产中的 API 设计,谢谢观赏。


作者:winter
链接:https://juejin.cn/post/7012596693271052325

0 个评论

要回复文章请先登录注册