[TOC]
# 第 6 章: 示例應(yīng)用
## 聲明式代碼
我們要開(kāi)始轉(zhuǎn)變觀念了,從本章開(kāi)始,我們將不再指示計(jì)算機(jī)如何工作,而是指出我們明確希望得到的結(jié)果。我敢保證,這種做法與那種需要時(shí)刻關(guān)心所有細(xì)節(jié)的命令式編程相比,會(huì)讓你輕松許多。
與命令式不同,聲明式意味著我們要寫表達(dá)式,而不是一步一步的指示。
以 SQL 為例,它就沒(méi)有“先做這個(gè),再做那個(gè)”的命令,有的只是一個(gè)指明我們想要從數(shù)據(jù)庫(kù)取什么數(shù)據(jù)的表達(dá)式。至于如何取數(shù)據(jù)則是由它自己決定的。以后數(shù)據(jù)庫(kù)升級(jí)也好,SQL 引擎優(yōu)化也好,根本不需要更改查詢語(yǔ)句。這是因?yàn)椋卸喾N方式解析一個(gè)表達(dá)式并得到相同的結(jié)果。
對(duì)包括我在內(nèi)的一些人來(lái)說(shuō),一開(kāi)始是不太容易理解“聲明式”這個(gè)概念的;所以讓我們寫幾個(gè)例子找找感覺(jué)。
```js
// 命令式
var makes = [];
for (i = 0; i < cars.length; i++) {
makes.push(cars[i].make);
}
// 聲明式
var makes = cars.map(function(car){ return car.make; });
```
命令式的循環(huán)要求你必須先實(shí)例化一個(gè)數(shù)組,而且執(zhí)行完這個(gè)實(shí)例化語(yǔ)句之后,解釋器才繼續(xù)執(zhí)行后面的代碼。然后再直接迭代 `cars` 列表,手動(dòng)增加計(jì)數(shù)器,把各種零零散散的東西都展示出來(lái)...實(shí)在是直白得有些露骨。
使用 `map` 的版本是一個(gè)表達(dá)式,它對(duì)執(zhí)行順序沒(méi)有要求。而且,`map` 函數(shù)如何進(jìn)行迭代,返回的數(shù)組如何收集,都有很大的自由度。它指明的是`做什么`,不是`怎么做`。因此,它是正兒八經(jīng)的聲明式代碼。
除了更加清晰和簡(jiǎn)潔之外,`map` 函數(shù)還可以進(jìn)一步優(yōu)化,這么一來(lái)我們寶貴的應(yīng)用代碼就無(wú)須改動(dòng)了。
至于那些說(shuō)“雖然如此,但使用命令式循環(huán)速度要快很多”的人,我建議你們先去學(xué)學(xué) JIT 優(yōu)化代碼的相關(guān)知識(shí)。這里有一個(gè)[非常棒的視頻](https://www.youtube.com/watch?v=65-RbBwZQdU),可能會(huì)對(duì)你有幫助。
再看一個(gè)例子。
```js
// 命令式
var authenticate = function(form) {
var user = toUser(form);
return logIn(user);
};
// 聲明式
var authenticate = compose(logIn, toUser);
```
雖然命令式的版本并不一定就是錯(cuò)的,但還是硬編碼了那種一步接一步的執(zhí)行方式。而 `compose` 表達(dá)式只是簡(jiǎn)單地指出了這樣一個(gè)事實(shí):用戶驗(yàn)證是 `toUser` 和 `logIn` 兩個(gè)行為的組合。這再次說(shuō)明,聲明式為潛在的代碼更新提供了支持,使得我們的應(yīng)用代碼成為了一種高級(jí)規(guī)范(high level specification)。
因?yàn)槁暶魇酱a不指定執(zhí)行順序,所以它天然地適合進(jìn)行并行運(yùn)算。它與純函數(shù)一起解釋了為何函數(shù)式編程是未來(lái)并行計(jì)算的一個(gè)不錯(cuò)選擇——我們真的不需要做什么就能實(shí)現(xiàn)一個(gè)并行/并發(fā)系統(tǒng)。
## 一個(gè)函數(shù)式的 flickr
現(xiàn)在我們以一種聲明式的、可組合的方式創(chuàng)建一個(gè)示例應(yīng)用。暫時(shí)我們還是會(huì)作點(diǎn)小弊,使用副作用;但我們會(huì)把副作用的程度降到最低,讓它們與純函數(shù)代碼分離開(kāi)來(lái)。這個(gè)示例應(yīng)用是一個(gè)瀏覽器 widget,功能是從 flickr 獲取圖片并在頁(yè)面上展示。我們從寫 html 開(kāi)始:
```html
<!DOCTYPE html>
<html>
<head>
<script src="https://cdnjs.cloudflare.com/ajax/libs/require.js/2.1.11/require.min.js"></script>
<script src="flickr.js"></script>
</head>
<body></body>
</html>
```
flickr.js 如下:
```js
requirejs.config({
paths: {
ramda: 'https://cdnjs.cloudflare.com/ajax/libs/ramda/0.13.0/ramda.min',
jquery: 'https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min'
}
});
require([
'ramda',
'jquery'
],
function (_, $) {
var trace = _.curry(function(tag, x) {
console.log(tag, x);
return x;
});
// app goes here
});
```
這里我們使用了 [ramda](http://ramdajs.com) ,沒(méi)有用 lodash 或者其他類庫(kù)。ramda 提供了 `compose`、`curry` 等很多函數(shù)。模塊加載我們選擇的是 requirejs,我以前用過(guò) requirejs,雖然它有些重,但為了保持一致性,本書將一直使用它。另外,我也把 `trace` 函數(shù)寫好了,便于 debug。
有點(diǎn)跑題了。言歸正傳,我們的應(yīng)用將做 4 件事:
1. 根據(jù)特定搜索關(guān)鍵字構(gòu)造 url
2. 向 flickr 發(fā)送 api 請(qǐng)求
3. 把返回的 json 轉(zhuǎn)為 html 圖片
4. 把圖片放到屏幕上
注意到?jīng)]?上面提到了兩個(gè)不純的動(dòng)作,即從 flickr 的 api 獲取數(shù)據(jù)和在屏幕上放置圖片這兩件事。我們先來(lái)定義這兩個(gè)動(dòng)作,這樣就能隔離它們了。
```js
var Impure = {
getJSON: _.curry(function(callback, url) {
$.getJSON(url, callback);
}),
setHtml: _.curry(function(sel, html) {
$(sel).html(html);
})
};
```
這里只是簡(jiǎn)單地包裝了一下 jQuery 的 `getJSON` 方法,把它變?yōu)橐粋€(gè) curry 函數(shù),還有就是把參數(shù)位置也調(diào)換了下。這些方法都在 `Impure` 命名空間下,這樣我們就知道它們都是危險(xiǎn)函數(shù)。在后面的例子中,我們會(huì)把這兩個(gè)函數(shù)變純。
下一步是構(gòu)造 url 傳給 `Impure.getJSON` 函數(shù)。
```js
var url = function (term) {
return 'https://api.flickr.com/services/feeds/photos_public.gne?tags=' + term + '&format=json&jsoncallback=?';
};
```
借助 monoid 或 combinator (后面會(huì)講到這些概念),我們可以使用一些奇技淫巧來(lái)讓 `url` 函數(shù)變?yōu)?pointfree 函數(shù)。但是為了可讀性,我們還是選擇以普通的非 pointfree 的方式拼接字符串。
讓我們寫一個(gè) `app` 函數(shù)發(fā)送請(qǐng)求并把內(nèi)容放置到屏幕上。
```js
var app = _.compose(Impure.getJSON(trace("response")), url);
app("cats");
```
這會(huì)調(diào)用 `url` 函數(shù),然后把字符串傳給 `getJSON` 函數(shù)。`getJSON` 已經(jīng)局部應(yīng)用了 `trace`,加載這個(gè)應(yīng)用將會(huì)把請(qǐng)求的響應(yīng)顯示在 console 里。

我們想要從這個(gè) json 里構(gòu)造圖片,看起來(lái) src 都在 `items` 數(shù)組中的每個(gè) `media` 對(duì)象的 `m` 屬性上。
不管怎樣,我們可以使用 ramda 的一個(gè)通用 getter 函數(shù) `_.prop()` 來(lái)獲取這些嵌套的屬性。不過(guò)為了讓你明白這個(gè)函數(shù)做了什么事情,我們自己實(shí)現(xiàn)一個(gè) prop 看看:
```js
var prop = _.curry(function(property, object){
return object[property];
});
```
實(shí)際上這有點(diǎn)傻,僅僅是用 `[]` 來(lái)獲取一個(gè)對(duì)象的屬性而已。讓我們利用這個(gè)函數(shù)獲取圖片的 src。
```js
var mediaUrl = _.compose(_.prop('m'), _.prop('media'));
var srcs = _.compose(_.map(mediaUrl), _.prop('items'));
```
一旦得到了 `items`,就必須使用 `map` 來(lái)分解每一個(gè) url;這樣就得到了一個(gè)包含所有 src 的數(shù)組。把它和 `app` 聯(lián)結(jié)起來(lái),打印結(jié)果看看。
```js
var renderImages = _.compose(Impure.setHtml("body"), srcs);
var app = _.compose(Impure.getJSON(renderImages), url);
```
這里所做的只不過(guò)是新建了一個(gè)組合,這個(gè)組合會(huì)調(diào)用 `srcs` 函數(shù),并把返回結(jié)果設(shè)置為 body 的 html。我們也把 `trace` 替換為了 `renderImages`,因?yàn)橐呀?jīng)有了除原始 json 以外的數(shù)據(jù)。這將會(huì)粗暴地把所有的 src 直接顯示在屏幕上。
最后一步是把這些 src 變?yōu)檎嬲膱D片。對(duì)大型點(diǎn)的應(yīng)用來(lái)說(shuō),是應(yīng)該使用類似 Handlebars 或者 React 這樣的 template/dom 庫(kù)來(lái)做這件事的。但我們這個(gè)應(yīng)用太小了,只需要一個(gè) img 標(biāo)簽,所以用 jQuery 就好了。
```js
var img = function (url) {
return $('<img />', { src: url });
};
```
jQuery 的 `html()` 方法接受標(biāo)簽數(shù)組為參數(shù),所以我們只須把 src 轉(zhuǎn)換為 img 標(biāo)簽然后傳給 `setHtml` 即可。
```js
var images = _.compose(_.map(img), srcs);
var renderImages = _.compose(Impure.setHtml("body"), images);
var app = _.compose(Impure.getJSON(renderImages), url);
```
任務(wù)完成!

下面是完整代碼:
```js
requirejs.config({
paths: {
ramda: 'https://cdnjs.cloudflare.com/ajax/libs/ramda/0.13.0/ramda.min',
jquery: 'https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min'
}
});
require([
'ramda',
'jquery'
],
function (_, $) {
////////////////////////////////////////////
// Utils
var Impure = {
getJSON: _.curry(function(callback, url) {
$.getJSON(url, callback);
}),
setHtml: _.curry(function(sel, html) {
$(sel).html(html);
})
};
var img = function (url) {
return $('<img />', { src: url });
};
var trace = _.curry(function(tag, x) {
console.log(tag, x);
return x;
});
////////////////////////////////////////////
var url = function (t) {
return 'https://api.flickr.com/services/feeds/photos_public.gne?tags=' + t + '&format=json&jsoncallback=?';
};
var mediaUrl = _.compose(_.prop('m'), _.prop('media'));
var srcs = _.compose(_.map(mediaUrl), _.prop('items'));
var images = _.compose(_.map(img), srcs);
var renderImages = _.compose(Impure.setHtml("body"), images);
var app = _.compose(Impure.getJSON(renderImages), url);
app("cats");
});
```
看看,多么美妙的聲明式規(guī)范啊,只說(shuō)做什么,不說(shuō)怎么做?,F(xiàn)在我們可以把每一行代碼都視作一個(gè)等式,變量名所代表的屬性就是等式的含義。我們可以利用這些屬性去推導(dǎo)分析和重構(gòu)這個(gè)應(yīng)用。
## 有原則的重構(gòu)
上面的代碼是有優(yōu)化空間的——我們獲取 url map 了一次,把這些 url 變?yōu)?img 標(biāo)簽又 map 了一次。關(guān)于 map 和組合是有定律的:
```js
// map 的組合律
var law = compose(map(f), map(g)) == map(compose(f, g));
```
我們可以利用這個(gè)定律優(yōu)化代碼,進(jìn)行一次有原則的重構(gòu)。
```js
// 原有代碼
var mediaUrl = _.compose(_.prop('m'), _.prop('media'));
var srcs = _.compose(_.map(mediaUrl), _.prop('items'));
var images = _.compose(_.map(img), srcs);
```
感謝等式推導(dǎo)(equational reasoning)及純函數(shù)的特性,我們可以內(nèi)聯(lián)調(diào)用 `srcs` 和 `images`,也就是把 map 調(diào)用排列起來(lái)。
```js
var mediaUrl = _.compose(_.prop('m'), _.prop('media'));
var images = _.compose(_.map(img), _.map(mediaUrl), _.prop('items'));
```
把 `map` 排成一列之后就可以應(yīng)用組合律了。
```js
var mediaUrl = _.compose(_.prop('m'), _.prop('media'));
var images = _.compose(_.map(_.compose(img, mediaUrl)), _.prop('items'));
```
現(xiàn)在只需要循環(huán)一次就可以把每一個(gè)對(duì)象都轉(zhuǎn)為 img 標(biāo)簽了。我們把 map 調(diào)用的 compose 取出來(lái)放到外面,提高一下可讀性。
```js
var mediaUrl = _.compose(_.prop('m'), _.prop('media'));
var mediaToImg = _.compose(img, mediaUrl);
var images = _.compose(_.map(mediaToImg), _.prop('items'));
```
## 總結(jié)
我們已經(jīng)見(jiàn)識(shí)到如何在一個(gè)小而不失真實(shí)的應(yīng)用中運(yùn)用新技能了,也已經(jīng)使用過(guò)函數(shù)式這個(gè)“數(shù)學(xué)框架”來(lái)推導(dǎo)和重構(gòu)代碼了。但是異常處理以及代碼分支呢?如何讓整個(gè)應(yīng)用都是函數(shù)式的,而不僅僅是把破壞性的函數(shù)放到命名空間下?如何讓應(yīng)用更安全更富有表現(xiàn)力?這些都是本書第 2 部分將要解決的問(wèn)題。
[第 7 章: Hindley-Milner 類型簽名](ch7.md)
