一:apply
apply是一个内置函数,它是通过函数名和参数计算该函数的值,其中的函数名和模块名是动态计算得出的。内置函数apply(Mod, Func, [Arg1, Arg2, ..., ArgN])会将模块Mod里的Func函数应用到Arg1, Arg2, ... ArgN这些参数上,它相当于:
Mod:Func(Arg1,Arg2,Arg3,...,ArgN).
%%其中上述中Mod是模块,Func是该模块的函数,Arg是参数
所有的
Erlang
内置函数也可以通过
apply
进行调用,方法是假定它们都属于
erlang
模块。因
此,要构建一个对内置函数的动态调用,可以编写以下代码:
1> apply(erlang,atom_to_list,[hello]).
"hello"
%%这其中就和apply(Mod, Func, [Arg1, Arg2, ..., ArgN])这个一样,erlang
相当于Mod是模块,atom_to_list相当于Func是函数,hello是参数。
注:应当尽量避免使用apply。当函数的参数数量能预先知道时,M:F(Arg1, Arg2, ... ArgN) 这种调用形式要比apply好得多。如果使用apply对函数进行调用,许多分析工具就无法 得知发生了什么,一些特定的编译器优化也不能进行。所以,尽量少用apply,除非绝对有必要。
二:算术表达式
任何类型的值的计算是表达式,
下面的表格展示了所有可用的算术表达式。每种算术操作都有1
或
2个参数(X,Y),这些参数在表格里显示为“整数”或“数字”(数字的意思是此参数可以是整数或浮点数)
操作符 | 描述 | 参数类型 | 优先级 |
---|---|---|---|
+ X | 正数 | 数字 | 1 |
- X | 负数 | 数字 | 1 |
X * Y | 进行乘法运算 | 数字 | 2 |
X / Y | 进行除法运算(一般结果是浮点数) | 数字 | 2 |
bont X | 对X执行按位取反(bitwise not) | 整数 | 2 |
X div Y | X被Y整除 | 整数 | 2 |
X rem Y | X除以Y的整数的余数(取余) | 整数 | 2 |
X band Y | 对X和Y执行按位与(bitwise and) | 整数 | 2 |
X + Y | 进行加法运算 | 数字 | 3 |
X - Y | 进行减法运算 | 数字 | 3 |
X bor Y | 对X和Y执行按位或(bitwise or) | 整数 | 3 |
X bxor Y | 对X和Y执行按位异或(bitwise xor) | 整数 | 3 |
X bsl N | 把X向左算术位移(arithmetic bitshift)N位 | 整数 | 3 |
X bsr N | 把X向右算法位移N位 | 整数 | 3 |
这些操作符相互之间根据
优先级
结合。一个复杂算术表达式的求值顺序由所含操作符的优先
级而定:所有优先级为
1
的操作符会首先求值,然后轮到所有优先级为
2
的操作符,以此类推。
可以用括号来改变默认的求值顺序:括号内的表达式会首先求值。优先级相同的操作符遵循
向左结合的规则,从左往右分别求值。
三:元数
一个函数的元数(arity
)是该函数所拥有的参数数量。在
Erlang
里,同一模块里的两个名称
相同、元数不同的函数是
完全
不同的函数。除了碰巧使用同一个名称外,它们之间
毫不相关
。
根据惯例,
Erlang
程序员经常将名称相同、元数不同的函数作为辅助函数使用。这里有一个
例子:
sum(L) -> sum(L,0),
sum([],N) -> N;
sum([H|T],N) -> sum(T,H + N).
%% 在这段函数中sum(L)函数累加列表L里的所有元素。它用到一个名为sum/2的辅助函数,但也可以是其他
%% 任何名称。即便把辅助函数命名为hedgehog/2(刺猬),程序的意思也不会有任何变化。不过,
%% sum/2是更好的命名选择,因为它提示程序的读者这是什么,而且还不必发明一个新名称(这总
%% 是很困难的)。
%% 我们经常会通过不导出辅助函数来“隐藏”它们。所以,定义sum(L)的模块只会导出sum/1,
%% 而不会导出sum/2。
四:属性
模块属性的语法是
-AtomTag(...)
,它们被用来定义文件的某些属性。(
注意:-record(...)
和-include(...)有着类似的语法,但是不算模块属性。
)模块属性有两种类型:预定义型和用
户定义型。
1.预定义型:
下列模块属性有着预先定义的含义,必须放置在任何函数定义之前。
(1) -module(modname).
%% 这是模块声明。modname必须是一个原子。此属性必须是文件里的第一个属性。按照惯例,
%% modname的代码应当保存在名为modname.erl的文件里。如果不这么做,自动代码加载就
%% 不能正常工作。
(2)-import(Mod,[Name1/Arity1,Name2/Arity2,...]).
%% import声明列举了哪些函数需要导入到模块中。上面这个声明的意思是要从Mod模块导入
%% 参数为Arity1的Name1函数,参数为Arity2的Name2函数,等等。
%% 一旦从别的模块里导入了某个函数,调用它的时候就无需指定模块名了。
(3)-export([Name1/Arity1,Name2/Arity2,...]).
%% 导出当前模块里的Name1/Arity1和Name2/Arity2等函数。函数只有被导出后才能在模块
%% 之外调用。
(4)-compile(Options).
%% 添加Options到编译器选项列表中。Options可以是单个编译器选项,也可以是一个编译
%% 器选项列表.
(5)-vsn(Version).
%% 指定模块的版本号。Version可以是任何字面数据类型。Version的值没有什么特别的语
%% 法或含义,但可以用于分析程序或者作为说明文档使用。
注:-compile(export_all).这个编译器选项经常会在调试程序时用到。它会导出模块里的所有函数,无需再显式使用-export标识了。
2.用户定义型:
-SomeTag(Value).
%% SomeTag必须是一个原子,而Value必须是一个字面数据类型。模块属性的值会被编译进模
%% 块,可以在运行时提取。
列如:
%% attrs.erl
-module(attrs).
-vsn(1234).
%% 这两个就是-SomeTag(Value)
-author({joe,armstrong}).
-purpose("example of attributes").
-export([fac/1]).
fac(1)->1:
fac(N)-N fac(N-1).
可以用下面的方式提取这些值:
1> attrs:module_info().
[{exports,[{fac,1},{module_info,0},{module_info,1}]},
{imports,[]}
{alLribules,[{vsn,[1234]},
{author,[{joe,armstrong}]},
{purpose,"example of attributes"}]},
{compile,[{options,[]}
{version,"4.8"},
{time,{2013,5,3,7,36,55},
{source,"/Users/joe/jaerlang2/code/attrs.erl"}]}]
源代码文件所含的用户定义属性再一次出现了,它们表现为
{attributes, ...}
的下属数据类型。元组{compile, ...}
包含了编译器添加的信息。
{version,"4.8"}
这个值是编译器的版本号,不应与模块属性里定义的vsn
标签相混淆。在上面的例子里,
attrs:module_info()
返回一个属性列表,内含所有与被编译模块相关的元数据。attrs:module_info(X)
(
X
可以是 exports、
imports
、
attributes
和
compile
中的一个)会返回与模块相关的单个属性。请注意,函数module_info/0
和
module_info/1
会在模块编译时自动创建。
要运行
attrs:module_info
,必须先把
attrs
模块的
beam
代码加载到
Erlang
虚拟机里。也可
以使用
beam_lib
模块来提取同样的信息,这样就
不必
载入
attrs
模块了,如:
2> beam lib:chunks("attrs.beam",[attributes]).
[ok,{attrs,[{attributes,[{author,[{joe,armstrong}]},
{purpose,"example of attributes"},
{vsn,[1234]}]}]}
%% beam_lib:chunks可以在不载入模块代码的情况下提取模块里的属性数据。
五:块表达式
块表达式用于以下情形:代码某处的
Erlang
语法要求单个表达式,但我们想使用一个表达式
序列。举个例子,在一个形式为
[E || ...]
的列表推导中,语法要求
E
是单个表达式,但我们也
许想要在
E
里做不止一件事情。
begin
Expr1,
...,
ExprN
end
你可以用块表达式归组一个表达式序列,就像子句的主体一样。begin ... end的值就是块
里最后那个表达式的值(ExprN)。
六:布尔值
Erlang
没有单独的布尔值类型。不过原子
true
和
false
具有特殊的含义,可以用来表示布尔值。
有时候编写的函数会返回两个可能的原子值中的一个。这时,正确的做法是确保它们返回一
个布尔值。与此同时,让你的函数名称反映出它们会返回布尔值也是一个好主意。
假设有一个文件列表
L
并想把它分成一个打开文件列表和一个关闭文件列表。可
以编写以下代码来利用标准库:
lists:partition(fun is file open/1,L)
%% 但如果用的是fi1 e_state/1函数,恐怕就要先编写一个转换程序才能调用库方法了。
lists:partition(fun (X)->
case file_state(X)of
open -> true;
closed -> false
end,L).
七:布尔表达式
可用的布尔表达式有四种。
(1)
not B1
:逻辑非
(2)
B1 and B2
:逻辑与
(3) B1 or B2
:逻辑或
(4)
B1 xor B2
:逻辑异或
在所有这些表达式里,
B1
和
B2
都必须是布尔值或者执行结果为布尔值的表达式。这里有一
些例子:
1>not true.
false
2>true a
and false.
false
3>true or false.
true
4>(2>1)0r(3>4).
true
八:字符集
从
Erlang
的
R16B
版开始,
Erlang
源代码文件都假定采用
UTF-8
字符集编码。在这之前用的是
ISO-8859-1
(
Latin-1
)字符集。这就意味着所有
UTF-8
可打印字符都能在源代码文件里使用,无
需使用任何转义序列。
Erlang
内部没有字符数据类型。字符串其实并不存在,而是由整数列表来表示。用整数列表
表示
Unicode
字符串是毫无问题的。
九:注释
Erlang
里的注释从一个百分号字符(
%
)开始,一直延伸到行尾。
Erlang
没有块注释。
注: 在代码示例里经常出现两个百分号字符(%%)。双百分号标记能被Erlang模式的Emacs
编辑器识别,并启动注释行自动缩进功能。
十:动态代码载入
动态代码载入是内建于
Erlang
核心的最惊人特性之一。它的美妙之处在于你无需了解后台的
运作就能顺利实现它。
它的思路很简单:每当调用
someModule:someFunction(...)
时,调用的总是最新版模块
里的最新版函数,
哪怕当代码在模块里运行时重新编译了该模块也是如此
。
如果在
a
循环调用
b
时重新编译了
b
,那么下一次
a
调用
b
时就会自动调用新版的
b
。如果有许
多不同进程正在运行而它们都调用了
b
,那么当
b
被重新编译后,所有这些进程就都会调用新版的
b
。为了了解它的工作原理,我们将编写两个小模块:
a
和
b
。
b
模块非常简单。
b.erl
-module(b).
-export([x/0]).
×() -> 1.
a.erl
-module(a)
-compile(export_all).
start(Tag)->
spawn (fun()-loop(Tag)end).
loop(Tag)->
sleep(),
Val b:x(),
io:format("Vsn1 (-p)b:x()=-p-n",[Tag,Val]),
loop(Tag).
sleep()->
receive
after 3000->true
end.
现在编译a和b,启动两个a进程:
1> c(b),
Hok,b}
2> c(a).
{ok,a}
3> a:start(one).
<0.41.0>
Vsnl (one)b:x() = 1
4> a:start(two).
<0.43.0>
Vsnl (one)b:x() = 1
Vsnl (two)b:x() = 1
Vsn1 (one)b:x() = 1
Vsn1 (two)b:x() = 1
这些
a
进程休眠
3
秒钟后唤醒并调用
b:x()
,然后打印出结果。现在进入编辑器,把模块
b
改
成下面这样:
%% 修改b.erl
-module(b)
-export([x/0]).
x() -> 2.
然后在
shell
里重新编译
b
。就会发生:
5> c(b).
{ok,b}
Vsnl (one)b:x() = 2
Vsn1 (two)b:x() = 2
Vsnl (one)b:x() = 2
Vsn1 (two)b:x() = 2
...
两个原版的
a
仍然在运行,但现在它们调用了
新版
的
b
。所以,在模块
a
里调用
b:x()
时,实
际上是在调用“
b
的最新版”。我们可以随心所欲地多次修改并重新编译
b
,而所有调用它的模块
无需特别处理就会自动调用新版的
b
。
如果重新编译a,则会发生原启动的a还运行原来的,而新启动的a会运行新的:
%% 这里修改a.erl
-module(a).
-compile(export_all).
start(Tag)->
spawn(fun()->loop(Tag)end).
loop(Tag)->
sleep(),
Val b:x(),
io:format("Vsn2 (-p)b:x()=-p-n",[Tag,Val]),
loop(Tag).
sleep()->
receive
after 3000 -> true
end.
编译a,并启动第三个a:
6>c(a).
ok,a}
Vsn1 (one)b:x() = 2
Vsn1 (two)b:x() = 2
7>a:start(three).
<0.53.0>
Vsn1 (one)b:x() = 2
Vsn1 (two)b:x() = 2
Vsn2 (three)b:x() = 2
Vsnl (one)b:x() = 2
Vsn1 (two)b:×() = 2
Vsn2 (three)b:x() = 2
启动新版的
a
后,我们看到了新版正在运行。但是,那些运行最初版
a
的现有进程仍然在正常地运行旧版的
a
。
注:当第三次修改a后并编译,则会第一次修改的a会自动终止,Erlang允许一个模块的两个版本同时运行:当前版和旧版。
十一:Erlang 的预处理器
Erlang
模块在编译前会自动由
Erlang
的预处理器进行处理。预处理器会展开源文件里所有的
宏,并插入必要的包含文件。
通常情况下,无需查看预处理器的输出,但在特定情形下(比如调试某个有问题的宏时),
应该保存预处理器的输出。要查看
some_module.erl
模块的预处理结果,可以在操作系统的
shell
里输入以下命令。
$ erlc -P some_module.erl
这会生成一个名为
some_module.P
的清单文件。
十二:转义序列
可以在字符串和带引号的原子里使用转义序列来输入任何不可打印的字符。表
4
列出了所有
可用的转义序列。
让我们在
shell
里举一些例子来展示这些约定方式是如何工作的。(注意:格式字符串里的
~w
是指忠实地打印列表,而不对输出结果进行美化。)如:
%% 控制字符
1> io:format("~w~n",["\b\d\e\f\n\r\s\t\v"]).
[8,127,27,12,10,13,32,9,11]
ok
%% 字符串里的八进制字符
2> io:format("-w-n",["\123\12\1"]).
[83,10,1]
ok
%% 字符串里的引号和反斜杠
3> io:format("-w-n",["\'\"\\"]).
[39,34,92]
ok
%% 字符编码
4> io:format("~w-n",["\a\z\A\Z"]).
[97,122,65,90]
ok
下面是一个转义序列表:
转义序列 | 含义 | 整数编码 |
---|---|---|
\b | 退格符 | 8 |
\d | 删除符 | 127 |
\e | 换码符 | 27 |
\f | 换页符 | 12 |
\n | 换行符 | 10 |
\r | 回车符 | 13 |
\s | 空格符 | 32 |
\t | 制表符 | 9 |
\v | 垂直制表符 | 11 |
\x{...} | 十六进制字符(是十六进制字符) | |
\a..\Z或\A..\Z | Crl+A至Ctrl+Z | 1至26 |
\' | 单引号 | 39 |
\" | 双引号 | 34 |
\\ | 反斜杠 | 92 |
\C | C的ASCⅡ编码(C是一个字符) | (一个整数) |
十三:表达式和表达式序列
在
Erlang
里,任何可以执行并生成一个值的事物都被称为
表达式
(
expression
)。这就意味着catch、
if
和
try...catch
这些都是表达式。而记录声明和模块属性这些不能被求值,所以它们不是表达式。
表达式序列(expression sequence
)是一系列由逗号分隔的表达式。它们在
->
箭头之后随处
可见。表达式序列
E1, E2,..., En
的值被定义为序列最后那个表达式的值,而该表达式在计算
时可以使用
E1, E2
等表达式所创建的绑定。它就等价于
LISP
里的
progn
。
十四:函数引用
我们有时想引用在当前或外部模块里定义的某个函数,可以用下列标记实现。
(1) fun LocalFunc/Arity
%% 用于引用当前模块里参数为Arity的本地函数LocalFunc。
(2) fun Mod:RemoteFunc/Arity
%% 用于引用Mod模块里参数为Arity的外部函数RemoteFunc。
具体实例:
-module(x1).
-export([square/1]).
square(X) -> XX.
double(L) -> lists:map(fun square/1,L).
%% 这square指的就是内部函数,lists:map指的就是外部函数
注:包含模块名的函数引用提供了动态代码升级的切换点。
十五:包含文件
包含文件的语法为:
(1) -include(Filename).
%% 按照Erlang的惯例,包含文件的扩展名是.hrl。FileName应当包含一个绝对或相对路径,
%% 使预处理器能找到正确的文件。
%% 包含库的头文件(library header file)时可以用下面的语法:
(2) -include_lib(Name).
实例为:
-include_lib("kernel/include/file.hrl").
%% 在这种情况下,Erlang编译器会找到正确的包含文件。(例子中的kernel是指定义该头
%% 文件的应用。)
包含文件里经常会有记录的定义。如果许多模块需要共享通用的记录定义,就会把它们放到
包含文件里,再由所有需要这些定义的模块包含此文件。
十六:列表操作:++和--
++
和
--
是用于列表添加和移除的中缀操作符。
A ++ B
使
A
和
B
相加(也就是附加)。
A -- B
从列表
A
中移除列表
B
。移除的意思是
B
中所有元素都会从
A
里面去除。请注意:如果
符号
X
在
B
里只出现了
K
次,那么
A
只会移除前
K
个
X
。
列如:
1> [1,2,3]++[4,5,6].
[1,2,3,4,5,6]
2> [a,b,c,1,d,e,1,x,y,1] -- [1].
[a,b,c,d,e,1,x,y,1]
3> [a,b,c,1,d,e,1,×,y,1] -- [1,1].
[a,b,c,d,e,x,y,1]
4> [a,b,c,1,d,e,1,x,y,1] -- [1,1,1].
[a,b,c,d,e,x,y]
5> [a,b,c,1,d,e,1,X,y,1] -- [1,1,1,1].
[a,b,c,d,e,x,y]
++
也可以用在模式里。在匹配字符串时,如:
f("begin" ++ T) -> ...
f("end" ++ T) -> ...
...
子句
1
里的模式会扩展成
[$b,$e,$g,$i,$n|T]
。