Erlang程式设计与问题解决/Erlang/OTP 程式库
本章介绍 Erlang 开源电信平台 (Open Telecom Platform, OTP) 的执行环境指令与程式库。
Erlang/OTP 平台提供一个编译器、一个直译器、一套程式库、一套伺服器协定、以及 CORBA 。 Erlang/OTP 的 Windows 版本平台有视窗模式的控制台可用。 Erlang 是函数语言,在 Erlang 控制台中,使用指令也是函数呼叫。当你启动控制台时,会载入 Erlang 程式库。 Erlang 程式库区分为许多模组,负责处理多种方面的问题与需求。 Erlang 控制台中,内定的模组为 erlang ,当执行 erlang 模组的函数时,可以不加上模组前缀字 ( erlang: ) 。使用其他模组则必须以模组名称做为前缀字。每一个模组提供许多函数,这些函数都为了执行与该模组有关的操作。
我们称各种模组提供的各种函数为:内建函数 ( Built-In Function, BIF ) 。
模组简介
[编辑]Erlang 分为几个应用程式,有 kernel 、 compiler 、 stdlib 、 mnesia ... 等等。
应用程式概观
[编辑]当你要使用 Erlang ,是以使用 Erlang runtime 系统为开始。 Erlang 以 kernel 应用程式开始,载入一些控制器、编码服务、错误记录服务、作业系统及网路服务模组等等。由 sasl 应用程式 ( 系统程式支援 ) 提供警讯、回报系统及系统工具。
当你在撰写 Erlang 程式时, stdlib 程式库提供了大量内建函数,提供一些应用方面的细节,包含资料、行程、档案、网路等各方面的操作功能。 mnesia 应用程式提供分散式的资料库管理系统,以关联式及物件式混搭模型做资料操作。
STDLIB 模组列表
[编辑]- 资料结构方面
- 有 array 模组、 lists 模组、 string 模组、 sets 模组、 ordsets 模组 ( ordered set ) 、 sofs 模组 ( set of sets ) 、 queue 模组 、 dict 模组、 orddict 模组 ( ordered dictionary ) 、 proplists 模组 ( property lists ) 、 digraph 模组及 digraph_utils 模组、 gb_trees 模组 ( general balanced trees ) 与 gb_sets ( 用 general balanced tree 做 ordered set ) 。
- 行程方面
- 有 proc_lib 模组 ( 提供行程操作 ) 、 pool 模组 ( 管理 Erlang 行程 ) 。
- 资料库方面
- qlc 模组 ( 提供对 ets 、 dets 与 mnesia 资料表查询的介面语言 ) 。
- 网路方面
- pg 模组 ( distributed, named process groups ) 、 slave 模组 ( 提供网路子节点操作 ) 。
- 样版
- 有一些模组提供应用程式样版,有 gen_server 模组、 gen_fsm 模组 ( finite state machine ) 、 gen_event 模组、 supervisor 模组、 supervisor_bridge 模组
- io 与 io_lib 模组
- 提供输入或输出函数。
- calendar 模组
- 处理日期格式。
- ets 模组
- 称为 Erlang Term Service ,提供对大量资料做随机存取的功能,在记忆体中处理资料。
- dets 模组
- ets 的磁碟版本,在档案中处理资料。
- filelib 模组
- 档案系统工具。
- file_sorter 模组
- 提供将档案内容排序或合并的功能。
- filename 模组
- 处理档案名称的撷取、分割或合并。
- zip 模组
- 操作 zip 档案。
- math 模组
- 提供常用的数学公式演算。
- random 模组:提供随机数操作。
- regexp 及 re 模组
- 对字串做正规表达检查。
- timer 模组
- 提供计时操作。
- unicode 模组
- 提供 Unicode 字码转换。
- sys 模组
- 提供系统资讯操作,包含统计程式执行结果、追踪程式执行 ... 等等。
Erlang 控制台模组
[编辑]当进入 Erlang 控制台时,会预先载入一些模组,其中有 c 模组,称为命令介面模组 (Command Interface Module) 。在 Erlang 控制台使用以下命令时,不需要前缀模组名称。
- ls/0, ls/1
- 类似 Unix/Linux 的档案列表命令 ls ,也类似 Windows 的 dir 。
ls/0 和 ls/1 都是没有传回值的函数,只有执行时会做出列出目录档案列表的效果。 ls/0 列出目前目录档案,而 ls/1 列出指定目录的档案。目录名称为字串。
- cd/1
- 更换目录,用法与 Window 、 Unix/Linux 的 cd 相同。
cd/1 需要一个参数,为绝对档案路径或相对档案路径。 cd/1 函数没有传回值,只有执行切换目录的效果。切换之后,会印出当前目录的路径。如果切换之后印出的目录路径不变,表示目录没有完成切换,隐含了所指定要切换的绝对档案路径或相对档案路径无效。
- c/1, c/2
- 编译 Erlang 程式。
c/1 和 c/2 是用来将 .erl 编译为 .beam 档案,以供程式执行。 c/1 直接编译目前目录能看到的模组,需要给它一个参数,为模组名称:例如,档名为 "test.erl" ,编译命令则是 "c(test)." 。当编译成功时,传回 {ok,模组名称} ;失败则传回 error 。程式如果需要警告或警示错误,会在编译时一并印出相关讯息。
> c(test). {ok,test} > c(empty). ./empty.erl:none: no such file or directory error
c/2 等同于 compile:file/2 ,需要一个模组名称与编译参数。
- halt/0, q/0
- 关闭 Erlang 控制台。
halt/0 是将 Erlang runtime 系统正常关闭。 q/0 是呼叫 init:stop/0 ,以在控制之内的方式使 Erlang 程序结束。
halt/0 是 erlang 模组内建函数 ( erlang:halt/0 ) 。有一些 erlang 模组的函数,使用时不必在前面加缀模组名称。
- help/0
- 列出命令列表。
Erlang 控制台还提供许多便利的命令,可以使用 help(). 命令,列出清单及说明。
lists 模组
[编辑]前面几章大量使用了 lists:seq/2 。当我需要用到一串数字,我不自己打字输入,而是叫 Erlang 帮忙产生的时候,就会用到 lists:seq/2 。 lists:seq/2 的定义是:参数给二个数字,它会产生从第一个数字到第二数字的递增序列。
列表的产生
[编辑]> lists:seq(1,100). [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22, 23,24,25,26,27,28,29|...]
有时,我想要为一个列表的每个项目都加上一个编号,这时会使用 lists:zip/2 ,把这个列表跟一个对等的整数列表两两贴齐,像拉上拉链一样。 lists:zip/2 的定义是:将二个长度相同的列表“拉上拉链”连接起来。以下例子,是将列表加上索引,使结果看起来像阵列。
-module(example). -export([indexing/1]). indexing(Xs) -> Is = lists:seq(0,length(Xs)-1), lists:zip(Is,Xs).
> example:indexing([a,b,c]). [{0,a},{1,b},{2,c}]
lists:map/2 容许你给一个函数,让列表的每一个项目都套用这个函数:例如,以下是将列表的每个项目都求平方。
> lists:map(fun (X) -> X * X end, lists:seq(1,10)). [1,4,9,16,25,36,49,64,81,100]
列表的增删
[编辑]有二个函数与 lists:map/2 很类似, lists:takewhile/2 是依序判断列表每一个项目,如果判断函数传回 true ,就从列表取出该项目,如此一直取到遇到不符合判断函数为止。, lists:dropwhil/2 相对地,是依序判断列表每一个项目,如果判断函数传回 true 就从列表删掉该项目,否则就中断,传回剩下的列表。也就是,前者从列表前段取最长一段完全符合判断函数的子列表,后者从列表前段删去最长一段完全符合判断函数的子列表。以下,示范二个例子,分别展示二个函数。
> lists:takewhile(fun (X) -> X > 5 end, [6,6,8,7,4,8]). [6,6,8,7] > lists:dropwhile(fun (X) -> X > 5 end, [6,6,5,7,4,8]). [5,7,4,8]
lists:filter/2 与前二者有相同的操作方式。 lists:filter/2 的定义是:根据指定的判断函数,将列表中符合条件的项目取出。
> lists:filter(fun(X)-> X /= nil end, [nil,{a,b,d},{b,nil,c},{d,nil,nil}]). [{a,b,d},{b,nil,c},{d,nil,nil}]
而这样的函数呼叫,与以下的列表列示 ( list comprehension ) 有相同的意义:
> [ X || X <- [nil,{a,b,d},{b,nil,c},{d,nil,nil}], X /= nil ]. [{a,b,d},{b,nil,c},{d,nil,nil}]
列表的更换顺序
[编辑]lists:reverse/1 可以把列表前后顺序倒转。 lists:sort/1 可以求排序之后的列表。
如果你有个列表,其中每一项目是以值组 ( tuple ) 表达,这个列表的排序是要用值组的其中一个项目做关键值排序。对于这样的需求, lists:keysort/2 可以提供服务:例如,对 [{8,c},{4,a},{6,b},{5,d}] 分别用每个项目的第一栏排序,或是用第二栏排序,结果为
> lists:keysort(1,[{8,c},{4,a},{6,b},{5,d}]). [{4,a},{5,d},{6,b},{8,c}] > lists:keysort(2,[{8,c},{4,a},{6,b},{5,d}]). [{4,a},{6,b},{8,c},{5,d}]
另外,有 lists:merge/2 函数可将二个已排序的列表做合并。
> lists:merge([1,3,5],[2,4,6]). [1,2,3,4,5,6] > lists:merge([2,4,6],[1,3,5]). [1,2,3,4,5,6] > lists:merge([4,5,6],[1,2,3]). [1,2,3,4,5,6]
列表撷取、分割、合并
[编辑]子列表的撷取,除了可以用 lists:takewhile/2 、 lists:dropwhile/2 之外,还可以用 lists:sublist/2 、 lists:sublist/3 ,直接指定要撷取的长度与位置。 lists:sublist/2 给一个长度参数,会取出指定长度的子列表。
> lists:sublist([1,2,3,4], 3). [1,2,3] > lists:sublist([1,2,3,4], 5). [1,2,3,4]
lists:sublist/3 给一个开始位置参数和一个长度参数,会从指定位置开始,取指定长度的子列表。
> lists:sublist([1,2,3,4],1,2). [1,2] > lists:sublist([1,2,3,4],2,4). [2,3,4]
须注意,用在列表的项目位置是以 1 为底,不像一般谈阵列元素位置都是以 0 为底。以上 lists:sublist/2 、 lists:sublist/3 的位置和长度参数都要用大于 0 的数字。
lists:split/2 可用指定的长度参数,将列表分割为前后二段,并且前段长度为指定的长度。长度参数有边界限制。
> lists:split(0, [a,b,c]). {[],[a,b,c]}
另外,有 lists:partition/2 可以根据一个判断函数,将列表分为符合的部份与不符合的部份。
> lists:partition(fun (X) -> X < 5 end, [1,5,6,2,3,7,8,4]). {[1,2,3,4],[5,6,7,8]}
有时,我们会拿到巢状的列表,列表的每个元素仍是列表。如果需要将这些深度的列表结构摊平,可以使用 lists:flatten/1 。
> lists:flatten([[[]],[],[[]]]). [] > lists:flatten([[[a,b],[c,d]],[[e,f] ]]). [a,b,c,d,e,f]
对于上例的情况, [[[a,b],[c,d]],[[e,f] ]] ,有时会想要把 [a,b] 、 [c,d] 、 [e,f] 等单元结构保留。这种情况恰好可以使用 lists:append/1 。
> lists:append([[[a,b],[c,d]],[[e,f] ]]). [[a,b],[c,d],[e,f]]
另外有个 lists:concat/1 函数,与 lists:append/1 很像,不过, lists:concat/1 是将列表中的每一项目都当作字串,并将原子项目转换为字串,结果是传回一个合并字串。
> lists:concat([a,b,c,d,e,f]). "abcdef"
不过在某方面, lists:concat/1 与 lists:append/1 拥有相同效果。
> lists:concat([[[a,b],[c,d]],[[e,f] ]]). [[a,b],[c,d],[e,f]]
列表元素操作
[编辑]列表的首元素可以由以 [H|_] 样式匹配而取出 H 值。最后一个列表元素则用 lists:last/1 取得。
至于任何一个位置的列表元素,则使用 lists:nth/2 取出。
> lists:nth(2, [a,b,c,d]). b
使用 lists:delete/2 可以将指定的元素,从列表中第一个符合的位置,把元素删除。
> lists:delete(1,[1,1,1,1]). [1,1,1]
效果与使用 -- [1] 相同。
> [1,1,1,1] -- [1]. [1,1,1]
另外有 lists:subtract/2 函数与 -- 运算子等价。
高阶列表函数
[编辑]lists:foldr/3 、 lists:foldl/3 提供以折叠方式处理一列资料。折叠的概念是说,每一折都是将新的一折折到已经折好的折叠组上。重写一次,每一折都是将“新的一折”“折到”“已经折好的折叠组”上。“新的一折”、“折到”、“已经折好的折叠组”,只要抓住这些元素,再加上“空的折叠组是 [] ”这样的基础,就可以用一个函数把这种折叠的过程表达出来。首先,“折到”是一个函数,它有二个参数分别是“新的一折”和“已经折好的折叠组”。以下例子是在 Erlang 控制台,用 lambda 表示法定义一个“折到”函数。
> Fold = fun (X, Y) -> [X|Y] end.
lists:foldr/3 和 lists:foldl/3 接受相同的三个参数,分别是,折叠函数、基本情况与要处理的列表。当我们想要从一个列表建立出另一个相同的列表时,折叠函数是上述的 Fold ,基本情况是 [] 。于是,以下例子展示重复一个列表自身而产生新列表的函数呼叫。
> lists:foldr(Fold, [], [a,b,c]). [a,b,c]
如果我们写折叠函数如下:
> Plus = fun (X, Y) -> X + Y end.
那么,以下程式实现 lists:sum/1 ,将一个列表值加总。
> lists:foldr(Plus, 0, lists:seq(1,10)). 55
练习
[编辑]- 一元二次数学式
- 请写一函数,将系数───依序为 [1,2,1] ───表示为一元二次数学式,即 X2 + 2X1 + X0 。当函数接受了一个数代表 X ,就会计算结果。
- 一元五次数学式
- 请写一函数,用到 lists:foldl/3 而不用递回,将系数───依序为 [3,0,5,2,4,1] ───表示为一元五次数学式,即 3X5 + 0X4 + 5X3 + 2X2 + 4X1 + X0 。当函数接受了一个数代表 X ,就会计算结果。
阵列模组
[编辑]Erlang/OTP标准程式库的阵列模组 array ,用来制作函数式、并且可延长的阵列。称为阵列,特色当然是以 0 为底。 ( 再次强调,阵列以 0 为底,而相较地,列表和字串以及值组都以 1 为底。 ) 阵列长度可以固定,也可以自动延长。阵列可以在产生时赋值,否则,阵列的每个元素以 undefined 为预设值。使用 array:reset/2 可以将阵列内容回复为预设值。使用 array:resize/2 或 array:resize/1 可以调整阵列的长度。须注意,阵列长度不会自动缩短,一但你用了索引值 i 在重新调整阵列长度之前,从索引值 0 ... i 的阵列空间会一直留著。
阵列的产生
[编辑]使用 array:new/1 建立新阵列,要加上一些参数,这些参数是分别是一些表示阵列长度、固定性及预设值的词汇。词汇表达为:
- 任何数字,或 {size, 数字} 代表阵列长度。
- fixed 或 {fixed, true} 代表固定阵列; {fixed, false} 代表变动阵列。
- {default, 值} 代表预设值。
打开 Erlang 主控台,输入以下情况并观察结果:
( 建立一個陣列,擁有一個元素。陣列固定長度,並且預設值為 10 。 ) > T = array:new([1,{fixed,true},{default,10}]). {array,1,0,10,10} ( 轉換為列表,直接看內容資料。預設值 10 被解讀為字元 \n 。 ) > array:to_list(T). "\n" ( 取陣列第 0 索引值的元素。 ) > array:get(0, T). 10 > array:get(1, T). ** exception error: bad argument in function array:get/2
由例子中可以学到,array:to_list/1 将阵列转换为列表, array:get/2 对阵列取指定索引的元素。
阵列的更动
[编辑]array:set/3 是将指定的阵列元素赋值,三个参数依序为索引值、要赋与的值、以及阵列识别项。如果阵列是固定长度,给超出长度的索引赋值会失败。如果阵列是变动长度,同样的动作会使阵列长度增加。
( 延續上例,對索引值 1 的陣列元素賦值,結果會失敗。 ) > array:set(1, hello, T). ** exception error: bad argument in function array:set/3 ( 將陣列放寬為變動長度 。 ) > T1 = array:relax(T). {array,1,10,10,10} > array:set(1, hello, T1). {array,2,10,10,{10,hello,10,10,10,10,10,10,10,10}} > array:to_list(array:set(1,hello,T1)). [10,hello]
在例子中的 array:relax/1 是将固定长度阵列改变为变动长度阵列。另外有 array:fix/1 是将变动长度阵列改变为固定长度阵列。
取得阵列属性
[编辑]array:size/1 可读取阵列长度。 array:is_fix/1 判断阵列的固定性。 array:default/1 读取阵列元素的预设值。
array:is_array/1 判断参数是否为阵列,不过,这个函数是做比较粗略的判断工作,并不会检查阵列是否有良好格式。
字串模组
[编辑]Erlang 称字串为整数列表,这表示字串拥有一部份列表的性质,而列表仍有许多字串所没有的用途与用法。所以,对字串处理, Erlang 提供了 string 模组来帮忙。
一进入 string 模组的范围,我们先看到 string:len/1 可以求字串长度。这与使用 erlang:length/1 求列表长度的功能相同。此外,你应该晓得,对字串处理,较明确的需求是:字串分割、合并,寻找子字串,取子字串,以及把字串转为数字或其他类型等等。
字串分割与合并
[编辑]string:tokens/2 使用一些分隔符号把字串分割,分隔符号参数为一个或多个符号。 string:join/2 使用一些分隔符号将字串合并,分隔符号参数只代表一个分隔符号。
一个分隔符号的例子:
> string:tokens("hello, my world", ", ."). ["hello","my","world"]
上例意思说是用逗号 "," 、空格 " " 或句号 "." 做分隔符号。字串分割结果是能将一般英文句子的字都拿出来,构成一个字串的列表。将来一旦考虑到一般英文句子的分解,你要想到冒号 ":" 、分号 ";" 以及单、双引号的情况,不一定都只用 string:tokens/2 就能妥善处理,因为单引号也有做连写词汇的用法 ( this is —— this's ) ,而且把逗点放在右边双引号的前面是英文的惯例。
以下为合并字串的例子,只使用一个分隔符号:
> string:join(["hello","my","world"], "=v="). "hello=v=my=v=world"
另外,有个 string:concat/2 可以将二个字串连接成一个字串:
> string:concat("hello,", " world"). "hello, world"
最后,有个独特的 string:words/2 ,使用一个字元符号来告诉我们,会分解出多少个段落。例子如下:
> string:words("hello, my world"). 3 > string:words("hello, my world", $m). 2
上例第一项呼叫 string:words/1 ,预设使用空格符号。第二项使用字母 m 为分隔符号,结果会有二个段落。 ( 在此要提醒,仔细阅读 API 文件,不要对函数的字面望文生义。像这个 string:words/1 并没有自动从句子中找出英文字这种聪明的功能,如果误解了它的意思,可能会误用。 )
子字串处理
[编辑]使用 string:str/2 可以判断一个字串是否包含另一个字串,如果找到子字串就传回子字串位置。需注意,如果没有找到子字串,是传回 0 。 找子字串是从左端开始找最靠左的子字串。同一个函数,有从右端找子字串的版本 string:rstr/2 ,是从右端开始找最靠右的子字串。
> string:str("hello, world", "hello, world!"). 0 > string:str("hello, world", "world"). 8 > string:str("coffe or tea or wine", "or"). 7 > string:rstr("coffe or tea or wine", "or"). 14
至于撷取子字串, Erlang STDLIB 提供了 string:substr/x 和 string:sub_string/x 二种版本。二种版本分别又可有二个参数,或有三个参数。
string:substr/2 和 string:sub_string/2 都是从字串中取指定开始位置到字串结尾的子字串。
> string:substr("hello, world", 7). " world" > string:sub_string("hello, world", 7). " world"
string:substr/3 从开始位置取指定长度的子字串。
> string:substr("hello, world", 7, 4). " wor"
string:sub_string/3 给一个开始位置、和一个结束位置,就取字串中这二个位置之间的子字串。
> string:sub_string("hello, world", 7, 11). " worl"
另外,依之前介绍的另一个子字串取法 string:words/2 ,根据一个字元将字串分解为许多字,还有个 string:sub_word/x 可以将所指定第几个字取出。
string:sub_word/2 预设分段符号是空格:
> string:sub_word("hello, world", 2). "world"
string:sub_word/3 将字串用指定符号分段,然后取出指定的第几个字:
> string:sub_word("hello, world", 2, $o). ", w"
字串转换
[编辑]当字串内容含有数字时,可以用 string:to_integer/1 和 string:to_float/1 转换为数字。用法为,每呼叫一次,会将字串从头开始找符合数字类型的子字串做转换,并且留下剩下的部份。
> string:to_integer("1 2 3"). {1," 2 3"} > {Int,Rest} = string:to_integer("1 2 3"), string:to_integer(Rest). {error,no_integer}
string:to_lower/1 和 string:to_upper/1 用来做字串转换小写或大写。
> string:to_upper("hello, world"). "HELLO, WORLD"
练习
[编辑]综合您对 Erlang 资料型态、列表模组及字串模组的认知,请做以下练习:
- 分解计算式
- 写一则函数,输入字串内容为合理的四则运算式,函数会将式子的数字与符号分解:例如, (1024+512)*256 分解为 ["(","1024","+","512",")","*","256"] 。
- 计算式后序化
- ["(","1024","+","512",")","*","256"] 是一则中序计算式,可对应为后序式 ["1024","512","+","256","*"] 。写一则函数,输入表达为字串列表的中序式,将中序式转换为后序式并输出。您可以利用列表的基本操作恰好是堆叠的特性。
- 计算式求值
- 写一则函数处理像 ["1024","512","+","256","*"] 这样的后序式输入。这个题目要用到堆叠,在遇到数字时放进堆叠,遇到运算符号就从堆叠中取二个数字求解,求解的结果再放入堆叠。如果计算式合理,最后堆叠只存有一个数字,就是解答。
输入/输出模组
[编辑]Erlang 标准程式库有 io 模组提供输入及输出介面函数,以及 io_lib 模组为输入及输出所需要的字串处理提供相关函数。
常用的函数为 io:format/2 函数,指定格式与一些要处理的资料,就可以按照格式将文字印在标准输出装置。
> io:format("~s, ~s~n", ["hello","world"]). hello, world ok
以上例子, hello, world 为印出到标准输出装置的文字,而 ok 讯息是函数的执行结果。
io:format/1 可以印一般的文字讯息:
> io:format("hello, world~n"). hello, world ok
在 io_lib 模组有一个对应的 io_lib:format/2 函数,是格式处理之后产出一列字元列表。例子如下:
> Result = io_lib:format("~wx~w=~w", [9, 8, 9*8]). ["9",120,"8",61,"72"]
所产出的文字可供做后续处理。
格式符号
[编辑]文字格式处理需要用许多格式符号,表示嵌入栏位或是特殊文字。以下列表简述几种常用的格式符号:
符号 | 说明 |
---|---|
~~ | 表示一个 ~ 文字。 |
~w | 表示 Erlang 词汇,可以印出原子及数字类型资料。 |
~.nB | 表示以 n 为基数的 n 进制数字转换。 |
~c | 表示字元。 |
~s | 表示字串。 |
格式符号都以 ~ 符号后接一个识别字母为主,有时在中间可以放一些调整参数:例如 ~10s 表示在十格空间中印一串文字,文字靠右。