How many kinds of squares password

#

上礼拜和yld吃饭,聊了聊最近的业内优秀开源项目以及算法方面的东西。yld表示自己当年能够用笔一字不改的手撕KMP算法,对现在功力的退步表示遗憾。
然后闲聊之际yld掏出手机解锁,由此引申出一道算法题:九宫格密码到底有多少种?

九宫格密码

经过尝试,安卓系统的九宫格密码有这样一些限制条件,假设九宫格的布局如下:

1
2
3
1 2 3
4 5 6
7 8 9

那么:

  1. 至少使用4个点
  2. 不能重复经过同一个点
  3. 路径上的中间点不能跳过(例如 1 -> 3 -> 6 -> 9 是不合法的,因为2被跳过了)
  4. 如果中间的点之前已经用过,那么该点就可以被跳过(例如 2 -> 1 -> 3 -> 6是合法的,因为在 1 -> 3之前2已经被使用过)

首先考虑最简单的情况

首先只考虑条件1和条件2,那么这种排列组合的方式一共有A(9, 4) + A(9, 5) + A(9, 6) + A(9, 7) + A(9, 8) + A(9, 9)种。
当然,如果你就直接把这个答案写上去的话我们就没办法继续往下写了。所以我们还是用代码一个一个的数出来吧-。-

(最近在学习scala,我用scala练习一下)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
object PasswordPermutation {

//长度为m的排列组合
def password_permutation_with_length(n: Int, m: Int) = {

def helper(llist: List[List[Int]], n:Int, m: Int): List[List[Int]] = {
if(m == 0) llist
else {
val pre_list = for(list <- llist ; i <- 1 to n; if(!list.contains(i))) yield
list ++ List(i)
helper(pre_list, n, m-1)
}
}

helper(List(Nil), n, m)
}

//满足条件1 2 的排列组合
def password_permutation(n: Int) = (for(i <- (4 to n).toList)
yield password_permutation_with_length(n, i)).flatten

//测试
def main(args: Array[String]) {
val result = password_permutation(9)
println("count:" + result.length)
}

}

然后加入限制条件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
object PasswordPermutation {

//所有规则3禁止的类别
val forbidden:Map[Int, List[Int]] = Map(
1 -> List(3, 7, 9),
2 -> List(8),
3 -> List(1, 7, 9),
4 -> List(6),
5 -> Nil,
6 -> List(4),
7 -> List(1, 3, 9),
8 -> List(2),
9 -> List(1, 3, 7)
)

//规则3与规则4的限制条件
def is_forbidden(list: List[Int], i: Int) = {
if(list isEmpty) false
else forbidden(list.last).contains(i) && !list.contains((list.last + i)/2)
}

//长度为m的排列组合(加入限制条件3 4)
def password_permutation_with_length(n: Int, m: Int) = {

def helper(llist: List[List[Int]], n:Int, m: Int): List[List[Int]] = {
if(m == 0) llist
else {
val pre_list = for(list <- llist ; i <- 1 to n; if(!list.contains(i) && !is_forbidden(list, i))) yield
list ++ List(i)
helper(pre_list, n, m-1)
}
}

helper(List(Nil), n, m)
}

//满足条件1 2 3 4的排列组合
def password_permutation(n: Int) = (for(i <- (4 to n).toList)
yield password_permutation_with_length(n, i)).flatten

//测试
def main(args: Array[String]) {
val result = password_permutation(9)
println("count:" + result.length)
}

}

运行得到结果:

1
count:389112

最后

原来只是这么简单一道题目啊-。- 还以为发现了不得了的东西呢-。-

fix-docker-x509-error

  1. 问题描述

由于公司内部证书被IT部门修改,我们在使用docker pull某些镜像的时候会抛出x509错误 x509: certificate signed by unknown authority。
例如,我在尝试用docker-compose获取最新版本的spark时会显示:

1
2
3
4
5
6
7
8
9
10
abuuu-VirtualBox# sudo docker-compose up
Pulling master (gettyimages/spark:latest)...
latest: Pulling from gettyimages/spark
357ea8c3d80b: Pulling fs layer
c6cf625461b9: Pulling fs layer
06fd4f43f066: Pulling fs layer
dd98390795f4: Waiting
36769b1579ad: Waiting
b2e57763c10f: Waiting
ERROR: error pulling image configuration: Get https://dseasb33srnrn.cloudfront.net/registry-v2/docker/registry/v2/blobs/sha256/4e/4ef9bff9a39ea255de6945d1480a771b4785b17a0da492fd6427e98ec5d624dd/data?Expires=1470184587&Signature=TpEb2htK0E8yUKVinb03onAc35rMqzC4JPJeWnXQ1DkmFifVORmP9-Vusc8vtZjFG3yCyWgfIL8zRVLhmj3koVtb~QLcx5eHcmHprzj6nxXt~GC-MuUT91t65Q2eOqwQDNQAwlcPxP9moxggWmoGQaHyII0bIwBtvdZ7GiUnE0w_&Key-Pair-Id=APKAJECH5M7VWIS5YZ6Q: x509: certificate signed by unknown authority

我们可以看出,在访问dseasb33srnrn.cloudfront.net时产生了证书验证错误。

  1. 解决方法

参考http://www.cnblogs.com/sting2me/p/5596222.html上描述的方式,用openssl访问该网站,并将被替换后的证书保存至本地并用update-ca-certificates更新:

代码如下(在Ubuntu14.04下测试通过)

1
2
3
echo -n | openssl s_client -showcerts -connect dseasb33srnrn.cloudfront.net:443 2>/dev/null | sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p' > /usr/local/share/ca-certificates/cloudfront.crt
update-ca-certificates
sudo service docker restart

然后重新执行就会发现成功了,从此以后就可以在上班的时候轻松摸鱼了(误)。

Parsing-text-table-with-jison-2

用jison解析一个文本表格2

在上一篇文章中,我们完成了一个最初版本的的Parser,它可以用来解析一个基本的表格文本。接下来,我们将以此为基础逐步完善更多内容。

将表格内容存入对象 Store cells in an object instead of array

我们知道在bison中,使用者可以通过yylval保存全局变量。类似的,在jison中,我们可以通过全局变量yy来储存。
官网描述:http://zaa.ch/jison/docs/#sharing-scope

那么,首先我们好奇的是,这个变量yy到底包含哪些内容。我们在执行时将它输出到控制台一探究竟:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
{ lexer:
{
yy: [Circular],
_input: '',
done: true,
_backtrack: false,
_more: false,
yyleng: 0,
yylineno: 16,
match: '',
matched: '+---------------------------------+---------+----------------+---------------+\r\n| VM | State | VM Type | IP |\r\n+---------------------------------+---------+----------------+---------------+\r\n| a/0 | running | a | 192.168.1.159 |\r\n| b/0 | running | b | 192.168.1.161 |\r\n| b/1 | running | b | 192.168.1.162 |\r\n| b/2 | running | b | 192.168.1.163 |\r\n| eeee/0 | running | iiiiiiiiiiiiii | 192.168.1.164 |\r\n| haaaaaaa/0 | running | iiiiiiiiiiiiii | 192.168.1.168 |\r\n| c/0 | running | iiiiiiiiiiiiii | 192.168.1.166 |\r\n| lllllllllloooooooooolllllller/0 | running | iiiiiiiiiiiiii | 192.168.1.167 |\r\n| n1/0 | running | iiiiiiiiiiiiii | 192.168.1.156 |\r\n| n2/0 | running | iiiiiiiiiiiiii | 192.168.1.157 |\r\n| n3/0 | running | iiiiiiiiiiiiii | 192.168.1.158 |\r\n| n4/0 | running | iiiiiiiiiiiiii | 192.168.1.165 |\r\n| u1/0 | running | iiiiiiiiiiiiii | 192.168.1.160 |\r\n+---------------------------------+---------+----------------+---------------+',
yytext: '',
conditionStack: [ 'INITIAL' ],
yylloc:
{
first_line: 17,
last_line: 17,
first_column: 78,
last_column: 78 },
offset: 0,
matches: [ '', index: 0, input: '' ] },
parser: { yy: {}, parseError: [Function: parseError] } }

我们看到,yy中储存了多个运行时用到的变量。我们很容易在里面找到bison中其他变量的对应,例如yytext和yyleng。
这些变量在语法解析的过程中均可以被访问到,因此,我们大胆猜想一下,如果我们想要保存/使用自己的变量,或许可以直接通过yy.myVarible进行访问。
我们修改jison文件如下(仅标出修改部分),并保存为table-v2.jison:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
%lex

%{
yy.convert = function(obj, header){
for(var i=0;i<obj._content.length;i++)
{
obj[header[i]]=obj._content[i];
}
delete obj._content;
return obj;
}
%}

%%

...

headerline
: line
{
yy.header = [];
for(var i=0;i<$1._content.length;i++){
yy.header.push($1._content[i]);
}
$$ = $1;
}
;

...

content
: line
{
$$ = {_content: []};
$$._content.push(yy.convert($1,yy.header));
}
| content line
{
$1._content.push(yy.convert($2,yy.header));
$$ = $1;
}
;

再运行下面的代码看看:

1
2
jison table-v2.jison
node table-v2.js sample.txt

控制台输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
{ _content:
[ { VM: 'a/0',
State: 'running',
'VM Type': 'a',
IP: '192.168.1.159' },
{ VM: 'b/0',
State: 'running',
'VM Type': 'b',
IP: '192.168.1.161' },
{ VM: 'b/1',
State: 'running',
'VM Type': 'b',
IP: '192.168.1.162' },
{ VM: 'b/2',
State: 'running',
'VM Type': 'b',
IP: '192.168.1.163' },
{ VM: 'eeee/0',
State: 'running',
'VM Type': 'iiiiiiiiiiiiii',
IP: '192.168.1.164' },
{ VM: 'haaaaaaa/0',
State: 'running',
'VM Type': 'iiiiiiiiiiiiii',
IP: '192.168.1.168' },
{ VM: 'c/0',
State: 'running',
'VM Type': 'iiiiiiiiiiiiii',
IP: '192.168.1.166' },
{ VM: 'lllllllllloooooooooolllllller/0',
State: 'running',
'VM Type': 'iiiiiiiiiiiiii',
IP: '192.168.1.167' },
{ VM: 'n1/0',
State: 'running',
'VM Type': 'iiiiiiiiiiiiii',
IP: '192.168.1.156' },
{ VM: 'n2/0',
State: 'running',
'VM Type': 'iiiiiiiiiiiiii',
IP: '192.168.1.157' },
{ VM: 'n3/0',
State: 'running',
'VM Type': 'iiiiiiiiiiiiii',
IP: '192.168.1.158' },
{ VM: 'n4/0',
State: 'running',
'VM Type': 'iiiiiiiiiiiiii',
IP: '192.168.1.165' },
{ VM: 'u1/0',
State: 'running',
'VM Type': 'iiiiiiiiiiiiii',
IP: '192.168.1.160' } ] }

干的不错,我们成功的将表格内容存入了对应的对象中,收工!

一些jison的坑

从上面的代码中可以看到,我在预编译阶段声明并定义了yy.convert函数。我们可以在语法分析阶段直接使用该函数。
但是,如果我们在预编译阶段声明我们要用到的变量例如yy.header = [];,则会出现严重问题。Jison在每一个语法匹配完成之后将重置yy.header为最初声明的空的Array。导致在匹配content语法时无法拿到正确的yy.header。
因此,我将yy.header的初始化放到了headerline的处理逻辑中,以此绕过这个坑。
我已经在github题了一个issue,暂时没有人回复。。。

然后

我想到可以继续扩展的功能点包括:

  1. 多行Header
  2. 表格中有空的Cell

这些会在接下来的文章中一一补充。

众人:才刚刚填了一个坑立马又挖了两个,眼看着就要烂尾了。。
abuuu:就算烂尾我也要挖-。-!

Parsing-text-table-with-jison

用jison解析一个文本表格

前言

本文受到文章如何愉快地写个小parser的启发,尝试用文中所介绍的Jison来进行文本解析处理。

什么是Jison

Jison的官网介绍是:

1
2
3
Parsers help computers derive meaning from arbitrary text. And Jison helps you build parsers!

Jison is essentially a clone of the parser generator Bison (thus Yacc,) but in JavaScript. It includes its own lexical analyzer modeled after Flex.

Jison是一个”Bison in JavaScript”。也就是说,首先Jison是一个类似Bison的parser生成器。区别在于,Jison使用JavaScript编写,可以同时用于NodeJS和浏览器执行环境中。

Jison的安装请参照http://zaa.ch/jison/docs/,在NodeJS的开发环境中使用`npm install jison -g`即可完成安装。

使用Jison

这里我们用一个简单的例子来介绍如何使用Jison。

最近的工作都在使用CloudFoundry,在使用BOSH的时候经常需要通过bosh vms去抓ip地址。这个命令会返回一个类似于下面格式的表格:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
+---------------------------------+---------+----------------+---------------+
| VM | State | VM Type | IP |
+---------------------------------+---------+----------------+---------------+
| a/0 | running | a | 192.168.1.159 |
| b/0 | running | b | 192.168.1.161 |
| b/1 | running | b | 192.168.1.162 |
| b/2 | running | b | 192.168.1.163 |
| eeee/0 | running | iiiiiiiiiiiiii | 192.168.1.164 |
| haaaaaaa/0 | running | iiiiiiiiiiiiii | 192.168.1.168 |
| c/0 | running | iiiiiiiiiiiiii | 192.168.1.166 |
| lllllllllloooooooooolllllller/0 | running | iiiiiiiiiiiiii | 192.168.1.167 |
| n1/0 | running | iiiiiiiiiiiiii | 192.168.1.156 |
| n2/0 | running | iiiiiiiiiiiiii | 192.168.1.157 |
| n3/0 | running | iiiiiiiiiiiiii | 192.168.1.158 |
| n4/0 | running | iiiiiiiiiiiiii | 192.168.1.165 |
| u1/0 | running | iiiiiiiiiiiiii | 192.168.1.160 |
+---------------------------------+---------+----------------+---------------+

对于这样一个格式规整的多行文本,如果用正则表达式去匹配整个表格并提取所有的内容,写出的代码可维护性会比较差。我们下面思考一下如何使用Jison来创建一个parser解析这个表格。

思路

首先做词法分析,

通过如下的正则表达式来描述所有的词法

1
2
3
4
5
6
\s+                                 /* skip whitespace */
<<EOF>> return 'EOF' /* End of file*/
"+" return 'HEAD' /* Header of the separate line */
("-")+"+" return 'TAIL' /* Tail of the separate line */
[^+|-]*[^\s+|-] return 'CELL' /* Content in the cell, this is what we want */
"|" return '|' /* Vertical bar of the cell */

然后是语法分析,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
%start table

%% /* language grammar */

table
: separateline headerline separateline content separateline EOF
{
console.log({header: $2, body: $4}); // Print out for debug
return {header: $2, body: $4}; // Separate lines are ignored
}
;

separateline
: HEAD
{$$ = $1;}
| separateline TAIL
{$$ = $1 + $2;}
;

headerline
: line
{$$ = $1}
;

content
: line
{$$ = {content: []};$$.content.push($1);}
| content + line
{$1.content.push($2); $$ = $1;}
;

line
: '|'
{$$ = {content: []};}
| line CELL '|'
{$1.content.push($2); $$ = $1;}
;

最后我们得到了jison文件table-v1.jison

生成parser

我们使用jison命令生成对应的parser:

1
jison table-v1.jison

执行之后,我们会得到名为table-v1.js的parser文件,该文件可以直接使用进行parse。我们首先将表格文本保存为sample.txt。然后执行命令:

1
node table-v1.js sample.txt

输出如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{ header: { content: [ 'VM', 'State', 'VM Type', 'IP' ] },
body:
{ content:
[ [Object],
[Object],
[Object],
[Object],
[Object],
[Object],
[Object],
[Object],
[Object],
[Object],
[Object],
[Object],
[Object] ] } }

这样,即完成了一个简单的解析。

然后

当然,仅仅解析成这样是不够的,我们希望最后能够得到一个这样的“开箱即用”的结果格式:

1
2
3
4
5
{     [ {'VM': 'a/0', 'State': 'running', 'VM Type': 'a', 'IP': '192.168.1.159'},
{'VM': 'b/0', 'State': 'running', 'VM Type': 'b', 'IP': '192.168.1.161'},
...
]
}

那么我们要怎么做呢?

且听下回分解。

另:
后面会完成的内容:
table-v1.jison重构为table-v2.jison
通过NodeJS封装parser调用
NodeJS与BOSH的集成
将parser服务化,对外提供service

众人:abuuu同学你列了这么多后面烂尾了怎么办
abuuu:那就删掉啊,不然要怎样(手动白眼)

Tail Recursion in Haskell

Today I was solving the problem of “calculate Fibonacci sequence” in Haskell.

The normal fibonacci function is implemented as:

1
2
3
4
fib :: Int -> Int
fib 0 = 0
fib 1 = 1
fib n = fib (n - 1) + fib (n - 2)

However, this implement runs very slow.

There is one possible way to optimize the code as follow:

1
2
3
4
fib :: Int -> Int
fib n = aux n (0, 1)
where aux n (a, b) | n == 0 = a
| otherwise = aux (n - 1) (b, a + b)

This version is a lot faster than the original version. Why?

The trick is tail recursion.

In previous code, when executing fib n = fib (n - 1) + fib (n - 2), there is context switch by pushing current context into call stack in order to resume after fib (n - 1) and fib (n - 2) are calculated.

However, by using tail recursion, because our function aux n (a, b) directly return the result from aux (n - 1) (b, a + b), program can re-use current stack space without change.

This video is a very good explanation:

https://www.youtube.com/watch?v=L1jjXGfxozc

Getting Concurrent With ES6 Generators

#用generator实现并发(译)

原文地址:http://davidwalsh.name/concurrent-generators

ES6 Generators:全系列

  1. The Basics Of ES6 Generators
  2. Diving Deeper With ES6 Generators
  3. Going Async With ES6 Generators
  4. Getting Concurrent With ES6 Generators

如果你已经阅读并消化了本系列的第一、第二和第三章节,你应该对ES6 generator已经相当有把握了。希望你真正的被激发来开始用它们做一些事情。

我们要探索的最后的话题是比较前沿的东西,它可能会让你的大脑有点混乱(老实的说,我的大脑仍然处于混乱状态)。所以慢慢的看完并且思考这些概念以及例子。并且还需要读一读其他关于这个话题的文章。

你在这里所做的投入,会从一个长远的角度给你带来回报。我完全确信JS完美的异步能力的未来会来自于这些概念。

标准CSP(Communicating Sequential Processes)

受限,我受到这个话题的激发完全是来自于David Nolen @swannodette。严肃的说,他所写得一切文章都值得一读。这里有一些可以让你起步的链接:

好的,下面我就来讲一讲我在这个题目上的探索。我并不是从一个正式的Clojure背景转向JS的,我也没有GO或者ClojureScript相关的经验。我很快发现我迷失在这些文章中,并且我必须要做大量的实验和猜测来收集这些文章中有用的部分。

在探索的过程中,我认为我已经学到了一些目标以及精神都想通的东西,但来自于并不是那么古板的思维方式。

我试图要做的是简历一个简单的Go-风格的CSP(已经ClojureScript core.async)API,同时(我希望)保留大部分潜在的能力。当然,那些比我聪明的人完全有可能迅速地看到我目前为止探索过程中错过的部分。如果这牙膏的话,我希望我的探索能够继续进行下去,并且我会继续和我们分享我的启示!

(部分)分解CSP理论

CSP是讲些什么的?它提到的“communicating”、“Sequential”是什么?“Processes”又是什么?

首先,CSP来自于Tony Hoare的书”Communicating Sequential Processes”。这是一些厚重的CS理论的东西,但如果你有兴趣在学术范围有所建树的话,那么这是最好的开始的地方。当然我并不是要用一种深奥的、令人头痛的、CS的方式来讲述它。我会用一种非正式的方式来描述。

所以,我们先从“sequential”这个词开始。这个部分你应该已经非常熟悉了。它所谈论的是另一种单线程行为以及我们用ES6 generators实现的同步风格代码的方式。

回忆一下,generator是的语法是像这样的:

1
2
3
4
5
function *main() {
var x = yield 1;
var y = yield x;
var z = yield (y * 2);
}

每一条语句都是有序的(按照顺序)执行,每次执行一条。yield关键词表明代码中有可能发生堵塞暂停(只堵塞当前generator自己代码,并不堵塞外部程序!)的地方,但是这并不改变*main()内部自上而下的代码处理顺序。很简单,是吧?

下面,让我们谈谈“processes”,这又是关于些什么的呢?

从本质上来说,一个generator就像是一个虚拟“进程”。它是一块自我包含的程序,它可以,如果JS允许这样做的话,完全和程序剩余部分并发执行。

事实上,这样说有一点点问题。如果generator访问共享内存(也就是说,如果它访问它自身内部本地变量中的“free viriables”),这就不是独立的了。但是让我们先假设一下我们的generator函数并不访问外部变量(因此在函数式编程中我们会把它叫做一个“combinator”)。这样它就可以在理论上以自己单独进程的方式运行。

但是上面我们所说的是“processes” – 复数形式 – 因为有一点很重要的是,同时有两个或多个进程在运行。换句话说,两个或更多的generator进行配对,一般来说是进行协作以便完成更大的任务。

为什么要分离generators而不仅仅用一个呢?最重要的原因是:内容隔离。如果你可以将任务XYZ分解为各个子任务X、Y和Z,那么它们的实现代码会更加容易看懂和维护。

当你将一个函数function XYZ()分解为X()Y()Z()也是同样的道理,这里X()会调用Y()Y()会调用Z()。我们将函数分解为单独的子函数来进行更好的代码隔离,使我们的代码更容易维护。

我们可以对多个generator做同样的事情。

最后我们来说一说“communicating”。这又是什么呢?这是从上面两个词推论出来的 – 协作 – 如果generator需要一起工作,它们需要一种通信渠道(不仅仅是访问共享词法作用域中的内容,而是实实在在的能够进行排他性访问的共享通信渠道)。

这个通信渠道是用来发送什么的呢?可以发送无论任何你需要发送的东西(数字、字符串、等等)。事实上,你甚至不需要真正的往通道中发送数据以便使用该通道进行通信。“Communication”可以是简单的协调 – 就像将控制权从一个地方转移到另一个地方。

为什么我们要转移控制权?主要的原因是JS是单线程的,并且在任何时刻它们中只有一个可以处于活动状态。其余的都处在暂停执行的状态,也就意味着它们处在任务的中途阶段,而只是暂停了,它们在等待必要时恢复。

任意独立的“进程”可以神奇的合作和交流,这似乎并不现实。松散耦合的目标是相当令人敬佩的,但它其实不切实际。

相反,似乎所有成功的CSP实现都是一种对某个特定领域内现有已知逻辑集合的因式分解,它的每一个部分都是特地为其他部分的协作而设计的。

或许我完全错了,但是我还没有看到任何方式让两个随机的generator函数可以非常容易的粘在一起变成一个CSP配对。他们都需要被设计成为另一方工作,接受同样的通信协议,等等。

JS中的CSP

这里有几个已经应用在JS中了的CSP理论。

我们之前提到的Dabid Nolen创建了几个有趣的项目,包括Om以及core.asyncKoa库(为node.js创建)有一些非常有趣的实现,主要是通过它的use(..)方法。另一个对core.async/Go CSP API非常“忠诚”的库是js-csp

你应该确切的研究一下这些伟大的项目来看看不同的实现方式以及关于如何探索JS中的CSP的例子。

asynquence的runner(..): 设计CSP

由于我一直以来都在积极地探索如何将CSP模式的并发性应用到我自己的JS代码中,因此为我的异步流控制库asynquence扩展CSP能力是非常自然的。

我已经有了runner(..)功能插件来处理generator的异步运行(见 “Part 3: Going Async With Generators”),所以在我看来,它可以很容易地扩展为用类-CSP的方式同时处理多个generator。

我所要解决的第一个问题:你怎么知道下面是哪一个generator要获得控制权?

给每个generator赋予一个ID并让其他generator知晓以便它们将消息或者控制权传递给另一个进程的方式未免太繁琐和笨重了。在经过多次试验之后,我选定了一个简单的round-robin的调度方法。因此如果你将三个generator A、B和C匹配起来之后,A会先获得控制权,然后B在A进行yield时接管,然后C在B进行yield时接管,然后再到达A,以此类推。

但是我们要怎样才能真正的传递控制呢?需要一个明确的API来描述它吗?再一次地,经过多次试验之后,我使用了一个更加隐式的实现方式,它和Koa的实现方式(碰巧)很相似:每个generator获得一个共享“token”的引用 – `yield它以便传递控制权转移的信号。

另一个问题则是消息通道应该是什么样的。一方面,你有一个非常确定了的API就像core.async以及js-csp中的那些(put(..)以及take(..))。在我自己的经验中,我则倾向于另一个方面:一个不那么正式的方法(它甚至不是一个API,只是一个共享的数据结构就像array这样),这看起来就够了。

我决定使用一个数组(称为messages)这样你便可以大刀阔斧的决定你要如何来使用它。可以将消息push()进入这个数组,也可以从这个数组中pop()消息,指定数组中会话相关的元素组建不同消息,并可以在这些空间内创建更加复杂的数据结构,等等。

我的设想是有些任务只需要非常简单的消息传递,而另外一些会非常的复杂,因此以其将简单的内容变得复杂,不如将消息通道变得正式而使其成为一个array(这样就不需要除了array自身以外的API了)。将消息传递机制转化为其他形式是非常容易的,你会发现它的妙用(见我们接下来会瘫倒的状态机的例子)。

最后,我留意到这些generator“进程”仍然可以从普通generator的异步能力中获益。换句话说,如果你yield出一个Promise(或asynquence sequence)而不是一个控制token,runner(..)机制将会暂停并等待将来的结果值而不会移交控制权 – 相反,它会将结果返回给当前的进程(generator)并使它继续享有控制权。

因此最后一点备受争议的应该是(如果我的想法都是正确的话),它和空间内的其它库都不一样。看起来真正的CSP对我这样的实现方式嗤之以鼻。但是我认为我的提议最终会变得非常非常有用。

一个简单的FooBar示例

好的,我们说够了理论,现在让我们来看看代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// Note: omitting fictional `multBy20(..)` and
// `addTo2(..)` asynchronous-math functions, for brevity

function *foo(token) {
// grab message off the top of the channel
var value = token.messages.pop(); // 2

// put another message onto the channel
// `multBy20(..)` is a promise-generating function
// that multiplies a value by `20` after some delay
token.messages.push( yield multBy20( value ) );

// transfer control
yield token;

// a final message from the CSP run
yield "meaning of life: " + token.messages[0];
}

function *bar(token) {
// grab message off the top of the channel
var value = token.messages.pop(); // 40

// put another message onto the channel
// `addTo2(..)` is a promise-generating function
// that adds value to `2` after some delay
token.messages.push( yield addTo2( value ) );

// transfer control
yield token;
}

OK,现在我们有两个generator“进程”, *foo()*bar()。你能留意到他们都处理了token对象(当然你也可以给它们任意命名)。tokenmessages熟悉是我们的共享消息通道。在初始时,它们充满了我们在初始化CSP运行时所产生的消息(见下文)。

yield token显示地将控制权传递给“下一个”generator(通过round-robin策略)。然而,yield multiBy20(value)以及yield addTo2(value)都在yield promise(从这些虚构的延迟数学函数中),这表明generator在这一点会暂停知道promise结束。在promise结束时,当前处于控制的generator会获取返回值并继续下去。

无论最后的yield返回值是什么,在本例中是yield "meaning of...表达式,它都是我们的CSP运行完成的消息(见下文)。

现在我们有了两个CSP进程generator,我们要如何执行它们呢?使用asynquence吧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// start out a sequence with the initial message value of `2`
ASQ( 2 )

// run the two CSP processes paired together
.runner(
foo,
bar
)

// whatever message we get out, pass it onto the next
// step in our sequence
.val( function(msg){
console.log( msg ); // "meaning of life: 42"
} );

显然,这只是一个简单的示例。但是我认为它准确的解释了这个概念。

现在或许你该自己试一试了(试着将返回值进行链式处理!)来确保你已经理解了这些概念并且能够编写你自己的代码了!

另一个玩具Demo的例子

让我们用一个经典的CSP例子来试一试,但是让我们从我所观测到的简单部分开始,而不是像通常人们做的那样从一个学术的角度开始。

Ping-pong。很有趣的运动是吧?这是我最喜欢的运动。

让我们设想一下你已经实现了进行一次乒乓球比赛的代码。你用一个循环来运行,然后你有两块代码(例如,用一个if或者switch实现的分支)每一块代表者一名选手。

你的代码运行得很好,你的游戏就看起来像是一个真正的乒乓球比赛一样!

但是我们之前看到的是CSP在什么方面非常有用?逻辑隔离。我们在乒乓球运动中要隔离的逻辑是?这两名选手!

因此,我们可以,站在一个非常高的角度,来给两个“进程”(generators)建模,每一个代表一名选手。随着我们的深入,我们会发现这些用来在两名选手之间传递控制权的“胶水代码”其自身是一个task,并且它的代码可以被放到第三个generator中,这里我们可以将其建模为一个裁判。

我们会过滤掉所有的领域相关问题,例如得分、赛制、物理学、策略、AI、控制、等等。我们唯一关心的只是模拟来回击球(这也就是我们的CSP控制权转移的描述)。

想看看Demo吗?点击这里运行(注:你需要使用最新版本的FF或者Chrome,它们支持ES6 JavaScript,来看到generator是如何运作的)。

好,下面我们逐条讲解我们的代码。

受限,asynquence sequence看起来长什么样呢?

1
2
3
4
5
6
7
8
9
10
11
12
ASQ(
["ping","pong"], // player names
{ hits: 0 } // the ball
)
.runner(
referee,
player,
player
)
.val( function(msg){
message( "referee", msg );
} );

我们将序列初始化为两条消息:["ping", "pong"]{hits: 0}。我们马上就会用到它们。

然后,我们设置CSP运行3个进程(通过轮询的方式):一个*referee()和两个*player()实例。

比赛最后的消息在我们的序列中不停传输,并最后作为裁判的消息被打印出来。

裁判的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function *referee(table){
var alarm = false;

// referee sets an alarm timer for the game on
// his stopwatch (10 seconds)
setTimeout( function(){ alarm = true; }, 10000 );

// keep the game going until the stopwatch
// alarm sounds
while (!alarm) {
// let the players keep playing
yield table;
}

// signal to players that the game is over
table.messages[2] = "CLOSED";

// what does the referee say?
yield "Time's up!";
}

我把控制token称为table以便和问题的domain(乒乓球)相匹配。语义上让一个选手击球时”yield the table”给另一个选手相当恰当,不是吗?

while循环*referee()只是不停的yield table回给选手只要它的手表还没有计时结束。当结束时,它会宣布"Time's up!"

现在,让我们来看看*player() generator(我们使用了两个实例)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function *player(table) {
var name = table.messages[0].shift();
var ball = table.messages[1];

while (table.messages[2] !== "CLOSED") {
// hit the ball
ball.hits++;
message( name, ball.hits );

// artificial delay as ball goes back to other player
yield ASQ.after( 500 );

// game still going?
if (table.messages[2] !== "CLOSED") {
// ball's now back in other player's court
yield table;
}
}

message( name, "Game over!" );
}

第一个选手从数组中获取他自己的名字("ping"),然后第二个选手获取他的名字(“pong”),因此他们可以同时辨别双方。双方都保留一个对共享ball对象的引用(包含了hits计数器)。

当选手还未听到裁判结束的消息时,他们通过将计数器hits增加1的方式”击打”ball(并且输出一条消息进行公告),然后他们等待500毫秒(只是来模拟一下球速并没有达到光速啦!)

如果比赛仍在继续,他们会”yield table”回到另一名选手中。

就是这样啦!

看看这里的Demo代码,通过将所有的代码放在一起看看它们是如何共同工作的。

状态机:Generator协作

我们最后一个例子:定义一个状态机作为一系列generator的协同程序,它们有一个简单的helper驱动。

Demo(用最新的FF或者Chrome打开)

首先,让我们顶一个一个help函数来控制我们的有限状态句柄:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
function state(val,handler) {
// make a coroutine handler (wrapper) for this state
return function*(token) {
// state transition handler
function transition(to) {
token.messages[0] = to;
}

// default initial state (if none set yet)
if (token.messages.length < 1) {
token.messages[0] = val;
}

// keep going until final state (false) is reached
while (token.messages[0] !== false) {
// current state matches this handler?
if (token.messages[0] === val) {
// delegate to state handler
yield *handler( transition );
}

// transfer control to another state handler?
if (token.messages[0] !== false) {
yield token;
}
}
};
}

state(..) helper工具方法创建了一个为一个具体的状态值准备的delegating-generator包装器,它会自动的运行状态机,并且将控制权在每个状态间进行传递。

出于惯例,我决定共享token.messages[0]位置来保存当前状态机的状态。这表明你可以从之前的步骤中传递一条消息作为初始状态。但是如果没有这样的初始消息被传递的话,我们会简单的将第一个状态定义为我们的初始状态。同样出于惯例,最终状态被断言为false。这很容易实现:

状态值可以是任何种类的值:numbers, strings,等等。只要这个值能被===处理,你就可以使用它来做你的状态。

在下面这个例子中,我展示了一个状态机从4个number形式的状态值之间进行转化,采用这种顺序1 -> 4 -> 3 -> 2。为了demo的目的,它也会进行计数以便进行不止一次的转换。当我们的generator状态机最终到达终止位(false)时,asynquence序列会移动到下一个步骤,如你所期待的那样。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// counter (for demo purposes only)
var counter = 0;

ASQ( /* optional: initial state value */ )

// run our state machine, transitions: 1 -> 4 -> 3 -> 2
.runner(

// state `1` handler
state( 1, function*(transition){
console.log( "in state 1" );
yield ASQ.after( 1000 ); // pause state for 1s
yield transition( 4 ); // goto state `4`
} ),

// state `2` handler
state( 2, function*(transition){
console.log( "in state 2" );
yield ASQ.after( 1000 ); // pause state for 1s

// for demo purposes only, keep going in a
// state loop?
if (++counter < 2) {
yield transition( 1 ); // goto state `1`
}
// all done!
else {
yield "That's all folks!";
yield transition( false ); // goto terminal state
}
} ),

// state `3` handler
state( 3, function*(transition){
console.log( "in state 3" );
yield ASQ.after( 1000 ); // pause state for 1s
yield transition( 2 ); // goto state `2`
} ),

// state `4` handler
state( 4, function*(transition){
console.log( "in state 4" );
yield ASQ.after( 1000 ); // pause state for 1s
yield transition( 3 ); // goto state `3`
} )

)

// state machine complete, so move on
.val(function(msg){
console.log( msg );
});

应该很容易看出这里将发生什么。

yield ASQ.after(1000)表明这些generator可以做任何基于异步流的promise/sequence操作,就像我们之前看到的那样。yield transition(..)是我们如何转移到一个新的状态上。

我们前面的state(..) helper实际上做了处理yield*代理以及状态转换的复杂工作,是的我们的状态句柄可以以一种非常简单和自然的格式进行编写。

总结

CSP的关键在于将两个或多个generator“进程”结合在一起,给予他们一个共享的通信渠道,以及一个它们之前传递控制权的方法。

JS中非常多的库都或多或少的有一些正式的实现,它们基本符合Go/Clojure/ClojureScript的API和语法。所有这些库的背后都有一些非常聪明的开发者,并且他们对于今后的研究都是一笔巨大的财富。

asynquence尝试了一种非正式的实现方式但仍然希望它能满足这个机制。如果没有别的,asynquence的runner(..)使上手使用CSP风格的generator非常简单,你可以进行尝试并且学习。

然而最好的部分仍然是asynquence CSP可以和其他异步功能(promise、generators、控制流等等)无缝结合,你可以使用你所拥有的任何工具,都在这样一个小小的库里。

在过去的4篇文章中我们已经讨论了相当多的关于generator的细节。我希望你对此感到兴奋并受到启发去探索如何改变你自己的JS代码!你会用generator来做些什么呢?

Going Async With ES6 Generators

#用Generator进行异步编程(译)

原文地址:http://davidwalsh.name/async-generators

ES6 Generators:全系列

  1. The Basics Of ES6 Generators
  2. Diving Deeper With ES6 Generators
  3. Going Async With ES6 Generators
  4. Getting Concurrent With ES6 Generators

现在你已经见识过了ES6 generator并且已经对它已经有所熟悉了,现在是时候开始使用它们来增强我们真实的代码了。

Generator的主要唱出在于它们提供了一个单线程的,同步样式的代码风格,同时允许你把异步隐藏为实现细节。这使得我们用一种非常自然的方式表达,专注于我们程序的步骤/声明的流程,而不必同时不得不遵循异步语法并避免陷阱。

换句话说,我们通过隔离对值的消费(我们的generator逻辑)与异步得到这些值的细节(generator迭代器中的next(..)),实现了能力与缺点的完美分离

结果呢?我们获得了异步代码的强大能力,同时也获得了(看上去是)同步代码的可读性以及可维护性。

那么我们如何实现这个非凡的能力呢?

最简单的异步

在最简单的场景下,generator不需要任何额外的操作来实现你的程序中并没有的异步操作。

例如,让我们设想你已经有了这样的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
function makeAjaxCall(url,cb) {
// do some ajax fun
// call `cb(result)` when complete
}

makeAjaxCall( "http://some.url.1", function(result1){
var data = JSON.parse( result1 );

makeAjaxCall( "http://some.url.2/?id=" + data.id, function(result2){
var resp = JSON.parse( result2 );
console.log( "The value you asked for: " + resp.value );
});
} );

要使用一个generator来表现同样的程序,你需要这样做:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function request(url) {
// this is where we're hiding the asynchronicity,
// away from the main code of our generator
// `it.next(..)` is the generator's iterator-resume
// call
makeAjaxCall( url, function(response){
it.next( response );
} );
// Note: nothing returned here!
}

function *main() {
var result1 = yield request( "http://some.url.1" );
var data = JSON.parse( result1 );

var result2 = yield request( "http://some.url.2?id=" + data.id );
var resp = JSON.parse( result2 );
console.log( "The value you asked for: " + resp.value );
}

var it = main();
it.next(); // get it all started

让我们来解释一下它是如何工作的:

request(..)功能函数基本上包装我们普通的makeAjaxCall(..)功能类以保证它的回调函数能调用generator iterator的next(..)方法。

对于request("..")调用,你会注意到它没有返回值(换句话说,它是undefined)。这不是什么大问题,但是它和我们在本文之后的实现方式有所不同:我们在这里实际上是调用了yield undefined

因此我们调用yield ..(和这个undefined值),它实际上什么也没做,它只是在这一点上暂停了我们的generator。它将会等待直到it.next(..)被调用来恢复它,这个调用我们已经排列在队列中(作为回调函数),在Ajax调用结束后发生。

但是yield ..表达式的结果又发生了什么?我们将它赋值到变量result1上。它是如何得到第一个Ajax调用的内部的值的呢?

因为当it.next(..)被作为Ajax回调函数调用时,它实际是在给它传递Ajax的响应结果,这表明值在那个当前暂停的时间点被发送回我们的generator内部,也就是result1 = yield ..表达式的中间!

这的确非常的酷并且超级强大。本质上,result1 = yield reequest(..)是在请求这个值,但是它(几乎!)完全对我们隐藏了 – 至少我们不需要在这里担心它 – 外表之下的实际实现是异步的。它通过隐藏yield中的暂停能力实现了异步,并且分离出generator的恢复能力到另外一个函数中,因此我们的main代码只需要进行一个(看起来是)同步的值的请求

对于第二个result2 = yield result(..)表达式也是一样:它对于暂停和恢复是透明的,并且提供了我们所需求的值,所有这些都没有让任何异步细节打扰到我们我代码。

当然yield出现了,因此那里的确有一个细微的提示“一些神奇的东西(异步)可能发生在那个时间点”。但是yield比起回调地狱(或者甚至是promise链的API冗余!)来已经是一个简单的语法信号/冗余了。

注意到我刚刚说了可能发生。这是一个相当强大的事情。上面的程序总是发出一个Ajax请求,但是如果它不这样呢?如果我们之后将我们的程序改为读取内存中之前得到的Ajax响应呢?或者一些程序中的复杂URL rouer可能在某些条件下立即响应一个Ajax请求而不需要真的从一个外部服务器获取呢?

我们可以改变request(..)的实现使它变成这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var cache = {};

function request(url) {
if (cache[url]) {
// "defer" cached response long enough for current
// execution thread to complete
setTimeout( function(){
it.next( cache[url] );
}, 0 );
}
else {
makeAjaxCall( url, function(resp){
cache[url] = resp;
it.next( resp );
} );
}
}

注意:这里有一个小技巧是需要使用setTimeout(..0)进行延迟以防cache已经在结果里面了。如果我们刚刚立即调用it.next(..),它会产生一个错误,因为(这就是那个技巧)generator尚未处于暂停状态。我们的函数调用request(..)首先被评估,然后yield暂停。因此我们不能再次在request(..)内部调用it.next(..),因为在那个时刻generator扔在执行(yield还没有被进行)。但是我们可以”之后“调用it.next(..),在当前线程执行完的一瞬间,也就是我们的setTimeout(..0)”伪造“的一个实现。我们会在下面有一个更好的实现

现在我们的main generator代码仍然看起来像:

1
2
3
var result1 = yield request( "http://some.url.1" );
var data = JSON.parse( result1 );
..

看到了吧?!我们的generator逻辑(也就是控制流)和不加cache的版本比起来完全不需要变化。

*main()中的代码仍然请求一个值,然后暂停直到它得到值。在我们当前的情境下,”暂停“可以非常长(发送一个真实的请求到服务器,一般为300-800ms)或者可能几乎立即结束(setTimeout(..0)进行延迟处理)。而我们的控制流并不关心。

这就是将异步行为抽象为实现细节真正的强大之处。

更好的异步

对于一个单独的异步generator工作,上面的实现已经相当不错了。但是它马上会到达局限,所以我们需要一个更强大的异步机制来和我们的generator做搭配,它能够承担更多的负担。这个机制是什么呢?就是Promise

如果你对于ES6的Promise还有点模糊不清,我写了一个5篇文章的系列,去读一读吧。我会在这里wait直到你回来的(偷笑,哈哈)。这只是个老掉牙的异步的笑话啦!

本文早先的Ajax代码都有同样的控制反转的问题(也就是”回调地狱“),就下你给我们最初的那个充满了回调的例子一样。到目前为止,我们缺乏这样一些东西:

  1. 没有明确的异常处理的方式。我们已经从上篇文章中学到,我们可以探测到一个Ajax调用时的异常(通过某种方式),通过it.throw(..)传递回我们的generator,然后使用try..catch在我们的generator逻辑中处理它。但是那只是更多的手动任务来接通“后端”(我们处理generator iterator的代码),并且如果我们需要非常多的generator是,它可能无法重复使用。

  2. 如果makeAjaxCall(..)工具类不受控制,并且它调用了多次的callback,或者信号同时成功与失败,等等。那么我们的generator会出故障(未捕获的异常,不期待的值,等等)。处理并且阻止这些问题很多都是手动工作,并且同样无法重用。

  3. 经常的,我们并不仅仅”并发“执行任务(例如两个并行的Ajax调用那样)。由于generatoryield表达式是一个单一暂停点,两个或两个以上的generator不可以在同时运行 – 它们不得不一次一个的执行,按顺序。因此,对于如何在单独的generator yield点发送多个任务,而不在表面之下进行大量的人工编码,是尚不可知的。

如你所见,所有这些问题都是可以被解决的,但是谁又希望每次都重新发明这些解决方法呢?我们需要一个更强大的模式,设计为专为基于generator的异步编码的可信的,可重用的解决方案

那个模式是?yield out promises,并且当他们被fulfill时让它们恢复generator。

回想一下上面我们所做的yield request(..),以及request(..)功能方法没有任何返回值,仅仅yield undefined是有效的吗?

让我们小小的对他进行调整。让我们改变我们的request(..)功能方法使其成为一个基于promise的方法,这样它会返回一个promise,并且这样的话我们yield out的东西实际上是一个promise(而不是undefined)。

1
2
3
4
5
6
function request(url) {
// Note: returning a promise now!
return new Promise( function(resolve,reject){
makeAjaxCall( url, resolve );
} );
}

现在,request(..)会构造一个Ajax调用结束后被处理的promise,并返回这个promise,所以它可以被yield出去,下一步呢?

我们会需要一个功能方法来控制我们的generator iterator,它接收这些被yield的promise然后将他们与恢复generator联通(通过next(..))。我现在会调用下面这个runGenerator(..)功能类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// run (async) a generator to completion
// Note: simplified approach: no error handling here
function runGenerator(g) {
var it = g(), ret;

// asynchronously iterate over generator
(function iterate(val){
ret = it.next( val );

if (!ret.done) {
// poor man's "is it a promise?" test
if ("then" in ret.value) {
// wait on the promise
ret.value.then( iterate );
}
// immediate value: just send right back in
else {
// avoid synchronous recursion
setTimeout( function(){
iterate( ret.value );
}, 0 );
}
}
})();
}

值得注意的关键点:

  1. 我们自动的对generator进行初始化(创建它的it迭代器),然后我们异步地运行it直到结束(done: ture)。

  2. 我们寻找要被yield出去(即在每个it.next(..)调用时的返回值value)的promise。如果有的话,我们通过在promise之上注册then(..)等待直到它结束。

  3. 如果任何立即的(即非promise)值被返回,我们简单地发送这个值到generator中以便它继续立即执行。

现在,我们怎么使用它呢?

1
2
3
4
5
6
7
8
runGenerator( function *main(){
var result1 = yield request( "http://some.url.1" );
var data = JSON.parse( result1 );

var result2 = yield request( "http://some.url.2?id=" + data.id );
var resp = JSON.parse( result2 );
console.log( "The value you asked for: " + resp.value );
} );

Bam!等一等…这不和我们之前的generator代码一样吗?是的。再一次地,generator的强大之处显示出来了。实际上是,我们现在在创建promise,将他们yield出去,然后在generator结束时恢复他们 – 所有这些都隐藏了实现细节! 当然并不是完全隐藏,只是从消费代码(我们generator内部的控制流)中分离出来了。

通过等待被yield出去的promise,并且发送完成结果回it.next(..),代码result1 = yield request()..得到了和之前完全相同的值。

但是现在我们在使用promise来管理generator代码中的异步部分,我们解决所有的来自于回调风格解决方案的倒转/信任问题。我们通过使用generator + promise得到所有上面的解决方案。

  1. 我们现在有了便于使用的内嵌的异常处理。我们在上面的runGenerator(..)中并没有显示它,但是从promise中监听一个异常并发送至it.throw(..)并不困难 – ranh9ou我们可以在我们的generator代码中使用try..catch来捕获并处理这些异常。

  2. 我们拥有了所有由promise提供的控制/可信性解决方案。不需要更多的关心。

  3. Promise拥有非常多位于上层的强大的抽象,它可以自动地处理复杂的多“并发”任务,等等。

例如yield Promise.all([ .. ])可以接受一个prmose的数组来“并发执行”任务,然后yield出一个单一的promise(给generator来处理),它在处理前等待所有的子promise结束(无论以何种顺序)。你从yield表达式返回的(当promise结束时)是一个所有子promise的响应数组,按照它们请求的顺序(无论它们的结束顺序是如何)。

首先让我们来看看异常处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// assume: `makeAjaxCall(..)` now expects an "error-first style" callback (omitted for brevity)
// assume: `runGenerator(..)` now also handles error handling (omitted for brevity)

function request(url) {
return new Promise( function(resolve,reject){
// pass an error-first style callback
makeAjaxCall( url, function(err,text){
if (err) reject( err );
else resolve( text );
} );
} );
}

runGenerator( function *main(){
try {
var result1 = yield request( "http://some.url.1" );
}
catch (err) {
console.log( "Error: " + err );
return;
}
var data = JSON.parse( result1 );

try {
var result2 = yield request( "http://some.url.2?id=" + data.id );
} catch (err) {
console.log( "Error: " + err );
return;
}
var resp = JSON.parse( result2 );
console.log( "The value you asked for: " + resp.value );
} );

当获取URL时promise被拒绝(或者任何形式的错误/异常),promise rejection会被映射为一个generator错误(使用我们之前没有描述的runGenerator(..)中的it.throw(..)),它会被try..catch语句捕获住。

现在,让我们来看一个更下复杂的例子,它使用了promise来管理更多异步的复杂问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
function request(url) {
return new Promise( function(resolve,reject){
makeAjaxCall( url, resolve );
} )
// do some post-processing on the returned text
.then( function(text){
// did we just get a (redirect) URL back?
if (/^https?:\/\/.+/.test( text )) {
// make another sub-request to the new URL
return request( text );
}
// otherwise, assume text is what we expected to get back
else {
return text;
}
} );
}

runGenerator( function *main(){
var search_terms = yield Promise.all( [
request( "http://some.url.1" ),
request( "http://some.url.2" ),
request( "http://some.url.3" )
] );

var search_results = yield request(
"http://some.url.4?search=" + search_terms.join( "+" )
);
var resp = JSON.parse( search_results );

console.log( "Search results: " + resp.value );
} );

Promise.all([ .. ])构造一个promise,它等待3个子promise。并且,被yield出提供给runGenerator(..)功能函数的主promise会被监听作为generator的恢复。子promise可以接收一个响应,它看起来像另一个URL,并且以链式连接另一个子promise到达新的地点。如果要学习更多promise链式表达,阅读这篇文章

任何异步的功能性/复杂性问题都可以由promise解决,而同步风格代码则可以通过使用generator yield出promise(的promise的promise…)来实现。这真是两全其美

runGenerator(..): 功能库

我们已经定义了我们自己的runGenerator(..)来启用这个强大的generator+promise组合。我们省略了这个功能函数的完全实现(为了简单起见),因为还有很多细节上和异常处理相关的内容需要完成。

但是,你并不想编写你自己的runGenerator(..)是吧?

我认为是的。

有非常多的promise/异步库提供了这样的功能。我在这里不会讲述,但你可以看一看Q.spawn(..)co(..)库,等等。

我会简单的介绍一下我自己的功能库:asynquencerunner(..)插件,我认为它比上面的那些库提供了一些特殊的适配性。我写了深入的两部分的blog关于asynquence的系列文章如果你感兴趣学到更多的话你可以去看一看。

首先,asynquence提供了功能类自动处理“首参数为错误风格”的回调:

1
2
3
4
5
6
function request(url) {
return ASQ( function(done){
// pass an error-first style callback
makeAjaxCall( url, done.errfcb );
} );
}

更加的友好了,不是吗!?

下一步,asynquence的runner(..)插件在aynquence序列(异步序列步骤)的中途消耗一个generator,所以你可以从之前的步骤向内传递消息,而你的generator可以向外或向下一步传递消息,而所有的错误会自动地如你期望的那样传播。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// first call `getSomeValues()` which produces a sequence/promise,
// then chain off that sequence for more async steps
getSomeValues()

// now use a generator to process the retrieved values
.runner( function*(token){
// token.messages will be prefilled with any messages
// from the previous step
var value1 = token.messages[0];
var value2 = token.messages[1];
var value3 = token.messages[2];

// make all 3 Ajax requests in parallel, wait for
// all of them to finish (in whatever order)
// Note: `ASQ().all(..)` is like `Promise.all(..)`
var msgs = yield ASQ().all(
request( "http://some.url.1?v=" + value1 ),
request( "http://some.url.2?v=" + value2 ),
request( "http://some.url.3?v=" + value3 )
);

// send this message onto the next step
yield (msgs[0] + msgs[1] + msgs[2]);
} )

// now, send the final result of previous generator
// off to another request
.seq( function(msg){
return request( "http://some.url.4?msg=" + msg );
} )

// now we're finally all done!
.val( function(result){
console.log( result ); // success, all done!
} )

// or, we had some error!
.or( function(err) {
console.log( "Error: " + err );
} );

asynquence runner(..)功能类接受一个可选的消息来开始generator,这个消息往往是由之前的步骤而来,并且在generator的token.messages数组中是可见的。

然后,和我们之前示范使用runGenerator(..)功能类一样,runner(..)监听一个被yield的poromise或asynquence序列(在这种情况下使用ASQ().all(..)序列来并发执行),并且等待它的结束然后恢复generator。

当generator结束时,最后yield出的值会传递给序列的下一个步骤。

并且,如果有任何错误在这个序列的任何地方发生,甚至是在generator内部发生,它会被传播给单独的or(..)被注册的错误处理者。

asynquence尝试将promise和generator尽可能简单的结合。你可以随心所欲的构造任何generator流和基于promise的序列步骤流。

ES7 async

ES7的时间轴上有一个提案,它看起来会被接受,来创建另一种函数:async function,它看起来是使用generator自动地包装一个像runGenerator(..)(或asynquencerunner(..))功能类。那样的话你可以发送promise并且async function会自动地将其包装并且在结束时恢复promise(甚至不需要使用iterator!)

所以它看起来可能会像这样:

1
2
3
4
5
6
7
8
9
10
11
async function main() {
var result1 = await request( "http://some.url.1" );
var data = JSON.parse( result1 );

var result2 = await request( "http://some.url.2?id=" + data.id );
var resp = JSON.parse( result2 );
console.log( "The value you asked for: " + resp.value );
}


main();

如你所见,一个async function可以被直接调用(就像main()一样),而不需要像runGenerator(..)ASQ().runner(..)的包装功能类来包装它。在内部,有别于使用yield,你将会使用await(另一个新的关键词)来告知async function在继续执行前等待promise的结束。

基本上,我们会拥有大多数包装库包装后的generator的能力,但是直接由原生语法支持

酷!是吧!

在同时,像asynquence这样的库给予我们这些执行功能函数来让我们使用异步generator更容易!

总结

简单的说:generator + yielded promise组合了双方最好的部分让我们得到了强大而优雅的同步语法+异步流程控制的能力。使用简单的包装功能函数(有非常多的功能库已经提供了这一点),我们可以自动的运行我们的generator到结束,包括正常结果以及出错的处理。

在ES7的大陆上,我们很可能会见到async function让我们可以不依靠功能库来达到(至少对基本的case可以这样实现)。

JavaScript中异步的未来是光明的,并且只会变得更光明!我应该戴上太阳镜。

但是我们还没有结束,我们还有最后一个部分想要发掘一下:

如果你可以将两个或多个generator连接在一起会怎么样呢?让他们单独但“并发”的执行,并且让他们在执行的过程中互相发送消息?那会是一种更强大的能力,不是吗?这个模式被称为”CSP”(communicating sequential processes)。我们会在下一篇文章中解锁CSP的强大能力。请继续关注!

Diving Deeper With ES6 Generators

#深入ES6Generators(译)

原文地址:http://davidwalsh.name/es6-generators-dive

ES6 Generators:全系列

  1. The Basics Of ES6 Generators
  2. Diving Deeper With ES6 Generators
  3. Going Async With ES6 Generators
  4. Getting Concurrent With ES6 Generators

如果你仍然不熟悉ES6 generator,首先去读一读并理解“第一部分:ES6Generator基础”。一旦你认为你已经了解了这些基础,现在我们就可以深入到一些细节之中了。

错误处理

ES6 generator设计时一个最为强大的部分就是它内部的代码语义是同步的,即使外部迭代控制是异步执行的。

这是个很棒而且复杂的方式,意味着你可以使用简单的错误处理技术,这个技术你可能已经非常熟悉了 – 也就是try..catch机制

例如:

1
2
3
4
5
6
7
8
    try {
var x = yield 3;
console.log( "x: " + x ); // may never get here!
}
catch (err) {
console.log( "Error: " + err );
}
}

尽管函数会在yield 3表达式处停止,并且可能随意暂停一段时间,如果一个错误没回传到generator,那个try..catch会捕获它!尝试用普通的异步能力(例如callback)来做做看?:)

但是,怎样才能使一个错误返回到generator中呢?

1
2
3
4
5
6
7
var it = foo();

var res = it.next(); // { value:3, done:false }

// instead of resuming normally with another `next(..)` call,
// let's throw a wrench (an error) into the gears:
it.throw( "Oops!" ); // Error: Oops!

这里,你可以看到我们使用了iterator上的另一个方法 – throw(..) – 它会“抛”一个异常到generator中,就像它正好在generator当前yield暂停处发生一样。try..catch捕获如你所期望的那样这个错误!

注:如果你throw(..)一个错误进入generator,而没有try..catch住它,那么这个错误会(和普通错误一样)传播会来(如果最终没有被捕获,则会成为一个未被捕获的异常)。因此:

1
2
3
4
5
6
7
8
9
function *foo() { }

var it = foo();
try {
it.throw( "Oops!" );
}
catch (err) {
console.log( "Error: " + err ); // Error: Oops!
}

显然,反方向的错误处理方式同样可行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function *foo() {
var x = yield 3;
var y = x.toUpperCase(); // could be a TypeError error!
yield y;
}

var it = foo();

it.next(); // { value:3, done:false }

try {
it.next( 42 ); // `42` won't have `toUpperCase()`
}
catch (err) {
console.log( err ); // TypeError (from `toUpperCase()` call)
}

generator代理

你可能想做的另一件事情是从你的generator函数内部调用另一个generator。我指的并不仅仅是普通方式实例化一个generator,而是代理你自己的的迭代控制到那一个另外的generator上。

例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function *foo() {
yield 3;
yield 4;
}

function *bar() {
yield 1;
yield 2;
yield *foo(); // `yield *` delegates iteration control to `foo()`
yield 5;
}

for (var v of bar()) {
console.log( v );
}
// 1 2 3 4 5

如在第一部分解释的那样(我使用function *foo(){ }而不是function* foo(){ }),我这里同样使用yield *foo()来代替很多其他文章/文档使用的yield* foo()。我认为这样可以更加准确/清晰地说明代码正在做些什么。

让我们分解一下看看它是如何工作的。yield 1yield 2直接发送他们的值到for..of循环外的next()的(隐式)调用中,就像我们已经理解/期望的那样。

但是接着程序就走到了yield*,然后你会注意到,我们正在通过实例化它(foo())yield到另一个generator。因此我们基本上是在yield或代理到另一个generator的iterator上 – 这可能是最准确的想象它的方式。

一旦yield*被(临时)从*bar()代理到*foo(),那么现在for..of循环的next()调用实际上是在控制foo(),因此yield 3yield 4发送他们的值到外面的for..of循环中。

一旦*foo()结束,控制权会返回到最初的generator上,它最终调用到yield 5

为了简化起见,这个例子仅仅yield值出去,但是当然如果你不适用一个for..of循环,而是就手动调用iterator的next(..)并且通过消息传递,这些消息会通过同样的行为穿过yield*代理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function *foo() {
var z = yield 3;
var w = yield 4;
console.log( "z: " + z + ", w: " + w );
}

function *bar() {
var x = yield 1;
var y = yield 2;
yield *foo(); // `yield*` delegates iteration control to `foo()`
var v = yield 5;
console.log( "x: " + x + ", y: " + y + ", v: " + v );
}

var it = bar();

it.next(); // { value:1, done:false }
it.next( "X" ); // { value:2, done:false }
it.next( "Y" ); // { value:3, done:false }
it.next( "Z" ); // { value:4, done:false }
it.next( "W" ); // { value:5, done:false }
// z: Z, w: W

it.next( "V" ); // { value:undefined, done:true }
// x: X, y: Y, v: V

尽管我们在这里只显示了一层代理,而*foo()yield*代理到另一个、另一个、另一个generator也是可以的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function *foo() {
yield 2;
yield 3;
return "foo"; // return value back to `yield*` expression
}

function *bar() {
yield 1;
var v = yield *foo();
console.log( "v: " + v );
yield 4;
}

var it = bar();

it.next(); // { value:1, done:false }
it.next(); // { value:2, done:false }
it.next(); // { value:3, done:false }
it.next(); // "v: foo" { value:4, done:false }
it.next(); // { value:undefined, done:true }

如你所见,yield *foo()代理迭代循环(这些next()调用)知道结束,然后一旦它这样做,任何从foo()return的值(在本例里:字符串"foo")会被设置为yield*表达式的结果值,然后被赋值给本地变量v

yieldyield*之间有一个有趣的区别:对于yiled表达式,其结果是任何同事子序列next(..)传入的值,但是对于yield*表达式,它仅仅接收被代理的generator的return值(因为next(..)发送的值是对代理透明的)。

你也可以通过yield*代理从两个方向进行错误处理(如前所示):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
function *foo() {
try {
yield 2;
}
catch (err) {
console.log( "foo caught: " + err );
}

yield; // pause

// now, throw another error
throw "Oops!";
}

function *bar() {
yield 1;
try {
yield *foo();
}
catch (err) {
console.log( "bar caught: " + err );
}
}

var it = bar();

it.next(); // { value:1, done:false }
it.next(); // { value:2, done:false }

it.throw( "Uh oh!" ); // will be caught inside `foo()`
// foo caught: Uh oh!

it.next(); // { value:undefined, done:true } --> No error here!
// bar caught: Oops!

如你所见,throw("Uh oh!")通过yield*代理抛出错误到*foo()内部的try..catch中。同样的,*foo()内部的throw "Oops!"抛回到*bar()中,然后它在另一个try..catch中捕获这个错误,如果我们没有捕获其中的某一个,那么这些错误会继续向外传播像你平常期望的那样。

总结

Generator具有同步的语法,这表示你可以穿过yield表达式使用try..catch错误处理机制。generator同时也有一个throw(..)方法将一个错误抛入generator的暂停位置,这当然也可以被generator内部的try..catch捕获。

yield*允许你从当前的generator代理迭代控制到另一个generator。其结果是yield*表现得像一个连接两个方向的通道,同时可供传递消息以及错误。

但是,到现在位置,我们还有一个基本的问题没有解决:generator是如何帮助我们处理异步模式的呢?我们现在在这两篇文章中所见的一切都是generator函数的同步迭代。

其中的关键就是建立一种机制,generator暂停以开始一个异步任务,然后在异步任务结束时恢复(通过他的iterator的next()调用)。我们将在下一篇文章中探索各种关于使用generator创造这样的异步控制机制的方式。请继续关注!

The Basics Of ES6 Generators

#ES6Generator基础(译)

原文地址:http://davidwalsh.name/es6-generators

ES6 Generators:全系列

  1. The Basics Of ES6 Generators
  2. Diving Deeper With ES6 Generators
  3. Going Async With ES6 Generators
  4. Getting Concurrent With ES6 Generators

Javascript ES6 带来的一种最激动人心的新特性是一种新的函数,称为generator。它的名字可能有一点奇怪,但是它的行为在第一次看来却更让人陌生。这篇文章旨在解释它的是如何工作的基础概念,并且使你明白为什么它们对于JS的未来如此的强大。

##运行至完成

当我们讨论generator时我们所要见到的第一件事情就是:它们与普通函数对于“运行至完成”的概念是多么的不同。

无论你是否意识到,你对于函数的基础已经常常可以断言一些事情:一旦函数开始执行,在其他JS代码可以执行之前,它总会执行到结束。

例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
setTimeout(function(){
console.log("Hello World");
},1);

function foo() {
// NOTE: don't ever do crazy long-running loops like this
for (var i=0; i<=1E10; i++) {
console.log(i);
}
}

foo();
// 0..1E10
// "Hello World"

在这里,for循环会花费非常长的时间直到结束,至少比一毫秒要长,但是我们的计时器回调console.log(...)语句无法在foo()函数执行时打断它,因此它被堵塞语句之后(在event-loop中)并耐心地等到自己的回合。

如果foo()可以被打断,会怎么样呢?那样不会对我们的程序造成极大的破坏吗?

那就是多线程编程的噩梦挑战。但我们非常幸运地处在JavaScript的世界里而不必担心这些事情,因为JS总是单线程的(只有一个命令/函数可以在一个时刻执行)。

注:Web Worker是一种机制,它允许你开启一个完全单独的线程让一部分JS程序在其中执行,该线程完全与你的主JS程序线程并行。此机制并未向我们的程序中引入多线程并发,是因为两个线程只能通过普通的异步事件进行通信,而它总是遵循event-loop的一次执行一个的行为,该行为是由“运行至完成”所要求的。

##运行..停止..运行

对于ES6 generator,我们有另一种类型的函数,它可能在中间暂停,一次或多次,并在之后恢复,允许其他代码在暂停的间隙执行。

如果你曾经阅读过任何有关并发或基于线程的编程的内容,你可能见过“协作”这个词,它基本上意味着一个进程(在我们的例子中,是一个函数)自身选择何时它允许一个中断,这样它可以与其他代码进行“协作”。与这个概念相对应的是“抢占”,它表明一个进程/函数可以不依照其意愿被打断。

ES6 generator函数在其并发行为上是“协作”式的。在generator函数体内,你可以使用新的yield关键词来从函数自身中暂停。没有任何东西可以从一个generator的外部暂停它,仅当generator(在内部)遇到一个yield时,它才会暂停自己。

但是,一旦generator使用yield暂停了自身,它自己无法进行恢复。必须使用一个外部的控制方式重启generator。我们会在下面解释这是如何发生的。

因此,基本上,一个generator函数可以停止以及被重启任意次数。事实上,你可以在一个无限循环(例如臭名昭著的while (true) { .. })中指定一个generator函数,尽管这基本上是非常疯狂的并且在普通的JS程序中是错误的,而使用generator函数这是完全正常的并且有时候正事你所希望做的那样。

甚至更为重要的是,停止和重启并不仅仅是控制generator函数的执行,它也使得执行时的双向的信息传递(出入generator)变为可能。使用普通函数时,你会在开始获取参数并在结束时return一个值。使用generator函数,你可以使用每一个yield发出消息,并且你可以在每次重启时回传消息。

请告诉我语法,谢谢!

让我们看看这些新的激动人心的generator函数的语法。

首先,它有新的声明语法:

1
2
3
function *foo() {
// ..
}

注意到这里的*了吗?这是一种新的语法并且看起来有一点奇怪。对于那些来自于其他语言的开发者来说,这可能看起来非常像函数返回值的指针。但是请不要被它迷惑!这仅仅是一个来标志特殊的generator函数类型的方式。

你可能见过其它文章/文档使用function* foo(){ } 而不是function *foo(){ }*的位置不同)。它们都是有效的,但是我最近决定使用function *foo() {}因为这样更加准确,因此我会在这里这样写。

现在,让我们来谈谈我们的generator函数的内容。generator函数在很多方面都只是普通的JS函数。在generator函数内部,新的语法非常少。

我们主要使用的语法,入上面所述,是yield关键词。yield ___被称为“yield表达式”(不是一个语句),因为当我们重启generator时,我们会回传一个值到内部,并且无论我们返回的是什么它都会被作为yield ___表达式的计算结果。

例:

1
2
3
4
function *foo() {
var x = 1 + (yield "foo");
console.log(x);
}

这里,当generator函数在yield "foo"暂停时,表达式会发送"foo"字符串出去,并且无论何时,只要generator被重启,无论什么值被发送,它会被作为这个表达式的结果,这个值然后会加上1并且赋值给x变量。

看见这里的双向交流了吗?你发送"foo"出去,暂停你自己,然后在某一个之后的时间点(可能是立即,也可能是距离现在非常长的时间!),generator会被重启并且提供你一个返回值。差不多yield关键词是某种创造向外请求一个值的方式。

在任何表达式的地方,你可以仅仅使用yield自身,这表明你向外yield一个undefined。像这样:

1
2
3
4
5
6
7
8
9
// note: `foo(..)` here is NOT a generator!!
function foo(x) {
console.log("x: " + x);
}

function *bar() {
yield; // just pause
foo( yield ); // pause waiting for a parameter to pass into `foo(..)`
}

##Generator迭代器

“Generator Iterator”。的确又长又难念,是吧?

迭代器是一种特殊的行为类型,事实上,它是一种设计模式,我们按某种顺序遍历一组值,每次通过调用next()获取一个值。例如设想一下对这样一个数组[1, 2, 3, 4, 5]使用一个迭代器。第一个next()调用会返回1,第二个next()会返回2,并一次类推。当所有的值都被返回之后,最后的next()会返回null或者false,或者其他的信号标志你已经迭代完容器内的所有值。

我们从外部控制generator函数的方式,是使用一个generator iterator构造并且交互。这听起来比实际上做起来要复杂。想想一下这个很蠢的例子:

1
2
3
4
5
6
7
function *foo() {
yield 1;
yield 2;
yield 3;
yield 4;
yield 5;
}

为了从*foo()这个generator函数中遍历值,我们需要构造一个迭代器,如何做呢?非常简单!

1
var it = foo();

噢!所以,用普通方式调用generator函数实际上并没有执行它的任何内容。

把这些塞进你的大脑有一点奇怪。你可能尝试疑问:为什么不是var it = new foo()?好吧,这个语法背后的原因很复杂并且不在我们所要讨论的范围内。

那么现在,开始迭代我们自己的generator函数,我们只需:

1
var message = it.next();

这会给我们从yield 1语句中提供1,但这并不是唯一我们得到的东西。

1
console.log(message); // { value:1, done:false }

我们事实上从每个next()调用中获取了一个对象,它包含一个value属性用来描述yield出的值,而done是一个布尔值它用来标志是否generator函数是否已经完全结束。

让我们继续我们的迭代:

1
2
3
4
console.log( it.next() ); // { value:2, done:false }
console.log( it.next() ); // { value:3, done:false }
console.log( it.next() ); // { value:4, done:false }
console.log( it.next() ); // { value:5, done:false }

有趣的是,当我们取出`5时,done仍然是false。这是因为*严格来说*,generator函数并没有完成。我们仍然需要调用最后一个next(),并且我们传入一个值时,它会被设置为yield 5`表达式的结果。只有那时,generator函数才算是结束了。

那么,现在:

1
console.log( it.next() ); // { value:undefined, done:true }

因此,最后我们generator函数的结果是我们完成了这个函数,并且没有提供任何结果(因为我们已经耗尽了所有的yield ___表达式)。

你可能在这一点上想:我能不能在generator函数中使用return呢,如果我这样做,最后的结果会被设置在value属性上吗?

###是…

1
2
3
4
5
6
7
8
9
function *foo() {
yield 1;
return 2;
}

var it = foo();

console.log( it.next() ); // { value:1, done:false }
console.log( it.next() ); // { value:2, done:true }

###…又不是

依赖来自于generator的return值不是一个好主意,因为当使用for..of来迭代generator函数(见下)时,最后的return值会被扔掉。

为了完整性考虑,我们也来看看早我们遍历一个generator函数时同时向内和向外发送消息。

1
2
3
4
5
6
7
8
9
10
11
12
function *foo(x) {
var y = 2 * (yield (x + 1));
var z = yield (y / 3);
return (x + y + z);
}

var it = foo( 5 );

// note: not sending anything into `next()` here
console.log( it.next() ); // { value:6, done:false }
console.log( it.next( 12 ) ); // { value:8, done:false }
console.log( it.next( 13 ) ); // { value:42, done:true }

你可以看到我们仍然可以通过最初的实例化调用foo(5)传递初始值,就像普通的函数那样。

第一个next(..)调用,我们不传递任何值,为什么?因为没有yield表达式接受我们传入的值。

但是如果我们的确向第一个next(..)调用传递了值,也不会有任何坏的事情发生,它只会被抛弃掉。对于这种情况,ES6描述了generator函数忽略未使用的值。(注:在写作这篇文章时,最新版本的Chrome和FF都表现如此,但是其他浏览器可能不完全兼容并可能不正确地对这种情况抛出一个异常)。

第二个next(12)调用将12传递给第一个yield (x + 1)表达式。第三个next(13)调用将13传递给第二个yield (y / 3)表达式。反复地重新阅读这段代码。对于很多人来说,在他们刚开始看这些代码的时候感觉奇怪。

##for...of

ES6同样在语法级别拥抱这种迭代器模式,通过直接提供对执行迭代器到结束的支持:for..of循环。

例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function *foo() {
yield 1;
yield 2;
yield 3;
yield 4;
yield 5;
return 6;
}

for (var v of foo()) {
console.log( v );
}
// 1 2 3 4 5

console.log( v ); // still `5`, not `6` :(

如你所见,由foo()创建的迭代器被for..of循环自动地捕获,并且会为你自动地进行迭代,每轮迭代获取一个值直到得到一个done:true。只要donefalse,它就会自动的提取value属性并分配给你的迭代变量中(在我们这里是v)。一旦done已经变为true,迭代循环停止(并且不对最后返回的value做任何处理,如果有的话)。

如之前所述,你可以看到for..of循环忽略并抛弃了return 6的值。同事由于没有暴露next()调用,for..of循环无法用于像我们之前那样向generator传入值的场合。

总结

好的,这就是generator的基础。如果你还有一些不明白的话,不必担心。我们每个人开始时都感觉像那样!

想一想这种新的玩具会如何改变你的代码是很自然的。尽管还有很多内容,我们刚刚触及了表面。所以在我们了解它们是多么强大之前我们必须要更深入地了解。

在你运行过上面的代码片段之后(试试最新的Chrome,FF或者node0.11+带上--harmony标志),你可能会产生以下的疑问:

  1. 如何进行错误处理?
  2. 一个generator可以调用另一个generator吗?
  3. 异步代码如何与generator交互?

这些,或更多的问题,会在接下来的几篇文章中阐述,请继续关注!