用erlang也算写了些代码了,主要包括使用RabbitMQ的练习,以及最近写的kl_tserver和icerl。其中icerl是一个实现了Ice的erlang库。
erlang的书较少,我主要读过<Programming Erlang>和<Erlang/OTP in Action>。其实erlang本身就语言来说的话比较简单,同ruby一样,类似这种本身目标是应用于实际软件项目的语言都比较简单,对应的语法书很快可以翻完。
这里我仅谈谈自己在编写erlang代码过程中的一些感受。
语法
erlang语法很简单,接触过函数式语言的程序员上手会很快。它没有类似common lisp里宏这种较复杂的语言特性。其语法元素很紧凑,不存在一些用处不大的特性。在这之前,我学习过ruby和common lisp。ruby代码写的比common lisp多。但是在学习erlang的过程中我的脑海里却不断出现common lisp里的语法特性。这大概是因为common lisp的语法相对ruby来说,更接近erlang。
编程模式
erlang不是一个面向对象的语言,它也不同common lisp提供多种编程模式。它的代码就是靠一个个函数组织出来的。面向对象语言在语法上有一点让我很爽的是,其函数调用更自然。erlang的接口调用就像C语言里接口的调用一样:
func(Obj, args)
Obj->func(args)
即需要在函数第一个参数传递操作对象。但是面向对象语言也会带来一些语法的复杂性。如果一门语言可以用很少的语法元素表达很多信息,那么我觉得这门语言就是门优秀的语言。
表达式/语句
erlang里没有语句,全部是表达式,意思是所有语法元素都是有返回值的。这实在太好了,全世界都有返回值可以让代码写起来简单多了:
Flag = case func() of 1 -> true; 0 -> false end,
命名
我之所以不想写一行python代码的很大一部分原因在于这门语言居然要求我必须使用代码缩进来编程,真是不敢相信。erlang里虽然没有此规定,却也有不同的语法元素有大小写的限定。变量首字母必须大写,atom必须以小写字母开头,更霸气的是模块命名必须和文件名相同。
变量
erlang里的变量是不可更改的。实际上给一个变量赋值,严格来说应该叫bound
,即绑定。这个特性完全就是函数式语言里的特性。其带来的好处就像函数式语言宣扬的一样,这会使得代码没有副作用(side effect)。因为程序里的所有函数不论怎样调用,其程序状态都不会改变,因为变量无法被改变。
变量不可更改,直接意味着全局变量没有存在的意义,也就意味着不论你的系统是多么复杂地被构建出来,当系统崩溃时,其崩溃所在位置的上下文就足够找到问题。
但是变量不可改变也会带来一些代码编写上的不便。我想这大概是编程思维的转变问题。erlang的语法特性会强迫人编写非常短小的函数,你大概不愿意看到你的函数实现里出现Var1/Var2/Var3这样的变量,而实际上这样的命名在命令式语言里其实指的是同一个变量,只不过其值不同而已。
但是我们的程序总是应该有状态的。在erlang里我们通过不断创建新的变量来存储这个状态。我们需要通过将这个状态随着我们的程序流程不断地通过函数参数和返回值传递下去。
atom
atom这个语法特性本身没问题,它就同lisp里的atom一样,没什么意义,就是一个名字。它主要用在增加代码的可读性上。但是这个atom带来的好处,直接导致erlang不去内置诸如true/false这种关键字。erlang使用true/false这两个atom来作为boolean operator的返回值。但erlang里严格来说是没有布尔类型的。这其实没什么,糟糕的是,对于一些较常见的函数返回值,例如true/false,erlang程序员之间就得做约定。要表示一个函数执行失败了,我可以返回false、null、failed、error、nil,甚至what_the_fuck,这一度让我迷惘。
list/tuple
erlang里的list当然没有lisp里的list牛逼,别人整个世界就是由list构成的。在一段时间里,我一直以为list里只能保存相同类型的元素,而tuple才是用于保存不同类型元素的容器。直到有一天我发现tuple的操作不能满足我的需求了,我才发现list居然是可以保存不同类型的。
list相对于tuple而言,更厉害的地方就在于头匹配,意思是可以通过匹配来拆分list的头和剩余部分。
匹配(match)
erlang的匹配机制是个好东西。这个东西贯穿了整个语言。在我理解看来,匹配机制减少了很多判断代码。它试图用一个期望的类型去匹配另一个东西,如果这个东西出了错,它就无法完成这个匹配。无法完成匹配就导致程序断掉。
匹配还有个方便的地方在于可以很方便地取出record里的成员,或者tuple和list的某个部分,这其实增强了其他语法元素的能力。
循环
erlang里没有循环语法元素,这真是太好了。函数式语言里为什么要有循环语法呢?common lisp干毛要加上那些复杂的循环(宏),每次我遇到需要写循环的场景时,我都诚惶诚恐,最后还是用递归来解决。
同样,在erlang里我们也是用函数递归来解决循环问题。甚至,我们还有list comprehension。当我写C++代码时,我很不情愿用循环去写那些容器遍历代码,幸运的是在C++11里通过lambda和STL里那些算法我终于不用再写这样的循环代码了。
if/case/guard
erlang里有条件判定语法if,甚至还有类似C语言里的switch…case。这个我一时半会还不敢评价,好像haskell里也保留了if。erlang里同haskell一样有guard的概念,这其实是一种变相的条件判断,只不过其使用场景不一样。
进程
并发性支持属于erlang的最大亮点。erlang里的进程概念非常简单,基于消息机制,程序员从来不需要担心同步问题。每个进程都有一个mailbox,用于缓存发送到此进程的消息。erlang提供内置的语法元素来发送和接收消息。
erlang甚至提供分布式支持,更酷的是你往网络上的其他进程发送消息,其语法和往本地进程发送是一样的。
模块加载
如果我写了一个erlang库,该如何在另一个erlang程序里加载这个库?这个问题一度让我迷惘。erlang里貌似有对库打包的功能(.ez?),按理说应该提供一种整个库加载的方式,然后可以通过手动调用函数或者指定代码依赖项来加载。结果不是这样。
erlang不是按整个库来加载的,因为也没有方式去描述一个库(应该有第三方的)。当我们调用某个模块里的函数时,erlang会自动从某个目录列表里去搜索对应的beam文件。所以,可以通过在启动erlang添加这个模块文件所在目录来实现加载,这还是自动的。当然,也可以在erlang shell里通过函数添加这个目录。
OTP
使用erlang来编写程序,最大的优势可能就是其OTP了。OTP基本上就是一些随erlang一起发布的库。这些库中最重要的一个概念是behaviour。behaviour其实就是提供了一种编程框架,应用层提供各种回调函数给这个框架,从而获得一个健壮的并发程序。
application behaviour
application behaviour用于组织一个erlang程序,通过一个配置文件,和提供若干回调,就可以让我们编写的erlang程序以一种统一的方式启动。我之前写的都是erlang库,并不需要启动,而是提供给应用层使用,所以也没使用该behaviour。
gen_server behaviour
这个behaviour应该是使用频率很高的。它封装了进程使用的细节,本质上也就是将主动收取消息改成了自动收取,收取后再回调给你的模块。
supervisor behaviour
这个behaviour看起来很厉害,通过对它进行一些配置,你可以把你的并发程序里的所有进程建立成树状结构。这个结构的牛逼之处在于,当某个进程挂掉之后,通过supervisor可以自动重新启动这个挂掉的进程,当然重启没这么简单,它提供多种重启规则,以让整个系统确实通过重启变成正常状态。这实在太牛逼了,这意味着你的服务器可以7x24小时地运行了,就算有问题你也可以立刻获得一个重写工作的系统。
热更新
代码热更新对于一个动态语言而言其实根本算不上什么优点,基本上动态语言都能做到这一点。但是把热更新这个功能加到一个用于开发并发程序的语言里,那就很牛逼了。你再一次可以确保你的服务器7x24小时不停机维护。
gen_tcp
最开始我以为erlang将网络部分封装得已经认不出有socket这个概念了。至少,你也得有一个牛逼的网络库吧。结果发现依然还是socket那一套。然后我很失望。直到后来,发现使用一些behaviour,加上调整gen_tcp的一些option,居然可以以很少的代码写出一个维护大量连接的TCP服务器。是啊,erlang天生就是并发的,在传统的网络模型中,我们会觉得使用one-thread-per-connection虽然简单却不是可行的,因为thread是OS资源,太昂贵。但是在erlang里,one-process-per-connection却是再自然不过的事情。你要是写个erlang程序里面却只有一个process你都不好意思告诉别人你写的是erlang。process是高效的(对我们这种二流程序员而言),它就像C++里一个很普通的对象一样。
在使用gen_tcp的过程中我发现一个问题,不管我使用哪一种模型,我竟然找不到一种温柔的关闭方式。我查看了几个tutorial,这些混蛋竟然没有一个人提到如何去正常关闭一个erlang TCP服务器。后来,我没有办法,只好使用API强制关闭服务器进程。
Story
其实,我和erlang之间是有故事的。我并不是这个月开始才接触erlang。早在2009年夏天的时候我就学习过这门语言。那时候我还没接触过任何函数式语言,那时候lua里的闭包都让我觉得新奇。然后无意间,我莫名其妙地接触了haskell(<Real World Haskell>),在我决定开始写点什么haskell练习时,我发现我无从下手,最后,Monads把我吓哭了。haskell实在太可怕了。
紧接着我怀揣着对函数式语言的浓烈好奇心看到了erlang。当我看到了concurrent programming的章节时,在一个燥热难耐的下午我的领导找到了我,同我探讨起erlang对我们的网游服务器有什么好处。然后,我结束我了的erlang之旅。
时隔四年,这种小众语言,居然进入了中国程序员的视野,并被用于开发网页游戏服务器。时代在进步,我们总是被甩在后面。