元组tuple
元组,用于保存固定数量的元素。创建元组的方法就是用大括号{}
把想要表达的值括起来,并用逗号分隔他们。例如,表示某人的年龄:{Jack, 18}
Erlang没有类型声明,创建一个坐标点,只需:
P = {10, 15}.
这样就创建了一个元祖并绑定在变量P上。
由于元组里的字段没有名字,为了记住该元组的用途,通常会写成以下方式:{point, 10, 15}
,这样提高使程序的可理解性。
元组是可以嵌套的,例如:
1> Person = {person, {name, Jack}, {age, 18}}.
{person, {name, Jack}, {age, 18}}
1. 创建元组
元组会在声明他们的时候自动创建,不再使用时被销毁。如果在构建新的元组时用到变量,那么新的元组会共享该变量所引用的数据结构的值。例如,
2> F = {firstname, jack}.
{firstname, jack}
3> L = {lastname, chan}.
{lastname, chan}
4> p = {person, F, L}.
{person, {firstname, jack}, {lastname, chan}}
2. 提取元组的值
通过使用模式匹配操作符=
从元组中提取一些值。
1> Point = {point,10,15}.
{point,10,15}
2> {point,X,Y} = Point.
{point,10,15}
3> X.
10
4> Y.
15
注意:等号两侧的元组必须要有相同数量的元素,而且两侧的对应元素必须绑定为相同的值。如:
5> Point1 = {point,5,5}.
{point,5,5}
6> {point,C,C} = Point1.
{point,5,5}
7> C.
5
如果有一个复杂的元组,就可以编写一个与该元组形状(结构)相同的模式,并在待提取值的位置加入未绑定的变量来提取该值。
Eshell V8.3 (abort with ^G)
1> Person = {person,{name,jack,chan},{age,18}}.
{person,{name,jack,chan},{age,18}}
2> {_, {_, Who, _}, _} = Person.
{person,{name,jack,chan},{age,18}}
3> Who.
jack
其中,"_
“被称为匿名变量。与正规变量不同,同一模式里的多个”_
"不必绑定相同的值。
列表list
列表,被用来存放任意数量的事物。创建列表的方法是用中括号[]
把列表元素括起来,并用逗号分隔开。
列表的第一个元素称为列表头head
,其余部分称为列表尾tail
。例如:列表[1, 2, 3, 4, 5]
的列表头为整数1,[2, 3, 4, 5]
为列表尾。
1. 定义列表
如果T是一个列表,那么[H | T]
也是一个列表,他的头是H
,尾是T
。竖线 |
把列表的头与尾分隔开。[]
是一个空列表。
无论何时,只要用[... | T]
表示一个列表,就应该确保T
是列表。此时称为格式正确的列表,否则称为不正确的列表。
可以给T
的开头添加不止一个元素,写法是[E1,E2,...,EN | T]
1> T = [{apple, 10}, {pear, 5}].
[{apple, 10}, {pear, 5}]
2> T1 = [{orange, 15} | T].
[{orange, 15}, {apple, 10}, {pear, 5}]
2. 提取列表元素
通过模式匹配操作来提取某个列表里的元素。如果有一个列表L
,那么表达式[X | Y] = L
会提取列表头作为X
,列表尾作为Y
。
3> [B1 | B2] = T1.
[{orange, 15}, {apple, 10}, {pear, 5}]
4> B1.
{orange, 15}
5> B2.
[{apple, 10}, {pear, 5}]
3. 列表处理注意点
Eshell V8.3 (abort with ^G)
1> A = [[1,2],[2,3],[3,4]].
[[1,2],[2,3],[3,4]]
2> [B,C|T] = A.
[[1,2],[2,3],[3,4]]
3> B.
[1,2]
4> C.
[2,3]
5> T.
[[3,4]]
6> D = [1,2,3].
[1,2,3]
7> E = [3,4,5,6].
[3,4,5,6]
8> F = [D|E].
[[1,2,3],3,4,5,6]
9> G = D ++ E.
[1,2,3,3,4,5,6]
4. 列表推导
[F(X) || X <- L]
标记表示“由F(X)
组成的列表(X
从列表L
中提取)”。[2 *X || X <- L]
表示“由2 * X
组成的列表”。
以下实现将列表中每一项的数字加倍:
1> Buy = [{apples,1},{pears,2},{newspaper,1}].
[{apples,1},{pears,2},{newspaper,1}]
2> [{Name,2*Num} || {Name,Num} <- Buy].
[{apples,2},{pears,4},{newspaper,2}]
注意,||
右侧的元组{Name, Number}
是一个模式,用于匹配Buy
中的各个元素。左侧的元组{Name, 2*Number}
是一个构造器
列表推导最常规的形式是下面这种表达式:
[X || Qualifier1, Qualifier2, ...]
X
是任意一条表达式,后面的限定符(Qualifier)可以是生成器、位串生成器或过滤器。
- 生成器(generator)的写法是
Pattern <- ListExpr
,其中的ListExp
必须是一个能够得出列表的表达式。 - 位串(bitstring)生成器的写法是
BitStringPattern <= BitStringExpr
,其中的BitStringExpr
必须是一个能够得出位串的表达式。 - 过滤器(filter)既可以是判断函数(即返回
true
或false
的函数),也可以是布尔表达式。
5. 列表常用函数
-
lists:keyfind(Key, N, TupleList) -> Tuple | false
在元组列表TupleList
中搜索第n
个元素与Key
相等的元组。如果找到这样的元组,则返回元组,否则返回false
。 -
lists:keystore(Key, N, TupleList1, NewTuple) -> TupleList2
返回TupleList1
的副本,其中第n
个元素等于Key
的元组T
的第一个出现位置将被替换为NewTuple
,如果没有这样的元组T
,则返回TupleList1
的副本,并将[NewTuple]
附加到末尾。 -
lists:keysort(N, TupleList1) -> TupleList2
返回一个包含列表TupleList1
的已排序元素的列表。排序是在元组的第n
个元素上执行的。 -
lists:sort(Fun, List1) -> List2
根据排序函数Fun
,返回一个包含List1
排序元素的列表。 -
lists:usort(List1) -> List2
返回一个包含List1
的已排序元素的列表,并删除重复项。 -
lists:keymember(Key, N, TupleList) -> boolean()
如果TupleList
中有一个元组,其第n
个元素与Key
相等,则返回true
,否则返回false
。 -
lists:member(Elem, List) -> boolean()
如果Elem
在列表List
中,则返回true
,否则返回false
。 -
lists:sublist(List1, Len) -> List2
返回List1
的子列表,从位置1
开始,包含Len
个元素。当Len
超过列表的长度时,将返回整个列表。 -
lists:sublist(List1, Start, Len) -> List2
返回List1
的子列表,从位置Start
开始,包含Len
个元素。当Start+Len
超过列表的长度时,将返回整个列表。 -
lists:foldl(Fun, Acc0, List) -> Acc1
对列表的连续元素Elem
调用Fun(Elem,AccIn)
,从AccIn==Acc0
开始。Fun/2
必须返回一个新的累加器,它被传递给下一个调用。函数返回累加器的最终值。如果列表为空,则返回Acc0
。 -
lists:foldr(Fun, Acc0, List) -> Acc1
同foldl/3,但遍历元素顺序为从右到左。如下:
> P = fun(A, AccIn) -> io:format("~p ", [A]), AccIn end.
#Fun<erl_eval.12.2225172>
> lists:foldl(P, void, [1,2,3]).
1 2 3 void
> lists:foldr(P, void, [1,2,3]).
3 2 1 void
-
lists:any(Pred, List) -> boolean()
Pred = fun((Elem :: T) -> boolean())
List = [T]
T = term()
如果列表中至少一个元素Elem
使得Pred(Elem)
返回true
,则返回true
。 -
lists:all(Pred, List) -> boolean()
如果列表中所有元素Elem
都使得Pred(Elem)
返回true
,则返回true
。 -
lists:nthtail(N, list) -> Tail
返回从N+1开始一直到列表的结尾的子列表。 -
lists:nth(N, List) -> Elem
返回列表的第n个元素。 -
erlang:hd(List)
返回列表的头,即第一个元素。 -
lists:last(List) -> Elem
返回列表中的最后一个元素。 -
lists:flatmap(Fun, List1) -> List2
//Fun = fun((A) -> [B])
//List1 = [A]
//List2 = [B]
//A = B = term()
Takes a function from As to lists of Bs, and a list of As (List1) and produces a list of Bs by applying the function to every element in List1 and appending the resulting lists.
That is, flatmap behaves as if it had been defined as follows:
flatmap(Fun, List1) -> append(map(Fun, List1)).
Example:
> lists:flatmap(fun(X)->[X,X] end, [a,b,c]).
[a,a,b,b,c,c]
记录record
记录是元组的另一种形式,通过使用记录,可以给元组里的各个元素关联一个名称。
用记录声明命名元组里的元素,语法如下:
-record(Name, {
%%以下两个键带有默认值
key1 = Default1,
key2 = Default2,
...
%%下一行就相当于Key3 = Undefined
Key3,
...
}).
-record
不是一个shell命令,在shell里要使用rr
。记录声明只能在Erlang源代码模块里使用,不能用于shell。
Name
是记录名。Key1
,Key2
这些是记录中各个字段的名称,他们必须是原子。记录里的每个字段都可以带一个默认值,如果创建记录时没有指定某个字段值,就会使用默认值。
记录的定义既可以保存在Erlang源代码文件里,也可以由扩展名为.hrl
的文件保存,然后包含在Erlang源代码文件里。
文件包含是唯一能确保多个Erlang模块共享相同记录定义的方式。
%% records.hrl
-record(todo, {status=reminder, who=jack, text}).
记录一旦定义,就可以创建该记录的实例了。
要在shell中创建实例,必须先把记录的定义读入shell。
1> rr("record.hrl").
[todo]
rr
是read records的缩写,即读取记录
1. 创建记录
2> #todo{}.
#todo{status = reminder,who = jack,text = undefined}
3> X1 = #todo{status = urgent, text = "So urgent"}.
#todo{status = urgent,who = jack,text = "So urgent"}
4> X2 = X1#todo{status = done}.
#todo{status = done,who = jack,text = "So urgent"}
语法#todo{key1=Val1,... ,keyn=Valn}
用于创建一个类型为todo
的新记录。所有键都是原子,而且必须与记录定义里所用一致。如果省略一个键,将会用记录定义里的值作为该键的默认值。
第4行复制了一个现有的记录。语法X1#todo{status=done}
的意思是创建一个X1
的副本(类型必须为todo
),并修改字段status
的值为done
。此操作不会改变原始记录。
2. 提取记录的值
若想一次性提取记录的多个字段,可使用模式匹配:
5> #todo{who = Who, text = Text} = X2.
#todo{status = done,who = jack,text = "So urgent"}
6> Who.
jack
7> Text.
"So urgent"
若只想提取单个字段,可使用"点语法":
8> X2#todo.text.
"So urgent"
3. 在函数里模式匹配记录
我们可以编写模式匹配记录字段或创建记录的函数,代码如下:
clear_status(#todo{status=S, who=W} = R) ->
%%在此函数中,S和W绑定了记录里的字段值
%%
%%R是*整个*记录
R#todo{status=done}
%%...
要匹配某个类型的记录,可以这样编写函数:
do_sth(X) when is_record(X, todo) ->
%%...
这个子句会在X
是todo
类型的记录时匹配成功。
4. 记录是元组的另一种形式
9> X2.
#todo{status = done,who = jack,text = "So urgent"}
10> rf(todo).
ok
11> X2.
{todo,done,jack,"So urgent"}
rf(todo)
命令使shell忘了todo
的定义,再次打印X2
时则显示成了元组。
12> rr("record.hrl").
[todo]
13> X2.
#todo{status = done,who = jack,text = "So urgent"}
再次读入定义后,打印X2
则显示记录。
无法添加记录定义中未定义的字段:
14> X3 = X2#todo{number = 1}.
* 1: field number undefined in record todo
15> X2 = X2#todo{status = undone}.
** exception error: no match of right hand side value
#todo{status = undone,who = jack,text = "So urgent"}
也无法直接更新某个变量对应的记录的字段值,本质上因为Erlang中的变量一旦绑定后是不允许改变的。
5. 以下原子不能作为记录的字段名
L = ['after','begin','case','try','cond','catch','andalso','orelse','end','fun',
'if','let','of','query','receive','when','bnot','not','div','rem','band','and',
'bor','bxor','bsl','bsr','or','xor'].
映射组maps
映射组,是键-值对的关联性集合。键可以是任何的Erlang数据类型。
映射组的语法与记录相似,不同之处在于省略了记录名,并且键-值分隔符是=>
或:=
#{Key1 Op Val1, Key2 Op Val2, ... , KeyN Op ValN}
1. 创建映射组
键和值可以是任何有效的Erlang数据类型。
1> F1 = #{a => 1, b => 2}.
#{a => 1,b => 2}
2> Facts = #{{wife, fred} => "Sue", {age, fred} => 45, {daughter, fred} => "Mary"}.
#{{age,fred} => 45,
{daughter,fred} => "Mary",
{wife,fred} => "Sue"}
映射在系统内部是作为有序集和存储的,打印时总是按键排序后的顺序,与映射组的创建方式无关。
3> F2 = #{b => 2, a => 1}.
#{a => 1,b => 2}
4> F1 = F2.
#{a => 1,b => 2}
2. 更新映射组
要基于现有的映射组更新一个映射组,语法如下:
NewMap = OldMap#{Key1 Op Val1, Key2 Op Val2, ... , KeyN Op ValN}
表达式Key => Val
有两种用途,一种是将现有的键Key
的只更新为Val
;另一种是给映射添加一个全新的Key-Val
对。这个操作总是成功的。
表达式Key := Val
的作用是将现有键Key
的值更新为Val
。如果Key
不存在时,则操作失败。
5> F3 = F1#{c => xx}.
#{a => 1,b => 2,c => xx}
6> F4 = F1#{c := 3}.
** exception error: {badkey,c}
in function maps:update/3
called as maps:update(c,3,#{a => 1,b => 2})
in call from erl_eval:'-expr/5-fun-0-'/2 (erl_eval.erl, line 255)
in call from lists:foldl/3 (lists.erl, line 1263)
使用映射组的最佳方式是,在首次定义某个键时总是使用Key => Val
,而在修改具体某个键的值时用Key := Val
。
3. 模式匹配映射组字段
用来更新映射组的:=
语法还可以作为映射组模式使用。
映射组模式里的键不能包含任何未绑定变量,但值可以包含未绑定变量。
1> Henry8 = #{class => king, born => 1491, died => 1547}.
#{born => 1491,class => king,died => 1547}
2> #{born := Born} = Henry8.
#{born => 1491,class => king,died => 1547}
3> Born.
1491
4> #{D => 1547}.
* 1: variable 'D' is unbound
5> #{D := 1547}.
* 1: only association operators '=>' are allowed in map construction
6> H1 = Henry8#{D => 123}.
* 1: variable 'D' is unbound
7> H1 = Henry8#{died => 123}.
#{born => 1491,class => king,died => 123}
8> H2 = Henry8#{died := 123}.
#{born => 1491,class => king,died => 123}
9> Henry8.
#{born => 1491,class => king,died => 1547}
可以在函数的头部使用包含模式的映射组,前提是映射组里所有的键都是已知的。
定义一个count_characters(Str)
函数,让它返回一个映射组,内含某个字符串里各个字符的出现次数。
count_character(Str) ->
count_character(Str, #{}).
count_character( [H | T], #{ H := N } = X ) ->
count_character(T, X#{ H := N+1 });
count_character( [H | T], X ) ->
count_character(T, X#{ H => 1});
count_character( [], X ) ->
X.
1> count_character("hello").
#{101=>1, 104=>1, 108=>2, 111=>1}
其中,101是e
的ASCII
码。
4. 操作映射组的内置函数
-
maps:new() -> #{}
返回一个新的空映射组。 -
erlang:is_map(M) -> boolean()
判断M是否为映射组,是则返回true,否则返回false。可用于关卡测试或函数主体中。 -
maps:to_list(M) -> [{K1,V1}, …, {Kn,Vn}]
将映射组M转换为一个键值列表,严格按升序排列
> Map = #{42 => value_three,1337 => "value two","a" => 1}.
> maps:to_list(Map).
[{42,value_three},{1337,"value two"},{"a",1}]
- maps:from_list([K1, V2], … , [Kn, Vn]) -> M
把包含键值对的列表转化为映射组M。如果同样的键出现不止一次,就是用最后一次出现所关联的值,前面的值被忽略。
> List = [{"a",ignored},{1337,"value two"},{42,value_three},{"a",1}].
> maps:from_list(List).
#{42 => value_three,1337 => "value two","a" => 1}
-
maps:size(M) -> NumberOfEntries
返回映射组里的条目数量 -
maps:is_key(Key, M) -> boolean()
如果Key是M中的一个键,那么返回true。否则为false。 -
maps:get(Key, Map) -> Val
返回映射组Map中与Key关联的值 -
maps:find(Key, Map) -> {ok, Val} | error
返回映射组Map中与Key关联的值,否则返回error -
maps:keys(Map) -> [Key1, … ,Keyn]
返回映射组所含的键的列表,按升序排列 -
maps:remove(Key, M) -> M1
返回一个新映射M1,除了键位Key的项被移除外,其他的与M一致。 -
maps:without( [Key1, … , Keyn], M ) -> M1
返回一个新映射M1,它是M的复制,但移除了带有[Key1, … , Keyn]列表内这些键的元素。 -
maps:difference(M1, M2) -> M3
M3是M1的复制,但移除了那些与M2里的元素具有相同键的元素。等价于:maps:without(maps:keys(M2), M1)
-
maps:fold(Fun, Init, MapOrIter) -> Acc
函数 F(K, V, AccIn) 递归调用映射组Map里的每一个键和值,函数 F/3肯定会返回一个新的上一次函数成功执行累积值,该函数返回最终的累积值。如果映射组Map是一个空的映射组,那么初始的累积值Init将会返回。
Fun = fun(K,V,AccIn) when is_list(K) -> AccIn + V end,
Map = #{"k1" => 1, "k2" => 2, "k3" => 3},
maps:fold(Fun, 1, Map).
%% Result: 7
- maps:values(Map) -> Values
返回映射组所含的值的列表,按升序排列,当Map不是映射组时,抛出{badmap,Map}异常。
5. 映射组排序
- 首先当
maps:size(A) > maps:size(B)
时,A > B
- 若相等,则当
maps:to_list(A) > maps:to_list(B)
时,A > B
例如,A = #{age => 23, person => "jim"} < B = #{email => "sue@123.com", name => "sue"}
,因为A
的最小键age
小于B
的最小键email
。
进程字典process_dict
每个Erlang进程都有一个被称为进程字典(process dictionary)的私有数据存储区域。进程字典是一个关联数组,它由若干个键和值组成。每个键只有一个值。
这个字典可以用下列内置函数进行操作:
- put(Key, Value) -> OldValue
给进程字典添加一个Key, Value
组合。put的返回值是OldValue
,也就是Key
之前关联的值。如果没有之前的值,就返回原子undefined
。 - get(Key) -> Value
查找Key
的值。如果字典里存在Key, Value
组合就返回Value
,否则返回原子undefined
。 - get() -> [{Key, Value}]
返回整个字典,形式是一个由{Key,Value}
元组所构成的列表。 - get_keys(Value) -> [Key]
返回一个列表,内含字典里所有值为Value的键。 - erase(Key) -> Value
返回Key的关联值,如果不存在则返回原子undefined
。最后,删除Key
的关联值。 - erase() -> [{Key, Value}]
删除整个进程字典。返回值是一个由{Key,Value}
元组所构成的列表,代表了字典删除之前的状态。
举栗:
1> put(a,1).
undefined
2> put(b,2).
undefined
3> get(a).
1
4> get().
[{b,2},{a,1}]
5> get_keys(1).
[a]
6> erase().
[{b,2},{a,1}]
ets和dets
ETS
是Erlang Term Storage
(Erlang数据存储)的缩写,DETS
则是Disk ETS
(磁盘ETS
)的缩写。
ETS
和DETS
执行的任务基本相同:它们提供大型的键-值查询表。ETS
常驻内存,DETS
则常驻磁盘。
ETS
是相当高效的:可以用它存储海量的数据(只要有足够的内存),执行查找的时间也是恒定的(在某些情况下是对数时间)。DETS
提供了几乎和ETS
一样的接口,但它会把表保存在磁盘上。因为DETS
使用磁盘存储,所以它远远慢于ETS
,但是运行时的内存占用也会小很多。另外,ETS
和DETS
表可以被多个进程共享,这就让跨进程的公共数据访问变得非常高效。
ETS
或DETS
表其实就是Erlang元组的集合。
ETS
表里的数据保存在内存里,它们是易失的。当ETS
表被丢弃或者控制它的Erlang进程终止时,这些数据就会被删除。保存在DETS
表里的数据是非易失的,即使整个系统崩溃也能留存下来。DETS
表在打开时会进行一致性检查,如果发现有损坏,系统就会尝试修复它。
1. 表的类型
ETS
和DETS
表保存的是元组。元组里的某一个元素(默认是第一个)被称为该表的键。通过键来向表里插入和提取元组。
一些表被称为异键表(set
),它们要求表里所有的键都是唯一的。
另一些被称为同键表(bag
),它们允许多个元素拥有相同的键。
基本的表类型(异键表和同键表)各有两个变种,它们共同构成四种表类型:
异键 → 各个元组里的键都必须是独一无二的
有序异键(ordered set
)→ 元组会被排序
同键 → 可以有不止一个元组拥有相同的键,但是不能有两个完全相同的元组。
副本同键(duplicate bag
)→ 可以有多个元组拥有相同的键,而且在同一张表里可以存在多个相同的元组。
2. ETS和DETS表有四种基本操作
- 创建一个新表或打开现有的表
用ets:new
或dets:open_file
实现。 - 向表里插入一个或多个元组
这里要调用insert(TableId, X)
,其中X
是一个元组或元组列表。 insert 在ETS和DETS里有着相同的参数和工作方式。 - 在表里查找某个元组
这里要调用lookup(TableID, Key)
。得到的结果是一个匹配 Key 的元组列表。 lookup在ETS和DETS里都有定义。
lookup
的返回值始终是一个元组列表,这样就能对异键表和同键表使用同一个查找函数。
如果表的类型是同键,那么多个元组可以拥有相同的键。如果表的类型是异键,那么查找成功后的列表里只会有一个元素。
如果表里没有任何元组拥有所需的键,就会返回一个空列表。 - 丢弃某个表
用完某个表后可以告知系统,方法是调用dets:close(TableId)
或ets:delete(TableId)
。
%% ets_test.erl
-module(ets_test).
-export([start/0]).
start() ->
lists:foreach(fun test_ets/1, [set, ordered_set, bag, duplicate_bag]).
test_ets(Mode) ->
TableId = ets:new(test, [Mode]),
ets:insert(TableId, {a, 1}),
ets:insert(TableId, {b, 2}),
ets:insert(TableId, {a, 1}),
ets:insert(TableId, {a, 3}),
List = ets:tab2list(TableId),
io:format("~-13w => ~p~n", [Mode, List]),
ets:delete(TableId).
这个程序会为四种模式各创建一个ETS表,然后向表内插入 {a,1}
、 {b,2}
、 {a,1}
和 {a,3}
。然后调用 tab2list
,它会把整个表转换成一个列表并打印出来。
1> ets_test:start().
set => [{b,2},{a,3}]
ordered_set => [{a,3},{b,2}]
bag => [{b,2},{a,1},{a,3}]
duplicate_bag => [{b,2},{a,1},{a,1},{a,3}]
ok
在异键型的表里,每个键都只能出现一次。如果先向表内插入元组 {a,1}
然后再插入 {a,3}
,那么最终值将是 {a,3}
3. 影响 ETS 表效率的因素
ETS
表在内部是用散列表表示的(除了有序异键表,它是用平衡二叉树表示的)。这就意味着使用异键表会带来少许空间开销,而使用有序异键表会带来时间开销。插入异键表所需的时间是恒定的,而插入有序异键表所需的时间与表内条目数量的对数成比例。
使用同键表的代价比使用副本同键表更高,因为每次插入时都需要与所有拥有相同键的元素比较是否相等。如果有大量元组都拥有同一个键,这么做就会很低效。
ETS
表保存在一个单独的存储区域里,与正常的进程内存无关。可以认为ETS
表归创建它的进程所有,当这个进程挂了或者调用 ets:delete
时,表就会被删除。ETS
表不会进行垃圾收集,这就意味着即使表里存储了海量的数据也不会产生垃圾收集的开销。
当一个元组被插入ETS
表时,所有代表这个元组的数据结构都会从进程的栈复制到ETS
表里。当你对表执行查询操作时,找到的元组会从ETS
表复制到进程的栈上。
所有的数据结构都是如此,除了大型的二进制数据。这些二进制型会存储在它们自己的堆外存储区域里。这个区域可以被多个进程和ETS
表共享,各个二进制型则由一个引用计数垃圾收集器管理,它会记录有多少个不同的进程和ETS
表在使用这个二进制型。如果某个特定二进制型的进程和表使用计数降至零,那么这个二进制型的存储区域就可以被回收。
4. 创建一个 ETS 表
创建ETS
表的方法是调用 ets:new
。创建表的进程被称为该表的主管。创建表时所设置的一组选项在以后是无法更改的。如果主管进程挂了,表的空间就会被自动释放。你可以调用ets:delete
来删除表。
ets:new
的参数如下:
-spec ets:new(Name, [Opt]) -> TableId
其中, Name
是一个原子。 [Opt]
是一列选项,源于下面这些参数。
- set | ordered_set | bag | duplicate_bag
创建一个指定类型的ETS表(我们之前讨论过)。 - private
创建一个私有表,只有主管进程才能读取和写入它。 - public
创建一个公共表,任何知道此表标识符的进程都能读取和写入它。 - protected
创建一个受保护表,任何知道此表标识符的进程都能读取它,但只有主管进程才能写入它。 - named_table
如果设置了此选项,Name
就可以被用于后续的表操作。 - {keypos, K}
用K
作为键的位置。通常键的位置是1
。基本上唯一需要使用这个选项的场合是保存Erlang记录(它其实是元组的另一种形式),并且记录的第一个元素包含记录名的时候。
注意:不使用任何选项打开一个ETS
表就相当于用[set,protected,{keypos,1}]
选项打开它。
5. 保存元组到磁盘
ETS
表把元组保存在内存里,而DETS
提供了把Erlang元组保存到磁盘上的方法。DETS
的最大文件大小是2GB
。
DETS
文件必须先打开才能使用,用完后还应该正确关闭。如果没有正确关闭,它们就会在下次打开时自动进行修复。因为修复可能会花很长一段时间,所以先正确关闭它们再结束程序是很重要的。
DETS
表有着和ETS
表不同的共享属性。DETS
表在打开时必须赋予一个全局名称。如果两个或更多本地进程用相同的名称和选项打开某个DETS
表,它们就会共享这个表。这个表会一直处于打开状态,直到所有进程都关闭它(或者崩溃)。
6. 范例:文件名索引
创建一个基于磁盘的表,它将把文件名映射到整数上,反之亦然。我们将定义函数filename2index
和它的逆函数 index2filename
。
为了实现这个索引,我们将创建一个DETS
表,然后用三种不同类型的元组填充它。
- {free, N}
N
是表里的第一个空白索引。当我们在表里输入一个新文件名时,就会指派索引N
给它。 - {FileNameBin, K}
FileNameBin
(一个二进制型)被指派了索引K
。 - {K, FileNameBin}
K
(一个整数)代表文件FilenameBin
。
请注意,每个新添加的文件都会在表里增加两个条目:一个File → Index
条目和一个逆向的Index → Filename
条目。这是出于效率的原因。当ETS
或DETS
表建立时,元组里只有一个元素充当键的角色。匹配某个非键元组元素虽然可行,但非常低效,因为它涉及对整个表进行搜索。当整个表都在磁盘上时,这个操作的代价尤其高昂。
现在来编写这个程序。首先编写打开和关闭DETS
表的函数,这个表用来保存所有的文件名。
%% lib_filenames_dets.erl
-module(lib_filenames_dets).
-export([
open/1,
close/0,
test/0,
filename2index/1,
index2filename/1
]).
open(File) ->
io:format("dets opened:~p~n", [File]),
Bool = filelib:is_file(File),
case dets:open_file(?MODULE, [{file, File}]) of
{ok, ?MODULE} ->
case Bool of
true -> void;
false -> ok = dets:insert(?MODULE, {free, 1})
end,
true;
{error, Reason} ->
io:format("cannot open dets table~n"),
exit({eDetsOpen, File, Reason})
end.
close() ->
dets:close(?MODULE).
filename2index(FileName) when is_binary(FileName) ->
case dets:lookup(?MODULE, FileName) of
[] ->
[{_, Free}] = dets:lookup(?MODULE, free),
ok = dets:insert(?MODULE, [{free, Free+1}, {Free, FileName}, {FileName, Free}]),
Free;
[{_, Free}] ->
Free
end;
filename2index(FileName) ->
io:format("filename2index arg:~p wrong~n", [FileName]).
index2filename(Index) when is_integer(Index) ->
case dets:lookup(?MODULE, Index) of
[] -> error;
[{_, Bin}] -> Bin
end;
index2filename(Index) ->
io:format("index2filename arg:~p wrong~n", [Index]).
test() ->
open(test_dets),
Index = filename2index(list_to_binary("test")),
io:format("The Index is ~p~n", [Index]),
Filename = index2filename(Index),
io:format("The Filename is ~p~n", [binary_to_list(Filename)]),
close().
open
的代码会自动初始化DETS
表,方法是在创建新表时插入 {free, 1}
元组。如果 File
存在, filelib:is_file(File)
就会返回 true
,否则返回 false
。请注意, dets:open_file
或者创建一个新文件,或者打开一个现有文件,这就是必须在调用 dets:open_file
前先检查文件是否存在的原因。
我 们 在 这 段 代 码 里 多 次 使 用 了 ?MODULE
宏 , 它 会 展 开 成 当 前 的 模 块 名 ( 即lib_filenames_dets
)。许多针对DETS的函数调用需要一个唯一的原子参数作为表名。单凭模块名就能生成一个唯一的表名。因为系统里不能有两个名称相同的Erlang模块,所以如果处处遵循这一惯例,就有理由认定我们有一个可以用于表名的唯一名称。
我每次都用 ?MODULE
宏而不是显式指定模块名,这是因为我有一个习惯,就是会在编写代码的过程中修改模块名。用了宏之后,即使修改了模块名,代码也会是正确的。
打开文件之后,向表里插入一个新文件名就很简单了。它是由 filename2index
调用产生的副作用实现的。如果文件名在表里,就会返回它的索引,否则就会生成一个新索引并更新表,而这次更新会使用三个元组。
filename2index(FileName) when is_binary(FileName) ->
case dets:lookup(?MODULE, FileName) of
[] ->
[{_, Free}] = dets:lookup(?MODULE, free),
ok = dets:insert(?MODULE, [{free, Free+1}, {Free, FileName}, {FileName, Free}]),
Free;
[{_, Free}] ->
Free
end;
filename2index(FileName) ->
io:format("filename2index arg:~p wrong~n", [FileName]).
dets:insert
的第二个参数既可以是一个元组,也可以是一个元组列表。出于效率的原因,文件名用一个二进制型表示。
filename2index
里有一处潜在的竞争状况。如果在 dets:insert
尚未被调用时有两个并行进程调用了 dets:lookup
, filename2index
就会返回一个不正确的值。为了让这个函数能正常工作,必须确保任一时刻都只能有一个进程调用它。
把索引转换成文件名非常简单。
index2filename(Index) when is_integer(Index) ->
case dets:lookup(?MODULE, Index) of
[] -> error;
[{_, Bin}] -> Bin
end;
index2filename(Index) ->
io:format("index2filename arg:~p wrong~n", [Index]).
执行test()如下:
1> lib_filenames_dets:test().
dets opened:test_dets
The Index is 1
The Filename is "test"
ok
7. ets常用接口
- ets:match/2
match方法进行匹配最简单,'$数字'
代表占位符,'_'
代表通配符,'$数字'
这种表示方式,数字相对大小代表相对位置顺序;
Eshell V8.3 (abort with ^G)
1> ets:new(countries, [bag, named_table]).
countries
2> ets:insert(countries, {yves, france, cook}).
true
3> ets:insert(countries, {sean, ireland, bartender}).
true
4> ets:insert(countries, {marco, italy, cook}).
true
5> ets:insert(countries, {chris, ireland, tester}).
true
6> ets:match(countries, {'$1', '_', '_'}).
[[sean],[marco],[yves],[chris]]
7> ets:match(countries, {'$1', '$0', '_'}).
[[ireland,sean],[italy,marco],[france,yves],[ireland,chris]]
8> ets:match(countries, {'$11', '$9', '_'}).
[[ireland,sean],[italy,marco],[france,yves],[ireland,chris]]
9> ets:match(countries, {'$11', '$99', '_'}).
[[sean,ireland],[marco,italy],[yves,france],[chris,ireland]]
10> ets:match(countries, {'$101', '$9', '_'}).
[[ireland,sean],[italy,marco],[france,yves],[ireland,chris]]
11> ets:match(countries, {'$2', ireland, '_'}).
[[sean],[chris]]
12> ets:match(countries, {'_', ireland, '_'}).
[[],[]]
13> ets:match(countries, {'$2', cook, '_'}).
[]
14> ets:match(countries, {'$0', '$1', cook}).
[[marco,italy],[yves,france]]
15> ets:match(countries, {'$0', '$0', cook}).
[]
16> ets:match(countries, {'$1', '$0', cook}).
[[italy,marco],[france,yves]]
%%如果没有数字占位符,是没有结果输出的,只是空列表
如果是需要所有字段,提取整个数据项,那就直接使用match_object
17> ets:match_object(countries, {'_', ireland, '_'}).
[{sean,ireland,bartender},{chris,ireland,tester}]
上面的例子countries
这个结构很简单,但是如果是一个字段稍多写的结构呢?很容易出现类似ets:match(zen_ets, {'$1','_','_','_','_','_' })
。这样的代码,不仅可读性差,而且一旦字段顺序发生变化,这里就容易出错。使用record
可以规避掉tuple
字段增减,顺序的问题。
- ets:select/2
对 ETS 表里的数据进行匹配比对
用法:
select(Tab, MatchSpec) -> [Match]
使用一个匹配描述从表 Tab 里匹配对象。此函数调用比 ets:match/2
和 ets:match_object/2
更常用。以下是一些最简单形式的匹配描述:
MatchSpec = [MatchFunction]
MatchFunction = {MatchHead, [Guard], [Result]}
MatchHead = "Pattern as in ets:match"
Guard = {"Guardtest name", ...}
Result = "Term construct"
这意味着匹配描述总是含有一个以上元组元素的列表(含有三个参数的元组元素),元组的第一个元素应是 ets:match/2
的文档中所描述的模式,第二个元素应是含 0 个或多个断言测试的列表,第三个元素应是包含关于实际返回值的描述的列表,通常是一个对返回值全描述的列表,即返回匹配对象的所有项目。
返回值的结构使用 MatchHead 所绑定的 “match variables”,或者使用特殊的匹配值 '$_'
(整个匹配对象)和 '$$'
(包含所有匹配值的列表)。
Eshell V8.3 (abort with ^G)
1> Tab = ets:new(test_ets, [private]).
16400
2> ets:insert(Tab, [{a,1},{b,2}]).
true
3> ets:select(Tab, [{{'$1', '$2'}, [], ['$$']}]).
[[b,2],[a,1]]
4> ets:select(Tab, [{{'$1', '$2'}, [], ['$_']}]).
[{b,2},{a,1}]
5> ets:select(Tab, [{{'$1', '$2'}, [], ['$1']}]).
[b,a]
6> ets:select(Tab, [{{'$1', '$2'}, [], ['$2']}]).
[2,1]
7> ets:select(Tab, [{{'$1', '$2'}, [], ['$1', '$2']}]).
[2,1]
字典dict
-
dict:new() -> dict()
创建字典 -
dict:size(Dict) -> integer() >= 0
获取字典大小 -
dict:store(Key, Value, Dict1) -> Dict2
在字典Dict1
中存储Key-Value
。如果Dict1
中已经存在Key
,则将相关联的值替换为Value
。 -
dict:to_list(Dict) -> List
将Dict
转化为列表[{Key, Value},...]
-
dict:from_list(List) -> Dict
将列表[{Key, Value},...]
转化为Dict
-
dict:update(Key, Fun, Dict1) -> Dict2
通过调用Fun
来更新字典中的值以获取新值。如果字典中不存在Key
,则生成异常。 -
dict:update(Key, Fun, Initial, Dict1) -> Dict2
通过调用Fun
来更新字典中的值以获取新值。如果字典中不存在Key
,则将Initial
存储为第一个值。 -
dict:update_counter(Key, Increment, Dict1) -> Dict2
向与Key
关联的Value
增加Increment
并存储结果。如果Key
不在字典中,Increment
将作为第一个值存储。
update_counter(Key, Incr, D) ->
update(Key, fun (Old) -> Old + Incr end, Incr, D).
-
dict:append(Key, Value, Dict1) -> Dict2
将Value
追加到与Key
关联的值的列表中。 -
dict:append_list(Key, ValList, Dict1) -> Dict2
将列表ValList
追加到与Key
关联的列表。如果与Key
关联的初始值不是列表,则会生成异常。 -
dict:erase(Key, Dict1) -> Dict2
从字典中删除Key
所对应得项。 -
dict:fetch(Key, Dict) -> Value
返回Dict
中与Key
关联的值。如果Key
不在字典中,则生成异常。 -
dict:fetch_keys(Dict) -> Keys
返回Dict
中所有Key
组成的列表。 -
dict:take(Key, Dict) -> {Value, Dict1} | error
返回字典中Key
所关联的Value
,和一个没有此值的新字典。如果字典中不存在Key
,则返回error
。 -
dict:filter(Pred, Dict1) -> Dict2
Dict2
是Dict1
中所有Key-Value
满足Pred(Key,Value)
为true
的字典。 -
dict:find(Key, Dict) -> {ok, Value} | error
在Dict
中搜索Key
,返回{ok,Value}
,其中Value
是与key
关联的值。如果字典中不存在Key
,则返回error
。 -
dict:fold(Fun, Acc0, Dict) -> Acc1
Fun = fun((Key, Value, AccIn) -> AccOut)
-
dict:is_empty(Dict) -> boolean()
如果Dict
为空则返回true
,否则返回false
。 -
dict:is_key(Key, Dict) -> boolean()
如果Key
存在于Dict
中则返回true
,否则返回false
。 -
dict:map(Fun, Dict1) -> Dict2
Fun = fun((Key, Value1) -> Value2)
对于Dict1
中的每一个Key-Value
都执行Fun
得到一个Dict2
。 -
dict:merge(Fun, Dict1, Dict2) -> Dict3
Fun = fun((Key, Value1, Value2) -> Value)
合并Dict1
,Dict2
。
merge(Fun, D1, D2) ->
fold(fun (K, V1, D) ->
update(K, fun (V2) -> Fun(K, V1, V2) end, V1, D)
end, D2, D1).
Eshell V8.3 (abort with ^G)
1> D0 = dict:new().
{dict,0,16,16,8,80,48,
{[],[],[],[],[],[],[],[],[],[],[],[],[],[],[],[]},
{{[],[],[],[],[],[],[],[],[],[],[],[],[],[],[],[]}}}
2> D1 = dict:store(files, [], D0).
{dict,1,16,16,8,80,48,
{[],[],[],[],[],[],[],[],[],[],[],[],[],[],[],[]},
{{[],[],[],[[files]],[],[],[],[],[],[],[],[],[],[],[],[]}}}
3> D2 = dict:append(files, f1, D1).
{dict,1,16,16,8,80,48,
{[],[],[],[],[],[],[],[],[],[],[],[],[],[],[],[]},
{{[],[],[],
[[files,f1]],
[],[],[],[],[],[],[],[],[],[],[],[]}}}
4> D3 = dict:append(files, f2, D2).
{dict,1,16,16,8,80,48,
{[],[],[],[],[],[],[],[],[],[],[],[],[],[],[],[]},
{{[],[],[],
[[files,f1,f2]],
[],[],[],[],[],[],[],[],[],[],[],[]}}}
5> D4 = dict:append(files, f3, D3).
{dict,1,16,16,8,80,48,
{[],[],[],[],[],[],[],[],[],[],[],[],[],[],[],[]},
{{[],[],[],
[[files,f1,f2,f3]],
[],[],[],[],[],[],[],[],[],[],[],[]}}}
6> dict:fetch(files, D4).
[f1,f2,f3]
7> D5 = dict:store(a, 1, D4).
{dict,2,16,16,8,80,48,
{[],[],[],[],[],[],[],[],[],[],[],[],[],[],[],[]},
{{[],
[[a|1]],
[],
[[files,f1,f2,f3]],
[],[],[],[],[],[],[],[],[],[],[],[]}}}
8> D6 = dict:store(a, 11, D5).
{dict,2,16,16,8,80,48,
{[],[],[],[],[],[],[],[],[],[],[],[],[],[],[],[]},
{{[],
[[a|11]],
[],
[[files,f1,f2,f3]],
[],[],[],[],[],[],[],[],[],[],[],[]}}}
9> dict:find(a, D6).
{ok,11}
10> {Value, D7} = dict:take(files, D6). // OTP20.0才有该方法。
** exception error: undefined function dict:take/2
11> dict:fetch(files, D6).
[f1,f2,f3]
12> dict:fetch_keys(D6).
[a,files]
Erlang中还提供了其他如queue、array等数据结构使用的模块与接口,在此不一一赘述啦,有需要可以看下文档。
注:本博客为通过《Erlang程序设计 第二版》学习Erlang时所做的笔记。学习更详细的内容,建议直接阅读《Erlang程序设计 第二版》。