Haskell语言和Erlang语言实现P2P协议的对比

大家好,我是Sadek Drobi,现在在GoTo大会上采访Jesper Louis Anderson。Jesper能向大家介绍下你自己吗?

大家好,我是Jesper。我就是丹麦的一个编程语言技术宅,喜欢摆弄编程语言,摆弄各种类型的编程语言,通过用它们实现各种东西来达到钻研语言的目的。我钻研的对象以函数式语言为主,但也会使用和研究命令式编程语言。尽量一个都不放过。
我知道你用Erlang和Haskell两种语言都实现过BitTorrent协议。是什么原因令你这样做?
一开始我只是想学习语言。我的想法是说,要学一门语言,必须用它来做点什么东西,一定要真正用过,不能只尝试那些玩具一样的例子。用一门语言去解决真正的 问题之后,才能确实掌握它的用法。所以我就带着这种观念开始学习Erlang,因为BitTorrent客户端对于并发和并行能力有很强烈的需求,我就想 到Erlang应该适合用来实现BitTorrent客户端。

开发过程的动力首先是自我挑战“我能做出来吗?”,其次是学习语言的愿望,希望去理解语言的机理。这些就是我用Erlang语言编写BitTorrent 客户端的动机。后来尝试Haskell语言的理由也是一样的。第二次因为已经实现过BitTorrent协议,可以说对问题域相当熟悉,知道它的原理,知 道那些黑暗的角落,所有不好下手的地方都清楚。但即便如此,实现工作仍然不简单,还是有一些困难要克服。不过毕竟提前知道哪些地方可能出问题,新语言学起 来更快一点。

因为可以集中精力在新语言的特性上面。要做的事情没变,还是实现BitTorrent协议,只剩下语言特性需要钻研。总之目标是学习语言,提高我自己对Haskell语言的认识水平。

那就等于说前后两次的经验是完全不同的:第一次你学习如何实现BitTorrent客户端,第二次你学习Haskell语言,用Haskell把事情重做一边?
是这样。
你一开始偏偏选了Erlang,它特别在哪里?
特别在哪里?我觉得Erlang有两点很特别:其一是它具有一种被动的错误处理观念,对于可能发生的错误,你要准备万全的应变计划去面对一切可能的情况, 把错误清理掉或者尽量纠正,尽量保证程序能继续运行下去,不退出或死机。系统尽其所能维持运转。这种观念会影响你对错误的处理方式,造成Erlang程序 独特的写作方式。这是Erlang非常突出的特点,引起我的兴趣和喜好。

第二个特点是它的内部模型,Erlang程序里面存在大量的小进程,而每个进程完全独立于其他进程。在一般的编程语言里面,多个进程之间往往共享内存空 间。Erlang从VM的角度来看不是这个样子。当然从更底层的角度,从内核的角度来看,还是存在一个共享的内存空间。但是在Erlang的思维里面,每 个进程被认为是独立的。这样的模型有其代价,即当某些内容要从一个进程转移到另一个进程的时候,必须完全复制一份。

在我的认知里面,唯有Erlang完全采用这样的模型。也许有些玩具语言也这样,但至少在比较流行的语言里面,没有第二个这样做的。一般的语言都采取共享内存的概念,不会让每个进程完全独立,甚至有自己的垃圾收集器。所以我认为这是Erlang的第二个独特的地方。

Erlang的特点正好有利于你实现BitTorrent客户端。那么当你换到Haskell语言的时候,没有了这些特性,你用什么东西来代替?
Haskell是共享内存的,它也可以fork进程什么的,但内存确实是共享的。好在它有持久化的不可变性。利用persistence immutability的概念,即使没有完全独立的进程,也能取得很好的效果。如果我向另一个进程发送一个数据结构,对方进程得到的本质上是一个副本。 假如我方改变刚才发送的数据结构,将得到一个新版本的数据结构,持久化的概念使双方不受变化的影响,对方看到的原始旧版本还是有效的。

大体上可以把这种特性看成给数据加上版本管理,它化解了进程不完全独立的问题。函数式语言Haskell具备不可变性和持久化的特点,为它解决很多这方面的问题。

错误处理在Haskell里面是怎样的?
这方面有点意思,因为Haskell的错误处理观念不像Erlang那样被动。Haskell程序出错的时候会抛出异常,这种错误处理方式看起来没什么特 别。然而Haskell的最大特点是它具有非常强的类型系统。所以当你主动发挥类型系统的表达能力的时候,类型系统会防止你写出存在错误的程序,从根子上 阻止错误发生,这样就达到减少程序中错误和错误状态的目的。从这个意义上说,Haskell语言的错误处理观念是主动、积极的。

这里头有一个要点,即对待类型系统有两种态度。如果你觉得不喜欢类型系统,那么类型系统往往就发挥不出它的效用。空有一个类型系统不去充分利用,而以勉强 的态度去将就适应存在类型系统这个事实,那么类型系统就会干扰刺激你。假如你换一种态度,把类型系统看作描述程序的手段来积极地运用,那么当程序中存在错 误的时候,有很大几率被类型系统所揭发,起到保护的作用。

所以要注意类型的用法,尽量让类型检查在出错的时候给你指出来:“这里不对!”甚至直接给你指出错在哪一行。所以我这样总结Haskell和Erlang 的异同,它们都极重视程序的正确性和健壮性,但在取得健壮程序的方式上,Haskell偏向于主动的方式,Erlang偏向于被动的方式。

我在写Haskell客户端的时候,处理异常的做法有点像Erlang的错误处理方式。我部分地实现了Erlang的错误处理机制,但没有全盘复制。所以当程序死掉的时候就死掉了,不会像Erlang那样死不了,也不存在重启一部分系统的情况。

Erlang有actor,而Haskell没有,你是怎么做的?
用了迂回的办法。Actor模型是个老模型,从1970年代就出现了,有着严格定义。Erlang在某种意义上实现了Actor模型,它的“类 Actor”特征非常鲜明。我开始编写Haskell客户端的时候,做了一个决定:既然已经有一个用Erlang风格的actor搭建的进程模型,何不直 接把它套用到Haskell。于是我费了一些时间来做这件事,用了Haskell并发库和CML。CML我以前就用过一阵子。我在CML的基础上重新实现 了actor模型。后来发现CML不适合做这件事,而且我不满意CML内部有些抽象泄露的情况。

所以我决定把CML那部分换成软件事务内存(software transactional memory)——STM模型,正好Haskell有这个。系统最后的样子,最底层是STM模型,上面是STM模型营造出来的一个“类Actor”的世 界,Haskell客户端的搭建工作就在这个世界里面进行。大体上是这样的过程。

在STM上面实现起来容易吗?
是的,我觉得挺容易的。就是有一个地方跟Erlang不一样:Erlang是向进程发送消息。发送消息的目标,那个标识符代表了一个进程。Haskell 客户端通过channel来发送消息,消息的发送途径是通道。这一点与Haskell属于静态类型语言有关。通道因为类型是确定的,用起来肯定简单很多。 而进程的话会遇到一个很难回答的问题:“那个进程的输入类型是什么?”因为肯定会遇到一些别的类型,别的很复杂的类型,类型层面的问题不容易解决。

通道的话,在语言层面几乎不需要操心类型的问题。所以在Haskell这样的静态类型语言里面,通道方案最为简单。因此当我编写Haskell版的 BitTorrent客户端的时候,我决定采用通道,而不在这方面照搬Erlang模型。另外由于STM已经包含了通道基本类型,基本不用再费什么事。只 有一件事情需要另外准备,即同时接收多条通道的能力,而STM连这方面的规划功能都给你准备好了。基本上就是万事具备的样子。

我记忆里面,只补充了一些进程处理结构和一些监控结构,用来重置部分系统,或者处理连接突然断开之类的事情。总量不超过四、五十行代码,就足以将大部分的Erlang模型复制过来,重新在Haskell里面实现。

在某种程度上,你的Erlang实现影响了你的Haskell实现。那么你的Haskell实现有没有反过来影响Erlang实现?可以说说吗?
有的。我先有的Erlang实现,然后当我用Haskell重新实现的时候,突然发现因为有通道这个东西,某些部分必须推翻掉。Haskell版的编写过 程让我对每个进程的相互关系有了更进一步的理解。我意识到有些进程存在用户节点意义上的局部性,只对单个用户节点有意义,还有一些进程存在Torrent 意义上的局部性,比如只作用于某个完成了的Torrent。

例如有局部的进程,围绕一个Torrent与一群用户节点进行通信,然后有全局的、与多个Torrents通信的进程。请想象一个例子,假设你希望在系统 中增加限制带宽的设施,限制上行带宽的占用量。这个东西是全局性的,因为你希望限制客户端整体占用的带宽。你还可以对每个Torrent单独限制其占用 量,这种情况属于在Torrent意义上的局部。

继续举例的话,还有比如对单个用户节点的限制,限制某节点允许占用的最大带宽。思路大概就是这个样子,我从这里头总结出来——全局、Torrent意义上 的局部、用户节点意义上的局部——这些关于局部性的系统规律。Erlang实现根本没考虑这些方面,它的基本思路就是一切都是互相联系的。后来我按照新的 思路回头去翻新Erlang版的进程模型,看到一个进程知道说“哦,它是Torrent局部的进程”,那就把它放在这个地方。那个地方一定不能放一个全局 的进程,不然它会同时含有多个Torrents的状态。

这些关于进程定位的知识被回馈到Erlang客户端,补其不足。大体上可以认为Haskell实现是第二次迭代,重新对Erlang实现进行局部修整算是第三次迭代。如果日后又对模型有了新的认识,那么出现第四次迭代也不足为奇。

Erlang和Haskell的类型系统截然不同。Haskell是静态类型的,而Erlang是动态类型的。你对两者的差异有何看法?当然静态类型和动态类型之争一直就没消停过。
我两种都喜欢,但出发点不一样。动态类型绝对有其优点,另一方面静态类型也绝对有其优点。反过来,两种模型又都有各自的缺点。重点是我偏好静态类型,喜欢 静态类型稍稍多于动态类型。但偏好并不影响我使用Erlang,Erlang一方面比较新,另一方面它可以把一些规范、契约写在代码里面。

有一个Erlang代码分析工具叫做Dialyzer,它能找出很多错误,找出那些本来会被静态类型系统拦下来的错误。虽然不是全部,但效果已经很好。另 外,动态类型系统意味着你不需要从一开始就想好程序的结构。而当我用静态类型语言编程的时候,往往要先勾勒一下类型的组成关系。

然后如果我一下想不清楚类型在代码如何呈现,会尝试着写写看。编着编着就会发现类型需要做一些调整。就这样来来回回,修正类型,改写程序,再修正类型,再 改写程序。静态类型语言用起来就是这个样子。但是对于动态类型语言,工作过程好像你就这么一路做下去,像捏橡皮泥那样一点一点成型,一开头是看不出清楚的 结构的。

本来轮廓比较模糊的东西,经过一次迭代出来一点结构,又经过一次迭代再出来一点结构。我发现很多时候两种方式下出来的结果是很接近的。动态类型语言的迭代 方式,捏橡皮泥的思路会把你引导到一个目标。静态类型语言的开发方式,来回加深对类型和程序理解的过程,也同样把你导向一个方向。

我就感觉其实有一个殊途同归的终点,不管你用这种还是那种方法论,都会慢慢地朝向一个终点靠拢。我觉得这种现象很有意思。

你谈了很多Haskell和Erlang语言。还有什么别的语言是你感兴趣的吗?
我对ML家族的语言很感兴趣。历史上的老ML语言大致分为两支:一支是基本上只在学院内使用的标准ML,另一支OCAML不但在学院内用得很多,在业界也 找到用武之地。笼统地说,标准ML是学院派的,OCAML是实践派的,实际用途多一些。这两支,以至整个ML家族我都觉得很有意思,因为它们像 Haskell一样是静态类型的,却不是“惰性的”

它们是“严格的”,写起来与其他语言感觉迥异。它们不是“纯的”, 所以允许可变的成分和命令式的代码。你可以写下命令式的代码,无需求助于monad之类。因为它特别,所以才有趣。用OCAML编程,OCAML程序优化 速度的方式,完全不同于Haskell程序的优化方式。虽然两种语言有很多相似的地方,但求值策略的差异(strict vs lazy)又使它们在很多方面差别很大。因此ML语言是我感兴趣的一种语言。

我还对Google Go语言感兴趣。因为它一方面算是处于很多种语言的中间地带,另一方面加入了并发模型。最早引起我的兴趣是因为它是一种带有现代的消息传递式并发模型的命 令式语言,这是它的特点。目前没有别的语言是这样的定位。因此我认为Go值得研究。Lisp也是我很感兴趣的语言,近来Clojure因为它可以在JVM 上面运行,在某种意义上充当了Lisp的接班人。

那么Clojure借助了JVM的一些优势,另一方面又意味着有些事情很难完成,因为JVM不是百分百适合函数式编程。不过我认为Clojure的开发者 平衡得很好。我还没有了解得很深入,但我认为是值得探索的领域,因为那里有些新鲜玩意正在冒出来。我希望了解他们的想法,了解他们关于语言发展方向的观 点,因为在我看来,他们选择的方向不同以往。不管什么时候,只要有人提出新的道路,都应该去看一看,说不定会有意思。

下一步可以试试用Clojure和Go语言来实现BitTorrent。
其实Go语言出来的时候,我就想过用它来实现BitTorrent客户端。虽然最后用了Haskell,不过项目刚开始的时候,最早的想法是用Go来实 现。Clojure和BitTorrent这个组合我倒是没想过,但应该不会再尝试。实现了那么多BitTorrent客户端,已经有点够了的感觉。我估 计会尝试一些完全不一样的事情,我肯定要用Clojure语言做点什么,但是要做点新东西。这个答案你看怎么样?
This entry was posted in Best Practices. Bookmark the permalink.

发表评论

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / 更改 )

Twitter picture

You are commenting using your Twitter account. Log Out / 更改 )

Facebook photo

You are commenting using your Facebook account. Log Out / 更改 )

Google+ photo

You are commenting using your Google+ account. Log Out / 更改 )

Connecting to %s