在上一個(gè)部分我們對(duì)比了Twisted與 Erlang,并將注意力集中在它們共有的一些思想上.結(jié)果表明使用Erlang也是非常簡(jiǎn)便的,因?yàn)楫惒絀/O和響應(yīng)式編程是Erlang運(yùn)行時(shí)和進(jìn)程模型的關(guān)鍵元素.
今天我們想走得更遠(yuǎn)一點(diǎn),去看一看 Haskell —— 另一種功能性語(yǔ)言,然而與Erlang有很大不同(當(dāng)然與Python也不同).這里面沒(méi)有太多的平行概念,但我們?nèi)匀粫?huì)發(fā)現(xiàn)藏在下面的異步I/O概念.
雖然Erlang是函數(shù)式語(yǔ)言,它主要關(guān)注可靠的并發(fā)模型.Haskell,另一方面,是徹頭徹尾函數(shù)式的,它無(wú)恥地利用了范疇論的概念,如 函子 和 單子.
不要慌.我們這里不會(huì)涉及那些復(fù)雜的東西(雖然我們可以).相反,我們將關(guān)注一個(gè)Haskell的更加傳統(tǒng)的功能特性:惰性. 像許多函數(shù)式語(yǔ)言一樣(除了Erlang), Haskell支持 惰性計(jì)算. 在懶惰計(jì)算語(yǔ)言中,程序的文字并不過(guò)多的描述怎樣計(jì)算需要計(jì)算的東西.具體實(shí)施計(jì)算的細(xì)節(jié)一般留給了編譯器和運(yùn)行時(shí)系統(tǒng).
同時(shí),需要進(jìn)一步指出,作為惰性計(jì)算推進(jìn)的運(yùn)行時(shí)可能一次只計(jì)算表達(dá)式的一部分(惰性的)而不是全部.一般地,運(yùn)行時(shí)只提供維持當(dāng)前計(jì)算繼續(xù)所需的最小計(jì)算量.
這里有一個(gè)使用Haskell head 語(yǔ)句的簡(jiǎn)單例子,這是一個(gè)提取列表第一個(gè)元素的函數(shù),對(duì)于列表1,2,3:
head [1,2,3]
如果你安裝了GHC Haskell運(yùn)行時(shí),你可以自己試一試:
[~] ghci
GHCi, version 6.12.1: http://www.haskell.org/ghc/ : ? for help
Loading package ghc-prim ... linking ... done.
Loading package integer-gmp ... linking ... done.
Loading package base ... linking ... done.
Prelude> head [1,2,3]
1
Prelude>
結(jié)果是 1, 正如所料.
Haskell列表的語(yǔ)法包含從前幾個(gè)元素定義列表的使用功能.例如,列表[2,4,..]是從2開(kāi)始的偶數(shù)序列.到哪結(jié)束呢?實(shí)際上并不會(huì)結(jié)束.Haskell列表[2,4,..]和其他如此表述的都是(概念上)無(wú)限列表.你可以在交互式Haskell提示符下計(jì)算它,這將試圖打印這個(gè)表達(dá)式的結(jié)果如下:
Prelude> [2,4 ..]
[2,4,6,8,10,12,14,16,18,20,22,24,26,28,30,32,34,36,38,40,42,44,46,48,50,52,54,56,58,60,62,64,66,68,70,72,74,76,78,80,82,84,86,88,90,92,94,96,98,100,102,104,106,108,110,112,114,116,118,120,122,124,126,128,130,132,134,136,138,140,142,144,146,
...
你不得不按 Ctrl-C 終止計(jì)算,因?yàn)樗约翰粫?huì)停下來(lái).但由于是惰性計(jì)算,在Haskell中應(yīng)用無(wú)限列表是沒(méi)有問(wèn)題的:
Prelude> head [2,4 ..]
2
Prelude> head (tail [2,4 ..])
4
Prelude> head (tail (tail [2,4 ..]))
6
這里我們分別獲取無(wú)限列表的第一、二、三個(gè)元素,沒(méi)看到任何無(wú)限循環(huán).這就是惰性計(jì)算的本質(zhì).Haskell運(yùn)行時(shí)只構(gòu)造完成 head 函數(shù)所需的列表,而不是先構(gòu)造整個(gè)列表(這將導(dǎo)致無(wú)限循環(huán)),再將整個(gè)列表傳遞給 head.這個(gè)列表的其余部分跟本沒(méi)有被構(gòu)造,因?yàn)樗鼈儗?duì)繼續(xù)推進(jìn)計(jì)算毫無(wú)意義.
當(dāng)我們引入 tail 函數(shù)時(shí),Haskell被迫進(jìn)一步構(gòu)造列表,但是又一次僅僅構(gòu)造了滿足下一次計(jì)算所需的列表.同時(shí),一旦計(jì)算結(jié)束,列表(未完成的)被丟棄了.
這里是一些部分計(jì)算無(wú)限列表的Haskell代碼:
Prelude> let x = [1..]
Prelude> let y = [2,4 ..]
Prelude> let z = [3,6 ..]
Prelude> head (tail (tail (zip3 x y z)))
(3,6,9)
zip 函數(shù)將所有列表壓縮在一起,之后抓取尾部的尾部的頭部.又一次,Haskell沒(méi)有發(fā)生任何問(wèn)題,僅僅構(gòu)造了計(jì)算所需的列表.我們可以將Haskell運(yùn)行時(shí)"消耗"這些無(wú)限列表的過(guò)程可視化:
http://wiki.jikexueyuan.com/project/twisted-intro/images/p21_haskell.png" alt="" /> 圖46 Haskell消耗一些無(wú)限列表
雖然我們將Haskell運(yùn)行時(shí)畫為一個(gè)簡(jiǎn)單的循環(huán),它可能被多線程實(shí)現(xiàn)(并且很可能如果你使用GHC版本的Haskell).但這幅圖的關(guān)鍵點(diǎn)在于它十分像一個(gè) reactor 循環(huán),消耗從網(wǎng)絡(luò)套接字傳來(lái)的數(shù)據(jù)片段.
你可以把異步I/O和 reactor 模式視為一種有限形式的惰性計(jì)算.異步I/O的格言是:"僅僅推進(jìn)你所擁有的數(shù)據(jù)".同時(shí)惰性計(jì)算的格言是:"僅僅推進(jìn)你所需的數(shù)據(jù)".進(jìn)一步,一個(gè)惰性計(jì)算語(yǔ)言在任何地方都使用這個(gè)格言,并不僅僅是有限范圍的I/O.
但關(guān)鍵點(diǎn)在于,對(duì)于惰性計(jì)算語(yǔ)言,做異步I/O小菜一碟. 編譯器和運(yùn)行時(shí)已經(jīng)被設(shè)計(jì)為一點(diǎn)一點(diǎn)地處理數(shù)據(jù)結(jié)構(gòu),因而惰性地處理到來(lái)的I/O數(shù)據(jù)流是標(biāo)準(zhǔn)問(wèn)題. 如此Haskell運(yùn)行時(shí),就像Erlang運(yùn)行時(shí),簡(jiǎn)單地集成異步I/O為套接字抽象的一部分. 我們以實(shí)現(xiàn)一個(gè)Haskell詩(shī)歌客戶端來(lái)展示這個(gè)概念.
我們第一個(gè)Haskell詩(shī)歌客戶端位于 haskell-client-1/get-poetry.hs. 同Erlang一樣,我們直接給出了完成版的客戶端,如果你希望學(xué)習(xí)更多,我們列出進(jìn)一步閱讀的參考.
Haskell同樣支持輕量級(jí)線程或進(jìn)程,盡管它們不是Haskell的核心,我們的Haskell客戶端為每首需要下載的詩(shī)歌創(chuàng)建一個(gè)進(jìn)程.關(guān)鍵函數(shù)是 runTask,它連接到一個(gè)套接字并且以輕量級(jí)線程啟動(dòng) getPoetry 函數(shù).
在這個(gè)代碼中,你將看到許多類型定義. Haskell,不像Python和Erlang,是靜態(tài)類型的.我們沒(méi)有為每個(gè)變量定義類型,因?yàn)镠askell可以自動(dòng)地推斷沒(méi)有顯示定義的變量(或者報(bào)告錯(cuò)誤如果不能推斷).許多函數(shù)包含IO類型(技術(shù)上叫單子),因?yàn)镠askell要求我們將有副作用的代碼從純函數(shù)中干凈地分離(如,執(zhí)行I/O的代碼).
getPoetry 函數(shù)包含如下行:
poem <- hGetContents h
看起來(lái)像從句柄一次讀入整首詩(shī)(如TCP套接字).但是Haskell,像往常一樣,是惰性的.Haskell運(yùn)行時(shí)包含一個(gè)或更多實(shí)際線程,它們?cè)谝粋€(gè)選擇循環(huán)中執(zhí)行異步I/O,如此便保存了惰性處理I/O流的可能性.
僅僅為說(shuō)明異步I/O正在進(jìn)行,我們引入一個(gè)"回調(diào)"函數(shù), gotLine,它為詩(shī)歌的每一行打印一些任務(wù)信息.但這不是一個(gè)真正的回調(diào)函數(shù),無(wú)論我們用不用它程序都會(huì)使用異步I/O.甚至叫它"gotLine"反映了一個(gè)必要的語(yǔ)言思維,它是Haskell程序外的一部分.無(wú)論怎樣,我們將一點(diǎn)點(diǎn)清掃它,先使Haskell客戶端運(yùn)轉(zhuǎn)起來(lái).
啟動(dòng)一些慢詩(shī)歌服務(wù)器:
python blocking-server/slowpoetry.py --port 10001 poetry/fascination.txt
python blocking-server/slowpoetry.py --port 10002 poetry/science.txt
python blocking-server/slowpoetry.py --port 10003 poetry/ecstasy.txt --num-bytes 30
現(xiàn)在編譯Haskell客戶端:
cd haskell-client-1/
ghc --make get-poetry.hs
這將創(chuàng)建一個(gè)二進(jìn)制 get-poetry.最后,針對(duì)我們的服務(wù)器運(yùn)行客戶端:
/get-poetry 10001 10002 1000
你將看到如下輸出:
Task 3: got 12 bytes of poetry from localhost:10003
Task 3: got 1 bytes of poetry from localhost:10003
Task 3: got 30 bytes of poetry from localhost:10003
Task 2: got 20 bytes of poetry from localhost:10002
Task 3: got 44 bytes of poetry from localhost:10003
Task 2: got 1 bytes of poetry from localhost:10002
Task 3: got 29 bytes of poetry from localhost:10003
Task 1: got 36 bytes of poetry from localhost:10001
Task 1: got 1 bytes of poetry from localhost:10001
...
輸出與前一個(gè)異步客戶端有點(diǎn)不同,因?yàn)槲覀冎淮蛴∫恍卸皇侨我鈮K的數(shù)據(jù).但你可以清楚地看到,客戶端是從所有服務(wù)器同時(shí)處理數(shù)據(jù),而不是一個(gè)接一個(gè).你同樣可以注意到客戶端立即打印第一首完成的詩(shī),不等其他還在繼續(xù)處理的詩(shī).
好了,讓我們清除還剩下的一點(diǎn)討厭東西并且發(fā)布一個(gè)僅僅抓取詩(shī)歌而不介意任務(wù)序號(hào)的新版本.它位于 haskell-client-2/get-poetry.hs. 注意它短多了,對(duì)于每個(gè)服務(wù)器,僅僅連接到套接字,抓取所有數(shù)據(jù),之后將其發(fā)送回去.
OK,讓我們編譯新的客戶端:
cd haskell-client-2/
ghc --make get-poetry.hs
針對(duì)相同的詩(shī)歌服務(wù)器組運(yùn)行它:
./get-poetry 10001 10002 10003
最終,你將看到屏幕上出現(xiàn)每首詩(shī)的文字.
注意到每個(gè)服務(wù)器同時(shí)向客戶端發(fā)送數(shù)據(jù).更重要的,客戶端以最快速度打印出第一首詩(shī)的每一行,而不去等待其余的詩(shī),甚至當(dāng)它正在處理其它兩首詩(shī).之后它快速地打印出之前積累的第二首詩(shī).
同時(shí)這所有發(fā)生的一切都不需要我們做什么.這里沒(méi)有回調(diào),沒(méi)有傳來(lái)傳去的消息,僅僅是一個(gè)關(guān)于我們希望程序做什么的簡(jiǎn)潔地描述,而且很少需要告訴它應(yīng)該怎樣做.其余的事情都是由Haskell編譯器和運(yùn)行時(shí)處理的.漂亮!
從Twisted到Erlang之后到Haskell,我們可以看到一個(gè)平行的移動(dòng),從前景到背景逐步深入異步編程背后的思想.在Twisted中,異步編程是其存在的核心激勵(lì)理念. Twisted實(shí)現(xiàn)作為一個(gè)與Python分離的框架(Python缺乏核心的異步抽象如輕量級(jí)線程),當(dāng)你用Twisted寫程序時(shí),將異步模型置于首位與核心.
在Erlang中,異步對(duì)于程序員仍然是可見(jiàn)的,但細(xì)節(jié)成為語(yǔ)言材料的一部分和運(yùn)行時(shí)系統(tǒng),形成一個(gè)抽象使得異步消息在同步進(jìn)程之間交換.
最后,在Haskell中,異步I/O僅僅是運(yùn)行時(shí)中的另一個(gè)技術(shù),大部分對(duì)于程序員是不可見(jiàn)的,因?yàn)樘峁┒栊杂?jì)算是Haskell的中心理念.
對(duì)于以上情況,我們還沒(méi)有介紹任何深邃的思想.我們僅僅指出許多有趣的異步模型出現(xiàn)的地方,這種模型可以被多種方式表達(dá).
如果這些激起你對(duì)Haskell的興趣,那么我們推薦"Real World Haskell"繼續(xù)你的學(xué)習(xí).這本書(shū)是介紹語(yǔ)言學(xué)習(xí)的典范.
雖然我沒(méi)有讀過(guò)它,我卻聽(tīng)說(shuō)到它飽受"Learn You a Haskell"的贊譽(yù).
完成了本系列的倒數(shù)第二部分,現(xiàn)在到了結(jié)束探索Twisted之外異步系統(tǒng)的時(shí)刻. 在 第二十二節(jié) 中,我們將做一個(gè)總結(jié),以及推薦一些學(xué)習(xí)Twisted的方法.
本部分原作參見(jiàn): dave @ http://krondo.com/blog/?p=2814
本部分翻譯內(nèi)容參見(jiàn)luocheng @ https://github.com/luocheng/twisted-intro-cn/blob/master/p21.rst