鍍金池/ 教程/ Python/ 惰性不是遲緩: Twisted和Haskell
小插曲 Deferred
異步編程模式與Reactor初探
使用Deferred新功能實現(xiàn)新客戶端
由twisted支持的客戶端
增強defer功能的客戶端
改進詩歌下載服務(wù)器
測試詩歌
更加"抽象"的運用Twisted
Deferred用于同步環(huán)境
輪子內(nèi)的輪子: Twisted和Erlang
Twisted 進程守護
構(gòu)造"回調(diào)"的另一種方法
Twisted 理論基礎(chǔ)
惰性不是遲緩: Twisted和Haskell
第二個小插曲,deferred
使用Deferred的詩歌下載客戶端
Deferreds 全貌
結(jié)束
取消之前的意圖
由Twisted扶持的客戶端
改進詩歌下載服務(wù)器
初識Twisted

惰性不是遲緩: Twisted和Haskell

簡介

在上一個部分我們對比了Twisted與 Erlang,并將注意力集中在它們共有的一些思想上.結(jié)果表明使用Erlang也是非常簡便的,因為異步I/O和響應(yīng)式編程是Erlang運行時和進程模型的關(guān)鍵元素.

今天我們想走得更遠一點,去看一看 Haskell —— 另一種功能性語言,然而與Erlang有很大不同(當然與Python也不同).這里面沒有太多的平行概念,但我們?nèi)匀粫l(fā)現(xiàn)藏在下面的異步I/O概念.

F —— 函數(shù)式

雖然Erlang是函數(shù)式語言,它主要關(guān)注可靠的并發(fā)模型.Haskell,另一方面,是徹頭徹尾函數(shù)式的,它無恥地利用了范疇論的概念,如 函子單子.

不要慌.我們這里不會涉及那些復雜的東西(雖然我們可以).相反,我們將關(guān)注一個Haskell的更加傳統(tǒng)的功能特性:惰性. 像許多函數(shù)式語言一樣(除了Erlang), Haskell支持 惰性計算. 在懶惰計算語言中,程序的文字并不過多的描述怎樣計算需要計算的東西.具體實施計算的細節(jié)一般留給了編譯器和運行時系統(tǒng).

同時,需要進一步指出,作為惰性計算推進的運行時可能一次只計算表達式的一部分(惰性的)而不是全部.一般地,運行時只提供維持當前計算繼續(xù)所需的最小計算量.

這里有一個使用Haskell head 語句的簡單例子,這是一個提取列表第一個元素的函數(shù),對于列表1,2,3:

head [1,2,3]

如果你安裝了GHC Haskell運行時,你可以自己試一試:

[~] 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列表的語法包含從前幾個元素定義列表的使用功能.例如,列表[2,4,..]是從2開始的偶數(shù)序列.到哪結(jié)束呢?實際上并不會結(jié)束.Haskell列表[2,4,..]和其他如此表述的都是(概念上)無限列表.你可以在交互式Haskell提示符下計算它,這將試圖打印這個表達式的結(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 終止計算,因為它自己不會停下來.但由于是惰性計算,在Haskell中應(yīng)用無限列表是沒有問題的:

Prelude> head [2,4 ..]
2
Prelude> head (tail [2,4 ..])
4
Prelude> head (tail (tail [2,4 ..]))
6

這里我們分別獲取無限列表的第一、二、三個元素,沒看到任何無限循環(huán).這就是惰性計算的本質(zhì).Haskell運行時只構(gòu)造完成 head 函數(shù)所需的列表,而不是先構(gòu)造整個列表(這將導致無限循環(huán)),再將整個列表傳遞給 head.這個列表的其余部分跟本沒有被構(gòu)造,因為它們對繼續(xù)推進計算毫無意義.

當我們引入 tail 函數(shù)時,Haskell被迫進一步構(gòu)造列表,但是又一次僅僅構(gòu)造了滿足下一次計算所需的列表.同時,一旦計算結(jié)束,列表(未完成的)被丟棄了.

這里是一些部分計算無限列表的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沒有發(fā)生任何問題,僅僅構(gòu)造了計算所需的列表.我們可以將Haskell運行時"消耗"這些無限列表的過程可視化:

http://wiki.jikexueyuan.com/project/twisted-intro/images/p21_haskell.png" alt="" /> 圖46 Haskell消耗一些無限列表

雖然我們將Haskell運行時畫為一個簡單的循環(huán),它可能被多線程實現(xiàn)(并且很可能如果你使用GHC版本的Haskell).但這幅圖的關(guān)鍵點在于它十分像一個 reactor 循環(huán),消耗從網(wǎng)絡(luò)套接字傳來的數(shù)據(jù)片段.

你可以把異步I/O和 reactor 模式視為一種有限形式的惰性計算.異步I/O的格言是:"僅僅推進你所擁有的數(shù)據(jù)".同時惰性計算的格言是:"僅僅推進你所需的數(shù)據(jù)".進一步,一個惰性計算語言在任何地方都使用這個格言,并不僅僅是有限范圍的I/O.

但關(guān)鍵點在于,對于惰性計算語言,做異步I/O小菜一碟. 編譯器和運行時已經(jīng)被設(shè)計為一點一點地處理數(shù)據(jù)結(jié)構(gòu),因而惰性地處理到來的I/O數(shù)據(jù)流是標準問題. 如此Haskell運行時,就像Erlang運行時,簡單地集成異步I/O為套接字抽象的一部分. 我們以實現(xiàn)一個Haskell詩歌客戶端來展示這個概念.

Haskell 詩歌

我們第一個Haskell詩歌客戶端位于 haskell-client-1/get-poetry.hs. 同Erlang一樣,我們直接給出了完成版的客戶端,如果你希望學習更多,我們列出進一步閱讀的參考.

Haskell同樣支持輕量級線程或進程,盡管它們不是Haskell的核心,我們的Haskell客戶端為每首需要下載的詩歌創(chuàng)建一個進程.關(guān)鍵函數(shù)是 runTask,它連接到一個套接字并且以輕量級線程啟動 getPoetry 函數(shù).

在這個代碼中,你將看到許多類型定義. Haskell,不像Python和Erlang,是靜態(tài)類型的.我們沒有為每個變量定義類型,因為Haskell可以自動地推斷沒有顯示定義的變量(或者報告錯誤如果不能推斷).許多函數(shù)包含IO類型(技術(shù)上叫單子),因為Haskell要求我們將有副作用的代碼從純函數(shù)中干凈地分離(如,執(zhí)行I/O的代碼).

getPoetry 函數(shù)包含如下行:

poem <- hGetContents h

看起來像從句柄一次讀入整首詩(如TCP套接字).但是Haskell,像往常一樣,是惰性的.Haskell運行時包含一個或更多實際線程,它們在一個選擇循環(huán)中執(zhí)行異步I/O,如此便保存了惰性處理I/O流的可能性.

僅僅為說明異步I/O正在進行,我們引入一個"回調(diào)"函數(shù), gotLine,它為詩歌的每一行打印一些任務(wù)信息.但這不是一個真正的回調(diào)函數(shù),無論我們用不用它程序都會使用異步I/O.甚至叫它"gotLine"反映了一個必要的語言思維,它是Haskell程序外的一部分.無論怎樣,我們將一點點清掃它,先使Haskell客戶端運轉(zhuǎn)起來.

啟動一些慢詩歌服務(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)建一個二進制 get-poetry.最后,針對我們的服務(wù)器運行客戶端:

/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
...

輸出與前一個異步客戶端有點不同,因為我們只打印一行而不是任意塊的數(shù)據(jù).但你可以清楚地看到,客戶端是從所有服務(wù)器同時處理數(shù)據(jù),而不是一個接一個.你同樣可以注意到客戶端立即打印第一首完成的詩,不等其他還在繼續(xù)處理的詩.

好了,讓我們清除還剩下的一點討厭東西并且發(fā)布一個僅僅抓取詩歌而不介意任務(wù)序號的新版本.它位于 haskell-client-2/get-poetry.hs. 注意它短多了,對于每個服務(wù)器,僅僅連接到套接字,抓取所有數(shù)據(jù),之后將其發(fā)送回去.

OK,讓我們編譯新的客戶端:

cd haskell-client-2/
ghc --make get-poetry.hs

針對相同的詩歌服務(wù)器組運行它:

./get-poetry 10001 10002 10003

最終,你將看到屏幕上出現(xiàn)每首詩的文字.

注意到每個服務(wù)器同時向客戶端發(fā)送數(shù)據(jù).更重要的,客戶端以最快速度打印出第一首詩的每一行,而不去等待其余的詩,甚至當它正在處理其它兩首詩.之后它快速地打印出之前積累的第二首詩.

同時這所有發(fā)生的一切都不需要我們做什么.這里沒有回調(diào),沒有傳來傳去的消息,僅僅是一個關(guān)于我們希望程序做什么的簡潔地描述,而且很少需要告訴它應(yīng)該怎樣做.其余的事情都是由Haskell編譯器和運行時處理的.漂亮!

討論與進一步閱讀

從Twisted到Erlang之后到Haskell,我們可以看到一個平行的移動,從前景到背景逐步深入異步編程背后的思想.在Twisted中,異步編程是其存在的核心激勵理念. Twisted實現(xiàn)作為一個與Python分離的框架(Python缺乏核心的異步抽象如輕量級線程),當你用Twisted寫程序時,將異步模型置于首位與核心.

在Erlang中,異步對于程序員仍然是可見的,但細節(jié)成為語言材料的一部分和運行時系統(tǒng),形成一個抽象使得異步消息在同步進程之間交換.

最后,在Haskell中,異步I/O僅僅是運行時中的另一個技術(shù),大部分對于程序員是不可見的,因為提供惰性計算是Haskell的中心理念.

對于以上情況,我們還沒有介紹任何深邃的思想.我們僅僅指出許多有趣的異步模型出現(xiàn)的地方,這種模型可以被多種方式表達.

如果這些激起你對Haskell的興趣,那么我們推薦"Real World Haskell"繼續(xù)你的學習.這本書是介紹語言學習的典范.

雖然我沒有讀過它,我卻聽說到它飽受"Learn You a Haskell"的贊譽.

完成了本系列的倒數(shù)第二部分,現(xiàn)在到了結(jié)束探索Twisted之外異步系統(tǒng)的時刻. 在 第二十二節(jié) 中,我們將做一個總結(jié),以及推薦一些學習Twisted的方法.

建議練習(獻給令人吃驚的狂熱者)

  1. 互相對比Twisted,Erlang和Haskell客戶端.
  2. 修改Haskell客戶端來處理連接詩歌服務(wù)器的失敗,以便它們能夠下載所有的能夠下載的詩歌并為那些不能下載的詩歌輸出合理的錯誤消息.
  3. 寫Haskell版本的對應(yīng)Twisted中的詩歌服務(wù)器.

參考

本部分原作參見: dave @ http://krondo.com/blog/?p=2814

本部分翻譯內(nèi)容參見luocheng @ https://github.com/luocheng/twisted-intro-cn/blob/master/p21.rst