前一阵在某个技术群里发现有人在讨论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..]wherefilterPrime(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(leti=from;i<to;i++){yieldi;}}
接下来,数组的filter并不能够很好地作用于无尽列表,所以我们需要一个针对无尽列表的filter函数,其代码如下:
function*filter(iter,condition){for(letvofiter){if(condition(v)){yieldv;}}}
最后是我们的重头戏 filterPrime 啦,只要读懂了Haskell,这算不上困难,实现代码如下:
function*filterPrime(iter){letp=iter.next().value;letrest=iter;yieldp;for(letvoffilterPrime(filter(iter,x=>x%p!=0)))yieldv;}
代码写好了,我们可以用JavaScript中独有的异步能力,来输出这个质数序列看看:
functionsleep(d){returnnewPromise(resolve=>setTimeout(resolve,d));}voidasyncfunction(){for(letvoffilterPrime(integerRange(2,Infinity))){awaitsleep(1000);console.log(v);}}();
好啦,虽然语法噪声稍微有点多,但是到此为止我们就实现了跟 Haskell 一样的质数序列算法。
除了无尽列表,generator 也很适合包装一些API,表达“不定项的列表”这样的概念。比如,我们可以对正则表达式的exec做一些包装,使它变成一个 generator。
function*execRegExp(regexp,string){letr=null;while(r=regexp.exec(string)){yieldr;}}
使用的时候,我们就可以用 for...of 结构了。下面代码展示了一个简单的词法分析写法。
lettokens=execRegExp(/let|var|\s+|[a-zA-Z$][0-9a-zA-Z$]*|[1-9][0-9]*|\+|-|\*|\/|;|=/g,"leta=1+2;")for(letsoftokens){console.log(s);}
这样的API设计,是不是比原来更简洁优美呢?
你看 generator 是一个潜力如此之大的语言特性,它为 JavaScripter 们打开了通往"无尽"数学概念的大门。所以,别再想着拿它模拟异步啦,希望看过本文,你能获得一点灵感,把 generator 用到开源项目或者生产中的 API 设计,谢谢观赏。