Programming Erlang 第5章笔记 高级顺序编程
高级顺序编程
译者: | gashero |
---|
现在我们已经可以很好的理解顺序编程了。本章包含如下内容:
- BIF:是 built-in function 的缩写,是包含在Erlang语言中的一部分。他们看起来像是在Erlang中写的一样,但是实际上是Erlang虚拟机实现的原始操作。
- binary:这是一种常用的原始数据类型,高效率的内存段。
- bit语法:模式匹配语法,用于打包和解包binary中的字段。
- 工具箱:包含一些小专题来完成顺序编程。
一旦你掌握了本节,你就会很了解Erlang的顺序编程了,你也可以准备深入学习并行编程了。
1 BIF
BIF就是内置在Erlang中的函数。通常用于Erlang程序无法实现的功能。例如转换list到tuple或者获取当前的时间和日期。要完成这些任务时,我们就需要调用BIF。
例如BIF的 tuple/to_list/1 转换tuple到list,而 time/0 返回当前时间:
1> tuple_to_list({12,cat,"hello"}). [12,cat,"hello"] 2> time(). {20,0,3}
所有的BIF其实是属于 erlang 模块的虽然大多数的BIF(比如tuple_to_list)是自动导入的,所以我们可以直接使用 tuple_to_list(…) 而不是 erlang:tuple_to_list(…) 。
你可以在手册页找到BIF的完整列表,或者在 http://www.erlang.org/doc/man/erlang.html 。
2 Binary
Binary 数据结构用以存储大量的原始数据。二进制对象存储数据具有比list和tuple更高的空间效率,而且,运行时系统也对二进制对象的输入和输出做了优化。
二进制对象书写和打印作一系列的整数或字符串,包含在双小于号和双大于号中。例如:
1> <<5,10,20>>. <<5,10,20>> 2> <<"hello">>. <<"hello">>
当你在二进制对象中使用整数形式时,每个数字必须在0-255的范围内。二进制对象 <<"cat">> 其实是 <<99,97,116>> 的速记形式;也就是说二进制对象使用ASCII字符构成字符串。
同字符串一样,如果二进制对象是可打印字符串,shell就会将二进制对象当作字符串打印;否则他会按照一个序列的整数来打印。
我们可以构造一个二进制对象或者解析二进制对象的元素,通过BIF,或者我们也可以使用BIF语法(查看5.3节)。在本节,只是看看使用BIF。
Note
@spec func(Arg1,…ArgN) -> Val
@spec代表什么?
这是一种Erlang类型符号,可以被转换成描述函数的文档,包括参数和返回值类型。这是一种很好的自省方式,不过对于想要包含更多细节,请参考附录A。
2.1 管理二进制对象的BIF
如下BIF可以管理二进制对象:
@spec list_to_binary(IoList) -> binary()
list_to_binary 返回一个通过参数IoList构造的二进制对象。这里的IoList是列表,其元素是0-255的整数、二进制对象或IoList:
1> Bin1=<<1,2,3>>. <<1,2,3>> 2> Bin2=<<4,5>>. <<4,5>> 3> Bin3=<<6>>. <<6>> 4> list_to_binary([Bin1,1,[2,3,Bin2],4|Bin3]). <<1,2,3,1,2,3,4,5,4,6>>
@spec split_binary(Bin,Pos) -> {Bin1,Bin2}
这个函数按照指定位置将二进制对象切割为两部分:
1> split_binary(<<1,2,3,4,5,6,7,8,9,10>>,3). {<<1,2,3>>,<<4,5,6,7,8,9,10>>}
@spec term_to_binary(Term) -> Bin
转换Erlang术语到二进制对象。
通过 term_to_binary 产生的二进制对象存储在叫做扩展术语格式中。转换来的术语可以存储在文件中、通过网络报文发送等等,而原始的术语还可以重建。这对于在文件中或远程机器上存储复杂数据结构非常有用。
@spec binary_to_term(Bin) -> Term
这个 term_to_binary 的反函数:
1> B=term_to_binary({binaries,"are",useful}). <<131,104,3,100,0,8,98,105,110,97,114,105,101,115,107, 0,3,97,114,101,100,0,6,117,115,101,102,117,108>> 2> binary_to_term(B). {binaries,"are",useful}
@spec size(Bin) -> int
获取二进制对象的字节数:
1> size(<<1,2,3,4,5>>). 5
3 比特语法
比特语法是一种扩展语法用以对二进制对象中的比特序列进行模式匹配。当你编写底层的用以解包二进制对象时,你会发现比特语法非常有用。比特语法最初设计用于协议编程(Erlang很擅长的方向)和产生高效率的打包数据。
假设我们有三个变量X/Y/Z,是我们需要从16bit的内存M中提取的字段。X占用3bit,Y占用7bit,Z占用6bit。在大多数语言中都是使用底层的位操作,包括移位和掩码。而在Erlang中,你可以这么写:
M=<<X:3,Y:7,Z:6>>
完整的比特语法会稍微复杂一点,所以我们继续下一小步。首先我们看一个简单的打包和解包RGB颜色到16bit字中的例子。然后我们会深入了解比特语法表达式。最后我们看3个实际的比特语法的例子。
3.1 打包和解包16bit颜色
我们来写一个简单的例子。假设我们想要描述一个16bit的RGB颜色。我们让5bit代表红色频道、6bit代表绿色频道、5bit代表蓝色频道。(使用更多的空间给绿色是因为,人眼对绿色更敏感)。
我们可以创建16bit的内存段Mem包含单一的RGB颜色组:
1> Red=2. 2 2> Green=61. 61 3> Blue=20. 20 4> Mem=<<Red:5,Green:6,Blue:5>>. <<23,180>>
注意在第4行我们创建了2字节的二进制对象,包含16bit,而shell打印的则是 <<23,180>> 。
想要解包一个字,我们编写如下模式:
5> <<R1:5,G1:6,B1:5>>=Mem. <<23,180>> 6> R1. 2 7> G1. 61 8> B1. 20
3.2 比特语法表达式
比特语法表达式是如下形式的:
<<>> <<E1,E2,...,En>>
每个元素Ei指定了二进制对象的一个字段。每个元素Ei有四种格式的可能:
Ei=Value | Value:Size | Value/TypeSpecifierList | Value:Size/TypeSpecifierList
无论使用哪种格式,在二进制对象中的总bit数必须可以被8整除。因为二进制对象实际上只是包含了多个字节的数据,所以没法保存不是以字节为单位的数据。
当你构造一个二进制对象时,Value必须已经是确定的了,可以是字符串、或者可以生成整数、浮点数、二进制对象的表达式。当用于模式匹配操作时,Value可以是已经绑定的或者尚未绑定的变量、整数、字符串、浮点数或二进制对象。
Size必须是一个得到整数的表达式。在模式匹配中,Size必须是整数或者值为整数的变量。Size不可以是尚未绑定的变量。
Size的值指定了数据段的单元数。缺省值依赖于类型。对整数缺省为8,浮点数缺省为64,而二进制对象则对应其长度。在模式匹配时,缺省值仅对最后一个元素有效。其他所有匹配时的二进制对象元素长度必须指定。
TypeSpecifierList是以连字符分割的一列元素,形式为End-Sign-Type-Unit。任何前述元素都可以省略,元素也可以在任何顺序。如果一个元素被省略,就使用其缺省值。
TypeSpecifierList中的项目的值可以是如下:
@type End=big | little | native
(@type是Erlang的类型符号,参阅附录A)
这是指定机器的字节序,native是运行时检测,依赖于具体的CPU。缺省是big。这个仅对从二进制对象中打包和解包整数时才有用。在从不同的字节序的机器上打包和解包二进制对象中的整数时,你必须注意正确的字节序。
有些时候,当你必须确定自己理解这些时,这里有些实验可以用。测试你所在的机器,可以尝试在shell中如下输入:
1> {<<16#12345678:32/big>>,<<16#12345678:32/little>>, <<16#12345678:32/native>>,<<16#12345678:32>>}. {<<18,52,86,120>>,<<120,86,52,18>>, <<120,86,52,18>>,<<18,52,86,120>>}这些输出展示了编码到二进制对象的比特语法。
如果你还是无法放心,那么可以用 term_to_binary/1 来完成转换工作,随后用 binary_to_term/1 完成解包。这样就不用担心字节序的问题了。因为在tuple中总是有正确的字节序。
@type Sign=signed | unsigned
这个参数仅用于模式匹配,缺省是unsigned。
@type Type=integer | float | binary
缺省是integer
@type Unit=1 | 2 | … 255
这个段的总单位数,这个单位数必须大于等于0,而且必须是8的整倍数。
Unit的缺省值依赖于Type,如果Type是integer则为1,如果Type是binary则为8。
如果你感觉比特语法有点复杂,不要怕。让比特语法匹配还算简单。最好的实践方法是在shell中不断的尝试,直到符合要求,然后把代码复制粘贴到程序中。我就这么干的。
3.3 高级比特语法例子
学习比特语法还是略有难度的,但是好处也是巨大的。本届包含3个实际的例子。所有代码都是从现实的程序中挖出来的。
3.3.1 寻找MPEG中的同步帧
假设我们需要一个程序管理MPEG音频数据。我们可能想要使用Erlang编写流媒体服务器而需要获得MPEG音频的tag和内容描述。想要实现这些,我们需要识别出数据流中的同步帧。
MPEG音频是从一大堆帧组成的。每个帧都有他自己的头和跟随的音频信息,不过没有文件头。原理上讲,你可以把一个MPEG文件分成很多段并且分别播放。任何相关软件都需要先读取MPEG流的头信息和同步帧。
一个MPEG头部以11bit的同步帧,就是11个连续的bit组成,后面跟真描述信息,例如:
AAAAAAAA AAABBCCD EEEEFFGH IIJJKLMM
字段 | 意义 |
AAAAAAAAAAA | 同步字(11bit,全是1) |
BB | 2bit是MPEG音频的版本号 |
CC | 2bit是层(layer)描述 |
D | 1bit,保护位(bit) |
其他相关细节这里不关心。基本上通过A-M的值,我们就可以计算一个MPEG帧的长度了。
想要找到同步点,我们首先假设我们已经正确的定位了MPEG帧的开始。我们使用位置找到并计算帧长度。不过也有可能定位到无效的数值。假设我们已经得到了帧长度,我们就可以跳过开始的下一帧,看看下一段是否是另外一个帧头部。
想要找到同步点,我们首先假设我们已经定位了MPEG头部。我们随后计算帧长度。然后发生如下步骤:
- 我们的假设是正确的,所以当我们向前跳过一个帧以后,我们会找到下一个MPEG头部。
- 我们的假设是错误的,我们定位的不是以11个1开头的帧头部标志,所以无法计算帧长度。
- 我们的假设不正确,但是我们定位了音乐数据的两个字节,看起来像是帧头部。在这种情况下,我们计算帧长度,但是当我们向前跳这个长度时却无法找到新的头部。
为了验证,我们会尝试3个连续的头部。同步帧计算程序如下:
find_sync(Bin,N) -> case is_header(N,Bin) of {ok,Len1,_} -> case is_header(N+Len1,Bin) of {ok,Len2,_} -> case is_header(N+Len1+Len2,Bin) of {ok,_,_} -> {ok,N}; error -> find_sync(Bin,N+1) end. error -> find_sync(Bin,N+1) end. error -> find_sync(Bin,N+1) end.
find_sync 尝试找到3个连续的MPEG帧头部。如果字节N在Bin世帧头部的开头,随后 is_header(N,Bin) 会返回 {ok,Length,Info} 。如果 is_header 返回 error ,那么N就无法指向正确的帧开始位置。我们可以在shell中做一个快速的测试来确保它工作正常:
1> {ok,Bin} = file:read("/home/joe/music/mymusic.mp3"). {ok,<<73,68,51,3,0,0,0,0,33,22,84,73,84,50,0,0,0,28, ...>> 2> mp3_sync:find_sync(Bin,1). {ok,4256}
这里使用 file:read_file 来读取整个文件到二进制对象。现在是函数 is_header
is_header(N,Bin) -> @page 94