文章目录
0. 数据类型
基础数据类型:
类型 | 说明 | 备注 |
---|---|---|
Number | 数字 | 包括 Integr 和 Float |
Integer | 整型 | 属于Number |
Float | 浮点型 | 属于Number |
Atom | 原子 | |
Tuple | 元组 | |
List | 列表 | |
String | 字符串 | |
fun | 函数型 | |
Union | ||
binary | 二进制型 | |
bitstring | 二进制串 | |
record | 记录 | |
map | 映射组 |
内部类型:
类型 | 说明 | 备注 |
---|---|---|
any() | ||
none() | ||
pid() | 进程id | |
port() | 端口 | |
reference() | ||
[] | ||
Atom | ||
binary() | ||
float() | ||
Fun | ||
Integer | ||
Tuple | ||
Union | ||
UserDefined | ||
ref |
1. 整数 (integer)
- 定义整数,用 base#value 表示2 ~ 36进制数(10进制除外),超出9的数用 abc…xyz 等字符代替。
语法:1> N10 = 15. %% 十进制 2> N2 = 2#00101010. %% 二进制 3> N8 = 8#123756322. %% 八进制 4> N16 = 16#af6bfa23. %% 十六进制
- 用 ASCII 编码表示整数或字符,格式:$char
1> $a. %% 字符 a 的ASCII 编码是 97 2> $1. %% 字符 1 的ASCII 编码是 49 3> $\n. %% 换行符 的ASCII 编码是 10 4> $\^c. %% Ctrl+C 的ASCII 编码是 3
2. 浮点数 (float)
erlang 没有单精度浮点数,而是采用 IEEE 754-1985格式的64位双精度浮点数表示。绝对值在10-323到10308范围内的实数可以用Erlang的浮点数表示。浮点数由五部分组成:
- 可选的正负号
- 整数部分
- 小数点
- 小数部分
- 可选的指数部分
- 定义浮点数
1> F1 = 1.0. 2> F2 = 3.1415926. 3> F3 = -2.3e+6. 4> F4 = 23.56E-27.
3. 原子 (atom)
4. 布尔值 (bool)
Erlang没有单独的布尔值类型。
原子 true 和 false 具有特殊的含义,可以用来表示布尔值。
如果所有的函数都返回布尔值,就能将它们用于标准库函数
5. 元组 (tuple)
元组会在声明时自动创建,不再使用时则被销毁
-
声明元组
P1 = {10, 45}. %% 声明元组 P2 = {point, 10, 45}. %% 声明元组,并增加可读性 P3 = {person, {name, joe}, {height, 1.82}}. %% 声明嵌套元组
-
元组取值
{point, X, Y} = {point, 10, 45}. %% X为10,Y为45 {point, C, C} = {point, 25, 25}. %% C为25,如果值不一样报错 {_, {_, Who}, _} = P3. %% Who为joe。_为占位符(匿名变量,值可不同)
6. 列表 (list)
列表的第一个元素叫表头(head),其余的叫表尾(tail)。注意列表头可以是任何事物,但列表尾通常仍然是个列表。
-
声明列表
Drawing = [{square,{10, 10}, 10}, {triang,{15, 10}, {25, 10}, {30, 40}}].
-
定义列表
T是个列表,[H|T]也是个列表,头是H,尾是T。竖线(|)把头与尾隔开。[]是个空列表。这是“格式正确的”列表% 列表 ThingsToBuy = [{apple, 10}, {pears, 6}, {milk, 3}]. % 多表头的列表 ThingsToBuy1 = [{oranges, 10}, {newspaper, 1} | ThingsToBuy].
-
提取列表元素
如果有一个非空列表 L,那么表达式 [X | Y] = L (X, Y都是未绑定变量)会提取列表头作为X,列表尾作为Y。% 列表 [Buy1 | ThingsToBuy2] = ThingsToBuy1. % Buy1 = {oranges, 4} % ThingsToBuy2 = [{newspaper,1}, {apples,10}, {pears,6}, {milk,3}] [Buy2, Buy3 | ThingsToBuy3] = ThingsToBuy2. % Buy2 = {newspaper,1} % Buy3 = {apples,10} % ThingsToBuy3 = [{pears,6}, {milk,3}]
-
列表元素操作
L -- Y % 从列表 L 中移除元素Y L ++ [H] % 不要这么干 这是很低效的
列表方法
方法 | 说明 |
---|---|
lists:map | 遍历列表 |
lists:filter | 过滤并生成一个新列表 |
lists:seq | 返回一个包含从N到M所有整数的列表 |
- 示例
Even = fun(X) -> (X rem 2) =:= 0 end. lists:map(Even, [1, 2, 3, 4, 5, 6, 8]). % [false,true,false,true,false,true,true] lists:filter(Even, [1, 2, 3, 4, 5, 6, 8]). % [2,4,6,8] lists:seq(1,5). % [1,2,3,4,5]
列表推导
列表推导(list comprehension)是无需使用fun、map或filter就能创建列表的表达式
- 普通形式 [ expression || Pattern <- ListExpr ]
- expression(表达式):用于生成新的元素
- Pattern(模式):用于匹配 list 中的各个元素
- ListExpr(列表):用生成新列表的源列表
[ 2*X || X <- [1,2,3,4,5] ]. % [2,4,6,8,10] map(F,L) -> [F(X) || X <- L]. % 等价于 map(F, [H | T]) -> [F(H) | map(F, T)].
- 常见形式 [ X || Qualifier1, Qualifier2, …]
[ bitstring || generator, filter ]-
generator(生成器):Pattern <- ListExpr
- ListExpr:必须是能得出列表的表达式
-
bitstring(位串):BitStringPattern <= BitStringExpr
- BitStringExpr 必须是能得出 bitstring 的表达式
-
filter(过滤器):既可以是判断函数(返回true或false),也可以是布尔表达式。
- 列表推导里的生成器部分起着过滤器的作用
[ X || {a, X} <- [{a,1}, {b,2}, {c,3}, {a,4}, hello, "wow"]]. % [1,4]
- 列表推导里的生成器部分起着过滤器的作用
-
Quicksort 快速排序
qsort( [ ] ) -> [ ]; qsort( [ Pivot | T ] ) -> qsort( [ X || X <- T, X < Pivot ] ) ++ [ Pivot ] ++ qsort( [ X || X <- T, X>= Pivot ] ).
- qsort( [23,6,2,9,27,400,78,45,61,82,14] )
参数 [Povot | T] 产生绑定 Pivot = 23, T = [6,2,9,27,400,78,45,61,82,14] - 将列表 T 分成两个表,一个包含T里所有小于Pivot的元素,另一个包含所有大于等于Pivot的元素
Smaller = [ X || X <- T, X < Pivot ]. = [6,2,9,14]
Bigger = [ X || X <- T, X >= Pivot ]. = [27,400,78,45,61,82] - 排序 Smaller 和 Bigger,并将它们与 Pivot 合并
qsort( [6,2,9,14] ) ++ [23] ++ [27,400,78,45,61,82]
= [2,6,9,14] ++ [23] ++ [27,45,61,78,82,400]
= [2,6,9,14,23,27,45,61,78,82,400]
1> qsort([23,6,2,9,27,400,78,45,61,82,14]). % [2,6,9,14,23,27,45,61,78,82,400]
- qsort( [23,6,2,9,27,400,78,45,61,82,14] )
-
毕达哥拉斯三元数组
pythag(N) -> [ {A,B,C} || A <- lists:seq(1, N), % A 提取列表[1,..N]里元素 B <- lists:seq(1, N), % B 提取列表[1,..N]里元素 C <- lists:seq(1, N), % C 提取列表[1,..N]里元素 A+B+C =< N, % 条件1:A 加 B 加 C 小于等于 N A*A+B*B =:= C*C % 条件2:并且 A方 加 B方 等于 C方 ].
-
- 回文构词
perms( [ ] ) -> [ [ ] ]; perms( L ) -> [ [ H | T ] || H <- L, T <- perms( L -- [ H ] ) ],
- 从 L 里 提取 H,然后从 perms( L – [H] )里提取 T,最后返回 [H | T]
- H <- L 相当于一个for循环,在每次循环中又执行一次 T <- perms( L – [ H ] )
- 相当于循环嵌套,又相当于递归
- 构造自然顺序的列表
- 总是向列表头添加元素。
- 从输入列表的头部提取元素,然后添加到输出列表的头部,形成的结果与输入列表顺序相反的输出列表。
- 如果顺序很重要,调用lists:reverse/1函数(高度优化过)
- 避免违反以上的建议。
7. 字符串 (string)
- 定义字符串
1> Name = "Kate". "Kate"
% 在 shell 中,如果列表内的所有整数都代表可打印字符 % 那么就会将其打印成字符串字面量。 3> [83, 117, 114, 112, 114, 105, 115, 101]. % "Surprise" % 否则,打印成列表记法 4> [1, 83, 117, 114, 112, 114, 105, 115, 101]. % [1,83,117,114,112,114,105,115,101]
- $语法 知道字符的ASCII码
I = $s. %115 [I-32, $u, $r, $p, $r, $i, $s, $e]. % "Surprise"
- 输出数字或Unicode字符
% 输出 可打印整数 X = [97,98,99]. % "abc" io:format("~w~n",["abc"]). % [97,98,99] % 输出 Unicode字符 X1 = "a\x{221e}b". % [97,8734,98] io:format("~ts~n", [X1]). % V10.5 版本中显示为 a\x{221E}b
- 字符集
- 从Erlang的R16B版开始,Erlang源代码文件都假定采用UTF-8字符集编码。在这之前用的是ISO-8859-1(Latin-1)字符集。这就意味着所有UTF-8可打印字符都能在源代码文件里使用,无需使用任何转义序列。
- Erlang内部没有字符数据类型。字符串其实并不存在,而是由整数列表来表示。用整数列表表示Unicode字符串是毫无问题的。
- 转义字符
转义字符 含义 整数编码 \b 退格符 8 \d 删除符 127 \e 换行符 27 \f 换页符 12 \n 换行符 10 \r 回车符 13 \s 空格符 32 \t 水平制表符 9 \v 垂直制表符 11 \x{…} 十六进制字符 ^a…^z 或 ^A…^Z Ctrl+A 至 Ctrl+Z 1 至 36 \’ 单引号 39 \" 双引号 34 \\ 反斜杠 92 \C C的 ASCII编码(C是一个字符) (一个整数)
严格来说,Erlang 里没有字符串。要在 Erlang 里表示字符串,可以选择一个由整数组成的列表或者一个二进制型。当字符串表示为一个整数列表时,列表里的每个元素都代表了一个 Unicode 代码点(codepoint)。
映射组(map)
键-值对的关联性集合
键可以是任意的Erlang数据类型
比元组占用更多的存储空间,查找起来也更慢
-
映射组适合以下的情形:
- 当键不能预先知道时用来表示键-值数据结构;
- 当存在大量不同的键时用来表示数据;
- 当方便使用很重要而效率无关紧要时作为万能的数据结构使用;
- 用作“自解释型”的数据结构,也就是说,用户容易从键名猜出值的含义;
- 用来表示键-值解析树,例如XML或配置文件;
- 用JSON来和其他编程语言通信。
- 键和值可以是任何有效的Erlang数据类型
- 映射组在系统内部是作为有序集合存储的,打印时总是使用各键排序后的顺序,与映射组的创建方式无关
-
语法
% 创建映射组 Map = #{ Key1 => Val1, Key2 => Val2, ... KeyN => ValN }. % 更新映射组 NewMap = OldMap#{K1 := V1, ...,Kn := Vn}.
-
举例
% 创建映射组 1> F1 = #{ a => 1, b => 2 }. #{a => 1,b => 2} 2> Facts = #{ {wife, fred} => "Sue", {age, fred} => 45, {daughter, fred} => "Mary", {likes, jim} => ["Man"] }. % 替换映射组 3> F2 = F1#{ c => xx }. % 新建键值 c #{a => 1,b => 2,c => xx} 4> F3 = F1#{ c := 3 }. % 错误,变量 F1 里没有 c 这个 key 5> F3 = F2#{ c := 6 }. % ok,F3 的 c 值为 6 % 取值 6> #{ c := X } = F3. % 单字段提取 X = 6 7> #{ d := Y } = F3. % 错误,匹配不到右边的值 8> #{ Z => 1547}. % 错误,变量 Z 未绑定
-
模式匹配映射组字段
可以在函数的头部使用包含模式的映射组,前提是映射组里所有的键都是已知的% 定义一个 count_characters(Str) 函数 % 让它返回一个映射组,内含字符串里各个字符的出现次数 % 这个例子运行不起来 以后在考究 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.
执行
1> count_characters("hello"). #{101=>1,104=>1,1-8=>2,111=>1}
-
方法
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).
6. 映射组排序
映射组在比较时首先会比大小,然后再按照键的排序比较键和值。
如果A和B是映射组,那么当maps:size(A) < maps:size(B)时A < B。
如果A和B是大小相同的映射组,那么当maps:to_list(A) < maps:to_list(B)时A < B。
举个例子,A = #{age => 23, person => “jim”}小于B = # {email => “sue@somplace.com”, name => “sue”}。这是因为A的最小键(age)比B的最小键(email)更小。
当映射组与其他Erlang数据类型相比较时,因为我们认为映射组比列表或元组“更复杂”,所以映射组总是会大于列表或元组
映射组可以通过io:format里的~p选项输出,并用io:read或file:consult读取。
7. 以JSON为桥梁
有两个内置函数可以让映射组和JSON数据相互转换内置函数 说明 maps:to_json(Map) -> Bin 把一个映射组转换成二进制型,它包含用JSON表示的该映射组请。注意,不是所有的映射组都能转换成JSON数据类型。映射组里所有的值都必须是能用JSON表示的对象。例如,值不能包含的对象有fun、进程标识符和引用等。如果有任何的键或值不能用JSON表示,maps:to_json就会失败。 maps:from_json(Bin) -> Map 把一个包含JSON数据的二进制型转换成映射组。 maps:safe_from_json(Bin) -> Map 把一个包含JSON数据的二进制型转换成映射组。Bin里的任何原子必须在调用此内置函数前就已存在,否则就会抛出一个异常错误。这样做是为了防止创建大量的新原子。出于效率的原因,Erlang不会垃圾回收(garbage collect)原子,所以连续不断地添加新原子会(在很长一段时间后)让Erlang虚拟机崩溃。 上面两种定义里的Map都必须是json_map()类型的实例
- type json_map() = [{json_key(), json_value()}].
- type json_key() = atom() | binary() | io_list() 并且
- type json_value() = integer() | binary() | float() | atom() | [json_value] | json_map()
JSON对象与Erlang值的映射关系如下。
- SON的数字用Erlang的整数或浮点数表示。
- JSON的字符串用Erlang的二进制型表示。
- JSON的列表用Erlang的列表表示。
- JSON的true和false用Erlang的原子true和false表示。
- JSON的对象用Erlang的映射组表示,但是有限制:映射组里的键必须是原子、字符串或二进制型,而值必须可以用JSON的数据类型表示。当来回转换JSON数据类型时,应当注意一些特定的转换限制。Erlang对整数提供了无限的精度。所以,Erlang会很自然地把映射组里的某个大数转换成JSON数据里的大数,而解码此JSON数据的程序不一定能理解它。
记录 (record)
记录其实就是元组的另一种形式
使用记录,可以给元组里的各个元素关联一个名称
- 应该在下列情形里使用记录:
- 当你可以用一些预先确定且数量固定的原子来表示数据时;
- 当记录里的元素数量和元素名称不会随时间而改变时;
- 当存储空间是个问题时,典型的案例是你有一大堆元组,并且每个元组都有相同的结构。
- 语法,不是shell命令,只能用在源代码模块里使用。
记录能保存在 .erl 文件(源代码)和 .hrl 文件(包含文件,类似c语言的 .h 头文件)-record(Name, { % 以下两个键带有默认值 key1 = Default1, key2 = Default2, ... % key3 = undefined key3, ... }).
- 举例,
- 定义记录。记录一旦被定义,就可以创建该记录的实例了
% records.hrl -record(todo, {status = reminder, who = joe, text}).
- 创建和更新记录
% 在 shell 里读取 包含记录的 .hrl 文件 1> rr("records.hrl"). [todo] % 创建记录,所有的键都是原子,而且必须与记录定义里所用的一致 2> #todo{}. #todo{status = reminder, who = joe, text=undefined} 3> X1 = #todo{ status = urgent, text = "Fix errata in book"}. #todo{status = urgent, who = joe, text=undefined} % 创建的是原始记录的副本 4> X2 = #todo{ status = done }. #todo{status = done, who = joe, text = "Fix errata in book"} % 提取记录字段 5> #todo{who = W, test = Txt} = X2. #todo{status = done, who = joe, text = "Fix errata in book"} 6> W. joe 7> Txt. "Fix errata in book" % 只提取单个字段,用 . 语法 8> X2#todo.text "Fix errata in book"
- 在函数里模式匹配记录
clear_status(#todo{status=S, who=W} = R) -> % 在此函数内部, S 和 W 绑定了记录里的字段值 % R是整个记录 R#todo{status=finished} % ...
- 要匹配某个类型的记录
do_something(X) when is_record(X, todo) -> %% ...
- 忘掉记录的定义,变换元组
9> X2 #todo{status=done,who=joe,text="Fix errata in book"} 10> rf(todo). % 忘掉记录todo的定义 11> X2. {todo,done,joe,"Fix errata in book"}
- 定义记录。记录一旦被定义,就可以创建该记录的实例了
函数(fun)
- 在模块里定义函数
% test.erl 求三角形第三边长度 hypot(X, Y) -> % 定义 命名函数 Double = fun(Z) -> 2*Z end, % 定义 匿名函数 Double(5), % 调用 匿名函数 math:sqrt(X*X + Y*Y). % 最后一句是函数的返回值
- 在 shell 里定义函数
1> Double = fun(X) -> 2*X end. % 定义 匿名函数 2> Double(5). % 调用 匿名函数
- 函数重载
% test.erl 转换华氏度与摄氏度 % 定义参数个数相同,但值不同的函数 TempConvert = fun({c, C}) -> {f, 32+ C*9/5}; ({f, F}) -> {c, (F-32)*5/9} % 省略了fun end. % 调用 TempConvert({c, 100}). % {f,212.0} TempConvert({f, 212}). % {c,100.0}
- 函数参数
% 定义以 fun 作为参数的函数,跟正常函数一样 Test = fun(F) -> F(X) end. % 用 匿名函数 当参数 lists:map(fun(X) -> 2*X end, [1, 2, 3, 4]). % [2,4,6,8]
- 返回函数
% 定义返回值为 fun 的函数 Fruit = [apple, pear, orange]. % fun() -> ( 返回值 ) end. 把 返回值 替换成 函数 即可 MakeTest = fun(L) -> (fun(X) -> lists.member(X, L) end) end. IsFruit = MakeTest(Fruit). % 把返回的函数赋值给 IsFruit IsFruit(pear). % true IsFruit(apple). % true IsFruit(dog). % false lists:filter(IsFruit, [dog, orange, cat, apple, bear]). % [orange, apple]
- 实现功能
- 内置函数遍历列表
lists:map(fun(X) -> 2*X end, [1, 2, 3, 4]). % [2,4,6,8]
- 定义抽象对象(实现 for 循环)
% lib_misc.erl for(Max, Max, F) -> [F(Max)]; for(I, Max, F) -> [F(I) | for(I+1, Max, F)]. % other_file.erl lib_misc:for(1, 10, fun(I) -> I end). % [1,2,3,4,5,6,7,8,9,10] lib_misc:for(1, 10, fun(I) -> I*I end). % [1,4,9,16,25,36,49,64,81,100]
- 重入解析代码(reentrant parsing code)
- 解析组合器(parser combinator)
- 惰性求值器(lazy evaluator)
- 内置函数遍历列表
- 定义列表处理函数
% mylists.erl sum( [H | T] ) -> H + sum(T); % 递归求和 sum( [] ) -> 0. % 如果是空 list 的时候,返回 0 map(_, []) -> []; map(F, [H|T]) -> [F(H)|map(F,T)]. % other.erl mylists:sum([1, 3, 10]). % 14 hello:map(fun(X) -> 2*X end, [1,2,3,4,5]). % [2,4,6,8,10]
- 计算总和
执行 shop1.erl% 文件 shop.erl -module(shop). -export([cost/1]). cost(oranges) -> 5; cost(newspaper) -> 8; cost(apple) -> 2; cost(pears) -> 9; cost(milk) -> 7; % 文件 shop1.erl -module(shop1). -export([totle/1]). total([{What, N} | T]) -> shop:cost(What) * N + total(T); total([]) -> 0.
语句匹配所得值为:What = milk,N = 3,T = []$ erl 1> c(shop). {ok,shop} 2> c(shop1). {ok,shop1} 3> shop1:total([]). 0 4> shop1.total([{milk, 3}]). 21
执行语句 shop:cost(What) * N + total(T)。
即:shop:cost(milk) => 7 * 3 + total([]) => 0 = 21
语句匹配所得值为:What = pears,N = 6,T = [{milk, 3}]5> shop1:total([{pears, 6}, {milk, 3}]). 75
6> Buy = [{oranges, 4}, {newspaper, 1}, {apple, 10}, {pears, 6}, {milk, 3}]. 7> shop1:total(Buy). 123
- 归集器
执行% lib_misc.erl % 不推荐,这样会遍历2次列表 odds_and_evens1(L) -> Odds = [X || X <- L, (X rem 2) =:= 1], Evens = [X || X <- L, (X rem 2) =:= 0], {Odds, Evens}. % 推荐,只遍历列表1次,把奇偶数分别添加到适合的列表里。 odds_and_evens2(L) -> odds_and_evens_acc(L, [], []). odds_and_evens_acc([H|T], Odds, Evens) -> case (H rem 2) fo 1 -> odds_and_evens_acc(T, [H|Odds], Evens); 0 -> odds_and_evens_acc(T, Odds, [H|Evens]); end; odds_and_evens_acc([], Odds, Evens) -> { lists:reverse(Odds), lists:reverse(Evens) }. % 得出得结果是反得,需要逆转
1> lib_misc:odds_and_evens1([1,2,3,4,5,6]). {[1,3,5], [2,4,6]} 2> lib_misc:odds_and_evens2([1,2,3,4,5,6]). {[1,3,5], [2,4,6]}
- apply
此内置函数能调用某个模块里的某个函数,并向它传递参数
与直接调用函数的区别在于模块名和/或函数名可以是动态计算得出的
尽量避免使用 apply
apply(Mod, Func, [Arg1, Arg2, ..., ArgN])
动态调用
Mod:Func(Arg1, Arg2, ..., ArgN)
效果相同,静态调用- 所有的Erlang内置函数也可以通过apply进行调用,
1> apply(erlang, atom_to_list, [hello]).
- Mod 参数不必是一个原子,也可以是一个元组
{Mod, P1, P2, ..., Pn}:Func(A1,A2,...An)
等同于
Mod:Func(A1,A2,...,An,{Mod,P1,P2,...,Pn})
- 所有的Erlang内置函数也可以通过apply进行调用,
- 元数
一个函数的元数(arity)是该函数所拥有的参数数量。
同一模块里的两个名称相同、元数不同的函数是完全不同的函数。
Erlang程序员经常将名称相同、元数不同的函数作为辅助函数使用。% 元数为 1 sum(L) -> sum(L, 0). % 累加列表 L 里的所有元素 % 元数为 2 辅助函数,不导出来隐藏它们 sum([], N) -> N; sum([H|T], N) -> sum(T, H+N).
函数 | 说明 |
---|---|
Lambda 函数 | 匿名函数 |
高阶函数 | 操作其它函数的函数 |
二进制型与位语法
二进制型(binary)是一种数据结构,它被设计成用一种节省空间的方式来保存大批量的原始数据。
Erlang虚拟机对二进制型的输入、输出和消息传递都做了优化,十分高效。
如果要保存大批量的无结构数据内容,二进制型应当是首选,比如大型字符串或文件的内容。
二进制型里的位数都会是8的整数倍,因此对应一个字节串。如果位数不是8的整数倍,就称这段数据为位串(bitstring)
二进制型
- 二进制型的编写和打印形式是双小于号与双大于号之间的一列整数或字符串
- 在二进制型里使用整数时,它们必须属于0至255这个范围。
- 二进制型<<“cat”>>是<<99,97,116>>的简写形式,二进制型是由字符串里这些字符的ASCII编码组成的。
- 和字符串类似,如果某个二进制型的内容是可打印的字符串,shell就会将这个二进制型打印成字符串,否则就打印成一列整数。
- 可以用内置函数来构建二进制型或提取它里面的元素,也可以使用位语法
- 创建二进制型
1> <<5, 10, 20>> <<5,10,20>> 2> <<"hello">> <<"hello">> 3> <<65, 66, 67>> <<"ABC">>
- 二进制内置函数
- list_to_banary(L) -> B
返回二进制型,通过把 io 列表(iolist) L 里的所有元素压扁后形成的(压扁就是移除列表里所有的括号)。
io列表本身是循环定义的,是指一个列表所包含的元素是0~255的整数、二进制型或者其他io列表。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>>
- term_to_binary(Term) -> Ext_B 把任何erlang数据类型转换成二进制型。
- binary_to_term(Bin) -> Term 将二进制型转回来
- byte_size(Bin) -> Size 返回二进制型的字节数
- split_binary(Bin,Pos) -> {B1,B2} 在 pos 处把二进制型 Bin 一分为二
- list_to_banary(L) -> B
位语法
位语法是一种表示法,用于从二进制数据里提取或加入单独的位或者位串
编写底层代码,以位为单位打包和解包二进制数据时,位语法是极其有用的
开发位语法是为了进行协议编程(这是Erlang的强项),以及生成操作二进制数据的高效代码
- 位语法表达式
<<>> <<E1,E2, ..., En>> % 每个 Ei 元素都标识出二进制型或位串里的一个片段 Ei = Value | Value:Size | Value/TypeSpecifierList | Value:Size/TypeSpecifierList <<Size:4, Data:Size/binary, ...>> % 二进制型里某个Size的值可以通过之前的模式匹配获得
- 如果表达式的总位数是8的整数倍,就会构建一个二进制型,否则构建一个位串。
- 当构建二进制型时,Value 必须是已绑定变量、字符串,或是能得出整数、浮点数或二进制型的表达式。
- 当被用于模式匹配操作时,Value 可以是绑定或未绑定的变量、整数、字符串、浮点数或二进制型。
- Size 必须是一个能够得出整数的表达式。
- 在模式匹配里,Size必须是一个整数,或者是值为整数的已绑定变量
- 如果Size在模式里所处的位置需要有一个值,它就必须是已绑定变量
- Size 的值指明了片段的大小。它的默认值取决于不同的数据类型,对整数来说是8,浮点数则是64,如果是二进制型就是该二进制型的大小。在模式匹配里,默认值只对最后那个元素有效。如果未指定片段的大小,就会采用默认值
- TypeSpecifierList(类型指定列表)是一个用连字符分隔的列表,形式为End-Sign-Type-Unit。前面这些项中的任何一个都可以被省略,各个项也可以按任意顺序排列。如果省略了某一项,系统就会使用它的默认值。
- End可以是 big | little | native
它指定机器的字节顺序
native是指在运行时根据机器的CPU来确定。
默认值是big,也就是网络字节顺序(network byte order)
这一项只和从二进制型里打包和解包整数与浮点数有关。当你在不同字节顺序的机器上打包和解包二进制型里的整数时,应当注意设置正确的字节顺序
编写位语法表达式时,可能有必要先做些试验。为了确定自己没有弄错,你可以尝试下面的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>>}
- Sign可以是 signed | unsigned
这个参数只用于模式匹配。默认值是unsigned - Type可以是 integer | float | binary | bytes | bitstring | bits | utf8 | utf16 | utf32
默认值是integer。 - Unit的写法是unit:1 | 2 | …256
integer、float和bitstring的Unit默认值是1,binary则是8。utf8、utf16和utf32 类型无需提供值。 - 一个片段的总长度是Size x Unit字节。binary类型的片段长度必须是8的整数倍。
- End可以是 big | little | native
- 定义
假设要把三个变量(X、Y和Z)打包进一个16位的内存区域。X应当在结果里占据3位,Y应当占据7位,而Z应当占据6位。在大多数语言里这意味着要进行一些麻烦的底层操作,包括位移位(bit shifting)和位掩码(bit masking)。而在Erlang里,只需要这么写:M = <<X:3, Y:7, Z:6>> %M的类型是binary 因为数据的总长度是16位,可以被8整除 M = <<X:2, Y:7, Z:6>> %M的类型是bitstring 因为总位数是15
- 打包和解包16 位颜色
假设想要表示一种16位RGB颜色。我们决定给红色通道分配5位,绿色通道分配6位,剩下的5位则分配给蓝色通道。(让绿色通道多使用1位是因为人眼对绿光更敏感。)1> Red = 2. 2> Green = 61. 3> Blue = 20. % 打包颜色, 定义一个16位数据的双字节二进制型 4> Mem = <<Red:5, Green:6, Blue:5>>. <<23,180>> % 解包颜色 5> <<R1:5, G1:6, B1:5>> = Mem.
- 例子
- 寻找MPEG音频数据里的同步点
MPEG音频数据是由许多的帧组成的。每一帧都有它自己的帧头,后面跟着音频信息。由于没有文件头,所以原则上可以把一个MPEG文件切成多个片段,而每一段都可以独立播放。任何读取MPEG流的软件都应当能找到头帧,然后同步MPEG数据。
MPEG头以一段11位的帧同步(frame sync)信息开头,它包含11个连续的1位,后面跟着描述后续数据的信息:AAAAAAAA AAABBCCD EEEEFFGH IIJJKLMM
AAAAAAAAAAA 同步字(11位,全为1)
BB 2位的MPEG音频版本ID
CC 2位的层描述
D 1位保护位
…
知道了从A到M的值,就能计算出一个MPEG帧的总长度
为了找到同步点,首先假设我们成功定位到了某个MPEG头的起始位置,然后试着计算该帧的长度。之后可能会发生以下某一种情况。- 假设正确,因此当向前跳过帧长后,会找到另一个MPEG头。
- 假设错误。可能没有定位到标记MPEG头起始位置的位序列(由11个连续的1组成)上,或者因为字的格式不正确而无法计算帧长。
- 假设错误,但定位到的若干字节音乐数据碰巧和MPEG头的起始位置很相似。在这种情况下,可以计算帧长,但是当向前跳过这段距离后,我们无法找到新的MPEG头。
为了确保万无一失,我们将寻找三个连续的MPEG头。同步的程序如下所示:
- 构建微软通用对象文件格式(Microsoft Common Object File Format,简称COFF)的二进制数据文件。打包和解包二进制数据文件(例如COFF)的典型做法是使用二进制型和二进制模式匹配
- 解包一个IPv4数据报(datagram)
- 寻找MPEG音频数据里的同步点
- 位串:处理位级数据
对位串(bitstring)的模式匹配是位级操作,这样我们就能在单次操作里打包和解包位的序列。
这对编写需要操作位级数据的代码来说极其有用,例如没有按照8位边界对齐的数据或者可变长度数据,它们的数据长度用位而不是字节来表示。
不能把一个位串写入文件或套接字(二进制型则可以),因为文件和套接字使用的单位是字节。1> B1 = <<1:8>>. % <<1>> 2> byte_size(B1). % 1 3> is_binary(B1). % true 4> is_bitstring(B1). % true 5> B2 = <<1: 17>>. % <<0,0,1:1>> 6> is_binary(B2). % false 7> is_bitstring(B2). % true 8> byte_size(B2). % 3 字节大小 9> bit_size(B2). % 17 位大小
提取出字节里各个单独的位,使用位推导(bit comprehension)位推导遍历二进制型并生成列表或二进制型。1> B = <<16#5f>>. % <<"_">> 包含单个字节的二进制型 2> [X || <<X:1>> <= B]. % [0,1,0,1,1,1,1,1] 包含字节里各个位的列表 3> << <<X>> || <<X:1>> <= B >>. % <<0,1,0,1,1,1,1,1>> 包含这些位的二进制型