programming erlang 笔记-第三章 顺序编程

第三章 顺序编程
本章让我们熟悉用erlang进行常规的顺序编程。
3.1 模块
模块是erlang中,最基本的代码组织单元,所有的代码都是以模块形式进行保存的(类似C++中的cpp,cxx,c文件)。
模块的后缀名为”erl”,编译后后缀为beam。模块的名称必须为atom(小写字母开头,可以包含字母数字,@, _,否则需要用 ” 将atom包含起来),比如我们写了一个计算几何图形面积的模块,命名为geometry.erl:

%%%geometry.erl
-module(geometry).
-export([area/1]).

area({rectangle, Width, Height}) -> Width * Height;
area({circle, R}) -> math:pi() * R * R.

上面就是一个完整的module,我相信此刻你肯定有很多的疑问,因为我刚刚接触erlang的时候也是如此。
让我错略的为您解释一下,希望能为您散去头顶丝丝的疑云。
每个moudule必须包含一些Attribute(属性)声明,以及由函数组成的代码。eralng是面向函数(FP)的编程语言,其代码中几乎处处皆函数,所有函数有返回值。上面geometry的module中,我们首先声明了module属性,注意这个属性加上erl后缀,一定就是我们保存的模块文件(如果你有破坏心理,你可以尝试一下让module声明和文件名不一致,那么后面编译的时候,一定会出现undef的错误),其次声明导出的函数列表,这就相当于输出的接口(如同C++中某个类的public函数,或者c中导出函数),这里我们只是向外暴露一个接口area,用来计算面积。接下来就是函数的定义部分了,我们的area函数包含两个clause(翻译成子句),子句用分号分割,最后一个子句作为函数的结尾用”.”结尾。在第二个子句中,我们调用了math:pi(),获取计算圆面积的PI指,这里就是erlang中函数调用的方法:M:F(A),M代表模块,F代表函数名称,A代表参数。用大括号{}包含的数据结构是tuple(这个词就不翻译了,不然有点蹩脚),与C中的struct有点类似。
经过上面一股脑的介绍,是不是明白了一些?自己动手写一下代码吧。
继续进行,写好代码以后我们需要编译:

$ > erl
1 > c(geometry).
{ok, geometry}

说明咱们的geometry编译成功!如果出现undef错误,那么很直接的告诉你,赶紧升级你erlang吧,我当前使用的R12B-3,它的math模块中有pi/1这个函数哦。

2 > geometry:area({circle, 1.4}).
6.157521601035994

我们的结果与书上的结果不一样?那是因为我们的math:pi/0比joe用的3.14159精确。
扩展这个程序
除了计算矩形和圆形,现在我们又想计算正方形的面积,好吧,别不情愿,让我们在增加一些代码。我们需要做的就是添加一个clause,现在area/1函数是这个样子:

area({rectangle, Width, Height}) -> Width * Height;
area({circle, R}) -> math:pi() * R * R; % . -> ;
area({square, X}) -> X * X.

看到我们上面的注释了么(%表示注释),我们添加了一个clause,那么我们原来以”.”结尾的子句,现在要以”;”结尾,而新添加的子句以.结尾,表明这个area/1的结束。如果你忘记了这些,好心的erlang编译器会提醒你的。
上面三个子句的顺序不是很重要,因为他们计算不同的图形的面积,我们在参数中都有标记,就是那些rectangle, [...]

使用erlang进行text和binary文件的解析(parsing text and binary files)

parsing text and binary files
(form: http://wagerlabs.com/ openpoker author)
Erlang最初主要是针对电信应用开发,因此一个必须要完成的工作就是text和binary数据的转化。
在我们的通讯服务器开发课程中,解析text和binary数据,是我们经常要做的事情。再这里我要向大家讲述如何用erlang高效的处理这些事情。
选择Strings 或 binaries
Erlang本身没有string类型,其采用integer list来表现string,比如:”a string”,对应的list为[$a, $ , $s, $t, $r, $i, $n, $g]。在32位的Erlang虚拟机中,integer为4字节,因此在list的实现内部,每个元素除去4字节存储自身Value,同时还需要4字节作为指针指向下一个元素,因此要表现一个string的字符,我们需要付出8字节的代价,而在64位的erlang虚拟机中,这个代价还要翻倍。
为什么要关心这些呢?
在实际的开发中,每个同服务器的连接都需要分配一定的内存,用来发送接收数据。很多的互联网协议,比如XML其通过字符来表现。比如我们接收10K的XML消息,如果将这些消息转化为String进行解析,那么消息所占用的内存将为80K或者160左右,显然这是非常低效的。
当服务器的并发连接为成百上千时,减少每个连接所消耗的内存,将变的非常有意义。任何数据再经过我们的代码处理成另一种Erlang的数据结构后,原有的数据将进行垃圾回收。如果我们产生的垃圾越少,那么我们的垃圾回收越高效,我们的服务器也就越高效。
因此我们需要把我们通过socket接收的binary保持原样,不要尝试将其转化为strings。binary的解析是非常迅速的,并且具有非常方便的构造和匹配语法。而String却没有这些好处。
Erlang中所有的输入输出函数都可以操作binary数据。同一个程序,采用binary作为内部数据处理的程序肯定比采用String要高效。
好了,让我们用锤子将Erlang String牢牢的钉在坟墓里吧。

将文本文件作为binary处理
假设我们要解析一个以逗号分割的文本文件。我们将解析每一行中的文本,将分割出的各个内容放入list,同时所有的行也放入一个list。我们要处理的文件不是很大,因此我们可以一次加载整个文件。下面是代码的一个最初样子:

-module(act).
-compile([export_all]).

parse(Filename) when is_list(Filename) ->
{ok, Bin} = file:read_file(Filename),
parse(Bin); %% 作者此处原为”.”,应该为”;”

parse(Bin) when is_binary(Bin) ->
parse(Bin, [], [], []).

上面代码中的元素为空的list,作为初始参数,其用来保存累积的值(称为Accumulator,在Erlang这样的FP编程中,其作为参数也用来保存状态)。三个[],分别对应Field,Line和Acc,Field用来保存我们正在处理的字段(一个string),Line用来保存我们正在处理行的field 列表,Acc则是保存我们处理的Line的列表。
上面的代码中有两个parse函数(确切的说是一个parse函数,两个clause):第一个parse的参数为要操作的文件名称(string, a list of integers), 第二个parse函数的参数为binary,即我们读取的文件内容。这两个完成不同功能函数同样的名称,仅仅是通过guard来进行区别。你也可以将第二个函数命名为parse1或do_parse,这样更清晰一些。
Erlang中的函数通过函数名和参数个数来进行区分,采用fun_name/num_args的形式来表示一个函数。由此可知,parse/0, parse/1是不同的函数。我们上面代码中两个parse函数其实是一个函数,仅仅通过guard [...]