[TOC]
# 特百惠
(譯者注:特百惠是美國家居用品品牌,代表產品是塑料容器。)
## 強大的容器

我們已經知道如何書寫函數式的程序了,即通過管道把數據在一系列純函數間傳遞的程序。我們也知道了,這些程序就是聲明式的行為規范。但是,控制流(control flow)、異常處理(error handling)、異步操作(asynchronous actions)和狀態(state)呢?還有更棘手的作用(effects)呢?本章將對上述這些抽象概念賴以建立的基礎作一番探究。
首先我們將創建一個容器(container)。這個容器必須能夠裝載任意類型的值;否則的話,像只能裝木薯布丁的密封塑料袋是沒什么用的。這個容器將會是一個對象,但我們不會為它添加面向對象觀念下的屬性和方法。是的,我們將把它當作一個百寶箱——一個存放寶貴的數據的特殊盒子。
```js
var Container = function(x) {
this.__value = x;
}
Container.of = function(x) { return new Container(x); };
```
這是本書的第一個容器,我們貼心地把它命名為 `Container`。我們將使用 `Container.of` 作為構造器(constructor),這樣就不用到處去寫糟糕的 `new` 關鍵字了,非常省心。實際上不能這么簡單地看待 `of` 函數,但暫時先認為它是把值放到容器里的一種方式。
我們來檢驗下這個嶄新的盒子:
```js
Container.of(3)
//=> Container(3)
Container.of("hotdogs")
//=> Container("hotdogs")
Container.of(Container.of({name: "yoda"}))
//=> Container(Container({name: "yoda" }))
```
如果用的是 node,那么你會看到打印出來的是 `{__value: x}`,而不是實際值 `Container(x)`;Chrome 打印出來的是正確的。不過這并不重要,只要你理解 `Container` 是什么樣的就行了。有些環境下,你也可以重寫 `inspect` 方法,但我們不打算涉及這方面的知識。在本書中,出于教學和美學上的考慮,我們將把概念性的輸出都寫成好像 `inspect` 被重寫了的樣子,因為這樣寫的教育意義將遠遠大于 `{__value: x}`。
在繼續后面的內容之前,先澄清幾點:
* `Container` 是個只有一個屬性的對象。盡管容器可以有不止一個的屬性,但大多數容器還是只有一個。我們很隨意地把 `Container` 的這個屬性命名為 `__value`。
* `__value` 不能是某個特定的類型,不然 `Container` 就對不起它這個名字了。
* 數據一旦存放到 `Container`,就會一直待在那兒。我們*可以*用 `.__value` 獲取到數據,但這樣做有悖初衷。
如果把容器想象成玻璃罐的話,上面這三條陳述的理由就會比較清晰了。但是暫時,請先保持耐心。
## 第一個 functor
一旦容器里有了值,不管這個值是什么,我們就需要一種方法來讓別的函數能夠操作它。
```js
// (a -> b) -> Container a -> Container b
Container.prototype.map = function(f){
return Container.of(f(this.__value))
}
```
這個 `map` 跟數組那個著名的 `map` 一樣,除了前者的參數是 `Container a` 而后者是 `[a]`。它們的使用方式也幾乎一致:
```js
Container.of(2).map(function(two){ return two + 2 })
//=> Container(4)
Container.of("flamethrowers").map(function(s){ return s.toUpperCase() })
//=> Container("FLAMETHROWERS")
Container.of("bombs").map(concat(' away')).map(_.prop('length'))
//=> Container(10)
```
為什么要使用這樣一種方法?因為我們能夠在不離開 `Container` 的情況下操作容器里面的值。這是非常了不起的一件事情。`Container` 里的值傳遞給 `map` 函數之后,就可以任我們操作;操作結束后,為了防止意外再把它放回它所屬的 `Container`。這樣做的結果是,我們能連續地調用 `map`,運行任何我們想運行的函數。甚至還可以改變值的類型,就像上面最后一個例子中那樣。
等等,如果我們能一直調用 `map`,那它不就是個組合(composition)么!這里邊是有什么數學魔法在起作用?是 *functor*。各位,這個數學魔法就是 *functor*。
> functor 是實現了 `map` 函數并遵守一些特定規則的容器類型。
沒錯,*functor* 就是一個簽了合約的接口。我們本來可以簡單地把它稱為 `Mappable`,但現在為時已晚,哪怕 *functor* 一點也不 *fun*。functor 是范疇學里的概念,我們將在本章末尾詳細探索與此相關的數學知識;暫時我們先用這個名字很奇怪的接口做一些不那么理論的、實用性的練習。
把值裝進一個容器,而且只能使用 `map` 來處理它,這么做的理由到底是什么呢?如果我們換種方式來問,答案就很明顯了:讓容器自己去運用函數能給我們帶來什么好處?答案是抽象,對于函數運用的抽象。當 `map` 一個函數的時候,我們請求容器來運行這個函數。不夸張地講,這是一種十分強大的理念。
## 薛定諤的 Maybe

說實話 `Container` 挺無聊的,而且通常我們稱它為 `Identity`,與 `id` 函數的作用相同(這里也是有數學上的聯系的,我們會在適當時候加以說明)。除此之外,還有另外一種 functor,那就是實現了 `map` 函數的類似容器的數據類型,這種 functor 在調用 `map` 的時候能夠提供非常有用的行為。現在讓我們來定義一個這樣的 functor。
```js
var Maybe = function(x) {
this.__value = x;
}
Maybe.of = function(x) {
return new Maybe(x);
}
Maybe.prototype.isNothing = function() {
return (this.__value === null || this.__value === undefined);
}
Maybe.prototype.map = function(f) {
return this.isNothing() ? Maybe.of(null) : Maybe.of(f(this.__value));
}
```
`Maybe` 看起來跟 `Container` 非常類似,但是有一點不同:`Maybe` 會先檢查自己的值是否為空,然后才調用傳進來的函數。這樣我們在使用 `map` 的時候就能避免惱人的空值了(注意這個實現出于教學目的做了簡化)。
```js
Maybe.of("Malkovich Malkovich").map(match(/a/ig));
//=> Maybe(['a', 'a'])
Maybe.of(null).map(match(/a/ig));
//=> Maybe(null)
Maybe.of({name: "Boris"}).map(_.prop("age")).map(add(10));
//=> Maybe(null)
Maybe.of({name: "Dinah", age: 14}).map(_.prop("age")).map(add(10));
//=> Maybe(24)
```
注意看,當傳給 `map` 的值是 `null` 時,代碼并沒有爆出錯誤。這是因為每一次 `Maybe` 要調用函數的時候,都會先檢查它自己的值是否為空。
這種點記法(dot notation syntax)已經足夠函數式了,但是正如在第 1 部分指出的那樣,我們更想保持一種 pointfree 的風格。碰巧的是,`map` 完全有能力以 curry 函數的方式來“代理”任何 functor:
```js
// map :: Functor f => (a -> b) -> f a -> f b
var map = curry(function(f, any_functor_at_all) {
return any_functor_at_all.map(f);
});
```
這樣我們就可以像平常一樣使用組合,同時也能正常使用 `map` 了,非常振奮人心。ramda 的 `map` 也是這樣。后面的章節中,我們將在點記法更有教育意義的時候使用點記法,在方便使用 pointfree 模式的時候就用 pointfree。你注意到了么?我在類型標簽中偷偷引入了一個額外的標記:`Functor f =>`。這個標記告訴我們 `f` 必須是一個 functor。沒什么復雜的,但我覺得有必要提一下。
## 用例
實際當中,`Maybe` 最常用在那些可能會無法成功返回結果的函數中。
```js
// safeHead :: [a] -> Maybe(a)
var safeHead = function(xs) {
return Maybe.of(xs[0]);
};
var streetName = compose(map(_.prop('street')), safeHead, _.prop('addresses'));
streetName({addresses: []});
// Maybe(null)
streetName({addresses: [{street: "Shady Ln.", number: 4201}]});
// Maybe("Shady Ln.")
```
`safeHead` 與一般的 `_.head` 類似,但是增加了類型安全保證。引入 `Maybe` 會發生一件非常有意思的事情,那就是我們被迫要與狡猾的 `null` 打交道了。`safeHead` 函數能夠誠實地預告它可能的失敗——失敗真沒什么可恥的——然后返回一個 `Maybe` 來通知我們相關信息。實際上不僅僅是*通知*,因為畢竟我們想要的值深藏在 `Maybe` 對象中,而且只能通過 `map` 來操作它。本質上,這是一種由 `safeHead` 強制執行的空值檢查。有了這種檢查,我們才能在夜里安然入睡,因為我們知道最不受人待見的 `null` 不會突然出現。類似這樣的 API 能夠把一個像紙糊起來的、脆弱的應用升級為實實在在的、健壯的應用,這樣的 API 保證了更加安全的軟件。
有時候函數可以明確返回一個 `Maybe(null)` 來表明失敗,例如:
```js
// withdraw :: Number -> Account -> Maybe(Account)
var withdraw = curry(function(amount, account) {
return account.balance >= amount ?
Maybe.of({balance: account.balance - amount}) :
Maybe.of(null);
});
// finishTransaction :: Account -> String
var finishTransaction = compose(remainingBalance, updateLedger); // <- 假定這兩個函數已經在別處定義好了
// getTwenty :: Account -> Maybe(String)
var getTwenty = compose(map(finishTransaction), withdraw(20));
getTwenty({ balance: 200.00});
// Maybe("Your balance is $180.00")
getTwenty({ balance: 10.00});
// Maybe(null)
```
要是錢不夠,`withdraw` 就會對我們嗤之以鼻然后返回一個 `Maybe(null)`。`withdraw` 也顯示出了它的多變性,使得我們后續的操作只能用 `map` 來進行。這個例子與前面例子不同的地方在于,這里的 `null` 是有意的。我們不用 `Maybe(String)` ,而是用 `Maybe(null)` 來發送失敗的信號,這樣程序在收到信號后就能立刻停止執行。這一點很重要:如果 `withdraw` 失敗了,`map` 就會切斷后續代碼的執行,因為它根本就不會運行傳遞給它的函數,即 `finishTransaction`。這正是預期的效果:如果取款失敗,我們并不想更新或者顯示賬戶余額。
## 釋放容器里的值
人們經常忽略的一個事實是:任何事物都有個最終盡頭。那些會產生作用的函數,不管它們是發送 JSON 數據,還是在屏幕上打印東西,還是更改文件系統,還是別的什么,都要有一個結束。但是我們無法通過 `return` 把輸出傳遞到外部世界,必須要運行這樣或那樣的函數才能傳遞出去。關于這一點,可以借用禪宗公案的口吻來敘述:“如果一個程序運行之后沒有可觀察到的作用,那它到底運行了沒有?”。或者,運行之后達到自身的目的了沒有?有可能它只是浪費了幾個 CPU 周期然后就去睡覺了...
應用程序所做的工作就是獲取、更改和保存數據直到不再需要它們,對數據做這些操作的函數有可能被 `map` 調用,這樣的話數據就可以不用離開它溫暖舒適的容器。諷刺的是,有一種常見的錯誤就是試圖以各種方法刪除 `Maybe` 里的值,好像這個不確定的值是魔鬼,刪除它就能讓它突然顯形,然后一切罪惡都會得到寬恕似的(譯者注:此處原文應該是源自圣經)。要知道,我們的值沒有完成它的使命,很有可能是其他代碼分支造成的。我們的代碼,就像薛定諤的貓一樣,在某個特定的時間點有兩種狀態,而且應該保持這種狀況不變直到最后一個函數為止。這樣,哪怕代碼有很多邏輯性的分支,也能保證一種線性的工作流。
不過,對容器里的值來說,還是有個逃生口可以出去。也就是說,如果我們想返回一個自定義的值然后還能繼續執行后面的代碼的話,是可以做到的;要達到這一目的,可以借助一個幫助函數 `maybe`:
```js
// maybe :: b -> (a -> b) -> Maybe a -> b
var maybe = curry(function(x, f, m) {
return m.isNothing() ? x : f(m.__value);
});
// getTwenty :: Account -> String
var getTwenty = compose(
maybe("You're broke!", finishTransaction), withdraw(20)
);
getTwenty({ balance: 200.00});
// "Your balance is $180.00"
getTwenty({ balance: 10.00});
// "You're broke!"
```
這樣就可以要么返回一個靜態值(與 `finishTransaction` 返回值的類型一致),要么繼續愉快地在沒有 `Maybe` 的情況下完成交易。`maybe` 使我們得以避免普通 `map` 那種命令式的 `if/else` 語句:`if(x !== null) { return f(x) }`。
引入 `Maybe` 可能會在初期造成一些不適。Swift 和 Scala 用戶知道我在說什么,因為這兩門語言的核心庫里就有 `Maybe` 的概念,只不過偽裝成 `Option(al)` 罷了。被迫在任何情況下都進行空值檢查(甚至有些時候我們可以確定某個值不會為空),的確讓大部分人頭疼不已。然而隨著時間推移,空值檢查會成為第二本能,說不定你還會感激它提供的安全性呢。不管怎么說,空值檢查大多數時候都能防止在代碼邏輯上偷工減料,讓我們脫離危險。
編寫不安全的軟件就像用蠟筆小心翼翼地畫彩蛋,畫完之后把它們扔到大街上一樣(譯者注:意思是彩蛋非常易于尋找。來源于復活節習俗,人們會藏起一些彩蛋讓孩子尋找),或者像用三只小豬警告過的材料蓋個養老院一樣(譯者注:來源于“三只小豬”童話故事)。`Maybe` 能夠非常有效地幫助我們增加函數的安全性。
有一點我必須要提及,否則就太不負責任了,那就是 `Maybe` 的“真正”實現會把它分為兩種類型:一種是非空值,另一種是空值。這種實現允許我們遵守 `map` 的 parametricity 特性,因此 `null` 和 `undefined` 能夠依然被 `map` 調用,functor 里的值所需的那種普遍性條件也能得到滿足。所以你會經常看到 `Some(x) / None` 或者 `Just(x) / Nothing` 這樣的容器類型在做空值檢查,而不是 `Maybe`。
## “純”錯誤處理

說出來可能會讓你震驚,`throw/catch` 并不十分“純”。當一個錯誤拋出的時候,我們沒有收到返回值,反而是得到了一個警告!拋錯的函數吐出一大堆的 0 和 1 作為盾和矛來攻擊我們,簡直就像是在反擊輸入值的入侵而進行的一場電子大作戰。有了 `Either` 這個新朋友,我們就能以一種比向輸入值宣戰好得多的方式來處理錯誤,那就是返回一條非常禮貌的消息作為回應。我們來看一下:
```js
var Left = function(x) {
this.__value = x;
}
Left.of = function(x) {
return new Left(x);
}
Left.prototype.map = function(f) {
return this;
}
var Right = function(x) {
this.__value = x;
}
Right.of = function(x) {
return new Right(x);
}
Right.prototype.map = function(f) {
return Right.of(f(this.__value));
}
```
`Left` 和 `Right` 是我們稱之為 `Either` 的抽象類型的兩個子類。我略去了創建 `Either` 父類的繁文縟節,因為我們不會用到它的,但你了解一下也沒壞處。注意看,這里除了有兩個類型,沒別的新鮮東西。來看看它們是怎么運行的:
```js
Right.of("rain").map(function(str){ return "b"+str; });
// Right("brain")
Left.of("rain").map(function(str){ return "b"+str; });
// Left("rain")
Right.of({host: 'localhost', port: 80}).map(_.prop('host'));
// Right('localhost')
Left.of("rolls eyes...").map(_.prop("host"));
// Left('rolls eyes...')
```
`Left` 就像是青春期少年那樣無視我們要 `map` 它的請求。`Right` 的作用就像是一個 `Container`(也就是 Identity)。這里強大的地方在于,`Left` 有能力在它內部嵌入一個錯誤消息。
假設有一個可能會失敗的函數,就拿根據生日計算年齡來說好了。的確,我們可以用 `Maybe(null)` 來表示失敗并把程序引向另一個分支,但是這并沒有告訴我們太多信息。很有可能我們想知道失敗的原因是什么。用 `Either` 寫一個這樣的程序看看:
```js
var moment = require('moment');
// getAge :: Date -> User -> Either(String, Number)
var getAge = curry(function(now, user) {
var birthdate = moment(user.birthdate, 'YYYY-MM-DD');
if(!birthdate.isValid()) return Left.of("Birth date could not be parsed");
return Right.of(now.diff(birthdate, 'years'));
});
getAge(moment(), {birthdate: '2005-12-12'});
// Right(9)
getAge(moment(), {birthdate: '20010704'});
// Left("Birth date could not be parsed")
```
這么一來,就像 `Maybe(null)`,當返回一個 `Left` 的時候就直接讓程序短路。跟 `Maybe(null)` 不同的是,現在我們對程序為何脫離原先軌道至少有了一點頭緒。有一件事要注意,這里返回的是 `Either(String, Number)`,意味著我們這個 `Either` 左邊的值是 `String`,右邊(譯者注:也就是正確的值)的值是 `Number`。這個類型簽名不是很正式,因為我們并沒有定義一個真正的 `Either` 父類;但我們還是從這個類型那里了解到不少東西。它告訴我們,我們得到的要么是一條錯誤消息,要么就是正確的年齡值。
```js
// fortune :: Number -> String
var fortune = compose(concat("If you survive, you will be "), add(1));
// zoltar :: User -> Either(String, _)
var zoltar = compose(map(console.log), map(fortune), getAge(moment()));
zoltar({birthdate: '2005-12-12'});
// "If you survive, you will be 10"
// Right(undefined)
zoltar({birthdate: 'balloons!'});
// Left("Birth date could not be parsed")
```
如果 `birthdate` 合法,這個程序就會把它神秘的命運打印在屏幕上讓我們見證;如果不合法,我們就會收到一個有著清清楚楚的錯誤消息的 `Left`,盡管這個消息是穩穩當當地待在它的容器里的。這種行為就像,雖然我們在拋錯,但是是以一種平靜溫和的方式拋錯,而不是像一個小孩子那樣,有什么不對勁就鬧脾氣大喊大叫。
在這個例子中,我們根據 `birthdate` 的合法性來控制代碼的邏輯分支,同時又讓代碼進行從右到左的直線運動,而不用爬過各種條件語句的大括號。通常,我們不會把 `console.log` 放到 `zoltar` 函數里,而是在調用 `zoltar` 的時候才 `map` 它,不過本例中,讓你看看 `Right` 分支如何與 `Left` 不同也是很有幫助的。我們在 `Right` 分支的類型簽名中使用 `_` 表示一個應該忽略的值(在有些瀏覽器中,你必須要 `console.log.bind(console)` 才能把 `console.log` 當作一等公民使用)。
我想借此機會指出一件你可能沒注意到的事:這個例子中,盡管 `fortune` 使用了 `Either`,它對每一個 functor 到底要干什么卻是毫不知情的。前面例子中的 `finishTransaction` 也是一樣。通俗點來講,一個函數在調用的時候,如果被 `map` 包裹了,那么它就會從一個非 functor 函數轉換為一個 functor 函數。我們把這個過程叫做 *lift*。一般情況下,普通函數更適合操作普通的數據類型而不是容器類型,在必要的時候再通過 *lift* 變為合適的容器去操作容器類型。這樣做的好處是能得到更簡單、重用性更高的函數,它們能夠隨需求而變,兼容任意 functor。
`Either` 并不僅僅只對合法性檢查這種一般性的錯誤作用非凡,對一些更嚴重的、能夠中斷程序執行的錯誤比如文件丟失或者 socket 連接斷開等,`Either` 同樣效果顯著。你可以試試把前面例子中的 `Maybe` 替換為 `Either`,看怎么得到更好的反饋。
此刻我忍不住在想,我僅僅是把 `Either` 當作一個錯誤消息的容器介紹給你!這樣的介紹有失偏頗,它的能耐遠不止于此。比如,它表示了邏輯或(也就是 `||`)。再比如,它體現了范疇學里 *coproduct* 的概念,當然本書不會涉及這方面的知識,但值得你去深入了解,因為這個概念有很多特性值得利用。還比如,它是標準的 sum type(或者叫不交并集,disjoint union of sets),因為它含有的所有可能的值的總數就是它包含的那兩種類型的總數(我知道這么說你聽不懂,沒關系,這里有一篇[非常棒的文章](https://www.fpcomplete.com/school/to-infinity-and-beyond/pick-of-the-week/sum-types)講述這個問題)。`Either` 能做的事情多著呢,但是作為一個 functor,我們就用它處理錯誤。
就像 `Maybe` 可以有個 `maybe` 一樣,`Either` 也可以有一個 `either`。兩者的用法類似,但 `either` 接受兩個函數(而不是一個)和一個靜態值為參數。這兩個函數的返回值類型一致:
```js
// either :: (a -> c) -> (b -> c) -> Either a b -> c
var either = curry(function(f, g, e) {
switch(e.constructor) {
case Left: return f(e.__value);
case Right: return g(e.__value);
}
});
// zoltar :: User -> _
var zoltar = compose(console.log, either(id, fortune), getAge(moment()));
zoltar({birthdate: '2005-12-12'});
// "If you survive, you will be 10"
// undefined
zoltar({birthdate: 'balloons!'});
// "Birth date could not be parsed"
// undefined
```
終于用了一回那個神秘的 `id` 函數!其實它就是簡單地復制了 `Left` 里的錯誤消息,然后把這個值傳給 `console.log` 而已。通過強制在 `getAge` 內部進行錯誤處理,我們的算命程序更加健壯了。結果就是,要么告訴用戶一個殘酷的事實并像算命師那樣跟他擊掌,要么就繼續運行程序。好了,現在我們已經準備好去學習一個完全不同類型的 functor 了。
## 王老先生有作用...
(譯者注:原標題是“Old McDonald had Effects...”,源于美國兒歌“Old McDonald Had a Farm”。)

在關于純函數的的那一章(即第 3 章)里,有一個很奇怪的例子。這個例子中的函數會產生副作用,但是我們通過把它包裹在另一個函數里的方式把它變得看起來像一個純函數。這里還有一個類似的例子:
```js
// getFromStorage :: String -> (_ -> String)
var getFromStorage = function(key) {
return function() {
return localStorage[key];
}
}
```
要是我們沒把 `getFromStorage` 包在另一個函數里,它的輸出值就是不定的,會隨外部環境變化而變化。有了這個結實的包裹函數(wrapper),同一個輸入就總能返回同一個輸出:一個從 `localStorage` 里取出某個特定的元素的函數。就這樣(也許再高唱幾句贊美圣母的贊歌)我們洗滌了心靈,一切都得到了寬恕。
然而,這并沒有多大的用處,你說是不是。就像是你收藏的全新未拆封的玩偶,不能拿出來玩有什么意思。所以要是能有辦法進到這個容器里面,拿到它藏在那兒的東西就好了...辦法是有的,請看 `IO`:
```js
var IO = function(f) {
this.__value = f;
}
IO.of = function(x) {
return new IO(function() {
return x;
});
}
IO.prototype.map = function(f) {
return new IO(_.compose(f, this.__value));
}
```
`IO` 跟之前的 functor 不同的地方在于,它的 `__value` 總是一個函數。不過我們不把它當作一個函數——實現的細節我們最好先不管。這里發生的事情跟我們在 `getFromStorage` 那里看到的一模一樣:`IO` 把非純執行動作(impure action)捕獲到包裹函數里,目的是延遲執行這個非純動作。就這一點而言,我們認為 `IO` 包含的是被包裹的執行動作的返回值,而不是包裹函數本身。這在 `of` 函數里很明顯:`IO(function(){ return x })` 僅僅是為了延遲執行,其實我們得到的是 `IO(x)`。
來用用看:
```js
// io_window_ :: IO Window
var io_window = new IO(function(){ return window; });
io_window.map(function(win){ return win.innerWidth });
// IO(1430)
io_window.map(_.prop('location')).map(_.prop('href')).map(split('/'));
// IO(["http:", "", "localhost:8000", "blog", "posts"])
// $ :: String -> IO [DOM]
var $ = function(selector) {
return new IO(function(){ return document.querySelectorAll(selector); });
}
$('#myDiv').map(head).map(function(div){ return div.innerHTML; });
// IO('I am some inner html')
```
這里,`io_window` 是一個真正的 `IO`,我們可以直接對它使用 `map`。至于 `$`,則是一個函數,調用后會返回一個 `IO`。我把這里的返回值都寫成了*概念性*的,這樣就更加直觀;不過實際的返回值是 `{ __value: [Function] }`。當調用 `IO` 的 `map` 的時候,我們把傳進來的函數放在了 `map` 函數里的組合的最末端(也就是最左邊),反過來這個函數就成為了新的 `IO` 的新 `__value`,并繼續下去。傳給 `map` 的函數并沒有運行,我們只是把它們壓到一個“運行棧”的最末端而已,一個函數緊挨著另一個函數,就像小心擺放的多米諾骨牌一樣,讓人不敢輕易推倒。這種情形很容易叫人聯想起“四人幫”(譯者注:《設計模式》一書作者)提出的命令模式(command pattern)或者隊列(queue)。
花點時間找回你關于 functor 的直覺吧。把實現細節放在一邊不管,你應該就能自然而然地對各種各樣的容器使用 `map` 了,不管它是多么奇特怪異。這種偽超自然的力量要歸功于 functor 的定律,我們將在本章末尾對此作一番探索。無論如何,我們終于可以在不犧牲代碼純粹性的情況下,隨意使用這些不純的值了。
好了,我們已經把野獸關進了籠子。但是,在某一時刻還是要把它放出來。因為對 `IO` 調用 `map` 已經積累了太多不純的操作,最后再運行它無疑會打破平靜。問題是在哪里,什么時候打開籠子的開關?而且有沒有可能我們只運行 `IO` 卻不讓不純的操作弄臟雙手?答案是可以的,只要把責任推到調用者身上就行了。我們的純代碼,盡管陰險狡詐詭計多端,但是卻始終保持一副清白無辜的模樣,反而是實際運行 `IO` 并產生了作用的調用者,背了黑鍋。來看一個具體的例子。
```js
////// 純代碼庫: lib/params.js ///////
// url :: IO String
var url = new IO(function() { return window.location.href; });
// toPairs = String -> [[String]]
var toPairs = compose(map(split('=')), split('&'));
// params :: String -> [[String]]
var params = compose(toPairs, last, split('?'));
// findParam :: String -> IO Maybe [String]
var findParam = function(key) {
return map(compose(Maybe.of, filter(compose(eq(key), head)), params), url);
};
////// 非純調用代碼: main.js ///////
// 調用 __value() 來運行它!
findParam("searchTerm").__value();
// Maybe(['searchTerm', 'wafflehouse'])
```
lib/params.js 把 `url` 包裹在一個 `IO` 里,然后把這頭野獸傳給了調用者;一雙手保持的非常干凈。你可能也注意到了,我們把容器也“壓棧”了,要知道創建一個 `IO(Maybe([x]))` 沒有任何不合理的地方。我們這個“棧”有三層 functor(`Array` 是最有資格成為 mappable 的容器類型),令人印象深刻。
有件事困擾我很久了,現在我必須得說出來:`IO` 的 `__value` 并不是它包含的值,也不是像兩個下劃線暗示那樣是一個私有屬性。`__value` 是手榴彈的彈栓,只應該被調用者以最公開的方式拉動。為了提醒用戶它的變化無常,我們把它重命名為 `unsafePerformIO` 看看。
```js
var IO = function(f) {
this.unsafePerformIO = f;
}
IO.prototype.map = function(f) {
return new IO(_.compose(f, this.unsafePerformIO));
}
```
看,這就好多了。現在調用的代碼就變成了 `findParam("searchTerm").unsafePerformIO()`,對應用程序的用戶(以及本書讀者)來說,這簡直就直白得不能再直白了。
`IO` 會成為一個忠誠的伴侶,幫助我們馴化那些狂野的非純操作。下一節我們將學習一種跟 `IO` 在精神上相似,但是用法上又千差萬別的類型。
## 異步任務
回調(callback)是通往地獄的狹窄的螺旋階梯。它們是埃舍爾(譯者注:荷蘭版畫藝術家)設計的控制流。看到一個個嵌套的回調擠在大小括號搭成的架子上,讓人不由自主地聯想到地牢里的靈薄獄(還能再低點么!)(譯者注:靈薄獄即 limbo,基督教中地獄邊緣之意)。光是想到這樣的回調就讓我幽閉恐怖癥發作了。不過別擔心,處理異步代碼,我們有一種更好的方式,它的名字以“F”開頭。
這種方式的內部機制過于復雜,復雜得哪怕我唾沫橫飛也很難講清楚。所以我們就直接用 Quildreen Motta 的 [Folktale](http://folktalejs.org/) 里的 `Data.Task` (之前是 `Data.Future`)。來見證一些例子吧:
```js
// Node readfile example:
//=======================
var fs = require('fs');
// readFile :: String -> Task(Error, JSON)
var readFile = function(filename) {
return new Task(function(reject, result) {
fs.readFile(filename, 'utf-8', function(err, data) {
err ? reject(err) : result(data);
});
});
};
readFile("metamorphosis").map(split('\n')).map(head);
// Task("One morning, as Gregor Samsa was waking up from anxious dreams, he discovered that
// in bed he had been changed into a monstrous verminous bug.")
// jQuery getJSON example:
//========================
// getJSON :: String -> {} -> Task(Error, JSON)
var getJSON = curry(function(url, params) {
return new Task(function(reject, result) {
$.getJSON(url, params, result).fail(reject);
});
});
getJSON('/video', {id: 10}).map(_.prop('title'));
// Task("Family Matters ep 15")
// 傳入普通的實際值也沒問題
Task.of(3).map(function(three){ return three + 1 });
// Task(4)
```
例子中的 `reject` 和 `result` 函數分別是失敗和成功的回調。正如你看到的,我們只是簡單地調用 `Task` 的 `map` 函數,就能操作將來的值,好像這個值就在那兒似的。到現在 `map` 對你來說應該不稀奇了。
如果熟悉 promise 的話,你該能認出來 `map` 就是 `then`,`Task` 就是一個 promise。如果不熟悉你也不必氣餒,反正我們也不會用它,因為它并不純;但剛才的類比還是成立的。
與 `IO` 類似,`Task` 在我們給它綠燈之前是不會運行的。事實上,正因為它要等我們的命令,`IO` 實際就被納入到了 `Task` 名下,代表所有的異步操作——`readFile` 和 `getJSON` 并不需要一個額外的 `IO` 容器來變純。更重要的是,當我們調用它的 `map` 的時候,`Task` 工作的方式與 `IO` 幾無差別:都是把對未來的操作的指示放在一個時間膠囊里,就像家務列表(chore chart)那樣——真是一種精密的拖延術。
我們必須調用 `fork` 方法才能運行 `Task`,這種機制與 `unsafePerformIO` 類似。但也有不同,不同之處就像 `fork` 這個名稱表明的那樣,它會 fork 一個子進程運行它接收到的參數代碼,其他部分的執行不受影響,主線程也不會阻塞。當然這種效果也可以用其他一些技術比如線程實現,但這里的這種方法工作起來就像是一個普通的異步調用,而且 event loop 能夠不受影響地繼續運轉。我們來看一下 `fork`:
```js
// Pure application
//=====================
// blogTemplate :: String
// blogPage :: Posts -> HTML
var blogPage = Handlebars.compile(blogTemplate);
// renderPage :: Posts -> HTML
var renderPage = compose(blogPage, sortBy('date'));
// blog :: Params -> Task(Error, HTML)
var blog = compose(map(renderPage), getJSON('/posts'));
// Impure calling code
//=====================
blog({}).fork(
function(error){ $("#error").html(error.message); },
function(page){ $("#main").html(page); }
);
$('#spinner').show();
```
調用 `fork` 之后,`Task` 就趕緊跑去找一些文章,渲染到頁面上。與此同時,我們在頁面上展示一個 spinner,因為 `fork` 不會等收到響應了才執行它后面的代碼。最后,我們要么把文章展示在頁面上,要么就顯示一個出錯信息,視 `getJSON` 請求是否成功而定。
花點時間思考下這里的控制流為何是線性的。我們只需要從下讀到上,從右讀到左就能理解代碼,即便這段程序實際上會在執行過程中到處跳來跳去。這種方式使得閱讀和理解應用程序的代碼比那種要在各種回調和錯誤處理代碼塊之間跳躍的方式容易得多。
天哪,你看到了么,`Task` 居然也包含了 `Either`!沒辦法,為了能處理將來可能出現的錯誤,它必須得這么做,因為普通的控制流在異步的世界里不適用。這自然是好事一樁,因為它天然地提供了充分的“純”錯誤處理。
就算是有了 `Task`,`IO` 和 `Either` 這兩個 functor 也照樣能派上用場。待我舉個簡單例子向你說明一種更復雜、更假想的情況,雖然如此,這個例子還是能夠說明我的目的。
```js
// Postgres.connect :: Url -> IO DbConnection
// runQuery :: DbConnection -> ResultSet
// readFile :: String -> Task Error String
// Pure application
//=====================
// dbUrl :: Config -> Either Error Url
var dbUrl = function(c) {
return (c.uname && c.pass && c.host && c.db)
? Right.of("db:pg://"+c.uname+":"+c.pass+"@"+c.host+"5432/"+c.db)
: Left.of(Error("Invalid config!"));
}
// connectDb :: Config -> Either Error (IO DbConnection)
var connectDb = compose(map(Postgres.connect), dbUrl);
// getConfig :: Filename -> Task Error (Either Error (IO DbConnection))
var getConfig = compose(map(compose(connectDB, JSON.parse)), readFile);
// Impure calling code
//=====================
getConfig("db.json").fork(
logErr("couldn't read file"), either(console.log, map(runQuery))
);
```
這個例子中,我們在 `readFile` 成功的那個代碼分支里利用了 `Either` 和 `IO`。`Task` 處理異步讀取文件這一操作當中的不“純”性,但是驗證 config 的合法性以及連接數據庫則分別使用了 `Either` 和 `IO`。所以你看,我們依然在同步地跟所有事物打交道。
例子我還可以再舉一些,但是就到此為止吧。這些概念就像 `map` 一樣簡單。
實際當中,你很有可能在一個工作流中跑好幾個異步任務,但我們還沒有完整學習容器的 api 來應對這種情況。不必擔心,我們很快就會去學習 monad 之類的概念。不過,在那之前,我們得先檢查下所有這些背后的數學知識。
## 一點理論
前面提到,functor 的概念來自于范疇學,并滿足一些定律。我們先來探索這些實用的定律。
```js
// identity
map(id) === id;
// composition
compose(map(f), map(g)) === map(compose(f, g));
```
*同一律*很簡單,但是也很重要。因為這些定律都是可運行的代碼,所以我們完全可以在我們自己的 functor 上試驗它們,驗證它們是否成立。
```js
var idLaw1 = map(id);
var idLaw2 = id;
idLaw1(Container.of(2));
//=> Container(2)
idLaw2(Container.of(2));
//=> Container(2)
```
看到沒,它們是相等的。接下來看一看組合。
```js
var compLaw1 = compose(map(concat(" world")), map(concat(" cruel")));
var compLaw2 = map(compose(concat(" world"), concat(" cruel")));
compLaw1(Container.of("Goodbye"));
//=> Container('Goodbye cruel world')
compLaw2(Container.of("Goodbye"));
//=> Container('Goodbye cruel world')
```
在范疇學中,functor 接受一個范疇的對象和態射(morphism),然后把它們映射(map)到另一個范疇里去。根據定義,這個新范疇一定會有一個單位元(identity),也一定能夠組合態射;我們無須驗證這一點,前面提到的定律保證這些東西會在映射后得到保留。
可能我們關于范疇的定義還是有點模糊。你可以把范疇想象成一個有著多個對象的網絡,對象之間靠態射連接。那么 functor 可以把一個范疇映射到另外一個,而且不會破壞原有的網絡。如果一個對象 `a` 屬于源范疇 `C`,那么通過 functor `F` 把 `a` 映射到目標范疇 `D` 上之后,就可以使用 `F a` 來指代 `a` 對象(把這些字母拼起來是什么?!)。可能看圖會更容易理解:

比如,`Maybe` 就把類型和函數的范疇映射到這樣一個范疇:即每個對象都有可能不存在,每個態射都有空值檢查的范疇。這個結果在代碼中的實現方式是用 `map` 包裹每一個函數,用 functor 包裹每一個類型。這樣就能保證每個普通的類型和函數都能在新環境下繼續使用組合。從技術上講,代碼中的 functor 實際上是把范疇映射到了一個包含類型和函數的子范疇(sub category)上,使得這些 functor 成為了一種新的特殊的 endofunctor。但出于本書的目的,我們認為它就是一個不同的范疇。
可以用一張圖來表示這種態射及其對象的映射:

這張圖除了能表示態射借助 functor `F` 完成從一個范疇到另一個范疇的映射之外,我們發現它還符合交換律,也就是說,順著箭頭的方向往前,形成的每一個路徑都指向同一個結果。不同的路徑意味著不同的行為,但最終都會得到同一個數據類型。這種形式化給了我們原則性的方式去思考代碼——無須分析和評估每一個單獨的場景,只管可以大膽地應用公式即可。來看一個具體的例子。
```js
// topRoute :: String -> Maybe(String)
var topRoute = compose(Maybe.of, reverse);
// bottomRoute :: String -> Maybe(String)
var bottomRoute = compose(map(reverse), Maybe.of);
topRoute("hi");
// Maybe("ih")
bottomRoute("hi");
// Maybe("ih")
```
或者看圖:

根據所有 functor 都有的特性,我們可以立即理解代碼,重構代碼。
functor 也能嵌套使用:
```js
var nested = Task.of([Right.of("pillows"), Left.of("no sleep for you")]);
map(map(map(toUpperCase)), nested);
// Task([Right("PILLOWS"), Left("no sleep for you")])
```
`nested` 是一個將來的數組,數組的元素有可能是程序拋出的錯誤。我們使用 `map` 剝開每一層的嵌套,然后對數組的元素調用傳遞進去的函數。可以看到,這中間沒有回調、`if/else` 語句和 `for` 循環,只有一個明確的上下文。的確,我們必須要 `map(map(map(f)))` 才能最終運行函數。不想這么做的話,可以組合 functor。是的,你沒聽錯:
```js
var Compose = function(f_g_x){
this.getCompose = f_g_x;
}
Compose.prototype.map = function(f){
return new Compose(map(map(f), this.getCompose));
}
var tmd = Task.of(Maybe.of("Rock over London"))
var ctmd = new Compose(tmd);
map(concat(", rock on, Chicago"), ctmd);
// Compose(Task(Maybe("Rock over London, rock on, Chicago")))
ctmd.getCompose;
// Task(Maybe("Rock over London, rock on, Chicago"))
```
看,只有一個 `map`。functor 組合是符合結合律的,而且之前我們定義的 `Container` 實際上是一個叫 `Identity` 的 functor。identity 和可結合的組合也能產生一個范疇,這個特殊的范疇的對象是其他范疇,態射是 functor。這實在太傷腦筋了,所以我們不會深入這個問題,但是贊嘆一下這種模式的結構性含義,或者它的簡單的抽象之美也是好的。
## 總結
我們已經認識了幾個不同的 functor,但它們的數量其實是無限的。有一些值得注意的可迭代數據類型(iterable data structure)我們沒有介紹,像 tree、list、map 和 pair 等,以及所有你能說出來的。eventstream 和 observable 也都是 functor。其他的 functor 可能就是拿來做封裝或者僅僅是模擬類型。我們身邊到處都有 functor 的身影,本書也將會大量使用它們。
用多個 functor 參數調用一個函數怎么樣呢?處理一個由不純的或者異步的操作組成的有序序列怎么樣呢?要應對這個什么都裝在盒子里的世界,目前我們工具箱里的工具還不全。下一章,我們將直奔 monad 而去。
[第 9 章: Monad](ch9.md)
## 練習
```js
require('../../support');
var Task = require('data.task');
var _ = require('ramda');
// 練習 1
// ==========
// 使用 _.add(x,y) 和 _.map(f,x) 創建一個能讓 functor 里的值增加的函數
var ex1 = undefined
//練習 2
// ==========
// 使用 _.head 獲取列表的第一個元素
var xs = Identity.of(['do', 'ray', 'me', 'fa', 'so', 'la', 'ti', 'do']);
var ex2 = undefined
// 練習 3
// ==========
// 使用 safeProp 和 _.head 找到 user 的名字的首字母
var safeProp = _.curry(function (x, o) { return Maybe.of(o[x]); });
var user = { id: 2, name: "Albert" };
var ex3 = undefined
// 練習 4
// ==========
// 使用 Maybe 重寫 ex4,不要有 if 語句
var ex4 = function (n) {
if (n) { return parseInt(n); }
};
var ex4 = undefined
// 練習 5
// ==========
// 寫一個函數,先 getPost 獲取一篇文章,然后 toUpperCase 讓這片文章標題變為大寫
// getPost :: Int -> Future({id: Int, title: String})
var getPost = function (i) {
return new Task(function(rej, res) {
setTimeout(function(){
res({id: i, title: 'Love them futures'})
}, 300)
});
}
var ex5 = undefined
// 練習 6
// ==========
// 寫一個函數,使用 checkActive() 和 showWelcome() 分別允許訪問或返回錯誤
var showWelcome = _.compose(_.add( "Welcome "), _.prop('name'))
var checkActive = function(user) {
return user.active ? Right.of(user) : Left.of('Your account is not active')
}
var ex6 = undefined
// 練習 7
// ==========
// 寫一個驗證函數,檢查參數是否 length > 3。如果是就返回 Right(x),否則就返回
// Left("You need > 3")
var ex7 = function(x) {
return undefined // <--- write me. (don't be pointfree)
}
// 練習 8
// ==========
// 使用練習 7 的 ex7 和 Either 構造一個 functor,如果一個 user 合法就保存它,否則
// 返回錯誤消息。別忘了 either 的兩個參數必須返回同一類型的數據。
var save = function(x){
return new IO(function(){
console.log("SAVED USER!");
return x + '-saved';
});
}
var ex8 = undefined
```