跳转到内容

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/2lists: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/3lists:foldl/3 提供以折叠方式处理一列资料。折叠的概念是说,每一折都是将新的一折折到已经折好的折叠组上。重写一次,每一折都是将“新的一折”“折到”“已经折好的折叠组”上。“新的一折”、“折到”、“已经折好的折叠组”,只要抓住这些元素,再加上“空的折叠组是 [] ”这样的基础,就可以用一个函数把这种折叠的过程表达出来。首先,“折到”是一个函数,它有二个参数分别是“新的一折”和“已经折好的折叠组”。以下例子是在 Erlang 控制台,用 lambda 表示法定义一个“折到”函数。

> Fold = fun (X, Y) -> [X|Y] end.

lists:foldr/3lists: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/2array:resize/1 可以调整阵列的长度。须注意,阵列长度不会自动缩短,一但你用了索引值 i 在重新调整阵列长度之前,从索引值 0 ... i 的阵列空间会一直留着。

阵列的产生

[编辑]

使用 array:new/1 建立新阵列,要加上一些参数,这些参数是分别是一些表示阵列长度、固定性及预设值的词汇。词汇表达为:

  1. 任何数字,或 {size, 数字} 代表阵列长度。
  2. fixed 或 {fixed, true} 代表固定阵列; {fixed, false} 代表变动阵列。
  3. {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/2string: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/1string: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/1string: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 表示在十格空间中印一串文字,文字靠右。

档案模组

[编辑]

集合模组

[编辑]

网络模组

[编辑]

Mnesai 分散式数据库

[编辑]