Jesper Louis Andersen根據(jù)他分別使用Haskell和Erlang語(yǔ)言編寫兩個(gè)BitTorrent客戶端——Combinatorrent和Etorrent的經(jīng)驗(yàn),向聽(tīng)眾闡述運(yùn)用這兩種語(yǔ)言進(jìn)行開(kāi)發(fā)的優(yōu)勢(shì)和困難。他特別詳細(xì)說(shuō)明了如何善用這兩種語(yǔ)言各自的精華之處,充分發(fā)揮消息傳遞機(jī)制強(qiáng)健的并發(fā)能力。
個(gè)人簡(jiǎn)介
Jesper熱愛(ài)鉆研編程語(yǔ)言,尤其是并發(fā)與函數(shù)式編程。他擔(dān)任領(lǐng)導(dǎo)人和主要開(kāi)發(fā)者的兩個(gè)開(kāi)源項(xiàng)目,分別用Erlang和Haskell語(yǔ)言實(shí)現(xiàn)了BitTorrent P2P內(nèi)容分發(fā)協(xié)議,并在項(xiàng)目中盡量發(fā)揮了各自實(shí)現(xiàn)語(yǔ)言的長(zhǎng)處。
大家好,我是Sadek Drobi,現(xiàn)在在GoTo大會(huì)上采訪Jesper Louis Anderson。Jesper能向大家介紹下你自己?jiǎn)幔?
大家好,我是Jesper。我就是丹麥的一個(gè)編程語(yǔ)言技術(shù)宅,喜歡擺弄編程語(yǔ)言,擺弄各種類型的編程語(yǔ)言,通過(guò)用它們實(shí)現(xiàn)各種東西來(lái)達(dá)到鉆研語(yǔ)言的目的。我鉆研的對(duì)象以函數(shù)式語(yǔ)言為主,但也會(huì)使用和研究命令式編程語(yǔ)言。盡量一個(gè)都不放過(guò)。
我知道你用Erlang和Haskell兩種語(yǔ)言都實(shí)現(xiàn)過(guò)BitTorrent協(xié)議。是什么原因令你這樣做?
一開(kāi)始我只是想學(xué)習(xí)語(yǔ)言。我的想法是說(shuō),要學(xué)一門語(yǔ)言,必須用它來(lái)做點(diǎn)什么東西,一定要真正用過(guò),不能只嘗試那些玩具一樣的例子。用一門語(yǔ)言去解決真正的問(wèn)題之后,才能確實(shí)掌握它的用法。所以我就帶著這種觀念開(kāi)始學(xué)習(xí)Erlang,因?yàn)锽itTorrent客戶端對(duì)于并發(fā)和并行能力有很強(qiáng)烈的需求,我就想到Erlang應(yīng)該適合用來(lái)實(shí)現(xiàn)BitTorrent客戶端。
開(kāi)發(fā)過(guò)程的動(dòng)力首先是自我挑戰(zhàn)“我能做出來(lái)嗎?”,其次是學(xué)習(xí)語(yǔ)言的愿望,希望去理解語(yǔ)言的機(jī)理。這些就是我用Erlang語(yǔ)言編寫B(tài)itTorrent客戶端的動(dòng)機(jī)。后來(lái)嘗試Haskell語(yǔ)言的理由也是一樣的。第二次因?yàn)橐呀?jīng)實(shí)現(xiàn)過(guò)BitTorrent協(xié)議,可以說(shuō)對(duì)問(wèn)題域相當(dāng)熟悉,知道它的原理,知道那些黑暗的角落,所有不好下手的地方都清楚。但即便如此,實(shí)現(xiàn)工作仍然不簡(jiǎn)單,還是有一些困難要克服。不過(guò)畢竟提前知道哪些地方可能出問(wèn)題,新語(yǔ)言學(xué)起來(lái)更快一點(diǎn)。
因?yàn)榭梢约芯υ谛抡Z(yǔ)言的特性上面。要做的事情沒(méi)變,還是實(shí)現(xiàn)BitTorrent協(xié)議,只剩下語(yǔ)言特性需要鉆研??傊繕?biāo)是學(xué)習(xí)語(yǔ)言,提高我自己對(duì)Haskell語(yǔ)言的認(rèn)識(shí)水平。
那就等于說(shuō)前后兩次的經(jīng)驗(yàn)是完全不同的:第一次你學(xué)習(xí)如何實(shí)現(xiàn)BitTorrent客戶端,第二次你學(xué)習(xí)Haskell語(yǔ)言,用Haskell把事情重做一邊?
是這樣。
你一開(kāi)始偏偏選了Erlang,它特別在哪里?
特別在哪里?我覺(jué)得Erlang有兩點(diǎn)很特別:其一是它具有一種被動(dòng)的錯(cuò)誤處理觀念,對(duì)于可能發(fā)生的錯(cuò)誤,你要準(zhǔn)備萬(wàn)全的應(yīng)變計(jì)劃去面對(duì)一切可能的情況,把錯(cuò)誤清理掉或者盡量糾正,盡量保證程序能繼續(xù)運(yùn)行下去,不退出或死機(jī)。系統(tǒng)盡其所能維持運(yùn)轉(zhuǎn)。這種觀念會(huì)影響你對(duì)錯(cuò)誤的處理方式,造成Erlang程序獨(dú)特的寫作方式。這是Erlang非常突出的特點(diǎn),引起我的興趣和喜好。
第二個(gè)特點(diǎn)是它的內(nèi)部模型,Erlang程序里面存在大量的小進(jìn)程,而每個(gè)進(jìn)程完全獨(dú)立于其他進(jìn)程。在一般的編程語(yǔ)言里面,多個(gè)進(jìn)程之間往往共享內(nèi)存空間。Erlang從VM的角度來(lái)看不是這個(gè)樣子。當(dāng)然從更底層的角度,從內(nèi)核的角度來(lái)看,還是存在一個(gè)共享的內(nèi)存空間。但是在Erlang的思維里面,每個(gè)進(jìn)程被認(rèn)為是獨(dú)立的。這樣的模型有其代價(jià),即當(dāng)某些內(nèi)容要從一個(gè)進(jìn)程轉(zhuǎn)移到另一個(gè)進(jìn)程的時(shí)候,必須完全復(fù)制一份。
在我的認(rèn)知里面,唯有Erlang完全采用這樣的模型。也許有些玩具語(yǔ)言也這樣,但至少在比較流行的語(yǔ)言里面,沒(méi)有第二個(gè)這樣做的。一般的語(yǔ)言都采取共享內(nèi)存的概念,不會(huì)讓每個(gè)進(jìn)程完全獨(dú)立,甚至有自己的垃圾收集器。所以我認(rèn)為這是Erlang的第二個(gè)獨(dú)特的地方。
Erlang的特點(diǎn)正好有利于你實(shí)現(xiàn)BitTorrent客戶端。那么當(dāng)你換到Haskell語(yǔ)言的時(shí)候,沒(méi)有了這些特性,你用什么東西來(lái)代替?
Haskell是共享內(nèi)存的,它也可以fork進(jìn)程什么的,但內(nèi)存確實(shí)是共享的。好在它有持久化的不可變性。利用persistence immutability的概念,即使沒(méi)有完全獨(dú)立的進(jìn)程,也能取得很好的效果。如果我向另一個(gè)進(jìn)程發(fā)送一個(gè)數(shù)據(jù)結(jié)構(gòu),對(duì)方進(jìn)程得到的本質(zhì)上是一個(gè)副本。假如我方改變剛才發(fā)送的數(shù)據(jù)結(jié)構(gòu),將得到一個(gè)新版本的數(shù)據(jù)結(jié)構(gòu),持久化的概念使雙方不受變化的影響,對(duì)方看到的原始舊版本還是有效的。
大體上可以把這種特性看成給數(shù)據(jù)加上版本管理,它化解了進(jìn)程不完全獨(dú)立的問(wèn)題。函數(shù)式語(yǔ)言Haskell具備不可變性和持久化的特點(diǎn),為它解決很多這方面的問(wèn)題。
錯(cuò)誤處理在Haskell里面是怎樣的?
這方面有點(diǎn)意思,因?yàn)镠askell的錯(cuò)誤處理觀念不像Erlang那樣被動(dòng)。Haskell程序出錯(cuò)的時(shí)候會(huì)拋出異常,這種錯(cuò)誤處理方式看起來(lái)沒(méi)什么特別。然而Haskell的最大特點(diǎn)是它具有非常強(qiáng)的類型系統(tǒng)。所以當(dāng)你主動(dòng)發(fā)揮類型系統(tǒng)的表達(dá)能力的時(shí)候,類型系統(tǒng)會(huì)防止你寫出存在錯(cuò)誤的程序,從根子上阻止錯(cuò)誤發(fā)生,這樣就達(dá)到減少程序中錯(cuò)誤和錯(cuò)誤狀態(tài)的目的。從這個(gè)意義上說(shuō),Haskell語(yǔ)言的錯(cuò)誤處理觀念是主動(dòng)、積極的。
這里頭有一個(gè)要點(diǎn),即對(duì)待類型系統(tǒng)有兩種態(tài)度。如果你覺(jué)得不喜歡類型系統(tǒng),那么類型系統(tǒng)往往就發(fā)揮不出它的效用??沼幸粋€(gè)類型系統(tǒng)不去充分利用,而以勉強(qiáng)的態(tài)度去將就適應(yīng)存在類型系統(tǒng)這個(gè)事實(shí),那么類型系統(tǒng)就會(huì)干擾刺激你。假如你換一種態(tài)度,把類型系統(tǒng)看作描述程序的手段來(lái)積極地運(yùn)用,那么當(dāng)程序中存在錯(cuò)誤的時(shí)候,有很大幾率被類型系統(tǒng)所揭發(fā),起到保護(hù)的作用。
所以要注意類型的用法,盡量讓類型檢查在出錯(cuò)的時(shí)候給你指出來(lái):“這里不對(duì)!”甚至直接給你指出錯(cuò)在哪一行。所以我這樣總結(jié)Haskell和Erlang的異同,它們都極重視程序的正確性和健壯性,但在取得健壯程序的方式上,Haskell偏向于主動(dòng)的方式,Erlang偏向于被動(dòng)的方式。
我在寫Haskell客戶端的時(shí)候,處理異常的做法有點(diǎn)像Erlang的錯(cuò)誤處理方式。我部分地實(shí)現(xiàn)了Erlang的錯(cuò)誤處理機(jī)制,但沒(méi)有全盤復(fù)制。所以當(dāng)程序死掉的時(shí)候就死掉了,不會(huì)像Erlang那樣死不了,也不存在重啟一部分系統(tǒng)的情況。
Erlang有actor,而Haskell沒(méi)有,你是怎么做的?
用了迂回的辦法。Actor模型是個(gè)老模型,從1970年代就出現(xiàn)了,有著嚴(yán)格定義。Erlang在某種意義上實(shí)現(xiàn)了Actor模型,它的“類Actor”特征非常鮮明。我開(kāi)始編寫Haskell客戶端的時(shí)候,做了一個(gè)決定:既然已經(jīng)有一個(gè)用Erlang風(fēng)格的actor搭建的進(jìn)程模型,何不直接把它套用到Haskell。于是我費(fèi)了一些時(shí)間來(lái)做這件事,用了Haskell并發(fā)庫(kù)和CML。CML我以前就用過(guò)一陣子。我在CML的基礎(chǔ)上重新實(shí)現(xiàn)了actor模型。后來(lái)發(fā)現(xiàn)CML不適合做這件事,而且我不滿意CML內(nèi)部有些抽象泄露的情況。
所以我決定把CML那部分換成軟件事務(wù)內(nèi)存(software transactional memory)——STM模型,正好Haskell有這個(gè)。系統(tǒng)最后的樣子,最底層是STM模型,上面是STM模型營(yíng)造出來(lái)的一個(gè)“類Actor”的世界,Haskell客戶端的搭建工作就在這個(gè)世界里面進(jìn)行。大體上是這樣的過(guò)程。
在STM上面實(shí)現(xiàn)起來(lái)容易嗎?
是的,我覺(jué)得挺容易的。就是有一個(gè)地方跟Erlang不一樣:Erlang是向進(jìn)程發(fā)送消息。發(fā)送消息的目標(biāo),那個(gè)標(biāo)識(shí)符代表了一個(gè)進(jìn)程。Haskell客戶端通過(guò)channel來(lái)發(fā)送消息,消息的發(fā)送途徑是通道。這一點(diǎn)與Haskell屬于靜態(tài)類型語(yǔ)言有關(guān)。通道因?yàn)轭愋褪谴_定的,用起來(lái)肯定簡(jiǎn)單很多。而進(jìn)程的話會(huì)遇到一個(gè)很難回答的問(wèn)題:“那個(gè)進(jìn)程的輸入類型是什么?”因?yàn)榭隙〞?huì)遇到一些別的類型,別的很復(fù)雜的類型,類型層面的問(wèn)題不容易解決。
通道的話,在語(yǔ)言層面幾乎不需要操心類型的問(wèn)題。所以在Haskell這樣的靜態(tài)類型語(yǔ)言里面,通道方案最為簡(jiǎn)單。因此當(dāng)我編寫Haskell版的BitTorrent客戶端的時(shí)候,我決定采用通道,而不在這方面照搬Erlang模型。另外由于STM已經(jīng)包含了通道基本類型,基本不用再費(fèi)什么事。只有一件事情需要另外準(zhǔn)備,即同時(shí)接收多條通道的能力,而STM連這方面的規(guī)劃功能都給你準(zhǔn)備好了?;旧暇褪侨f(wàn)事具備的樣子。
我記憶里面,只補(bǔ)充了一些進(jìn)程處理結(jié)構(gòu)和一些監(jiān)控結(jié)構(gòu),用來(lái)重置部分系統(tǒng),或者處理連接突然斷開(kāi)之類的事情??偭坎怀^(guò)四、五十行代碼,就足以將大部分的Erlang模型復(fù)制過(guò)來(lái),重新在Haskell里面實(shí)現(xiàn)。
在某種程度上,你的Erlang實(shí)現(xiàn)影響了你的Haskell實(shí)現(xiàn)。那么你的Haskell實(shí)現(xiàn)有沒(méi)有反過(guò)來(lái)影響Erlang實(shí)現(xiàn)?可以說(shuō)說(shuō)嗎?
有的。我先有的Erlang實(shí)現(xiàn),然后當(dāng)我用Haskell重新實(shí)現(xiàn)的時(shí)候,突然發(fā)現(xiàn)因?yàn)橛型ǖ肋@個(gè)東西,某些部分必須推翻掉。Haskell版的編寫過(guò)程讓我對(duì)每個(gè)進(jìn)程的相互關(guān)系有了更進(jìn)一步的理解。我意識(shí)到有些進(jìn)程存在用戶節(jié)點(diǎn)意義上的局部性,只對(duì)單個(gè)用戶節(jié)點(diǎn)有意義,還有一些進(jìn)程存在Torrent意義上的局部性,比如只作用于某個(gè)完成了的Torrent。
例如有局部的進(jìn)程,圍繞一個(gè)Torrent與一群用戶節(jié)點(diǎn)進(jìn)行通信,然后有全局的、與多個(gè)Torrents通信的進(jìn)程。請(qǐng)想象一個(gè)例子,假設(shè)你希望在系統(tǒng)中增加限制帶寬的設(shè)施,限制上行帶寬的占用量。這個(gè)東西是全局性的,因?yàn)槟阆M拗瓶蛻舳苏w占用的帶寬。你還可以對(duì)每個(gè)Torrent單獨(dú)限制其占用量,這種情況屬于在Torrent意義上的局部。
繼續(xù)舉例的話,還有比如對(duì)單個(gè)用戶節(jié)點(diǎn)的限制,限制某節(jié)點(diǎn)允許占用的最大帶寬。思路大概就是這個(gè)樣子,我從這里頭總結(jié)出來(lái)——全局、Torrent意義上的局部、用戶節(jié)點(diǎn)意義上的局部——這些關(guān)于局部性的系統(tǒng)規(guī)律。Erlang實(shí)現(xiàn)根本沒(méi)考慮這些方面,它的基本思路就是一切都是互相聯(lián)系的。后來(lái)我按照新的思路回頭去翻新Erlang版的進(jìn)程模型,看到一個(gè)進(jìn)程知道說(shuō)“哦,它是Torrent局部的進(jìn)程”,那就把它放在這個(gè)地方。那個(gè)地方一定不能放一個(gè)全局的進(jìn)程,不然它會(huì)同時(shí)含有多個(gè)Torrents的狀態(tài)。
這些關(guān)于進(jìn)程定位的知識(shí)被回饋到Erlang客戶端,補(bǔ)其不足。大體上可以認(rèn)為Haskell實(shí)現(xiàn)是第二次迭代,重新對(duì)Erlang實(shí)現(xiàn)進(jìn)行局部修整算是第三次迭代。如果日后又對(duì)模型有了新的認(rèn)識(shí),那么出現(xiàn)第四次迭代也不足為奇。