1-语法基础
shell终端
在终端输入erl即可进入erl环境的终端
命令 | 说明 |
---|---|
help() | 打印可用的shell函数 |
h() | 打印先前输入过的命令 |
v(N) | 取出第N号提示符对应的计算结果 |
cd(Dir) | 更改当前自录(Dir应是双引号字符串) |
ls()和ls(Dir) | 打印目录内容 |
pwd() | 打印工作目录(当前目录) |
q() | 退出(init:stop() 的简写) |
i() | 打印当前系统的运行时信息 |
memory() | 打印内存使用信息 |
数据类型
- 数值类型
- 整数
- 浮点数
- 二进制串/位串
- 原子
- 元组
- 列表(和字符串)
- 唯一标识符
- PID
- 端口
- 引用
- Fun函数
数值
整数
- 正数 101
- 负数 -101
- 十六进制数字 16#FFffFFff
- 二进制 2#1010
- ASCII码查询 $9 (查询数字9的ASCII码)
浮点数
- 正数 3.14
- 负数 -0.123
- 科学记数法 6.23e-11
运算符
算术运算符
- 加法 +
- 减法 -
- 乘法 *
- 除法 / (结果是浮点数不会被截断)
- 除法 div (如7 div 3 = 3 这个结果小数点位会被截断)
位运算符
- 左移 bsl (1 bsl 2 结果是1左移了两位)
- 右移 bsr (1 bsl 2 结果是1左移了两位)
- 等等
二进制串/位串
- 二进制串是无符号8位字节序列
- 位串是广义的二进制串,长度不必是8的整数倍
二进制串语法: <<0,1,2,3,….,255>>
二进制串例子:
<<"hello",32,"dude">>.
原子
可以将其称为类似其他语言的枚举、常量,符号
原子的格式是:
- 首字母小写 如: ok errr undifined. trap_exit
- 后面可以是大写字母 、数字、下划线和@ 如 route66 abc@ddd
- 如果用上其他字符需要加上单引号 ‘ 如 ‘@#$%’
- 原子长度上限是255个字符
- 目前总数限制100多万 原子一旦创建就不会被销毁除非重启系统 避免全局动态创建原子
- Erlang常用原子 如 ok用于返回 true和false用于布尔运算 undefined用于未知占位符
元组
元组是一个定长 有序序列,元组用大括号括起来 如
三个元素的元组 称为三元组
{1,2,3}
元组也可以嵌套元组如:
{complex, {nested,"struct",{here}}}
列表
列表就是0个或多个Erlang项式(项式也可以是列表),空表也被称为nil
列表的格式中括号包括 如:
[]
[1,2,3]
[[1,2,3],[4,5,6]]
添加元素:
元素无法简单高效的方式实现添加元素,用列表却可以,动态添加元素 列表可以这样做:
使用 | (管道符) 在以现有的列表为基础上创建一个新的、更长的列表,并使原列表成为新列表的一部分.
如:
- 为空列表添加一个元素1
[1 | []]
结果为[1]
- 为列表左侧添加元素2
[2 | [1]]
添加之后的列表结果为 [2,1]
- 同时添加多个元素
[3,4,5 | [2]]
结果为 [3,4,5,2]
注意!!!
使用逗号隔开和管道符号隔开的元素是不一样的逗号隔开的结果后面的位置的元素是列表
管道符号是用来拼接列表的
8> [1,2,[3,4]].
[1,2,[3,4]]
9> [1,2|[3,4]].
[1,2,3,4]
追加列表
追加列表就是两个列表拼接,追加列表可以使用 ++ 符号
如下拼接两个列表
[1,2,3,4] ++ [5,6,7,8]
拼接的结果为: [1,2,3,4,5,6,7,8]
++ 运算符 右侧的列表不会被修改
左侧列表的长度决定了++的性能
字符串
双引号字符串等价于列表
列表元素对应字符串各数值的数值编码
例如
“abcd” 等价于 [97,98,99,100] 还可以写作[$a,$b,$c,$d]
“” 等价于 []
Erlang Shell 打印一个列表结果展示字符串还是展示列表会判断 列表中每个元素是否都为可打印字符
例如:
[97,98,99,100] 打印的结果就是 “abcd”
[97,98,99,1000] 打印的结果还是 [97,98,99,1000] 1000不是可打印字符
进程标识符 PID
每个进程都有一个唯一标识符,这个唯一标识符号称为PID (只要进程不重启 PID 就不会变化)
shell 会以 这样的格式打印进程PID 箭头括号,里面是3个数字用两个圆点符号隔开,列入: <0.78.0>
shell中可以通过self().来查看当前进程的标识符
进程pid仅仅用于调试使用 ,不能随便由用户自行创建pid类型的数据
端口标识符
端口与进程差不多 只是还具备与Erlang外界通信,shell打印端口的格式为#Port<端口数据> 端口数据有一个圆点例如: #Port<0.472>
引用ref
引用又被称为ref,由make_ref()函数生成,输出格式为#Ref<引用数据> 引用数据由3个圆点例如: #Ref<0.0.0.39>
引用常被用于要求保证唯一性的一次性标签或Cookie
函数fun
Erlang被称为函数式语言
函数式语言的特点就是可以像处理数据一样处理函数,函数也可以做为参数,也可以做为结果(这一点很厉害了 很适合做钩子(回调) 插件)
shell按 #Fun<…> 打印函数
项式 比较
Erlang的各种数据都可以通过比较< 、 > 或者= 符号进行比较
分为同种类型数据比较和不同种类型数据比较
同种类型 一般按数值大小进行比较
如:
1<2
3.14 > 3
"zzz" > "zzy"
不同种类型数据的比较:
数值 < 原子 < 列表(字符串)
注意:
Erlang的比较运算符看起来不像箭头: 如小于符号为 =< (与其他语言不同) 大于符号为 >=
相等比较
完全相等(类型也要一致)
=:=
完全不等(考虑类型)
=/=
不考虑类型的相等判断 ==
不考虑类型的不等于判断 /=
模块与函数
Erlang将模块用作代码的容器,每个模块的名字都是一个全局唯一的原子
函数的参数的个数称为元数,具有1个参数的函数叫做一元函数、2个参数的函数叫做二元函数
另外还有没有参数的函数例如self() 可以称为空元函数
Erlang中模块中的所有函数都是内置函数(BIF)
编写第一个模块:
新建一个demo.erl文件 如下所示:
%% 注释使用双%%开头
%% -module是模块声明 必须有 后面用句号. 结尾
-module(demo).
%% -export 这是导出声明 告知编译器哪些函数是外部可见的 后面用句号. 结尾
%% 此处没有用-export 列出的函数都是内部函数外部不可以调用 shell也不可以
-export([pie/0]).
%% 这里是一个函数 函数包含函数首部: 函数名字和参数列表 和函数体 (描述函数的用途) 它们两个由 -> 箭头符号隔开
%% 函数的返回值就是函数体中表达式的值 不需要像其他编程语言一样有个return 函数末尾必须有句号. 结尾
pie() ->
3.14.
输入erl打开shell
在shell中输入c(demo). 编译模块demo
然后demo:pie(). 调用函数pie
然后可以看到如下结果
编译产生的目标文件为demo.beam如下所示:
demo.beam为一个我们看不懂的目标文件,更适合Erlang虚拟机加载的文件
如果不想进shell也可以使用独立的编译器erlc 执行erlc demo.erl 来编译代码
变量
变量名字
- 必须以大写字母开头或者下划线开头
- 命名规范遵循驼峰命名法则
- 下划线开头的变量一般不用来后续使用 只用来表明这是一个变量
变量使用规范
- 变量只能被赋值一次 (类似Java的final变量)
- =符号对变量的第一次操作是赋值,第二次操作就是比较运算
f(变量) 函数可以解除变量的绑定让变量重新赋值
模式匹配
模式匹配的作用:
- 选定控制流分支
- 完成变量的赋值
- 拆解数据结构
= 号运算符就是模式匹配(第一次左边是变量右边是值的时候可以当作赋值后面等号运算看作模式匹配)
等号左边是模式,右边是普通表达式
如果把赋值看作模式匹配则可以视作 值绑定到变量上
例如
Eshell V10.3.5.19 (abort with ^G)
1> {A,B,C} = {1970,"Richard",male}.
{1970,"Richard",male}
2> A.
1970
3> B.
"Richard"
4> C.
male
5>
可以看到上面元组的值绑定(模式匹配)
用模式匹配来做两个值是否相等的判断的逻辑:
Eshell V10.3.5.19 (abort with ^G)
1> {point,X,X}={point,2,2}.
{point,2,2}
2> {rect,Y,Y}={rect,1,2}.
** exception error: no match of right hand side value {rect,1,2}
3>
模式匹配失败之后的错误:
badmatch no match of right hand value
下划线_ 代表省略模式 也可以称为匿名变量(其实也不是变量只是个占位符) (一般不做任何处理了)
列表的模式匹配
Eshell V10.3.5.19 (abort with ^G)
1> [1,2,3|Rest]=[1,2,3,4,5,6,7].
[1,2,3,4,5,6,7]
2> Rest.
[4,5,6,7]
3>
++也可以出现在模式匹配中例如:
Eshell V10.3.5.19 (abort with ^G)
1> "http://" ++ Rest = "http://www.erlang.org".
"http://www.erlang.org"
2> Rest.
"www.erlang.org"
3>
使用模式匹配来做选择
这个就要用到函数
我们可以定义多个同名函数,但是参数不同,让调用方法的代码通过传递不同的参数来匹配不同的函数
例如
test(one) ->
one;
test(two) ->
two;
test(A) ->
A.
上面三个函数前两个都是以分号;结尾,最后一个以句号.结尾
当我们调用方法的时候
test(one). 会走第一个
test(two). 会走第二个
test(1). 或者test(“a”). 会走第三个
这种方法的模式匹配很类似其他语言的方法重载
Erlang在做方法的模式匹配的时候是从上往下一个方法一个方法判断的,直到匹配成功一个,如果全部都匹配不上则会抛出一个异常
Erlang的变量在作用域结束的时候,对应的值未被其他程序用到,便被当作垃圾,等待垃圾回收机制进行回收.
case和if表达式
case判断
语法为
case 变量 of
匹配变量的值变量1 ->
逻辑1;
匹配变量的值变量2 ->
逻辑2;
end.
保护式: 变量后面可以加when
case 变量 of
匹配变量的值变量1 when is_boolean(变量1) ->
逻辑1;
匹配变量的值变量2 ->
逻辑2;
end.
如下所示:
计算面积的代码:
area(Shape)->
case Shape of
{circle, Radius} ->
Radius * Radius * Math:pi();
{square, Side} ->
Side * Side;
end.
if 判断
语法:
if
布尔运算1 -> 逻辑1;
布尔运算2 -> 逻辑2;
true -> 前面布尔运算都无法匹配时的逻辑
end.
函数
fun函数可以被当作一个种值赋值给某个变量
创建fun函数
fun either_or_both/2
为fun函数绑定变量
F = fun either_or_both/2
使用函数变量,然后调用fun函数
yesno(F) ->
case F(true,false) of
true -> io:format("yes ~n");
false -> io:format("no ~n");
end.
匿名fun函数:
fun ()->0 end
匿名fun函数的特征就是没有函数的名字,然后以fun开头 以 end结尾
匿名fun函数要发挥作用必须与变量进行绑定
闭包 (不影响外部)
fun函数内部用到了fun函数外部的绑定的变量,它给这些变量当前的值做了个快照并封存起来,这个就是闭包
异常与try-catch
Erlang的异常分为三个种类:
- error
- exit
- throw
error: 运行时异常,比如除零错误、匹配运算失败、找不到匹配的函数子句,一旦促使某个进程崩溃,则Erlang的错误日志管理器将会记录
exit: 进程即将停止,一般不需要捕获此类异常 exit也在进程正常终止时候使用,不会被汇报至错误管理器
throw: 处理用户自定义的异常 主动抛出一个异常 可以直接跳出深层递归,如果外层代码也没try-catch那将会抛出一个nomatch的error信息
每种异常都有一个与之对应的抛出异常的内置函数
- throw(SomeTerm)
- exit(Reason)
- erlang:error(Reason)
作为特例,进程调用exit(normal)所抛出的异常不会被捕获,该进程会像完成使命后寿终
正寝一样终止。这意味着其他(与之链接的)进程不会将之视作反常的终止行为(其余所有的退出原因都会被视作反常)。
try-catch
try
some unsafe function()
catch
oops -> got_throw_oops;
throw:Other -> {got_throw,Other};
exit:Reason ->{got_exit,Reason};
error:Reason -{got error,Reason}
end
位于try和catch之间的是正文(body)或保护区(protected section)。
在正文内抛出并试图传播出去的任何异常都会被捕获并与catch和end之间列出的子句做匹配。
如果没有匹配的子句,那么该异常会继续传播,就好像正文外围根本没有过ty表达式。
与此类似,如果正文在求值过程中没有触发任何异常,则正文的结果也就是整个表达式的结果,就好像try和catch…end根本不存在。
唯一的区别在于,一旦异常发生且能够与某个子句相匹配,该子句的结果便会成为
整个表达式的结果。
这些子句的模式有些特殊一它们可以用冒号(:)作为异常的类别(error、exit或throw)
和被抛出的项式的分隔符。如果省略类别,则默认为throw.。一般情况下不应该去捕获error和exit,除非你明确知道自己在做什么。这种做法违背了速错的理念,还有可能掩盖真正的症结。
某些情况下,你需要运行一些不那么可信的代码并捕获从中抛出的所有东西。这时你可以使用以
下模式来捕获所有异常:
_:_ -> got_some_exception
(需要检查异常中的数据的话,可以使用c1ass:Term->… )
另外,需要注意的是一旦进入catch部分,代码就不再受到保护。catch子句中抛出的新的
异常,会传播到try表达式之外。
after
最后,你还可以给任意try表达式加上一个after段。其作用在于确保某段具有副作用的代
码的执行。在你离开try表达式之前,无论表达式的其余部分发生了什么,after段的代码都会
被执行。这种机制通常用于完成各种形式的资源释放——例如,在以下这个示例中是确保某文件
的关闭:
{ok,FileHandle} = file:open("foo.txt",[read]),
try
do_something_with _file(FileHandle)
after
file:close(FileHandle)
end
栈轨迹
通常,你所见到的异常并不包含执行栈的轨迹,它被存储于内部。你可以通过调用内置函数
erlang:get_stacktrace()来查看当前进程最近抛出的异常的栈轨迹。
栈轨迹(stack trace)是异常发生那一刻位于栈的顶部的那些调用的逆序列表(最后一个调
用位于最前)。每个函数都被表示成{Module,Function,Args}的形式,其中Module和
Function都是原子,Args要么是函数的元数,要么是函数被调用时的参数列表,这取决于当时
列表速构
通过以下方法你可以便捷地从中创建出一个仅含正整数的新列表
[X || X <- ListofIntegers,X>0]
ListofIntegers是数据来源部分
逗号, 后面的表示约束条件
总结下上面的格式
[计算部分 ||生成器部分,约束条件部分]
<- 部分表示生成器 生成器一般生成对应的变量让约束部分用然后再让计算部分用
最终结果是个列表
映射、过滤、模式匹配
映射是指针对元素完成一些运算后再将运算结果放入结果列表。例如,以下的列表速构能够从源列表中选出所有的正偶数 rem表示求余运算)并求出它们的平方:
[math:pow(X,2) || X <- ListofIntegers,X >0,X rem 2 == 0]
生成器本身就内置了一个约束条件:只有与模式相匹配的元素才在考虑范围内;其余元素统统忽略不计。此外,借助模式你还可以抽取出元素的不同组成部分并将之用在约束条件或模板中。
可以从中选出那些面积不小于10的矩形,并创建一个与之相对应的面积列表,如下:
[{area,H*W} || {rectangle,H,W} <- Shapes,H*W >10]
比特位语法与位串速构
位串可以写作<<Segment1,·..,SegmentN>>
Segment区段指示符可以为以下形式之一:
Data
Data:Size
Data/Typespecifiers
Data:Size/Typespecifiers
Data必须是整数、浮点数或另一个位串。
比特位语法中的模式匹配
可以用同样的语法来构造和分解元组, 也可以用同样的比特位语法来分解位串中的数据。相较于手工完成各种位移和掩码运算,用比特位语法来解析各种怪异的文件格式和协议数据显得手到擒来,也更不容易出错。作为一个经典示例,下面将为你展示如何利用函数子句中的模式来解析P报文首部的内容:
只要传入的报文的尺寸足够进行匹配,且Version字段为4,报文便会被解析为相应的变量,
大部分变量都被解析为整数,只有OptionsAndPadding(一个长度取决于先前解析出的IHL字段的位串)和RemainingData.段除外,其中后者包含报文首部之后的所有数据。从一个二进制串中抽取另一个二进制串并不涉及数据复制,因此这种运算的成本很低。
位串速构
存在于很多函数式编程语言之中的列表速构的思想,也被扩展到了Erlangl的比特位语法中。
位串速构酷似列表速构,**只是[….]被换成了<<…..>>**。
以一个小整数列表为例,所有整数都在0和7之间,你可以按每个数3比特位将它们打包成位串,如下:
<< <<X:3>>||X<- [1,2,3,4,5,6,7]>>
运行结果如下:
Eshell V10.3.5.19 (abort with ^G)
1>
1> << <<X:3>>||X<- [1,2,3,4,5,6,7]>>.
<<41,203,23:5>>
2>
shell会将上式得到的位串打印成《<<41,203,23:5>>。请注意末尾的23:5一位串的总长度
8+8+5=21比特位,考虑到输入列表包含7个元素,这个结果是正确的。
解码:
位串转普通数组
那么如何对这样一个位串进行解码呢?当然还是用位串速构咯!区别在于这次你需要将生成
器中的<-换成<=,表示从位串中提取内容,而<-只能从列表中选取元素:
<< <<X:8>>|| <<X:3>> <= <<41,203,23:5>> >>
运行情况如下:
2> << <<X:8>>|| <<X:3>> <= <<41,203,23:5>> >>.
<<1,2,3,4,5,6,7>>
3>
[X|| <<X:3>> <= <<41,203,23:5>>]
记录
记录语法让你可以使用记录,它们本质上就是标记元组,但避免了使用元组时增减字段所带
来的麻烦以及必须记住各个字段在元组中的顺序的问题。使用记录时的第一要务就是写下记录声明,就像这样:
-record(customer,{name="<anonymous>",address,phone}).
该声明告诉编译器你将要使用一个四元组(3个字段加上标记),其中第一个元素总是原子
customer。其他字段的顺序与记录声明中一致,因此name总是第二个字段。
创建记录元组
你可以使用以下几种语法来创建新的记录元组:
#customer{}
#customer{phone=”55512345”}
#customer{name=”Sandy Claws”,address=”Christmas Town”,phone=”55554321”}
记录名之前必须加上#,这样编译器才会将之与记录声明相匹配。在{…}之内,你可以选
择任意字段按任意顺序进行赋值(一个都不选也行)。(编译器会按声明中的顺序为它们排序。)
未赋值的那些字段将被置为默认值,即原子undefined,除非你在声明中另行指定了默认值。
宏定义
宏由define指令定义,既可以带参数也可以不带参数,举例如下:
-define(PI,3.14).
-define(pair(X,Y),{X,Y}).
在代码中使用宏(按其定义进行展开)时,必须加一个问号作为前缀:
circumference(Radius)-Radius * 2 * ?PI.
undef指令可用于移除宏定义(前提是该宏定义存在)。例如,经由下列几行代码之后
-define(foo,false).
-undef (foo).
-define(foo,true).
常用的预定义宏
?MODULE 模块名称
?FILE 源文件
?LINE 所处行
文件包含
通过使用包含指令,Erlangi源码文件可以包含另一个文件,形式如下:
-include("filename.hrl").
hrl通常只有声明没有函数实现
include_lib 相对于Erlang的 kernel目录
条件编译
条件编译就是让编译器按特定条件忽略程序的某些部分。这种手法常用于生成程序的多种版
本,比如专用于调试的版本。以下预处理指令可以控制编译器在特定的时机忽略特定位置的代码:
-ifdef (MacroName).
-ifndef (MacroName).
-else.
-endif.
进程
派生和链接
进程派生函数有2个
匿名函数参数或者MFA参数
Pid spawn(fun()->do_something() end)
Pid spawn(Module,Function,ListofArgs)
先派生进程再用link(Pid) 创建链接会引入竞态条件,spawn_link(…) 则可以确保进程创建与进程链接创建的原子性,从而避免竞态条件。
进程监视
链接有一个替代品,称作监视。这是一种单向链接,可以让一个进程在不影响目标进程的情
况下对目标进程施行监视。
Ref monitor(process,Pid)
由Pid标识的进程一旦退出,实施监视的进程将会收到一条含有唯一引用Ref的消息。
靠抛异常来终结进程
exit类异常用于终止运行中的进程。这类异常可通过BIF exit/1抛出:
exit(Reason)
除非被进程捕获,否则该调用将令进程终止,并将Reason作为退出信号的一部分发送给所有与该进程链接的进程。
直接向进程发送退出信号
进程除了在意外退出时会自动发送信号以外,还可以直接向其他进程发送退出信号。收发双方事先无须链接:
exit(Pid,Reason)
注意这里用的是exit/2,而非exit/1一它们是完全不同的函数(却不幸都名为exit)。
该信号终止的不是发送方,而是接收方。如果Reason是原子kill,接收方将无法捕获该信号,从而被强制终止。
设置trap_exit标志
默认情况下,一旦接收到来自相互链接的其他进程的退出信号,进程就会退出。为了避免这
种行为并捕捉退出信号,进程可以设置trap_exit标志:
process_flag(trap_exit,true)
这样一来,除了无法捕获的信号(k111)以外,外来的退出信号都会被转换成无害的消息。
消息接收与选择性接收
接收消息的进程可以用receive表达式从信箱队列中提取消息。
receive
Pattern1 when Guard1 -> Body1;
PatternN when GuardN -> BodyN
after Time ->
TimeoutBody
end
after…段可选,如果省略,receive永不超时。否则,Time必须是表示毫秒数的整数或原子infinity。如果Time为0,receive永不阻塞。无论哪种情况,只要信箱中没有匹配的消息,receive便会一直等到匹配的消息到达或发生超时为止,无论先后。等待期间进程将被挂起,只有新消息到来时才会被唤醒。
注册进程
每个Erlang系统都有一个本地进程注册表-用于注册进程的简单命名服务
一个名称一次只能注册一个进程
使用如下命令可以查看当前注册进程:
使用!向进程发送消息
Eshell V10.3.5.19 (abort with ^G)
1> registered().
[erts_code_purger,standard_error,user,inet_db,
standard_error_sup,erl_signal_server,kernel_safe_sup,
logger_std_h_default,logger_sup,logger_proxy,
logger_handler_watcher,erl_prim_loader,logger,rex,
global_group,kernel_sup,kernel_refc,global_name_server,
user_drv,file_server_2,code_server,application_controller,
init]
2> init!{stop,stop}.
自定义注册进程
Eshell V10.3.5.19 (abort with ^G)
1> Pid = spawn(timer,sleep, [30000]).
<0.80.0>
2> register(one,Pid).
true
3> whereis(one).
<0.80.0>
4> whereis(one).
undefined
5> whereis(one).
undefined
6>
要想跟位于另一个Erlang节点上的注册进程通信,可以这样做:
6>{some_node.name,some_registered_name} ! Message.
消息投递与信号
Erlang进程互相用! 运算符发送的消息只是Erlang通用信号系统的一种特殊形式。
- 濒死进程向与之链接的相邻进程发送的退出信号;
- 一小部分信号对程序员不可见,尝试链接两个进程时发送的链接请求。
投递信号的基本原则:
如果进程P1向同一个目标进程P2先后发送两个信号S1和S2(不论进程在发送S1和发送S2
之间做了些什么,也不论两个信号的间隔时间有多久),这两个信号将按发送顺序到达P2
(如果都能到达的话)。尽力投递所有信号。同一Erlang运行时系统内,进程之间不存在消息丢失的危险。但是,
在两个依靠网络互联的Erlang系统之间,一旦网络连接断开,消息就有可能丢失(部分依
赖于传输协议)。连接恢复后,有可能出现上例中的S2最终抵达但S1却丢失的情况。
进程字典
作为自身状态的一部分,每个进程都有一个私有的进程字典,这是一个可以用任何值作为键
的简单哈希表,用于存储Erlang:项式。通过内置函数put(Key,Value)和get(Key,Value)可以从中存取项式。我们不打算详细介绍进程字典,在此我们只想告诉你,无论进程字典看起来多么诱人都不要去碰它。
不要用:
- 最简单的原因在于它让程序的行为难以理解。程序的行为无法再直接通过代码看出,
- 更重要的是它令进程间迁移任务的难度增加甚至趋于不可能。 我们需要的无状态的进程 使用了这个就让进程成为了有状态的进程
在某些情况下进程字典也有合理的用法,但一般而言,总会有更好的数据存储方案(需要的
话甚至可以在不同进程间共享信息)。这种方案的名字比较怪,叫作ETS表。
ETS表
ETS代表Erlang项式存储(Erlang Term Storage)。所谓ETS表就是一张用于存储Erlang.项式(即任意Erlang数据)且可以在进程间共享的表。不过这不是违背了基本的引用透明性和避免共享的原则了吗?难不成我们走了后门偷偷摸摸地干起破坏性更新的勾当来了?四个字:进程语义。
ETS表很像是简化了的数据库服务器:与外界隔离,并持有一些供多方使用的数据。
从同一张表中读到的内容也可能随时间的不同而不同。它们也可以确信自己之前读取到的内容绝不会被偷偷摸摸地修改。 例如: 在表中查找一个条目,你就会得到该条目下当前存储的元组。即便有人旋即将表中同一位置更新成了新的元组,你读到的数据也不会受到影响。 (复制了一份快照数据)
基本用法:
标准库中的ets模块可用于创建和操控ETS表。
- 创建新表: 可以调用ets:new(Name,Options) 函数。其中名称Name必须是原子,Options必须是列表。除非设置named_table选项,ets:new/2会返回一个表标识符,用于完成针对新创建的表的各种操作。
- 写入数据例如,以下代码将创建一张表并向表中写入两个元组:
T = ets:new(mytable,[]) ets:insert(T,{17,hello}), ets:insert(T,{42,goodbye})
ETS表和数据库的另一个相似点在于,它同样只存储数据行—也就是元组。存储任何Erlang
数据之前,都要先将之放入元组。其原因在于ETS会将元组中的一个字段用作表索引,默认采用第一个字段。(可以通过建表参数调整。)这样你便可以按第一列元素在表中查找数据行:
- 查询元素:
这将返回[{17,he11o}]。 !!! 查询结果是个列表类型ets:lookup(T,17)
使用递归代替循环
Erlang没有循环的概念,不过使用递归很容易实现循环 比如求0~N之间数字的和的函数
sum(0) -> 0;
sum(N) -> sum(N-1) + N .
尾递归 (递归调用层数越深越要考虑尾递归)
在递归方法后面做运算:如下
sum(N) -> sum(N-1) + N.
尾调用优化的意思是,当编译器识别出尾调用(函数返回前的最后一个任务)时,会生成一
段特殊的代码,这段代码会在执行尾调用之前从栈中扔掉当前调用的所有信息。此时当前调用基本无事可做,只需告知被调用的函数后续即将发生一次尾调用:“嘿!完事儿的时候直接把结果告诉我的调用者就行了,我收工了哦。”因此,尾调用不会导致栈的膨胀。(作为特例,调用相同函数的尾递归调用可以重用栈顶的调用信息,省得扔掉之后还要再重新创建。)本质上,尾调用只是“在必要时清理一些东西,然后执行跳转”。
正是基于这个原因,尾递归函数即便不停不歇地运行也不会将栈空间耗尽,同时还能达到和
while循环一样高的效率。
技术咨询与支持,可以扫描微信公众号进行回复咨询
Erlang10, Erlang语法6