记录其实就是元组的另一种形式。通过使用记录,可以给元组里的各个元素关联一个名称。
映射组是键-值的关联性集合。键可以是任意的Erlang数据类型。
1. 何时使用映射组或记录
使用记录的情况:
1. 当你可以用一些预定且数量固定的原子来表示数据时;
2. 当记录里的元素数量和元素名称不会随着时间而改变时;
3. 当存储空间是个问题时,典型的案例是你有一大堆元组,并且没一个元组都有相同的结构。
使用映射的情况:
1. 当键不能预先知道时用来表示键-值数据结构;
2. 当存在大量不同的键时用来表示数据;
3. 当方便使用很重要而效率无关紧要时作为万能的数据结构实现;
4. 用作“自解释型”的数据结构,也就是说,用户容易从键名猜出键值的含义;
5. 用来表示键-值解析树,例如XML或配置文件;
6. 用Json来和其他编程语言通信。
2. 通过记录命名元组里的项
一旦命名了元组里的元素,就可以通过名称来指向他们,而不必记住它们在元组里面的具体位置。
-record(Name,{
%%以下两个键带有默认值
key1 = Default1,
key2 = Default2,
……
%%下一行就相当于key 3 = undefined
key3,
……
}).
举个栗子,假设想要操作一个待办项列表。我们首先定义一个todo记录,然后将它保存在一个文件里(记录的定义既可以保存在Erlang源代码文件里,也可以由扩展名为.hrl的文件保存,然后包含在源代码文件里)。
文件包含是唯一能确保多个Erlang模块共享记录定义的方式。类似于C语言中的.h文件保存公众定义,然后包含在源代码文件里。
-record(todo, {status=reminder, who = joe, text}).
记录一旦被定义,就可以创建记录的实例了。
在Shell中,必须先把记录的定义读入shell,然后才能创建记录。我们将用shell函数rr(read records 的缩写,即读取记录)来实现。
rr("records.hrl").
2.1 创建和更新记录
创建新的记录:
#todo{}.
X1 = #todo{status=urgent, text="Fix errata in book"}.
复制一个现有的记录
X2 = X1#todo{status=done}.
X2是创建了X1的一个副本(类型必须是todo),并修改字段status的值为done。请记住生成的是原始记录的一个副本,原始记录并没有变化。
2.2 提取记录字段
在一次操作中提取记录的多个字段,可以使用模式匹配。
#todo{who = W, text = Txt} = X2.
%todo{status = done, who = joe, text = "Fix errata in book"}
W.
%joe
TXt
%"Fix errata in book"
在(=)的左侧编写一个记录模式,如果匹配成功变量就会绑定记录里的相应字段。
如果只是想要记录里的单个字段,就可以使用“点语法”来提取该字段。
X2#todo.text.
%"Fix errata in book"
2.3 在函数里模式匹配记录
编写模式匹配记录字段或者创建新记录的函数,代码如下:
clear_status(#todo{status = S, who = W} = R) ->
%在此函数的内部,S和W绑定了记录里的字段
%R是*整个*记录
R#todo{status=finished}
%%……
要匹配某个类型的记录,可以这样编写函数的定义:
do_something(X) when is_record(X, todo) ->
%%……
此时会在X是todo类型的记录时匹配成功。
2.4 记录是元组的另一种形式
X2.
%todo{status = done, who = joe,text = "Fix errata in book"}
在Shell中忘记todo记录的定义
rf(todo).
%ok
X2.
%{todo,done,joe,"Fix errata in book"}
3. 映射组
映射组从Erlang的R17版开始使用。
映射组有以下属性。
1. 映射组的语法与记录相似,不同之处是省略了记录名,并且键-值分隔符是=>或:=。
2. 映射组是键-值对的关联集合。
3. 映射组里的键可以是完全绑定的Erlang数据类型(即数据结构里没有任何未绑定变量)。
4. 映射组里的各个元素根据键进行排序。
5. 在不改变键的情况下更新映射组是一种节省空间的操作。
6. 查询映射组里某个键的值是一种高效的操作。
7. 映射组有明确的顺序。
3.1 映射组语法
映射组的语法如下:
#{Key1 Op Val1, Key2 Op Val2, .... , KeyN Op ValN}
它的语法与记录相似,但是散列符号(即#)之后没有记录名,而Op是=>或者:=这两个符号的其中一个。
举例:
F1 = #{a => 1, b =>2}.
%#{a => 1, b => 2}.
Facts = #{ {wife, fred} = "Sue", {age, fred} => 45,
{daughter,fred} => "Mary",
{likes, jim} => [...]}
%#{{age,fred} => 45, {daughter,fred}=>"Mary", ...}
映射组在系统内部是作为有序集合存储的,打印时总是使用个键排序后的顺序,与映射组的创建方式无关。
举例如下:
F2 = #{b => 2, a => 1}.
%#{a => 1, b => 2}.
基于现有的映射组更新一个映射组,我们会使用如下语法,其中的Op(更新操作)是=>或:=
NewMap = OldMap#{K1 Op V1, ...., Kn Op Vn}
表达式k=>V有两种用途,一种是将现有键K的值更新为新值V,另一种是给映射组添加一个全新的K-V对。这个操作总是会成功的。
表达式K := V的作用是将现有键K的值更新为新值V。如果被更新的映射组不包含键值K,这个操作就会失败。
使用:=有两个重要的原因:
1. 拼错了=键名的时候可以进行提醒;
2. 如果在更新操作中只使用:=操作符,那么我们就知道新旧映射组都带有一组相同的键,因此可以共享相同的键描述。
使用映射组最佳的方式是:首次定义某个键时总是使用Key = Val,而在修改操作时都使用Key := Val。
3.2 模式匹配映射组字段
用来更新映射组的:=语法还可以作为映射组模式使用。和之前一样,映射组模式里的键不能包含任何未绑定的变量,但是现在的值可以包含未绑定的变量了(在模式匹配成功后绑定)。
Henry8 = #{class => King, born => 1491, died => 1547}.
%#{born => 1491, class=> King, died => 1547}.
#{born := B} = Henry8.
%#{born => 1491, class=> King, died => 1547}.
B.
%1494
#{D => 1547}
%variable 'D' unbound
可以在函数的头部使用包含模式的映射组,前提是映射组里所有的键都是已知的。
例子如下:
%统计字符串中各个字符出现的次数
-module(test_count_characters).
-export([count_characters/1]).
count_characters(Str) ->
count_characters(Str,#{}).
count_characters([H|T], #{H := N} = X) ->
count_characters(T, X#{H := N+1});
count_characters([H|T], X) ->
count_characters(T, X#{H => 1});
count_characters([],X) ->
X.
3.3 操作映射组的内置函数
maps:new() -> #{} %返回一个空的映射组
erlang:is_map(M) %如果M是映射组返回true否则返回false。可以用在关卡测试或函数主体中。
map:to_list(M) -> [{K1, V1}, ... ,{Kn, Vn}] %把映射组M里的所有键和值转换成一个键值列表,键值在生成的列表里严格按照升序排列。
maps:from_list([{K1, V1}, ...., {Kn, Vn}]) -> M %把一个包含键值对的列表转换成一个映射组M。如果同样的键不止一次的出现,就使用列表里第一键所关联的值,后续的值都会被忽略。
maps:size(Map) -> numberOfEntries %返回映射组里的条目数量。
maps:is_key(Key, Map) -> boolean()%如果映射组包含一个键未key的项就返回true,否则返回false。
maps:get(Key, Map) -> val %返回映射组里与Key关联的值,否则抛出一个异常错误。
maps:find(Key, Map) -> {ok, Value} | error。%返回映射组与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:difference(M1, M2) ->
maps:without(maps:keys(M2), M1).
3.4 映射组排序
映射组在比较时,首先会比大小,然后再按照键的排序比较键和值。
3.5 以JSON为桥梁
JSON与Erlang值的映射关系如下:
1. JSON的数字用Erlang的整数或浮点数表示。
2. JSON的字符串用Erlang的二进制。
3. JSON的列表用Erlang的列表。
4. JSON的true和false用Erlang的原子true和false表示。
5. JSON的对象用Erlang的映射组表示,但是有限制:映射组里的键必须是原子、字符串或者二进制,而值必须可以用JSON的数据类型表示。
4. 小结
可以类比C/C++语言等的映射关系,等等来理解与思考记录与映射组,并加入函数式编程的思维,先宏观上的了解,然后再一点点去实验,希望尽快掌握。