Getting Started with Koa, part 2

#开始使用Koa,第二部分(译)

http://blog.risingstack.com/getting-started-with-koa-part-2/

原作者: Gellért Hegyi https://twitter.com/native_cat

上一节我们掌握了generators并且明白了一点,那就是我们可以以同步的形式编写代码而异步的执行。这非常棒,因为同步代码非常简单,优雅而且具有更好的可读性,而异步代码可能会导致“尖叫”与“哭泣”(回调地狱)。

这一章将讲述解决这个痛苦的工具,这样我们就可以只编写那些有趣的部分。它会给予基本Koa特性和机制的介绍。

前述

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// First part
var thunkify = require('thunkify');
var fs = require('fs');
var read = thunkify(fs.readFile);

// Second part
function *bar () {
try {
var x = yield read('input.txt');
} catch (err) {
console.log(err);
}
console.log(x);
}

// Third part
var gen = bar();
gen.next().value(function (err, data) {
if (err) {
gen.throw(err);
}
gen.next(data.toString());
});

在上一篇文章的最后一个例子里,如你所见,我们可以将其分为3个重要地部分。第一是我们必须创造我们的thunkified functions,它会被用在一个generator中。然后我们必须使用thunkified functions编写我们的generator functions。最后一个部分是我们调用并遍历generators,处理异常以及其他。如果你想一想你会发现,最有一部分和我们程序的本质并没有什么关系,基本上它让我们运行一个generator。幸运地是,有一个模块帮我们这样做。来见一见co

CO

Co是一个node的基于generator的流程控制模块。下面的代码和上面做的事情完全一样,但是我们摆脱了调用generator的代码。唯一我们需要做的事情,是将generator传递给一个函数co,调用它,然后魔力就发生了。好吧,其实并不是什么魔力,它只是为你处理所有的generator调用代码,因此我们不需要为那操心了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var co = require('co');
var thunkify = require('thunkify');
var fs = require('fs');

var read = thunkify(fs.readFile);

co(function *bar () {
try {
var x = yield read('input.txt');
} catch (err) {
console.log(err);
}
console.log(x);
})();

就像我们已经知道的那样,你可以防止一个yieldz在任何东西之前来对一些东西进行评价。因此并不是只有trunks可以被yield。因为co想要创建一个简单的控制流,它对一些特殊的类型进行yield。当前支持yield的类型

  • thunks(函数)
  • array (并发执行)
  • objects (并发执行)
  • generators (代理)
  • generator functions (代理)
  • promises

我们已经讨论过thunks是如何工作的了,因此让我们去看看其他的。

并发执行

1
2
3
4
5
6
7
8
9
10
11
var read = thunkify(fs.readFile);

co(function *() {
// 3 concurrent reads
var reads = yield [read('input.txt'), read('input.txt'), read('input.txt')];
console.log(reads);

// 2 concurrent reads
reads = yield { a: read('input.txt'), b: read('input.txt') };
console.log(reads);
})();

如果你yield一个数组或一个对象,它将并行地评估它的内容。当然你的集合也可以是thunksgenerators。你可以nest,它会穿过数组或者对象并发执行你所有的函数。注意:被yield的结果不会是展开的,它会保留同样的结构。

1
2
3
4
5
6
7
8
9
10
11
var read = thunkify(fs.readFile);

co(function *() {
var a = [read('input.txt'), read('input.txt')];
var b = [read('input.txt'), read('input.txt')];

// 4 concurrent reads
var files = yield [a, b];

console.log(files);
})();

你也可以通过在thunk的调用之后进行yield实现并发。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var read = thunkify(fs.readFile);

co(function *() {
var a = read('input.txt');
var b = read('input.txt');

// 2 concurrent reads
console.log([yield a, yield b]);

// or

// 2 concurrent reads
console.log(yield [a, b]);
})();

代理

你当然也可以yield generators。注意我们并不需要使用yield *

1
2
3
4
5
6
7
8
9
10
11
12
13
var stat = thunkify(fs.stat);

function *size (file) {
var s = yield stat(file);

return s.size;
}

co(function *() {
var f = yield size('input.txt');

console.log(f);
})();

我们过一下产不多所有你使用co时进行yield的可能性。这里有一个最新的示例(取自于co的github页面)把它们结合起来。

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
var co = require('co');
var fs = require('fs');

function size (file) {
return function (fn) {
fs.stat(file, function(err, stat) {
if (err) return fn(err);
fn(null, stat.size);
});
}
}

function *foo () {
var a = yield size('un.txt');
var b = yield size('deux.txt');
var c = yield size('trois.txt');
return [a, b, c];
}

function *bar () {
var a = yield size('quatre.txt');
var b = yield size('cinq.txt');
var c = yield size('six.txt');
return [a, b, c];
}

co(function *() {
var results = yield [foo(), bar()];
console.log(results);
})();

我相信现在你已经足够掌握generators了,你已有有了一个关于如何使用这些工具操作异步控制流的很好的概念。

现在是时候转到我们整个系列的主题,Koa了!

Koa

你所需要知道的,关于koa模块自身的信息并没有多少。你甚至可以看它的源码,只有4个文件,每个文件约300行。Koa遵循你写的每个程序都只做并且做好一件事情的传统。因此你会看到,每个好的koa模块都是(并且每个node模块都应该是)简短的。只做一件事情并且重度依赖于其他模块。你应该记住这些并且依照它进行开发。它会有利于每个人,你以及其他阅读你源代码的人。记住这一点然后让我们移动到Koa的主要特性。

应用

1
2
var koa = require('koa');
var app = koa();

创建一个Koa应用只是简单的引入模块函数。这提供给你一个对象,它包含了一个generators(中间件)的数组,对一个新的请求以类似栈的方式进行执行。

级联

一个重要的术语,当使用Koa时,它指的是中间件。因此让我们先弄清楚这个。

Koa中的中间件是处理请求的函数。一个由Koa创建的服务器可以有一个与它关联的中间件的栈。

Koa中的级联的意思是,控制流穿过一系列的中间件。在Web开发中这是非常重要的,你可以简单地用这种方式构造复杂的行为。Koa使用generator非常直观并且简洁地实现它。它yield下游,然后控制流程回到上游。要向流程中添加一个generator,调用use函数并传入一个generator。试着猜猜看为什么下面的代码对每一个到达的请求产生A, B, C, D, E的输出!

这是一个服务器,因此listen函数进行你所认为的行为,它监听一个特殊的端口。(它的参数和纯node的listen方法是一样的)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
app.use(function *(next) {
console.log('A');
yield next;
console.log('E');
});

app.use(function *(next) {
console.log('B');
yield next;
console.log('D');
});

app.use(function *(next) {
console.log('C');
});

app.listen(3000);

当一个新的请求到达时,它开始流经中间件,以你所编写的顺序执行。因此在示例中,请求触发了第一个中间件,它输出A,然后到达yield next当一个中间件到达yield next,它会走向下一个中间件直到离开。因此我们到达第二个中间件它输出B。然后又跳转到最后一个C。没有更多的中间件了,我们完成了下游流程,现在我们开始返回到之前的中间件(就像一个栈),D。然后第一个中间件结束,F,然后我们成功的完成了上游。

在这一点上,koa模块自身并没有包含任何其他的复杂性 - 因此我们不拷贝/粘贴已经很完备的Koa站点的文档,你可以再那里阅读。这里是这些部分的链接:

Context

Request

Response

Others

让我们看一个例子(也是来自于Koa的站点),它使用而来HTTP功能。第一个中间件计算响应时间。看看你能够多么容易地接触到响应的开始和结束,多么优雅地分离这些函数行为。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
app.use(function *(next) {
var start = new Date;
yield next;
var ms = new Date - start;
this.set('X-Response-Time', ms + 'ms');
});

app.use(function *(next) {
var start = new Date;
yield next;
var ms = new Date - start;
console.log('%s %s - %s', this.method, this.url, ms);
});

app.use(function *() {
this.body = 'Hello World';
});

app.listen(3000);

完成

现在你已经熟悉Koa的核心了,你可以说你的旧web框架已经做了所有其他有用的事情并且你需要它们。但是也请记住那里还有数以万计的功能你从未使用过,或者还有一些工作并未按照你想象的方式工作。这就是Koa以及现代node框架好的方面。你可以从npm中以简短模块的方式向你的应用添加需要的功能,并且它会完全按照你需要的方式运行。

在下一章,我们会看到,使用Koa以上述哲学创建一个网站是多么简单。再那之后,再使用这些你已经了解的知识来感受一下Koa和它的自然感吧。

Getting Started with Koa, part 1 - Generators

#开始使用Koa,第一部分 - Generators (译)

原文地址:http://blog.risingstack.com/introduction-to-koa-generators/

原作者: Gellért Hegyi https://twitter.com/native_cat

Koa是一个小巧而简单的web框架,由Express的team带来,其目标是创造一个更加现代的开发web的方式。

在本系列中,你会了解Koa的机制,学习如何高效正确地使用它来编写web应用。第一部分包含一些基本概念(generators, thunks)

为什么是Koa?

Koa包含的关键特性允许你简单并且快速地编写web应用(并且不包含callback)。它使用来自于ES6的新语言元素使得控制逻辑管理在Node中比其他框架更加容易。

Koa自身非常小巧,这是是因为不同于试下流行的web框架(例如express),Koa追随高度模块化的原则,意味着每一个模块只做一件事情。将这句话记在脑中,让我们开始吧!

你好Koa

1
2
3
4
5
6
7
8
var koa = require('koa');  
var app = koa();

app.use(function *() {
this.body = 'Hello World';
});

app.listen(3000);

在我们开始之前,为了运行示例以及你自己的ES6代码,你需要使用0.11.9或更高的版本并设置--harmony标志位。

你可以从上面的示例中看到,这里没有什么让人感兴趣的点,除了在函数声明之后比较陌生的*号。这样,就使这个函数变成了一个generator函数。

Generators

当你执行函数时,如果能够在任何点暂停它,进行一些其他计算,做一些其他操作,再返回到这个函数里,并带有一些值,然后继续,这样不好吗?

这是另外一种迭代器(像循环一样)。那就是一个generator做的最好的事情,它在ES6中被实现,因此我们可以轻松使用它。

让我们来构造一些generators!首先我们需要创建你的generator函数,它看起来和普通的函数完全一样,除了一点,你需要在function关键词后放置一个*符号。

1
function *foo () { }

现在我们就有了一个generator函数。当我们调用这个函数时它会返回一个迭代对象,因此不像普通的函数调用,当我们调用一个generator时,代码并没有开始执行,其原因我们会在之后讲述,我们将手动地遍历它。

1
2
function *foo (arg) { }  // generator function  
var bar = foo(123); // iterator object

通过它返回的对象bar,我们可以遍历这个函数。为了开始并且迭代到下一个generator步骤我们可以简单的调用barnext()方法,当next()被调用时,函数开始执行,或从上一次停止的地方执行,直到它到达一个暂停点。

但是除了继续,它也返回一个对象,该对象给予有关generator的信息。它有一个value属性,标识当我们暂停generator时当前迭代的值。另外一个布尔值是done,它用来标识generator已经结束执行。

1
2
3
function *foo (arg) { return arg }  
var bar = foo(123);
bar.next(); // { value: 123, done: true }

像我们看到的那样,实际上示例那里并没有任何暂停,因此当它返回一个对象的时候done就变成了true。如果你在generator中指定一个return值,它会在最后一个迭代对象(当donetrue的时候)中被返回。现在我们要做的只是来暂停一个generator。和我们所说的那样,它就像是遍历一个函数并且在每个迭代周期它产出(yield)一个值(在我们暂停的地方),因此我们使用yield关键词进行暂停。

yield

yield [[expression]];

调用next()会使generator开始执行并且它会运行直到遇到一个yield。然后它返回一个带有valuedone属性的对象,这里value持有表达式的值。该表达式可以是任何形式。

1
2
3
4
5
6
7
8
9
10
11
function* foo () {  
var index = 0;
while (index < 2) {
yield index++;
}
}
var bar = foo();

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

当我们再一次调用next()时,yield值会在generator中返回并且它会继续执行。也可以在generator中从迭代对象接受一个值(使用next(val)),然后当它geneator继续时它会被返回到generator中。

错误处理

如果你在迭代对象的值中发现了错误,你可以使用throw()方法并在generator中捕获这个错误。这使得错误处理在generator中是非常友好的。

1
2
3
4
5
6
7
8
9
10
11
12
function *foo () {  
try {
x = yield 'asd B'; // Error will be thrown
} catch (err) {
throw err;
}
}

var bar = foo();
if (bar.next().value == 'B') {
bar.throw(new Error("it's B!"));
}

for … of

在ES6中有一个循环类别,可以用来在generator中进行遍历,即for...of循环。该遍历器会进行执行直到donefalse。留意一点,如果你使用这个循环,你将无法在一个next()的调用中中传递值,并且该循环会抛弃返回值。

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

for (v of foo()) {
console.log(v);
}

yeild *

如之前所说,你可以yield几乎任何东西,甚至可以yield一个generator,但是那样你就必须使用yield *,这被称为代理。你正在代理到另一个generator上,因此你可以使用一个迭代对象对多个generator进行遍历。

1
2
3
4
5
6
7
8
9
10
11
12
13
function *bar () {  
yield 'b';
}

function *foo () {
yield 'a';
yield *bar();
yield 'c';
}

for (v of foo()) {
console.log(v);
}

Thunks

为了完全理解Koa,thunks是另外一种你必须掌握的概念。它们主要用来帮助调用另外一个函数。你可以把它和lazy evaluation联系起来。对我们来说,最重要的是它们可以用来在一个函数调用的外部从参数列表中移动node的回调函数。

1
2
3
4
5
6
7
var read = function (file) {  
return function (cb) {
require('fs').readFile(file, cb);
}
}

read('package.json')(function (err, str) { })

有一个小型的模块叫做thunkify,它将一个普通的node函数转化为一个thunk。你可以质疑它的使用,但是其结果是它可以很好的移除generator中的回调。

首先,我们需要将想要在generator中使用的node函数装换为一个thunk。然后在generator中使用这个thunk,就像它会返回一个值一样,否则我们就必须进入到回调中了。当调用起始next()时,它的value会是一个函数,它的参数是被thunkified的函数的回调。在回调中我们可以检验错误(并且进行throw,如果需要的话)或者用接收到的值调用next()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var thunkify = require('thunkify');  
var fs = require('fs');
var read = thunkify(fs.readFile);

function *bar () {
try {
var x = yield read('input.txt');
} catch (err) {
throw err;
}
console.log(x);
}
var gen = bar();
gen.next().value(function (err, data) {
if (err) gen.throw(err);
gen.next(data.toString());
})

慢慢花时间了解这个例子的每一个细节,因为这对于理解Koa非常重要。如果你专注于示例的generator部分,你会看到它非常棒。它有同步代码的简洁,优秀的错误捕获,而它仍然是异步执行的。

待续…

最后的例子看起来很繁琐,但是在下一部分我们会找到一些工具来处理繁琐的部分,仅剩优美的部分。并且我们最终会明白Koa以及它流畅的机制,正事该机制使得web开发如此简单。

火焰中的Nodejs

#火焰中的Nodejs

原文链接:http://techblog.netflix.com/2014/11/nodejs-in-flames.html

我们一直在忙于用Node.js构建我们的下一代Netflix.com网络应用。你可以从我们几个月前放在NodeConf.eu的这个演讲中了解更多。今天,我想要分享一些我们在新应用栈的性能调优中学到的一些东西。

我们首先得知了一个可能的问题,当我们注意到我们的Node.js应用中的请求延迟会随时间逐渐增加。并且,应用比起我们的期望消耗了更多的CPU,并且CPU消耗的增长与更高的延迟是密切相关的。我们一边使用循环重启的方式作为一个临时解决方案,一边努力地在我们Linux EC2环境中使用新的性能检测工具与技术想要找到问题的根源。

##火焰上升

我们留意到我们的Node.js应用请求延迟随着时间慢慢增长。特别的,一些方法的延迟会从刚开始时的1ms每小时增加到10ms。并且我们也发现了相关的CPU使用率的增长。

skitch.png

这幅图描绘了对于每个时间区域的请求延迟(以ms为单位)。每种颜色和一个不同的AWS AZ相关。你可以看到延迟稳定地以每小时10ms的速度增长,在实例重启之前能达到60ms的峰值。

##浇灭火焰

最初我们猜想我们自己的request handler里面有一些错误,比如memory leak,因此导致了延迟的增长。我们通过对应用单独的压力测试尝试验证这一猜想,增加了一些系统变量监控:既有我们的request handler单独的延迟,也有整个request的延迟。同时,我们将Node.js的heap size增加到32Gb。

我们发现我们的request handler的延迟在整个性能测试的周期中都是一个常量1ms。我们同时也看到进程的heap size处在约1.2Gb的常量中。但是,整体的request延迟和CPU使用率持续的增长。这宣告了我们自己的handler无罪,并且将问题指向了更深处。

有一些其他的东西消耗了额外的60ms来处理这个request。我们所需要的是一个方法来描述应用的CPU使用率并且将其可视化以便识别我们在哪里部分我们消耗了CPU绝大多数的时间。进入CPU火焰图和Linux Perf Events来进行急救。

对于这些不熟悉火焰图的人,最好的了解方式是阅读Brendan Gregg的精彩的文章,这篇文章解释了它们是什么 - 这里我们给出一个快速的总结(从文章中直接提取)。

  • 每一个方块表示一个stack中的函数(一个”stack框架”)
  • y轴显示的是stack深度(stack中的框架数量)。最上层的方块显示了当前在CPU的函数。其下面的所有都是它的祖先,紧贴着它下面的是它的父函数,就像我们之前看到的stack trace一样。
  • x轴显示的是样例群体。它并不是像绝大多数图片那样从左到右的显示时间流逝。它们的左右顺序不表示任何含义(仅仅是按照字母表顺序排列)。
  • 如果有多个CPU并行执行和采样的话,样本数量可能超过所耗时间。
  • 颜色并不明显,它们是随机选择成为暖色调。它被成为“火焰图”是因为它显示了现在CPU什么更”hot”。并且它是可交互的,鼠标放置于SVG之上可以看到细节。

之前的Node.js火焰图只被用在使用了DTrace的系统上,使用Dave Pacheco的Node.js jstack()支持。然而,最近Google v8组添加了v8的perf_events支持,它包含Linux上类似的Javascript symbol。Brendan写了关于如何使用这个新支持特性的指南,在Node.js版本0.11.13中到来。在Linux中创建Node.js火焰图

flame.png

这里是火焰图的原始SVG。我们立刻就注意到在我们的应用中有一些非常高的stack(y轴),我们也能看到我们耗费了非常多的时间在这些stack上(x轴)。更进一步的研究发现,看起来这些stack框全是对Express.js的router handle以及router handle.next函数的引用。Express.js的源代码中我们看到了一些有趣的“花边新闻”。

  • 所有endpoint的Route handler都储存在一个全局的数组中
  • Express.js通过递归的方式进行遍历并调用这些handler直到它发现正确的路由handler

对于这种应用场景,一个全局数组并不是一个理想的数据结构。我们不清楚为什么Express.js不选择使用一个常量时间的数据结构例如map来保存它的handler。每个请求都需要在路由数组中进行昂贵的O(n)查找以便发现它的route handler。更为复杂的是,数组是通过递归进行遍历的。这也就解释了为什么我们会在火焰图中看到如此高的stack。有趣的是,Express。js甚至允许你对一个路由设置很多相同的route handler。你可能不经意地将将请求链设置成这样:

1
[a, b, c, c, c, c, d, e, f, g, h]

对于路由c的请求会在第一次发现c handler时停止(数组的位置2)。但是对于路由d 的请求会在位置6处停止,在轮询a,b以及非常多的c之间无用地耗费了时间。我们通过执行下面的纯express应用来验证它。

1
2
3
4
5
6
7
8
9
10
11
var express = require('express');
var app = express();
app.get('/foo', function (req, res) {
res.send('hi');
});
// add a second foo route handler
app.get('/foo', function (req, res) {
res.send('hi2');
});
console.log('stack', app._router.stack);
app.listen(3000);

执行该Express.js应用来返回这些route handler。

1
2
3
4
5
6
7
8
9
10
11
12
stack [ { keys: [], regexp: /^\/?(?=/|$)/i, handle: [Function: query] },
{ keys: [],
regexp: /^\/?(?=/|$)/i,
handle: [Function: expressInit] },
{ keys: [],
regexp: /^\/foo\/?$/i,
handle: [Function],
route: { path: '/foo', stack: [Object], methods: [Object] } },
{ keys: [],
regexp: /^\/foo\/?$/i,
handle: [Function],
route: { path: '/foo', stack: [Object], methods: [Object] } } ]

注意到这里对/foo有两个完全一样的route handler。如果Express.js在对于一个路由有多余一个route handler时可以抛出一个异常就好了。

到现在为止,我们主要的猜想变成了handler数组随着时间在增长,由于每个handler都被调用,因此导致了延迟的增长。极有可能我们在代码中的某处泄露了handler,可能是由于重复handler问题导致的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
[...
{ handle: [Function: serveStatic],
name: 'serveStatic',
params: undefined,
path: undefined,
keys: [],
regexp: { /^\/?(?=\/|$)/i fast_slash: true },
route: undefined },
{ handle: [Function: serveStatic],
name: 'serveStatic',
params: undefined,
path: undefined,
keys: [],
regexp: { /^\/?(?=\/|$)/i fast_slash: true },
route: undefined },
{ handle: [Function: serveStatic],
name: 'serveStatic',
params: undefined,
path: undefined,
keys: [],
regexp: { /^\/?(?=\/|$)/i fast_slash: true },
route: undefined },
...
]

有些东西在以10次每小时的速度添加同样的Express.js静态route handler中。更进一步的基准测试显示了仅仅是遍历每一个handler耗费约1ms的CPU时间。这和我们所见的延迟问题是相关联的,我们看到的响应时间的增加是每小时10ms。

最终结果是:它是由我们代码中的一个周期性函数(10次/小时)导致的。其主要的目的是为了从一个外部源刷新我们的route handler。它是通过从数组中删除旧的handler并添加新的handler来实现的。不幸地是,它在每次执行时也在不经意间增加了一个同样路径的静态route handler。由于Express.js允许对同一路径含有多个route handler,这些重复的handler被全部添加进了数组中。更糟糕的是,它们是被添加到了其他API handler的前面,这表示在任何我们server的请求被执行之前,它们全部都会被调用到。

这完全解释了为什么我们的清秋延迟是以每小时10ms的速度增长的。事实上,当我们修复代码使之停止添加重复route handler之后,我们的延迟以及CPU使用率的增长都消失了。

graph.png

现在,在我们部署了修复代码之后,我们的延迟降低到了1ms并且能够一直保持了。

当烟雾散去

我们从这个悲惨的经验中学到了什么呢?首先,在我们将依赖对象放入生产环境之前,我们必须完全的了解它。我们对Express.js的API进行了错误的假设而没有深度到代码基础中。结果,我们错误的使用了Express.js的API就是我们最终性能问题的根源。

第二,在处理性能问题时,可观察性是至关重要的。火焰图使我们能够洞察到我们的应用程序在哪部分花费了最多的时间在CPU上。难以想象在没有能够对Node.js的stack进行采样以及用火焰图可视化他们的条件下我们该如何解决这个问题。

为了进一步提高可观察性,我们迁移到了Restify,它给了我们更多的同茶行,可见性以及对我们应用程序的操作性。这已经超出了本文的范围,因此请期待我们之后关于如何在Netlix中利用Node.js文章。

本文原作者: Yunong Xiao @yunongx

脚注:

1 特别的,注意在这个代码片段中,next()被递归地调用来遍历名为stack的全局route handler数组。

2 Restify提供了非常多的机制以获取你的应用的可见性。从DTrace支持,到与node-bunyan日志框架集成。

神经网络骇客指南(翻译中)

原文见:http://karpathy.github.io/neuralnets/

##神经网络骇客指南(译)

各位好,我是一名斯坦福的计算机科学的博士。作为我研究的一部分,我已经在深度学习上研究了好几年,我有几个“pet project”,其中一个是ConvNetJS - 一个用来训练神经网络的Javascript库。Javascript允许一个人轻松地将现在所发生的事情可视化,并且可以实现多样的参数选择设置,但是我仍然经常听到人们想要一些更加彻底的话题。这篇文章(我打算慢慢地写到几个章节那么长)是我一份谦逊地尝试。我把它放在网上而不是以一个PDF文件的形式呈现是因为,所有的图书都应该这样,并且最终希望它能包括一些动画和演示。

我对神经网络的个人经验是:当我抛开一切整篇、密集的反向传播方程的推导,而仅仅开始写代码时,一切都清晰多了。因此这个教程会包含非常少的数学(我不认为这是有必要的,而且有些时候会混淆一些简单的概念)。由于我的背景是计算机科学以及物理,我会以骇客的角度来看待问题。我会围绕着代码以及物理直觉而不是数学推导来展示。基本上我会以一种“我刚开始学习时希望被那样教导”的方式努力地呈现算法。

“...当我开始编写代码时一切都清晰多了。”

你可能会想急切地跳进去学习神经网络、反向传播、它们如何能应用于数据集上、等等。但是在我们到达那里之前,我想先让我们忘掉这一切。让我们后退一步,明白什么是真正的核心。让我们从元电路开始谈起。

###第一章:元电路

在我看来,思考神经网络的最佳方式是将其比作元电路。在这里,实际的值(而不是布尔值{0,1})沿着边沿“流动”并且在门出交汇。但是不同于门电路的等,我们的二进制门包含例如*(乘)、+(加)、max或者一元门例如exp等等。不同于基本的布尔电路,我们最终也会有gradients在同样的边沿流动,但是是向相反的方向。这已经有点超前了,我们还是先专注一下,从简单的开始。

基本情况:电路中的单门

让我们先考虑一个单一的、简单的、包含一个门的电路。示例如下:

simple circuit with one gate

这个电路接受两个实际值xy并且在*门中计算x * y。 Javascript的版本会非常简单,看起来像这样:

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
var forwardMultiplyGate = function(x, y) {
return x * y;
};
forwardMultiplyGate(-2, 3); // returns -6. Exciting.
```
以数学的形式我们可以认为这个门实现了函数:

f(x, y) = x * y

在这个例子中,我们所有的门都会接受一个或两个输入,并产生一个**单一**的输出值。

##### 目标

我们在学习时所感兴趣的问题看起来像下面这样:

1. 我们为一个已知电路提供一些具体的输入(例如 `x = -2`,`y = 3`)
2. 电路计算出一个输出值(例如 `-6`)
3. 那么问题的核心变为:我们如何轻微的改变输入以便增加输出?

在这个例子中,我们应该向什么方向改变`x,y`以便得到一个比`-6`更大的数字呢?注意到,例如`x = -1.99`以及`y = 2.99`时`x * y = -5.95` 这是一个比`-6.0`更大的数。别被它搞晕了:`-5.95`是比`-6.0`更大的。这个增量为`0.05`,尽管`-5.95`的大小(到0的距离)更小一些。

##### 策略#1:本地随机搜索

好,等一下,现在我们有一个电路,我们有一些输入并且我们希望轻微地改变它们以便增加输出?为什么这个很难?我们可以简单的“转发”电路来计算对于任何给定的`x`和`y`的输出,所以这不是很简单吗?我们为什么不随机调整`x`和`y`来跟踪效果最好的策略呢:
```js

// circuit with single gate for now
var forwardMultiplyGate = function(x, y) { return x * y; };
var x = -2, y = 3; // some input values

// try changing x,y randomly small amounts and keep track of what works best
var tweak_amount = 0.01;
var best_out = -Infinity;
var best_x = x, best_y = y;
for(var k = 0; k < 100; k++) {
var x_try = x + tweak_amount * (Math.random() * 2 - 1); // tweak x a bit
var y_try = y + tweak_amount * (Math.random() * 2 - 1); // tweak y a bit
var out = forwardMultiplyGate(x_try, y_try);
if(out > best_out) {
// best improvement yet! Keep track of the x and y
best_out = out;
best_x = x_try, best_y = y_try;
}
}

当我执行它时,我的到了best_x = -1.9928best_y = 2.9901,以及best_out = -5.9588。因为-5.9588-6.0更大,所以我们就搞定了,是吗?并不是这样:如果你能负担得起时间的话,它对于仅包含几个门的微小的问题来说是一个完美的策略。但是对于接受上百万输入的大量的电路来说,并非如此。结果是,我们可以做的更好。

策略#2:数值梯度

这里就有一个更好的方法。再次记住,在我们前期我们被提供了一个电路(例如,我们的电路是一个单一的*门)和一些特殊的输入(例如x = -2, y = 3)。门会计算出结果(-6),而现在我们希望对xy进行微调以便得到更高的输出。

对于我们接下来要做的事情,一个很棒的直觉是这样:想象一下获取来自于门电路的输出值输出值,并且对其正向加压。正向电压会反过来通过们进行传输并且引起对输入xy的推动。这个推动就告诉了我们应该如何改变xy以便增加输出值。

在我们这个特定的例子中,这个推动力可能是什么样子的呢?考虑一下,我们可以凭直觉知道施加在x上的力应该是正向的,因为使x轻微地增大会增加电路的输出。例如,将xx = -2增加到x = -1会使我们得到-3 - 远大于-6。在另一方面,我们会希望在y上施加负向的力,而使它变得更小(因为更小的y,例如从y = 3降到y = 2会使我们的结果更高:2 * -2 = -4,同样比-6更大)。但这毕竟是我们脑中的直觉。随着我们的深入,我们会了解到,我这里提到的牵引力实际上是基于输入值(xy)的导数输出值。你可以已经听说过这些:

导数可以被认为是一种施加于各个输入值的力,用于使输出变得更高。

所以我们如何来精确地评价这个牵引力(导数)呢?实际上有一个非常简单的方法。我们反向地来操作:不同于增加电路的输出值,我们一个接一个地迭代每个输入值,轻微地增加它并检测输出值如何改变。输出的改变就是导数。尽管我们到现在为止还是凭借直觉。我们还是看一下数学定义。我们可以写出函数关于某个输入的导数,例如对x的导数可以写成:


f(x,y)x=f(x+h,y)f(x,y)h

在这里,h是非常小的,这是你改变的总量。并且,如果你不太熟悉计算的话,必须要注意的是,在等式的左边,横线并不表示除法。这个符号∂f(x,y)/∂x是一个整体:函数 f(x,y) 对x的编导。等式右边的横线表示除法。我知道这非常让人迷惑,但这是一个标准符号。无论怎样,我希望这并没有太吓人,因为它的确很简单:电路被赋予一些f(x,y)的初始值,然后我们将其中的一个输入改变一个非常小的量h,并且获取新的输出f(x+h,y)。将二者相减我们能得到改变量,然后除以h我们就得到了对于任意改变量的标准量。从另一方面说,下面的代码这正反应了我上面阐述:

1
2
3
4
5
6
7
8
9
10
11
12
13
var x = -2, y = 3;
var out = forwardMultiplyGate(x, y); // -6
var h = 0.0001;

// compute derivative with respect to x
var xph = x + h; // -1.9999
var out2 = forwardMultiplyGate(xph, y); // -5.9997
var x_derivative = (out2 - out) / h; // 3.0

// compute derivative with respect to y
var yph = y + h; // 3.0001
var out3 = forwardMultiplyGate(x, yph); // -6.0002
var y_derivative = (out3 - out) / h; // -2.0

让我们对x进行研究,我们将输入从x变为x + h,然后电路反馈

拥抱JavaScript中的异步2(译)

#拥抱JavaScript中的异步2(译)
Andy White

##本系列的上一期

##简介

在之前的文章中(第一部分),我简要的讨论了一些JavaScript事件循环的基础,函数调用栈,闭包,以及一些基本的回调模式,这些内容都与异步编程相关。在本文中,我想要继续讨论更多JavaScript异步的异步话题。

首先,我想要回应来自Redditor的对我之前一篇文章的评论,该评论拒绝“整个应用应该被构建成为一个异步运行的系统”的思想。这个评论很棒,并且我的确赞同。在我前面的文章中,我并没有在暗示你必须在普通的回调或者其他低级语言特性上构建整个应用来处理你代码中的异步API。但即使你不这样做你也会很快的在其他地方遇到异步代码,而你需要理解并且拥抱其工作方式,这样才能更好的在JavaScript上取得成功。如何拥抱异步代码完全取决于你(以及你的目标平台的支持),有非常多的资源,库,或其他内容可以来帮助你。编写异步代码比起编写同步代码需要更小心以及更多的语言/库的支持,一旦你开始在你的代码中引入异步模式,其异步性往往会不断扩张并且需要越来越多的代码以便维护其一致性以及正确的行为。JavaScript在其核心中并没有对异步代码有太多语言层面的支持,而这个现状正在由新的语言特性改善,例如原生的promiseES6生成器Node.js中的libers或类似的库,还有数以百计的已有的异步模块以及库在类似于npm的仓库中。

但是,在本文中,我仍然想要定位于低层次,并且谈谈更加底层的JavaScript中的异步代码模式:events和promises。

##Events

在JavaScript中的事件是一种用来在JavaScript的对象之间进行通信的公用订阅机制。事件和回调非常相似:事件的发布者为感兴趣的对象提供一种订阅方式用来在事件发生时接收通知。订阅一个事件代表注册一个回调函数,当事件发生时回调它。当事件发生时,事件发布者简单的调用其注册的任何回调函数。和回调一样,事件可以同步或异步地发生,事件监听回调也可以被同步或异步地调用。

JavaScript原生地将事件用在例如DOM事件的场合,例如点击、鼠标移动、表格提交,等等。即使在非浏览器环境下的JavScript中,事件也被广泛地使用:例如Node.js的EventEmitter。在Node.js中,事件也在stream中出现。

使用事件的主要好处在于他们可以被多个监听器所消费。当事件发生时,事件的发布者可以调用多个被注册的回调函数,因此多个对象可以被通知到。它也可以在某块之间创造松耦合,因为发布者不应该关心“什么”或者“多少”消费者订阅了自己,并且消费者不需要知道发布者内部在做些什么。

大多数的JavaScript框架(浏览器端或非浏览器端)都支持一些事件方式,包括jQuery、AngularJS、Backbone、React、Ember,以及之前提到的Node.js,包含各种各样的EventEmitter以及stream

下面是一个简单的使用基于事件的API的示例。这个例子是用Node.js实现的,使用基本的EventEmitter模块。

// Get the constructor function for the Node.js EventEmitter
var EventEmitter = require("events").EventEmitter;

// Clock is our event publisher - when started, it will publish a "tick" event 
// every second.
function Clock() {
    this.emitter = new EventEmitter();
}

// Starts the clock ticking
Clock.prototype.start = function() {
    var self = this;
    this.interval = setInterval(function() {
        self.emitter.emit("tick", new Date());
    }, 1000);
};

// Stops the clock from ticking
Clock.prototype.stop = function() {
    if (this.interval) {
        clearInterval(this.interval);
        this.interval = null;
    }
};

// Register a callback for the "tick" event
Clock.prototype.onTick = function(callback) {
    this.emitter.on("tick", callback);
};

// Create our clock
var clock = new Clock();

// Register an event for the clock's tick event
clock.onTick(function(date) {
    console.log(date);
});

// Start the clock
clock.start();

这个基础的Node.js程序输出类似如下:

% node clock.js
Wed Oct 15 2014 14:08:01 GMT-0600 (MDT)
Wed Oct 15 2014 14:08:03 GMT-0600 (MDT)
Wed Oct 15 2014 14:08:04 GMT-0600 (MDT)
Wed Oct 15 2014 14:08:05 GMT-0600 (MDT)
Wed Oct 15 2014 14:08:06 GMT-0600 (MDT)
Wed Oct 15 2014 14:08:07 GMT-0600 (MDT)
Wed Oct 15 2014 14:08:08 GMT-0600 (MDT)
...repeats forever...

这里,Clock的onTick函数允许任意数量的对象注册回调到每一次的时间点上。在示例中,我们只注册了一个订阅者,而实际上我们可以注册更多。

事件是一种又用的同步或异步通信机制,但是他们本身并不有助于解决异步调用的顺寻问题,你可以使用其他的技术来帮助你,例如回调。

Promises

Promise是另外一种处理JavaScript对象间异步通信的机制。在过去几年中,Promise已经在JavaScript中变得非常流行了,并且现在已经有许多Promise的实现可供挑选,包括即将到来的ECMAScript6的原生Promise实现。

当异步任务完成或失败时通知其他模块方面,Promise与回调十分相似,但是实现的方式与回调以及事件有一些不同。在回调中,一个异步API函数接受一个或多个函数入参,当任务结束或失败时API函数会使用它们,然而一个基于Promise的函数并不接受回调作为参数,而是返回一个其他模块可以注册完成或者失败回调的Promise对象。而且,另一个回调与Promise的巨大不同之处在于,Promise对象会在满足条件之后继续持有返回值或错误对象,因此其他模块可以检验Promise的状态,访问其对象,即使Promise已经完成。使用回调以及事件时,回调的调用者以及事件的发布者都不会持有最后一次的值,因此如果一个感兴趣的模块错过了一个事件,它们可能就无法检测到这个事件已经发生,也无法得知随该事件一起被发出的值是什么了。

当我们谈到Promise时,我们引入了一个较为具体的术语,也就是Promise/A+规范的描述。当然也有一些其他的Promise规范,但Promise/A+规范似乎是最流行的。网络上有非常多的Promise教学,因此我不会在这里具体讲述,而我的确想提供一个简单的示例来演示Promise是如何被用在顺序的异步函数调用上的。我将使用非常流行的、功能强大的库Q来进行演示。

这是一个非常“刻意”的例子,但它演示了顺序的异步调用如何能和Promise一起使用。

function begin() {
    console.log("begin");
    return 0;
}

function end() {
    console.log("end");
}

function incrementAsync(i) {
    var defer = Q.defer();

    setTimeout(function() {
        i++;
        console.log(i);
        defer.resolve(i);
    }, 0);

    return defer.promise;
}

Q.fcall(begin)
    .then(incrementAsync)
    .then(incrementAsync)
    .then(incrementAsync)
    .then(end);

这个例子的输出是:

begin
1
2
3
end

这个例子的主要驱动方式是Q promise链,由Q.fcall以begin为参数开始。Q.fcall是一个Q提供的静态方法,用来执行所提供的函数,并返回一个值的Promise。入参函数可以返回一个Promise值也可以返回一个非Promise值,但无论哪种方式,Q将会从Q.fcall返回一个Promise。由于Q.fcall总是返回一个Promise,你可以使用then方法在一个Promise上链接其他函数,then函数是Promise的基础方法。返回一个Promise的函数通常被成为”thenable”的函数,意味着你可以使用.then()在它之上链接回调函数。

上面的第一个.thenincrementAsync函数链接到由Q.fcall(begin)创造的Promise中。incrementAsync函数接受一个数字类型的参数,设置一个超时机制来异步地增加值,然后返回一个增加完结果的值的Promise。incrementAsync函数创造了一个Q的deferred对象(使用Q.defer()),这个对象是Promise的“创造者”进行操作的。Promise的创造者有义务在某一个时间点满足或者拒绝这个Promise,典型的时间点就是异步调用成功或者失败的时刻。在Q里,它是通过在deferred对象上调用.resolve()reject()实现的。在incrementAsync中,Promise是通过增加i来满足的,然后调用了.resolve(i),也就表示这个Promise被满足了,并且提供了一个值来传递到链接的下一个函数中。传递给.resolve()的值被传递到链接的下一个函数中作为函数的第一个参数。在Q的Promise链中,每一个方法都可以为一个值的Promise或者朴素的值,Q会基于成功满足或拒绝的条件顺序执行执行该链。Promise不需要被一个值满足,它可以不使用任何值,仅仅表示异步调用已经成功,没有任何值来提供。

Promise/A+规范要求Promise总是被异步地处理,因此上面例子中的setTimeout实际上是多余的,我们用它只是原来强调incrementAsync是天然异步的。

Promise是有一点复杂的话题,很难在一篇文章中讲清楚,但有数不胜数的资源用以将来的学习。

未来

JavaScript作为一种语言以及生态系统,正在迅速地发展。只有非常多激动人心的语言特性正被开发出来支持异步代码。其中最令人激动的是ES6 generator,它是一种非常的强大的、JavaScript编程的新方式。我在此不会讲述这个话题,但网络上有非常多好的教程和指南。

结论

异步编程是JavaScript中需要理解的重要内容,并且有非常多的方式来拥抱它。对于如何处理异步代码并没有一种定论,但是理解不同的可选项是非常重要的,这样你就可以根据你的需求选取正确的解决方案。

用Stream解决编程挑战(译)

#用Stream解决编程挑战(译)

我第一次使用Node.js解决编程挑战的经验简直是让人焦虑不已。我设计了一个切实可行的解决方法,但我却无法找到一个有效的方法来解析输入。它的格式非常的简单:文本通过stdin的pipe输入。很简单,是吧?但我一般的时间都耗费在这个小小的细节上,最终我得到了一个非常脆弱的、缝缝补补的、处处hack过的代码,这至今仍然让我感到不寒而栗。

这份经验激励着我找到一种管用的方式来完成编程挑战。在解决了更多问题之后我终于找到了一种我希望大家都能觉得又用的方式。

##模式

主要的思想是:创造一个problem的stream,将每一个problem转化为一个solition。这个流程由4个步骤组成:

  1. 将输入打散为行的stream。
  2. 将这些行转化为和问题相关的数据结构
  3. 解决问题
  4. 格式化solution并输出

对于那些熟悉stream的人来说,这个模式看起来像这样:

var split = require("split"); // dominictarr’s helpful line-splitting module

process.stdin
    .pipe(split()) // split input into lines
    .pipe(new ProblemStream()) // transform lines into problem data structures
    .pipe(new SolutionStream()) // solve each problem
    .pipe(new FormatStream()) // format the solutions for output
    .pipe(process.stdout); // write solution to stdout

##我们的问题

为了让这个教程更接地气一点,让我们来解决一个Google Code Jam challenge。这个问题是让我们验证数独游戏的解答。输入看起来像这样:

2                  // number of puzzles to verify
3                  // dimensions of first puzzle (3 * 3 = 9)
7 6 5 1 9 8 4 3 2  // first puzzle
8 1 9 2 4 3 5 7 6
3 2 4 6 5 7 9 8 1
1 9 8 4 3 2 7 6 5
2 4 3 5 7 6 8 1 9
6 5 7 9 8 1 3 2 4
4 3 2 7 6 5 1 9 8
5 7 6 8 1 9 2 4 3
9 8 1 3 2 4 6 5 7
3                  // dimensions of second puzzle
7 9 5 1 3 8 4 6 2  // second puzzle
2 1 3 5 4 6 8 7 9
6 8 4 9 2 7 4 5 1
1 3 8 4 6 2 7 9 5
5 4 6 8 7 9 2 1 3
9 2 7 3 5 1 6 8 4
4 6 2 7 9 5 1 3 8
8 7 9 2 1 3 5 4 6
3 5 1 6 8 4 9 2 7

我们输出的格式应该是:

Case #1: Yes
Case #2: No

其中“Yes”表明解答是正确的。

让我们开始吧。

##建立

我们的第一步就是要从stdin中提取输入。在Node中,stdin是一个可读的stream。基本上,一个可读stream会在数据可读后立刻发送数据(更多的解释,参见readable stream docs)。下面这行代码会输出所有输入到stdin中的内容:

process.stdin.pipe(process.stdout);

pipe方法从可读stream中获取所有的数据并写入一个可写stream。

可能从这份代码中并不显而易见,但是process.stdin会以大块byte的形式pipe数据,而我们感兴趣的是以行为分隔的文本。为了将这种大块数据分解成行,我们可以将process.stdin pipe进入dominictarr所写的split模块中。首先npm install split,然后:

var split = require("split");

process.stdin.setEncoding("utf8"); // convert bytes to utf8 characters

process.stdin
     .pipe(split())
     .pipe(process.stdout);

##使用transform stream构造问题

现在我们有了由行组成的序列,我们可以开始进行我们真正的工作了。我们会将这些行转化为一串代表数独问题的二维数组中。然后,我们pipe每个数独问题到另一个流并用它来检验它是否是一个正确的解答。

Node的原生transform stream提供了我们所需要的抽象。一个transform stream将写入它的数据进行转化,并将结果以一个可读stream的方式输出。有点疑惑?我们下面会让你清楚一些。

为了创建一个transform stream,我们要继承stream.Transform并调用它的构造函数。

var Transform = require("stream").Transform;
var util = require("util");

util.inherits(ProblemStream, Transform); // inherit Transform

function ProblemStream () {
    Transform.call(this, { "objectMode": true }); // invoke Transform's constructor
}

你会注意到,我们传递了objectMode的flag到Transform的构造函数中。原始的Stream上只接受string和buffer。而我们希望输出一个二维数组,所以我们需要打开object模式。

Transform stream有两个重要的方法:_transform_flush_transform在每当有数据写入stream时被调用。我们使用这个方法来将一系列的行转化为一个数组解答。_flush将在transform stream被通知没有更多的数据会被写入时被调用。这个函数有助于我们结束任何尚未结束的任务。

让我们草拟我们的transform函数:

ProblemStream.prototype._transform = function (line, encoding, processed) {
     // TODO
}

_transform接受3个参数。第一个是写入stream的数据。在我们这个情况下,就是一行文本。第二个参数是stream编码,在此我们设为utf8。最后一个参数是一个无参的回调函数用来提供已经结束输入处理的信号。

当你在实现_transform函数的时候要牢记两点:

  1. 调用processed回调函数并不向output stream中添加任何内容。它仅仅是一个信号,标志着我们已经完成了传递给_transform的内容的处理
  2. 如果要输出一个值,使用this.push(value)

记住这些,让我们再来看看输入。

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

我们马上就遇到了一个问题:我们的_transform方法每行被调用一次,但是前面三行每一行都代表不同的意义。第一行描述了要解决多少个问题,第二行是接下来的解答由几行组成,第三行是解答内容。我们的stream需要用不同的方式处理每一行。

幸运的是,我们可以将状态保存在transform stream内部:

var Transform = require("stream").Transform;
var util = require("util");

util.inherits(ProblemStream, Transform);

function ProblemStream () {
    Transform.call(this, { "objectMode": true });

    this.numProblemsToSolve = null;
    this.puzzleSize = null;
    this.currentPuzzle = null;
}

通过这些变量,我们就可以追踪到我们正处在行序列的何处。

ProblemStream.prototype._transform = function (line, encoding, processed) {
    if (this.numProblemsToSolve === null) { // handle first line
        this.numProblemsToSolve = +line;
    }
    else if (this.puzzleSize === null) { // start a new puzzle
        this.puzzleSize = (+line) * (+line); // a size of 3 means the puzzle will be 9 lines long
        this.currentPuzzle = [];
    }
    else {
        var numbers = line.match(/\d+/g); // break line into an array of numbers
        this.currentPuzzle.push(numbers); // add a new row to the puzzle
        this.puzzleSize--; // decrement number of remaining lines to parse for puzzle

        if (this.puzzleSize === 0) {
            this.push(this.currentPuzzle); // we've parsed the full puzzle; add it to the output stream
            this.puzzleSize = null; // reset; ready for next puzzle
        }
    }
    processed(); // we're done processing the current line
};

process.stdin
    .pipe(split())
    .pipe(new ProblemStream())
    .pipe(new SolutionStream()) // TODO
    .pipe(new FormatStream()) // TODO
    .pipe(process.stdout); 

让我们花点时间来回顾一下代码。记住_transform会为每行所调用。第一行_transform接收到对应的需要解决问题的数目。由于numProblemsToSolve是null,所以这个逻辑分支会被执行。被传递到_transform的第二行是解答的尺寸。由于我们已经知道解答的尺寸,第三行是构造数据结构的开始。一旦解答被构造,我们会将一个完整的解答推送到transform stream的输出端,然后准备创建一个新的解答。循环此过程直到我们读完所有行。

##解决所有的问题吧!

解析完并构造出数独解答的数据结构之后,我们终于可以开始解答这个问题了。

“解答问题”的任务,可以被解释为“将一个问题转化为一个解答”。这就是我们的下一个stream所要做的。

util.inherits(SolutionStream, Transform);

function SolutionStream () {
    Transform.call(this, { "objectMode": true });
}

然后,我们定义一个_transform方法,它接受一个problem参数,并返回一个布尔值。

SolutionStream.prototype._transform = function (problem, encoding, processed) {
    var solution = solve(problem);
    this.push(solution);
    processed();

    function solve (problem) {
        // TODO
        return false;
    }
};

process.stdin
    .pipe(split())
    .pipe(new ProblemStream())
    .pipe(new SolutionStream())
    .pipe(new FormatStream()) // TODO
    .pipe(process.stdout);

不像ProblemStream一样,这个stream会为每一个输入构造一个输出,_transform会为每个问题执行一次,我们需要解决所有的问题。

我们所有要做的就是写一个函数来决定是否数独问题被解决了,我把这个留给你自己来解答。

修饰输出

现在我们解决了这个问题,我们的最后一步是格式化输出。如你所料,我们又将使用一个transform stream。

我们的FormatStream接受一个解答并转化为一个字符串传递到process.stdout中。

还记得输出格式吗?

Case #1: Yes
Case #2: No

我们需要纪录问题号,并将布尔值转化为”Yes”或者”No”。

util.inherits(FormatStream, Transform);
function FormatStream () {
    Transform.call(this, { "objectMode": true });

    this.caseNumber = 0;
}

FormatStream.prototype._transform = function (solution, encoding, processed) {
    this.caseNumber++;

    var result = solution ? "Yes" : "No";

    var formatted = "Case #" + this.caseNumber + ": " + result + "\n";

    this.push(formatted);
    processed();
};

现在,将FormatStream连接到我们的pipeline中,我们就完成了

process.stdin
    .pipe(split())
    .pipe(new ProblemStream())
    .pipe(new SolutionStream())
    .pipe(new FormatStream())
    .pipe(process.stdout);

从Github上获取完整代码。

最后一条提示

使用pipe的最大好处是你可以在任何可读/可写流中重用你的代码。如果你需要解答一个来自网络的问题,将process.stdinprocess.stdout改为网络stream,所有的一切应该可以直接使用。

对于每一个问题你可以需要做相应的微调,但我希望它给出了一个好的开始。

拥抱JavaScript中的异步(译)

#拥抱JavaScript中的异步(译)

Andy White

##简介

如果你已经写过不计其数的Javascript代码,那么你会意识到,异步编程并不仅仅是一种”nice to have”的能力,而是一种必需品。为了充分利用语言与生态系统,它必须被理解和接受。

##回调

在JavaScript中,最简单的展示异步API的方式之一便是使用一个接受另一个函数作为参数的函数。这些函数参数就是所谓的“回调”或“回调函数”,因为他们给予主函数一个用来回调的钩子 – 通过在恰当的时机,调用一次或多次你所提供的函数。因为JavaScript将函数视为一等公民,你可以将函数实例作为参数传递给其他函数,或者从其他函数中返回函数实例,就像你可以轻松的传递或返回数值,字符串,布尔类型,对象或者数组一样。会点函数可以被主函数在任何时机,以任何的次数,同步或异步的方式,使用任何绑定的上下文以及参数所调用,这样就提供了一种非常灵活以及强大的在JavaScript模块之间进行通信的机制。

JavaScript的事件循环与回调

在深入了解回调以及异步代码之前,对JavaScript的事件循环与回调有一个基本的认知是非常重要的。在最基本的形式中,JavaScript是一个单线程的运行时,因此它并不支持类似于多线程、多进程或内部进程通信的技术。尽管看起来(实际也是)有这些限制,缺少多线程实际上会让你作为一个javaScript开发者的认识更简单一些,并且允许几种有趣的在编译、压缩、清理(transpilation)的优化技术。

因为只有单线程,你将永远不会遇到基本的来自线程的挑战,例如竞争条件,资源冲突,线程死锁等。当JavaScript解释器开始执行一个代码块时(例如,一次函数调用),它将一直执行这块代码直到同步代码结束(该函数中所包含的最后一句同步代码)。在执行这段同步代码的期间,任何非同步的函数调用(例如外部事件句柄调用,异步函数调用,异步回调等等)将简单的被放置于队列中等待运行时事件循环之后执行。一旦同步代码执行完,事件循环将从队列中获取下一块代码并执行它,知道同步代码的结束,以此类推。这样,你可以安全的断言,你在一个函数中编写的任何一串的同步代码,在其他代码执行之前,将总是不被打断地执行到结束。你也无法使用CPU直到你yield它(或者运行时干掉了你的堵塞或者死循环代码)。这对于应用开发非常有帮助,但也需要一些仔细的考虑与计划,尤其是当你需要用到异步API时,你代码的顺序就非常重要了。

是”下一个tick”还是”事件循环的下一个回合”

你经常听到JavaScript开发者提到”下一个tick”还是”事件循环的下一个回合”。基本上,这些概念意味着,当当前同步代码执行完毕后,这些代码将被置于等待执行的队列中,时间循环正准备从队列中获取下一块要执行的代码。所有的异步API都暗示代码将会在之后的的”tick”或者“时间循环的回合”被执行。下一个tick的概念可能更具体的取决于你的JavaScript平台,但是基本上,它仅仅指的是一个函数调用已经被置于队列中为了将来执行。在这个条件下,”之后”这个词可能是也可能不是指向一个确定的时间延迟,但它总是表示代码会在当前同步代码执行完之后被执行。

非堵塞操作

由于JavaScript的单线程特性,因此时间敏感的操作,例如IO操作,必须全部都是非堵塞且异步的,这样这些操作就不会堵塞主应用的时间循环。当事件循环被堵塞时,没有其他应用逻辑会被执行,应用程序往往陷于完全停止的情景。长时间的操作全部都应该被异步调用,并且在操作结束(或者失败)时使用一些异步完成的回调进行处理。对于长时间的操作,使用进度回调函数也非常常见,这样进度的增长可以被报告出来(例如大文件的拷贝操作时的百分比通知)。所有这些回调都是简单被加入到事件队列中,并且在事件循环的未来某一个回合被执行,这样就不会有任何代码在任何时刻堵塞事件循环。

同步vs异步回调API

对于回调,一个重要的方面是,它可以是同步的,也可以是异步的。通常而言,理解回调将被同步还是异步调用是非常重要的,因为你可能会有一些依次执行的代码,它们不应该被执行直到这些回调函数基于的API调用结束。对于同步的回调API,你基本上不需要做任何事情来实现所期望的顺序,因为回调函数将同步地执行到结束,而对于异步回调API,你往往必须用另一种形式编写代码以确保调用顺序的正确性。

基于同步的”each”函数

一个基于”each”函数的同步回调可以很好的阐明这个问题。注意:我现在忽略了回调上下文(this)以及Function.prototype.callapply

// Helper function for logging something to the console
function logItem(item) {
    console.log(item);
}

// Synchronous "each" function - invokes the callback for each item
// in the array
function each(arr, callback) {
    for (var i = 0; i < arr.length; ++i) {
        // Invoke the callback synchronously for each iteration
        callback(arr[i]);
    }
}

// Try it out!
console.log("begin");
each([1, 2, 3], logItem); // "logItem" is our "callback" function here
console.log("end");

这个示例会在控制台打印:

begin
1
2
3
end

除了for循环以及each函数会被同步执行直到到达console.log("end")语句,这个示例没有其他特殊的。

基于异步的”each”函数 (第一次尝试)

一个异步版本的”each”可能看起来像这样。在这里,我使用setTimeout来强制使回调的执行变得异步。主要setTimeout只是一个帮助函数用来推迟一个函数的调用 – 从当前同步代码执行结束后算起(也就是,下一个事件循环的回合)。setTimeout也接受一个最小时间延迟的参数,但是在这里,我只是使用延迟0毫秒,仅仅让它异步执行,而不产生任何延迟。

function asyncEach(arr, callback) {
    for (var i = 0; i < arr.length; ++i) {
        // Enqueue a function to be called later
        // Note: this code does not do what we might expect...
        setTimeout(function() {
            callback(arr[i]);
        }, 0);
    }
}

console.log("begin");
asyncEach([1, 2, 3], logItem);
console.log("end");

由于回调函数现在异步执行,你应该期待代码执行结果为:

begin
end
1
2
3

但是,令人吃惊(或者,其实并不吃惊)的是,代码结果为:

begin
end
undefined
undefined
undefined

这个代码错误可能在某一个时刻绊倒过每一个JavaScript开发人员。这里发生了什么?其实,因为我们在循环中使用了setTimeout,而不是在每次循环中调用callback(arr[i]),我们实际上将一个函数调用延迟了,延迟到当前同步代码块的结束(也就是,for循环)。在这个例子中,我们纪录了begin,然后延迟3个回调函数的调用,然后纪录end,然后释放CPU到事件循环中。事件循环又开始执行我们延迟的回调函数,按顺序执行,而我们期望它的结果应该是纪录arr[0]arr[1]arr[2]

JavaScript的范围和闭包

为什么结果会是打印三次undefined而不是1,2,3呢?这就涉及到另一个重要的内容,JavaScript函数与范围:闭包)的概念。当你在JavaScript中创建一个函数,这个函数可以访问它被创建的那个范围的所有东西,包括任何你在函数内部创建的新变量。JavaScript不像C,C++,Java,C#那样,没有块级作用域。取而代之的是在函数级别定义作用域。你在函数中定义的任何变量,在函数的其他地方或任何内部函数中都是可以被访问到的。有趣的是,不仅仅一个函数能访问它当前环境作用域的所有变量,函数实例在其整个生命周期同样也“持有”(“关闭”)该作用域,即使其父函数(调用它的函数)已经返回或离开该作用域。只要该函数仍然“存活”(被某些东西引用,还未被回收),那么它就会持有该作用域,即使父函数早已消失。由于函数实例可以从一个函数中被返回,一个函数就可以轻易地在父函数或调用函数生命周期之外存活。这中“持有”作用域有事会导致微小的内存泄露,但我们现在不讨论它。在上面的asyncEach示例中,实际发生的事情是:每一个我们在for循环中延迟的回调函数保存了到当前作用域的引用(当时这个作用域还存在着),并且持有该作用域即使for循环以及asyncEach函数已经退出。而回调函数存活在for循环之外,因为回调函数实例被通过setTimeout添加至事件队列中,因此作用域变量例如arri还活着,但是现在i的值变成了3,因为for循环在之前已经同步执行到结束了。在每一个回调中,logItem函数每次都访问arr[3],也就是undefined。

有很多种方式可以处理这个问题,但大多数都围绕着围绕我们希望之后捕获的变量周围添加一个额外的函数作用域。

基于异步的”each”函数 (第二次尝试)

一个在每个回调函数中获得所期望的i值的解决方法是,在我们希望捕获的变量周围引入一个“立即执行的函数表达式”(immediately-invoked function expression, IIFE)。一个IIFE有多种用法,其中一个就是当你没别的办法获取一个作用域时,强制创造一个作用域(例如在for循环中)。这在循环中是不被建议的,但是它可以起到作用:

// Not recommended
function asyncEach2(arr, callback) {
    for (var i = 0; i < arr.length; ++i) {
        // Use an IIFE wrapper to capture the current value of "i" in "iCopy"
        // "iCopy" is unique for each iteration of the loop.
        (function(iCopy) {
            setTimeout(function() {
                callback(arr[iCopy]);
            }, 0);
        }(i));
    }
}

console.log("begin");
asyncEach([1, 2, 3], logItem);
console.log("end");

现在我们能得到所期望的结果了:

begin
end
1
2
3

这之所以能够起作用是因为我们在每一个函数迭代周期创造了一个内部函数作用域,并且我们创建了新的变量iCopy并赋予其每个迭代周期中i的值。iCopy在每一个循环周期都是独一无二的,因此我们不再会遇到在作用域外引用一个变量的问题,在之前的示例中,我们在得到它之前它就变掉了。

基于异步的”each”函数 (第三次尝试)

一个更倾向的解决问题的方式不是在循环内部使用IIFE,而是在循环外创建一个函数以创造我们的函数作用于,像这样:

function asyncEach3(arr, callback) {
    // Utility inner function to create a wrapper function for the callback
    function makeCallbackWrapper(arr, i, callback) {
        // Create our function scope for use inside the loop
        return function() {
            callback(arr[i]);
        }
    }

    for (var i = 0; i < arr.length; ++i) {
        setTimeout(makeCallbackWrapper(arr, i, callback), 0);
    }
}

console.log("begin");
asyncEach3([1, 2, 3], logItem);
console.log("end");

这一次,我们使用了一个单独的函数makeCallbackWrapper来为每个循环迭代创造我们的函数作用域。这次代码更简洁,容易阅读和维护,并且避免了”循环内IIFE”所带来的性能问题。

基于异步的”each”函数 (第四次尝试)

另一个更先进的在for循环内部创造作用域的方式是使用函数绑定或者partial application,就像使用原生的Function.prototype.bind函数(只有新的浏览器支持),或者使用由UnderscoreLo-DashjQuery三者任意一个提供的bind实现。

函数绑定以及partial application是更大的话题,会在今后的博客文章中讨论。

function asyncEach4(arr, callback) {
    for (var i = 0; i < arr.length; ++i) {
        // boundCallback is a new function which has arr[i] permanently
        // set (partially applied) as its first argument.  The "null" argument
        // is the binding for the `this` context variable in the callback, which
        // we don't care about in this example...
        var boundCallback = callback.bind(null, arr[i]);
        setTimeout(boundCallback, 0);
    }
}

console.log("begin");
asyncEach4([1, 2, 3], logItem);
console.log("end");

异步代码的执行顺序

我们要如何才能在使用asyncEach时保持日志纪录顺序呢,像原先同步的each例子一样:

begin
1
2
3
end

下篇文章讨论。

总结

这只是一个JavaScript中基于回调的API以及异步编程的基本介绍。在今后的博客中,我将会探索如何在asyncEach函数中获取你所期望的顺序执行的功能,这样我们仍然能打印begin, 1, 2, 3, end即使回调函数是异步的情况。我也会讨论JavaScript中异步编程的其他问题,包括函数上下文变量context是如何与回调一起工作的,“回调地狱”的概念,以及Javascript中的promise是如何解决这些问题的。

Design-of-Q

/*
本文档的目的是通过增量的、回顾其主要设计决策的方式来构建一个promise库,以此解释promises如何工作以及为何采用这样独特的方式实现。旨在让用户可以自由的体验到不同的实现方式以便满足其自身的需求,而不必错过任何重要的细节。

-

设想一下你正在编写一个无法立即返回一个值的函数。最显而易见的API是:将最终结果传递到一个作为参数的回调函数中,来代替将其返回。
*/

var oneOneSecondLater = function (callback) {
    setTimeout(function () {
        callback(1);
    }, 1000);
};

/*
这是一个解决这种琐碎问题的一个非常简单的方案,但它还有很多进步的空间。

一个更通用的解决方案会同时为返回值以及被抛出的异常提供同样的工具(即回调函数)。有几个显而易见的方式来扩展回调模式以处理异常。其中一个是同时提供一个普通的回调函数(callback)以及一个处理错误的回调函数(errback)。
*/

var maybeOneOneSecondLater = function (callback, errback) {
    setTimeout(function () {
        if (Math.random() < .5) {
            callback(1);
        } else {
            errback(new Error("Can't provide one."));
        }
    }, 1000);
};

/*
还有其他的解决方案,区别在于将错误作为回调函数的一个参数传入,以位置或者“哨兵值“进行区分。但是,这样的解决方案没有一个事实上考虑了被抛出的异常。异常以及try/catch块的目的是延后明确处理异常的时间直到程序已经回到一个有意义尝试从异常中恢复的点。如果异常没有被处理,我们需要一些机制来隐式地传播异常。

Promises

考虑一个更普遍的解决方式,代替返回值或者抛出异常,函数返回一个表示函数最终结果的对象,要么成功,要么失败。这个对象,无论是从打比方还是命名的角度来说,就是一个最终要被处理的promise,我们可以在promise之上调用函数来观测它是被满足还是被拒绝。如果这个promise被拒绝而并没有被主动的观测到,那么其他衍生的promise会被因为一些原因被隐式地拒绝。

在下面这个特殊的设计迭代中,我们将promise设计为一个带有以回调函数为参数的”then”函数的对象。
*/

var maybeOneOneSecondLater = function () {
    var callback;
    setTimeout(function () {
        callback(1);
    }, 1000);
    return {
        then: function (_callback) {
            callback = _callback;
        }
    };
};

/*
这种设计有两个不足:

  • 第一个then的调用者决定被使用的回调函数。如果每一个被注册的回调函数都会被resolution通知到的话会更好。
  • 如果回调函数在promise被构造多于一秒之后注册,那么它不会被调用。

一个更普遍的解决方式在于接受任意数量的回调函数并且允许他们无论在超时(更普遍的说
,resolution事件被触发)前或后均可注册。我们通过将promise设置为一个两种状态的对象来实现。

一个promise最初处于unresolved状态,并且所有的回调函数都被加入一个pending状态观测者的数组中。当promise被resolve之后,所有的观测者都会被通知到。我们通过判断是否这个pending回调函数的队列存在的方式来区分状态是否被转换,我们在resolution之后会扔掉它(这个队列)。
*/

var maybeOneOneSecondLater = function () {
    var pending = [], value;
    setTimeout(function () {
        value = 1;
        for (var i = 0, ii = pending.length; i < ii; i++) {
            var callback = pending[i];
            callback(value);
        }
        pending = undefined;
    }, 1000);
    return {
        then: function (callback) {
            if (pending) {
                pending.push(callback);
            } else {
                callback(value);
            }
        }
    };
};

/
这样已经足够好了,如果将其改为一个功能函数的话会非常有用处。一个deferred是一个拥有两个部分的对象:一个用来注册观测者,另一个用来将resolution通知观测者。
(见 design/q0.js)
/

var defer = function () {
    var pending = [], value;
    return {
        resolve: function (_value) {
            value = _value;
            for (var i = 0, ii = pending.length; i < ii; i++) {
                var callback = pending[i];
                callback(value);
            }
            pending = undefined;
        },
        then: function (callback) {
            if (pending) {
                pending.push(callback);
            } else {
                callback(value);
            }
        }
    }
};

var oneOneSecondLater = function () {
    var result = defer();
    setTimeout(function () {
        result.resolve(1);
    }, 1000);
    return result;
};

oneOneSecondLater().then(callback);

/
现在这个resolve有一个瑕疵:它能够被调用多次,从而改变被promised了的结果。它没有能够符合“一个函数只能要么返回一个值要么抛出一个错误”的事实要求。我们可以通过只允许第一次调用来设定resolution的方式保护结果免于意外或者恶意的重置。
/

var defer = function () {
    var pending = [], value;
    return {
        resolve: function (_value) {
            if (pending) {
                value = _value;
                for (var i = 0, ii = pending.length; i < ii; i++) {
                    var callback = pending[i];
                    callback(value);
                }
                pending = undefined;
            } else {
                throw new Error("A promise can only be resolved once.");
            }
        },
        then: function (callback) {
            if (pending) {
                pending.push(callback);
            } else {
                callback(value);
            }
        }
    }
};

/*
你可以设置一个参数,在这种情况下要么抛出一个错误或者忽略之后所有其他resolution。一个测试就是给予resolver一串worker,并竞争resolve该promise,而剩余的resolution会被忽略。如果你不想让这些worker知道谁获胜了,这也是可行的。下文中,所有的示例都会忽略多重resolution而不是抛出异常。

现在,defer可以同时处理多resolution和多observation的情况。(见 design/q1.js)


源于两个单独的立场,这个设计衍生出了几种不同的变化。第一种立场是:将promise和deferred中的resolver部分分离或者结合都是有用的。通过某种方式从其他值中识别promise也是有用的。

-

将promise从resolver中分离开允许我们在最小特权原则下进行编码。给予某人一个promise,应该仅仅给予他观测resolution的权力,而给予某人一个resolver,应该仅仅给予他决定resolition的权力。一方的权力绝不不应该被授予另一方。通过时间的检验我们发现,任何过度的授权都会不可避免地被滥用,并且这将会非常难以编写。

然而,分离的不好之处在于,快速地废除promise对象会给垃圾回收器带来额外负担。

-

以外,有非常多的方式来区分promise以及其他值。最显而易见并且最重要的区分方式是使用原型继承(design/q2.js)
*/

var Promise = function () {
};

var isPromise = function (value) {
    return value instanceof Promise;
};

var defer = function () {
    var pending = [], value;
    var promise = new Promise();
    promise.then = function (callback) {
        if (pending) {
            pending.push(callback);
        } else {
            callback(value);
        }
    };
    return {
        resolve: function (_value) {
            if (pending) {
                value = _value;
                for (var i = 0, ii = pending.length; i < ii; i++) {
                    var callback = pending[i];
                    callback(value);
                }
                pending = undefined;
            }
        },
        promise: promise
    };
};

/*
使用原型继承的缺点在于它使得一个项目中只能使用一种promise库。这会难以实施,使实施依赖变成一种灾难。

另一种实现方式是使用duck-typing,通过是否存在某种约定命名的方法来区分promise以及其他对象。在我们的案例中,CommonJS/Promises/A创立了通过是否使用了”then”的方式来区分promise以及其他值。这个方式的缺点是无法判断那些只是碰巧有一个”then”方法的对象。在实际状况中,这并不是一个问题,并且这种实现”可以then”的微小差异是可以被管理的。
*/

var isPromise = function (value) {
    return value && typeof value.then === "function";
};

var defer = function () {
    var pending = [], value;
    return {
        resolve: function (_value) {
            if (pending) {
                value = _value;
                for (var i = 0, ii = pending.length; i < ii; i++) {
                    var callback = pending[i];
                    callback(value);
                }
                pending = undefined;
            }
        },
        promise: {
            then: function (callback) {
                if (pending) {
                    pending.push(callback);
                } else {
                    callback(value);
                }
            }
        }
    };
};

/
下一个大的步骤是使它可以简单的生成promise,使用从旧的promise中获取的值来构造新的promise。假设你收到从数个函数调用中得到的两个数字的promise,我们应该可以创建他们的和的promise。考虑一下这用callback是怎样实现的。
/

var twoOneSecondLater = function (callback) {
    var a, b;
    var consider = function () {
        if (a === undefined || b === undefined)
            return;
        callback(a + b);
    };
    oneOneSecondLater(function (_a) {
        a = _a;
        consider();
    });
    oneOneSecondLater(function (_b) {
        b = _b;
        consider();
    });
};

twoOneSecondLater(function (c) {
    // c === 2
});

有非常多的原因证明,这种实现是非常脆弱的,特别是它需要明确的编码来进行通知(在本例中是使用一个哨兵值),是否是一个回调函数被调用了。此外,我们必须注意考虑到在事件循环结束前被发出的条件:consider函数需要在它被使用之前出现。

在下面的几个步骤中,我们会能够用promise实现它,使用更少的代码以及隐式地处理错误传递。
*/

var a = oneOneSecondLater();
var b = oneOneSecondLater();
var c = a.then(function (a) {
    return b.then(function (b) {
        return a + b;
    });
});

[译]函数响应式编程

你所期待已久的函数响应式编程简介

原文地址:https://gist.github.com/staltz/868e7e9bc2a7b8c1f754

你一定对学习这个被称为(函数)响应式编程的东西感兴趣。

这东西学起来很难,缺乏好的资料使它变得更难。当我开始学习的时候,我尝试着寻找教程。我只找到少量的实践指南,而它们也仅仅是挠了挠表面,从未挑战去建立整个架构。当你想了解一些函数的时候,库的文档往往对你没什么帮助。我的意思是,老实说,看看这个:

合并元素的索引,然后把一个可观测序列的可观测序列转化为一个可观测序列,仅仅从最近的可观测序列中取值。通过这种方法将每一个可被观测的序列中的对象投射到一个新的可观测序列的序列上。

天呐。。

我读了两本书,其中一本只是描绘了蓝图,另一本则一头钻进“如何使用FRP库”的问题中。最后我通过那种困难的方式学习了响应式编程:一边构造响应式编程项目一边学习。在我在Futurice的工作中,我在一个真正的项目上使用了它,当我遇到难题的时候我的一些同事帮助了我。

在学习的旅途中最困难的是用函数响应式编程进行思考。很多时候需要抛弃那些典型的、有状态的编程习惯,并迫使你的大脑在另一种模式下工作。我没有在网上找到任何这方面相关的内容,我任何需要这样一个“如何用函数响应式编程进行思考”的教程,以便你们可以开始迈出第一步。在那之后,库文档会帮你照亮后面的道路。希望这可以帮到你们。

#什么是函数响应式编程(FRP)?

对于函数相应式编程,网上有很多不好的解释以及定义。维基百科和平常一样说得太空泛以及理论化了。Stackoverflow的规范的答案显然不适合新手。Reactive Manifesto看起来像是你要讲给你公司里的项目经理或者商务人士所听的。微软Rx术语“Rx = Observables + LINQ + Schedulers”过于沉重(那么的“微软化”),我们大多数人都会感到困惑。像“reactive”,“”propagation of change”这种术语和我们典型的MV*以及最爱的编程语言已经做到了。我的框架当然是视图(Views)响应模型(Models)的,变化当然是可以传递的,否则的话什么也不会呈现。

所以,我们就不要继续说上面那些了。

####函数响应式编程就是通过异步数据流进行编程

在某种程度上,这并不是什么新的东西。事件总线或者典型的单击事件就已经是异步事件流了,你在它们上面可以进行观察或者做点其他的事情。函数响应式编程就是那些玩意儿再加上些内固醇。你可以对任何东西创造数据流,而不仅仅是click或者hover事件。流非常的廉价并且无处不在,任何东西都可以是流,变量、用户输入、属性、缓存、数据结构,等等。比如,设想一下你的Twitter feed是一个像单击事件一样的数据流。你可以监听这个是流并且做出相应的反应。

更重要的是,你拥有了一个神奇的工具箱来连接、创造并且过滤任何这些流。这就是“函数式”的魔力。一个流可以作为另一个流的输入,甚至可以是多个流作为一个流的输入。可以合并两个流。你可以过滤一个流而得到你另一个只包含你所感兴趣的事件的流。你可以把数据从一个流映射到另一个流。

如果流对于函数响应式编程如此重要,让我们仔细的来看看它们,从我们熟悉的“点击一个按钮”的事件流开始。

image

流是正在进行的事件按时间排序得到的序列。它可以广播三种不同的东西:一个值(属于某种类型)、一个错误,或者一个“已完成”的信号。设想一下,比如“已完成”会在当前包含此按钮的窗口或者视图被关闭时发生。

我们只采用异步的方式捕获这些被广播的事件,通过定义一个“当某个值被广播时执行”的函数,一个“当错误被广播时执行”的函数以及一个“当完成被广播时执行”的函数来完成。有时候后两个可以被省略,你可以只专注于定义那个捕获某个值的函数。对这个流进行“监听”被成为订阅。我们所定义的函数是观察者。流是被观测的主体(或被称为“可观测对象”)。这正是观察者设计模式

另一个画这张图的方式是使用ASCII,我们将在本教程的某些部分使用:

--a---b-c---d---X---|->

a, b, c, d 是被广播的值
X 是一个错误
| 是“已完成”信号
---> 是时间轴

既然这已经感觉如此熟悉了,并且我不想让你们感到厌烦,让我们来做点新的事情:我们接下来要用原有的单击事件流构建出一些新的单击事件流。

首先,让我们创造一个计数事件流用来表明这个按钮被按了多少次。在函数响应式编程的公共库里,每一个流都有很多种函数附在它的上面,例如map, filter, scan 等等。当你调用其中一个函数时,比如clickStream.map(f),它会基于当前的点击流返回一个新的流。它并不对原先的点击流做任何修改。这就是所谓的不变性,它和FRP流的关系就像煎饼和糖浆一样如此的美好。这允许我们进行链式调用,比如:clickStream.map(f).scan(g):

点击流: ---c----c--c----c------c-->
       vvvvv map(c 变为 1)    vvvv
       ---1----1--1----1------1-->
       vvvvvvvvv scan(+) vvvvvvvvv
计数流: ---1----2--3----4------5-->

函数map(f)根据你所提供的函数f,将每一个被广播的值替换成新的值,并放入新的流中。在我们的例子里,我们把每一次点击映射为数字1.函数scan(f)汇集这个流上前面所有的值,产生x = g(accumulated, current),而g在本例中只是一个简单的相加函数。这样,每当点击发生的时候,counterStream就会广播一次点击总数。

为了展现FRP的真正威力,我们考虑这样一个场景:你想要一个双击的事件流,为了让它更有趣一点,我们希望这个流把“三击”(更一般的情况,大于两次的点击)也考虑为双击。做一次深呼吸,想象一下在传统的、有状态的方式下你会怎样做?我敢打赌这一定相当麻烦并且涉及到许多用来保持状态和纪录时间间隔的变量。

好吧,在FRP中这非常简单。事实上,它的逻辑只需要4行代码。但首先,让我们忽略代码,用思维图是最好的理解以及构建流的方式。

image

灰色方格里面是讲一个流转化为另一个流的函数。简言之,我们首先把单击聚集到一个列表中,无论何时250毫秒的“事件沉默”发生(这正是buffer(stream.throttle(250ms)所做的)。现在不用担心理解细节,我们现在只是在演示使用FRP。它的结果是一个列表的流,在此基础上我们通过apply()将每一个列表映射为表示它的长度的数字。最后我们通过filter(x >= 2)忽略数字1,这样就完成了用3个步骤创建我们所需要的流。我们接下来就可以订阅(监听)这个流并按我们所希望的进行处理。

我希望你喜欢这种方法的美丽之处。这个例子仅仅是冰山一角:你可以用这种方法处理不同类型的流。例如:在API返回结果的流中,有很多其他可用的函数。

#“为什么我应该考虑采用FRP?”
FRP提高了你的代码的抽象层次,这样你就可以专注于业务逻辑所互相依存的事件中,而不必摆弄大量的实现细节。用FRP写出的代码可能会更简洁。

在现代网络以及移动应用程序中,它的好处更明显,这些应用往往与数据事件相关的UI事件有着高度的交互。10年前,与网页进行交互基本上就是向后端提交一个长的表单,并且在前端进行简单的呈现。而现在,应用程序已经进化到更实时:修改一个变淡字段可以自动触发保存到后端,“喜欢”一些内容可以实时反映给其他已连接的用户,等等。

当今的应用程序都有非常多各式各样的实时事件的使得与用户的高度交互体验成为可能。我们需要适当的工具来妥善处理它,而函数响应式编程就是答案。

#Thinking in FRP,例
让我们来考虑一个真正的场景,一个现实世界的例子来一步步引导你如何用FRP进行思考。没有集合的示例,没有解释不完全的内容。在本教程的最后我们将能够产生真正的功能代码,同时我们也会知道为什么我们这样做。

我选择了JavaScriptRxJS作为这例子的工具,只有一个原因:JavaScript是最熟悉的语言,同时Rx*库家族被广泛用于许多语言和平台。(.NET, Java, Scala, Clojure, JavaScript, Ruby, Python, C++, Objective-C/Cocoa, Groovy, etc).因此,无论你使用什么样的工具,你都可以从接下来的教程中获益。

#实现一个“你可能感兴趣的人”推荐框
在Twitter上有一个UI元素用来建议你可以关注的账号
image

我们要关注模仿其核心功能,它们是:

  • 在开启时,从API读取账号数据并展示3个建议
  • 在点击“刷新”时,向3行读取3个其他账号建议
  • 在点击 “X” 按钮时,关闭其当前对应的账号并显示另外一个
  • 每一行显示账号的头像并链接到它们的主页

我们可以省去其他功能和按钮因为它们是次要的。并且Twitter最近关闭了未经授权的公共API。让我们来为Github来构建一个类似的UI。Github提供了获取用户的API。

如果你想要快速浏览的话,完整的代码已经放在http://jsfiddle.net/staltz/8jFJH/48/了。

#请求和响应

如何用FRP解决这个问题?让我们开始,(几乎)所有的东西都可以是一个流。那就是FRP的咒语。让我们从最简单的功能开始:“当启动时,从API获取加载3个账户信息”。这没有任何特别之处,仅仅是(1) 发送一个请求, (2)获取响应 (3)呈现响应。所以我们继续,让我们的请求成为一个流。起初这会觉得有点过于简单了,但是我们需要从最基本的开始,不是吗?

在启动时我们只需要执行一个请求,所以如果我们将其建模为一个数据流,它会是一个只广播一个值的流。之后,我们知道我们会发出很多请求,但现在,只有一个。

--a------|->

这里a是一个字符串 'https://api.github.com/users'

这是一个我们想要请求的URL,当请求事件发生时,它告诉我们两件事情:“什么时候”与“什么”。“什么时候”请求应该被执行,也就是什么时候事件应该被广播。以及“什么”应该被响应,也就是被广播的值是什么:一个包含URL的字符串。

创建一个带有单一值这样的流在Rx*是非常简单的。流的官方术语是“可观测对象”,因为它可以被观测,但是我觉得这是个非常愚蠢的名字,所以我把它成为一个流。

var requestStream = Rx.Observable.returnValue('https://api.github.com/users');

但是现在,这只是一个字符串的流,没法做其他操作,因此,我们要在这个值被广播的时候触发一些事情。这就是通过描述这个流做到的。

requestStream.subscribe(function(requestUrl) {
  // 执行请求
  jQuery.getJSON(requestUrl, function(responseData) {
    // ...
  });
}

注意我们在使用JQuery Ajax回调(我们假设你应该已经知道了)来处理异步请求。但是等一下,FRP是用来处理异步数据流的。那个请求对应的相应不能是一个包含“将来一段时间会到达数据”的流吗?嗯,在概念的层面上,看起来的确是这样,所以让我们来试一下。
requestStream.subscribe(function(requestUrl) {
// 执行请求
var responseStream = Rx.Observable.create(function (observer) {
jQuery.getJSON(requestUrl)
.done(function(response) { observer.onNext(response); })
.fail(function(jqXHR, status, error) { observer.onError(error); })
.always(function() { observer.onCompleted(); });
});

  responseStream.subscribe(function(response) {
    // 处理响应
  });
}

Rx.Observable.create()所做的就是通过显式通知每一个观察者(或者说是“订阅者”)数据事件(onNext())或者错误(onError())来创造你自己的流。我们刚刚所做的只是包装JQuery Ajax Promise。等一下,这是否说明一个Promise就是一个可观测对象呢?

image

是的。

可观测对象是一个Promise++。在Rx中你可以通过var stream = Rx.Observable.fromPromise(promise)很容易的把一个Promise转化为一个可观测对象,所以我们就这样用吧。唯一的区别在于,可观测对象并不与Promise/A+兼容,但是在概念上是没有冲突的。Promise是一个简单的带有单一广播值的可观测对象。FRP流相比起Promise而言允许很多返回值。

这样很棒,说明了FRP至少和Promise一样强大。如果你相信Promise有点大肆炒作了,那么留意一下FRP的能力。

现在回到我们的例子,如果你注意到了的话,我们在subscribe()中调用了另一个,这看起来就像是传说中的回调地狱。而且,创建responseStream是基于requestStream的。就像你之前听到的那样,在FRP中我们有在其他流以外简单的转换以及创建新的流的方式。我们正应该那样做。

你现在应该知道的一个基本的函数是map(f),它获取流A中的所有值,对其调用f(),然后产生出流B的一个对应的值。如果我们对我们的请求和响应那样做的话,我们就可以把请求URL映射到响应Promise(伪装成流)中。

var responseMetastream = requestStream
  .map(function(requestUrl) {
    return Rx.Observable.fromPromise(jQuery.getJSON(requestUrl));
  });

这样我们就创建了一个名为”metaStream”的野兽:一个流的流。不用惊慌,metastream就是一个流,并且它所广播的值是另一个流。你可以把它想象成是指针:每一个被广播的值是一个指向另一个流的指针。在我们的例子中,每一个请求URL被映射到一个指向包含了对应的响应Promise流的指针。

image

一个为响应创建的metaStream看起来让人疑惑,似乎并没有帮助到我们。我们只是想要一个简单的响应流,其中每一个广播的值是一个JSON对象,而不是一个JSON对象的Promise。过来和Flatmap先生问声好吧:它是一种将metaStream“平坦化”的map(),它通过把所有广播给“分支”流的东西广播给“主干”流来实现。Flatmap不是一种“修复”,metaStream也不是一个bug,他们都是用来处理FRP中异步响应的真正的工具。

var responseStream = requestStream
  .flatMap(function(requestUrl) {
    return Rx.Observable.fromPromise(jQuery.getJSON(requestUrl));
  });

image

好了,因为我们是根据请求流来定义响应流的,因此如果我们之后在请求流上有更多的的事件发生,那么我们会有对应的事件如预期一样在响应流上发生:

requestStream:  --a-----b--c------------|->
responseStream: -----A--------B-----C---|->

(小写字母是一个请求, 大写字母是其对应的响应)

这样最终我们就得到了一个响应流,我们可以用它来呈现我们所接收到的数据。

responseStream.subscribe(function(response) {
  // render `response` to the DOM however you wish
});

把所有的代码连接起来,现在我们有了:

var requestStream = Rx.Observable.returnValue('https://api.github.com/users');

var responseStream = requestStream
  .flatMap(function(requestUrl) {
    return Rx.Observable.fromPromise(jQuery.getJSON(requestUrl));
  });

responseStream.subscribe(function(response) {
  // render `response` to the DOM however you wish
});

#刷新按钮

我还没有提交,返回的JSON是一个100用户的列表。API只允许我们指定页的偏移量,不允许指定页的大小,因此我们仅仅使用了3个数据而浪费了其他97个。我们现在暂时可以忽略这个问题,因为之后我们会看到我们是如何缓存响应的。

每次刷新按钮被点击,请求流应该广播一个新的URL,以便于我们得到一个新的响应。我们需要两个东西:一个单击事件的流(任何东西都可以成为流),并且我们需要根据刷新点击流来改变请求流。好在RxJS自带了从事件监听器构造可观测对象的工具。

var refreshButton = document.querySelector('.refresh');
var refreshClickStream = Rx.Observable.fromEvent(refreshButton, 'click');

既然刷新点击事件自身并不包含任何API URL,我们需要把每一个点击映射到一个实际的URL。现在我们将请求流改变为映射到API端和随机偏移量参数的刷新点击流。

var requestStream = refreshClickStream
  .map(function() {
    var randomOffset = Math.floor(Math.random()*500);
    return 'https://api.github.com/users?since=' + randomOffset;
  });

因为我是如此的愚蠢,我没有任何自动化的测试,我刚刚弄坏了我们之前构造的功能。现在在加载时不会有请求发出了,它仅仅在刷新按钮单击时才会发出。呃!我需要这两个行为:请求会在刷新单击或者加载时被发出。

我们知道如何为我们两个功能构造不同的流:

var requestOnRefreshStream = refreshClickStream
  .map(function() {
    var randomOffset = Math.floor(Math.random()*500);
    return 'https://api.github.com/users?since=' + randomOffset;
  });

var startupRequestStream = Rx.Observable.returnValue('https://api.github.com/users');

但是现在我们如何才能把它们合二为一呢?嗯,有merge()这个函数。用图来解释,它是这样工作的:

stream A: ---a--------e-----o----->
stream B: -----B---C-----D-------->
          vvvvvvvvv merge vvvvvvvvv
          ---a-B---C--e--D--o----->

现在它应该变得简单了:

var requestOnRefreshStream = refreshClickStream
  .map(function() {
    var randomOffset = Math.floor(Math.random()*500);
    return 'https://api.github.com/users?since=' + randomOffset;
  });

var startupRequestStream = Rx.Observable.returnValue('https://api.github.com/users');

var requestStream = Rx.Observable.merge(
  requestOnRefreshStream, startupRequestStream
);

这里有一个更干净的方式,不需要中间流:

var requestStream = refreshClickStream
  .map(function() {
    var randomOffset = Math.floor(Math.random()*500);
    return 'https://api.github.com/users?since=' + randomOffset;
  })
  .merge(Rx.Observable.returnValue('https://api.github.com/users'));

更短一些,可读性更高一些:

var requestStream = refreshClickStream
  .map(function() {
    var randomOffset = Math.floor(Math.random()*500);
    return 'https://api.github.com/users?since=' + randomOffset;
  })
  .startWith('https://api.github.com/users');

startWith()方法按你所想象的那样工作。无论你的输入流看起来什么样,startWith(x)的输出流会在开始包含x,但是我并没有足够的DRY(Don’t Repeat Yourself),我重复了API的字符串。一种修复它的方式是将startWith()移动到refreshClickStream的附近,其本质就是在加载时“模拟”一次刷新操作。

var requestStream = refreshClickStream.startWith('startup click')
  .map(function() {
    var randomOffset = Math.floor(Math.random()*500);
    return 'https://api.github.com/users?since=' + randomOffset;
  });

好的,如果你现在返回我们之前“弄坏自动化测试”的地方,你应该看到两者唯一的区别在于我添加了startWith()

#用流为3个关注推荐建模
直到现在,我们仅仅在发生responseStream的subscribe()的呈现这一步触及到推荐UI元素。现在对于刷新按钮,我们有一个问题:只要你点击了“刷新”,当前的3个推荐并没有被清除。新的推荐仅仅在响应到达之后才能被获取,但是为了让UI看起来能好一些,我们需要在刷新按钮单击时清除当前的推荐。

refreshClickStream.subscribe(function() {
  // clear the 3 suggestion DOM elements 
});

不,别那么快,伙计。这样是不好的,因为饿哦们现在有两个能影响推荐DOM元素的订阅者(另一个是responseStream.subscribe()),并且听起来这并没有真正做到关注点隔离。还记得FRP的咒语吗?

image

因此让我们将推荐建模成一个流,其中每一个被广播的值是一个包含推荐数据的JSON对象。我们会分别为3个推荐做这件事情。我们为推荐1所做的流看起来像是这样:

var suggestion1Stream = responseStream
  .map(function(listUsers) {
    // get one random user from the list
    return listUsers[Math.floor(Math.random()*listUsers.length)];
  });

其他的suggestion2Streamsuggestion3Stream可以简单的从suggestion1Stream拷贝过来。这违反了DRY原则,但是这能够让我们的教程示例更简单。而且我任何思考如何避免这种情况是一个很好的锻炼。

不像原来那样在responseStream的subscribe()中进行呈现,我们在这里做:

suggestion1Stream.subscribe(function(suggestion) {
  // render the 1st suggestion to the DOM
});

回到“刷新时,清空推荐”,我们可以将刷新点击映射到空的推荐数据,并在suggestion1Stream包含它,像这样:

var suggestion1Stream = responseStream
  .map(function(listUsers) {
    // get one random user from the list
    return listUsers[Math.floor(Math.random()*listUsers.length)];
  })
  .merge(
    refreshClickStream.map(function(){ return null; })
  );

当我们呈现时,我们将空解释为“没有数据”,这样来隐藏其UI元素。

suggestion1Stream.subscribe(function(suggestion) {
  if (suggestion === null) {
    // hide the first suggestion DOM element
  }
  else {
    // show the first suggestion DOM element
    // and render the data
  }
});

现在蓝图是这样:

refreshClickStream: ----------o--------o---->
     requestStream: -r--------r--------r---->
    responseStream: ----R---------R------R-->   
 suggestion1Stream: ----s-----N---s----N-s-->
 suggestion2Stream: ----q-----N---q----N-q-->
 suggestion3Stream: ----t-----N---t----N-t-->

其中Nnull的意思

为了做得更好,我们也可以在启动时呈现“空”的推荐。这是通过为推荐流添加startWith(null)来做到的:

var suggestion1Stream = responseStream
  .map(function(listUsers) {
    // get one random user from the list
    return listUsers[Math.floor(Math.random()*listUsers.length)];
  })
  .merge(
    refreshClickStream.map(function(){ return null; })
  )
  .startWith(null);

其结果是:

refreshClickStream: ----------o---------o---->
     requestStream: -r--------r---------r---->
    responseStream: ----R----------R------R-->   
 suggestion1Stream: -N--s-----N----s----N-s-->
 suggestion2Stream: -N--q-----N----q----N-q-->
 suggestion3Stream: -N--t-----N----t----N-t-->

#关闭推荐并使用被缓存的响应
我们还有一个功能需要实现:每一个推荐都应该有一个x按钮来关闭它,并在原地加载另一个推荐。乍一想,你可能想说,每个按钮被点击时发送一个新的请求没什么问题:

var close1Button = document.querySelector('.close1');
var close1ClickStream = Rx.Observable.fromEvent(close1Button, 'click');
// and the same for close2Button and close3Button

var requestStream = refreshClickStream.startWith('startup click')
  .merge(close1ClickStream) // we added this
  .map(function() {
    var randomOffset = Math.floor(Math.random()*500);
    return 'https://api.github.com/users?since=' + randomOffset;
  });

这没办法工作。它会关闭并重新加载所有的推荐,而不只是我们点击的那一个。我们有非常多的方式解决这个问题,为了保持这件事情很有趣,我们通过重用之前的响应来解决这个问题。API的页大小为100而我们之前仅仅使用了3个,因此我们还有非常多的新鲜数据可以使用,而不需要更多的请求。

再一次的,让我们用流来思考。当一个close1点击事件发生,我们希望使用最近一次被responseStream广播的响应来从响应列表中得到一个随机的用户。像这样:

    requestStream: --r--------------->
   responseStream: ------R----------->
close1ClickStream: ------------c----->
suggestion1Stream: ------s-----s----->

在Rx*中有一个连接符函数被称为combineLatest似乎能解决我们的需求。它包含两个流A和B作为输入,当其中任何一个流广播一个值,combineLatest把两个流中最近被广播的值连接起来,并输出一个结果c = f(x,y),其中f是一个你定义的函数。用图表来看更容易解释:

stream A: --a-----------e--------i-------->
stream B: -----b----c--------d-------q---->
          vvvvvvvv combineLatest(f) vvvvvvv
          ----AB---AC--EC---ED--ID--IQ---->

f是大写函数

我们可以在close1ClickStreamresponseStream上使用combineLatest(),因此每当第一个关闭按钮被点击时,我们能广播最近一次的响应并为suggestion1Stream创建一个新的值。另一方面,combineLatest()是对称的:每当一个新的响应被广播到responseStream时,它会结合最近一次的“close 1”点击事件来创建一个新的推荐。这非常有趣,因为它允许我们简化之前suggestion1Stream的代码,如下:

var suggestion1Stream = close1ClickStream
  .combineLatest(responseStream,             
    function(click, listUsers) {
      return listUsers[Math.floor(Math.random()*listUsers.length)];
    }
  )
  .merge(
    refreshClickStream.map(function(){ return null; })
  )
  .startWith(null);

还有一个未解的难题。combineLatest()使用最近的两个来源,但是如果其中一个源还未广播任何值,那么combineLatest()不能向输出流创建数据事件。如果你看看上面的ASCII表格,你会发现当第一个流广播值a的时候,我们没有任何输出。直到第二个流广播了一个值b之后我们才得到第一个输出值。

解决之道有很多,我们使用最简单的。即,在开始时模拟一次对”close 1”的点击:

var suggestion1Stream = close1ClickStream.startWith('startup click') // we added this
  .combineLatest(responseStream,             
    function(click, listUsers) {l
      return listUsers[Math.floor(Math.random()*listUsers.length)];
    }
  )
  .merge(
    refreshClickStream.map(function(){ return null; })
  )
  .startWith(null);

#总结

你得到的所有代码如下:

var refreshButton = document.querySelector('.refresh');
var refreshClickStream = Rx.Observable.fromEvent(refreshButton, 'click');

var closeButton1 = document.querySelector('.close1');
var close1ClickStream = Rx.Observable.fromEvent(closeButton1, 'click');
// and the same logic for close2 and close3

var requestStream = refreshClickStream.startWith('startup click')
  .map(function() {
    var randomOffset = Math.floor(Math.random()*500);
    return 'https://api.github.com/users?since=' + randomOffset;
  });

var responseStream = requestStream
  .flatMap(function (requestUrl) {
    return Rx.Observable.fromPromise($.ajax({url: requestUrl}));
  });

var suggestion1Stream = close1ClickStream.startWith('startup click')
  .combineLatest(responseStream,             
    function(click, listUsers) {
  return listUsers[Math.floor(Math.random()*listUsers.length)];
}
  )
  .merge(
    refreshClickStream.map(function(){ return null; })
  )
  .startWith(null);
// and the same logic for suggestion2Stream and suggestion3Stream

suggestion1Stream.subscribe(function(suggestion) {
  if (suggestion === null) {
    // hide the first suggestion DOM element
  }
  else {
    // show the first suggestion DOM element
    // and render the data    
  }
});

你可以在这里找到这个工作示例:http://jsfiddle.net/staltz/8jFJH/48/

这段代码虽短但是很密集:它包括通过适当分析关注点来管理多种事件,甚至还包括了响应的缓存。函数式风格使代码看起来更像陈述而非命令。我们并不是给定一串指令去执行,我们只是通过定义流之间的关系来完成。比如,我们使用FRP告诉程序suggestion1Streamclose 1流与最近一次响应用户的结合,同时,当刷新或者程序开始发生时将其置为null

同时,还要注意,代码中包含极少量的逻辑控制语句例如if, for, while,同时也没有JavaScript应用中常见的回调风格的控制流,这非常令人印象深刻。如果你想的话,你甚至可以在上述subscribe()中完全移除ifelse而使用filter()(我将把实现细节留给你做练习)。在FRP中,我们有与流相关的函数例如map, filter, scan, merge, combineLatest, startWith,以及更多的控制事件驱动程序流程的函数。这个函数工具集允许你完成更多功能而使用更少的代码。

#接下来的事情

如果你认为Rx*会成为你FRP的首选库,你需要花点时间来了解非常长的函数列表函数列表包括:变形、合并以及创建Observable。如果你想要通过流的图表来理解这些函数,看一看RxJava的非常有用的文档。无论什么时候你陷入困境,试着画出这些图,思考他们,看看这一长串的函数列表,然后更多的思考。以我的经验来看这个工作方法非常有效。

一旦你开始使用Rx*开始编程,你绝对需要理解Cold vs Hot Observables的内容。如果你忽略了它,它早晚会回来痛咬你的。我已经警告过你了。通过学习函数式编程进一步提高你的技巧,并熟悉影响Rx*的副作用。

然而函数响应式编程并不仅仅是Rx*。你可以使用Bacon.js来直观的使用它,而不需要理会那些在使用Rx*时会遇到的奇怪的问题。Elm语言,则是自成一派:它是一个可以编译成JavaScript + HTML + CSS的FRP语言,还有一个时间浏览的调试器。它非常棒。

FRP非常适合重事件的前端程序或应用程序。但是这并不仅仅是客户端的事情。它在后端以及贴近数据库的场景也能有很好的发挥。事实上,RxJava在Netflix的API中是一个允许服务器端进行并发执行的重要组件。FRP不是一个局限于特定类型的应用程序或者语言的框架。它是一个你在编写任何事件驱动软件时都可以使用的范例。

如果这篇文章帮到了你,记得来Twitter转发~