Windows环境下安装Erlang
在http://www.erlang.org/downloads下载安装了Erlang之后,再配置path系统变量Erlang安装目录/bin
。之后在cmd命令行中,输入erl命令,即可进入Erlang shell环境。
基本概念
注释
Erlang里的注释从一个百分号字符(%)开始,一直延伸到行尾。Erlang没有块注释。
根据规范,与代码同处一行的注释以一个%开头,独占一行的注释以两个%%开头。
整数和浮点数
整数无大小限制,$符号加字符可以得到字符的编码数值,比如$a,可以得到a的编码数值。
在Erlang里浮点数都是双精度浮点数。
运算符
有两种除法运算,使用/
运算符返回浮点数,div
运算符返回整数,rem
运算符是求余,也可以使用math模块的运算函数。
任何类型之间都可以使用比较运算符进行比较,数值小于原子,元组小于列表,而原子既小于元组也小于列表。
小于或等于运算符写作=<
,大于或等于运算符写作>=
。
=:=
是完全相等运算符(值和类型都相同),=/=
是不完全相等运算符。如果想执行不考虑类型的比较,比如比较2和2.0,则需要使用==
运算符,其否定形式是/=
。
位串
<< >>内用逗号分隔整数序列,整数取值范围是0~255,也可以使用字符串代替整数。
变量和符号常量
Erlang是一种函数式语言,所以一旦定义了X = 123
,那么X永远就是123,不允许改变。如果再执行X = abc
,则会抛错,当可以重复执行X = 123
。等号只是模式匹配,而不是赋值操作,通过以下观察最后例子123=X
和12=X
,可以明白一些,这种机制有点类似于==操作,可以用来做测试使用。
1> X=123.
123
2> X=abc.
** exception error: no match of right hand side value abc
3> X=123.
123
4> Y=123.
123
5> X=Y.
123
6> K=abc.
abc
7> X=K.
** exception error: no match of right hand side value abc
8> 123=X.
123
9> 12=X.
** exception error: no match of right hand side value 123
没有可变变量将如何编程?在Erlang里怎样表达X = X + 1这类概念?Erlang的方式是创建一个名字未被使用过的新变量(比方说X1),然后编写X1 = X + 1。
请注意Erlang的变量以大写字母开头。所以X、This和A_long_name都是变量。以小写字母开头的名称(比如monday或friday)不是变量,而是符号常量,它们被称为原子(atom)。
原子
在Erlang里,原子被用于表示常量值。原子以小写字母开头,后接一串字母、数字、下划线(_)或at(@)符号。
原子还可以放在单引号(')内。可以用这种引号形式创建以大写字母开头(否则会被解释成变量)或包含字母数字以外字符的原子,例如'Monday'、'Tuesday'、'+'、'*'和'an atom with spaces'。
true和false这两个原子可以用于布尔运算。undefined表示未知量的占用符。
元组
元组把一些数量固定的项目归组成单一的实体,元祖这样定义:P = {joe, 1.82}
。
在Erlang中,=不是赋值语句,而是模式匹配操作符。如果想要提取P = {joe, 1.82}
里面的1.82这个值,需要执行{joe,X}=P
,这样1.82这个值就会赋给变量X。
列表
列表(list)被用来存放任意数量的事物。创建列表的方法是用中括号把列表元素括起来,并用逗号分隔它们。
列表的第一个元素被称为列表头(head),假设把列表头去掉,剩下的就被称为列表尾(tail)。访问列表头是一种非常高效的操作。
可以给列表T的开头添加不止一个元素,写法是[E1,E2,..,En|T]
。
和其他情况一样,我们可以用模式匹配操作来提取某个列表里的元素。如果有一个非空列表L,那么表达式[X|Y] = L
(X和Y都是未绑定变量)会提取列表头作为X,列表尾作为Y。
字符串
严格来说,Erlang里没有字符串。Name="Hello"
,Hello
其实只是一个列表的简写,这个列表包含了代表字符串里各个字符的整数字符代码。
当shell打印某个列表的值时,如果列表内的所有整数都代表可打印字符,它就会将其打印成字符串字面量。否则,打印成列表记法。不需要知道代表某个字符的是哪一个整数,可以把“美元符号语法”用于这个目的
1> [1,2,3].
[1,2,3]
2> [83,117,114,112,114,105,115,101].
"Surprise"
3> [$S,$u,$r,$p,$r,$i,$s,$e].
"Surprise"
模式匹配
模式 = 单元 结 果
{X,abc} = {123,abc} 成功:X = 123
{X,Y,Z} = {222,def,"cat"} 成功:X = 222,Y = def,Z = "cat"
{X,Y} = {333,ghi,"cat"} 失败:元组的形状不同
X = true 成功:X = true
{X,Y,X} = {{abc,12},42,{abc,12}} 成功:X = {abc,12},Y = 42
{X,Y,X} = {{abc,12},42,true} 失败:X不能既是{abc,12}又是true
[H|T] = [1,2,3,4,5] 成功:H = 1,T = [2,3,4,5]
[H|T] = "cat" 成功:H = 99,T = "at"
[A,B,C|T] = [a,b,c,d,e,f] 成功:A = a,B = b,C = c,T = [d,e,f]
模块和函数
模块
模块是Erlang的基本代码单元。模块保存在扩展名为.erl的文件里,而且必须先编译才能运行模块里的代码。编译后的模块以.beam作为扩展名。
创建模块文件,文件名是name.erl。
-module(name)
-export([FuncName1/N1,FuncName2/N2,...])
-module(name)
表明此文件是哪个模块,name
是文件名(除掉erl后缀)。-export([FuncName1/N1,FuncName2/N2,...])
指明了模块里哪些函数可以从模块外部进行调用,斜杠后面的N是数字,表示函数有多少个参数。未从模块里导出的函数只能在模块内调用。
函数
Erlang是一种函数式编程语言。函数式编程语言表示函数可以被用作其他函数的参数,也可以返回函数。操作其他函数的函数被称为高阶函数(higher-order function),而在Erlang中用于代表函数的数据类型被称为fun。
-module(my_module).
-export([pie/0]).
pie() ->
3.14.
以-
开头的语句属于声明,-module
声明指定了模块的名字,必须和文件名一致。-export
声明指定了模块内哪些函数是外部可以调用的,pie/0
后面的0的函数参数数量。
pie() -> 3.14.
是函数定义,函数首部(函数名和参数列表)和函数体使用->
分隔,函数的返回值不需要return关键字,返回值就是函数体中表达式的值,但要注意函数定义的末尾必须加上句号.
。
-module(geometry).
-export([area/1]).
area({rectangle,Width,Height}) -> Width*Height;#第一个子句
area({squre,Side}) -> Side*Side.#第二个字句
area函数有两个子句。这些子句由一个分号隔开,最后的子句以句号加空白结束。每条子句都有一个头部和一个主体,两者用箭头(->)分隔。头部包含一个函数名,后接零个或更多个模式,主体则包含一列表达式,它们会在头部里的模式与调用参数成功匹配时执行。这些子句会根据它们在函数定义里出现的顺序进行匹配。
1> c(geometry).
{ok,geometry}
2> geormetry:area({rectangle,10,5}).
50
3> geormetry:area({square,3}).
9
内置函数和标准库
erlang模块
包含了整个Erlang系统最核心的函数,io模块
处理文本输入输出,dict模块
提供了基于散列的关联数组(字典),array模块
提供了可扩展的、带整数索引的数组。erlang模块中的有些函数会被自动导入,比如self()
函数其实erlang:self()
,因为它已经被自动导入,所以可以省略erlang:
。
标点符号使用
- 逗号(,)分隔函数调用、数据构造和模式中的参数
- 分号(;)分隔子句。我们能在很多地方看到子句,例如函数定义,以及case、if、try..catch和receive表达式
- 句号(.)(后接空白)分隔函数整体,以及shell里的表达式
有一种简单的方法可以记住这些:想想英语。句号分隔句子,分号分隔子句,逗号则分隔下级子句。逗号象征短程,分号象征中程,句号则象征长程。
编译
在erl文件目录下进入shell,执行c(模块名)
即可对模块名.erl
进行编译,编译完成后会产生.beam
文件,之后在shell里就可以通过模块名:方法名()
调用模块里的函数。
在Erlang里有一些用在Shell的函数,格式是name(args)。
case表达式
case Expression of
Pattern1 [when Guard1] -> Expr_seq1;
Pattern2 [when Guard2] -> Expr_seq2;
...
end
case的执行过程如下:首先,Expression被执行,假设它的值为Value。随后,Value轮流与Pattern1(带有可选的关卡Guard1)、Pattern2等模式进行匹配,直到匹配成功。一旦发现匹配,相应的表达式序列就会执行,而表达式序列执行的结果就是case表达式的值。如果所有模式都不匹配,就会发生异常错误(exception)。
if表达式
if
Guard1 ->
Expr_seq1;
Guard2 ->
Expr_seq2;
...
end
它的执行过程如下:首先执行Guard1。如果得到的值为true,那么if的值就是执行表达式序列Expr_seq1所得到的值。如果Guard1不成功,就会执行Guard2,以此类推,直到某个关卡成功为止。if表达式必须至少有一个关卡的执行结果为true,否则就会发生异常错误。
记录与映射组
记录
对于小型元组而言,记住各个元素代表什么几乎不成问题,但当元组包含大量元素时,给各个元素命名就更方便了。一旦命名了这些元素,就可以通过名称来指向它们,而不必记住它们在元组里的具体位置。记录其实就是元组。
-record(Name,{
%%以下两个键带有默认值
key1=Default1,
key2=Default2,
...
%%下一行就相当于key3=undefined
key3,
...
}
在之前的例子里,Name是记录名。key1、key2这些是记录所含各个字段的名称,它们必须是原子。记录里的每个字段都可以带一个默认值,如果创建记录时没有指定某个字段的值,就会使用默认值。
记录的定义既可以保存在Erlang源代码文件里,也可以由扩展名为.hrl
的文件保存,然后包含在Erlang源代码文件里。
-record{todo,{status=reminder,who=joe,text}}.
%% 创建记录
1> X1 = #todo{status=urgent,text="Fix errate in book"}.
2> X2 = X1#todo{status=done}.
%% 提取记录
3> #todo{who=W,text=Txt}=X2.
4> W.
5> Txt.
6> X2%todo.text.
映射组:关联式键-值存储
#{ Key1 Op Val1,Key2 Op Val2,...KeyN Op ValN }
NewMap = OldMap#{ Key1 Op Val1,...KeyN Op ValN }
Op是=>或:=这两个符号的其中一个。
表达式K => V
有两种用途,一种是将现有键K的值更新为新值V,另一种是给映射组添加一个全新的K-V对。这个操作总是成功的。
表达式K := V
的作用是将现有键K的值更新为新值V。如果被更新的映射组不包含键K,这个操作就会失败。
键和值可以是任何有效的Erlang数据类型。
异常处理
Erlang有两种方法来捕捉异常错误。第一种是把抛出异常错误的调用函数封装在一个try...catch表达式里,另一种是把调用封装在一个catch表达式里。
try FucOrExpressionSeq of
Pattern1 [when Guard1] -> Expression1;
Pattern2 [when Guard2] -> Expression2;
...
catch
ExceptionType1: ExPattern1 [when Guard1] -> ExExpression1;
ExceptionType2: ExPattern2 [when Guard2] -> ExExpression2;
...
after
AfterExpressions
end
try...catch就像是case表达式的强化版。它基本上就是case表达式加上最后的catch和after区块。
try...catch的工作方式如下:首先执行FuncOrExpessionSeq。如果执行过程没有抛出异常错误,那么函数的返回值就会与Pattern1(以及可选的关卡Guard1)、Pattern2等模式进行匹配,直到匹配成功。如果能匹配,那么整个try...catch的值就通过执行匹配模式之后的表达式序列得出。
如果FuncOrExpressionSeq在执行中抛出了异常错误,那么ExPattern1等捕捉模式就会与它进行匹配,找出应该执行哪一段表达式序列。ExceptionType是一个原子(throw、exit和error其中之一),它告诉我们异常错误是如何生成的。如果省略了ExceptionType,就会使用默认值throw。
指令
宏定义
-define(PI,3.14).
circumference(Radius) -> Redius * 2 * ?PI.
引用宏定义必须以问号?开头,在正式编译之前,这段代码会被展开成
circumference(Radius) -> Redius * 2 * 3.14).
常见的预定义宏:MODULE
宏,它展开后当前正在编译的模块的名称,还有FILE
和LINE
宏表示当前处于哪个源文件的哪一行。
文件包含
-include("filename.hrl").
预处理器会读取被包含文件的内容并将之插入到include指令所处的位置,这类文件一般只有声明,没有函数,一般出现在模块源文件的头部,所以也叫头文件,按照惯例,Erlang头文件以.hrl
为扩展名。
-include.
指令会在当前目录以及列于包含路径内的目录查找名为finename.hrl的文件。如果要包含其他Erlang应用或库的头文件,需要使用-includelib(应用名/include/file.erl).
指令,因为应用名一般带有版本号(比如应用名-0.0.1),使用-includelib
指令可以省略应用名后的版本号,防止因为其他应用升级造成找不到文件。
-includelib
会相对于Erlang系统现有应用(尤其是所有随Erlang一并发布的标准库)的安装路径来查找文件。
条件编译
-ifdef(MacroName).
xxx %如果定义了名字为MacroName的宏,就编译这里的代码
-ifndef(MacroName). %如果没定义了名字为MacroName的宏,就编译这里的代码
-else.
-endif.
并发
Erlang的并发是基于进程(process)的。进程是一些独立的小型虚拟机,可以执行Erlang函数。
三个新的基本函数:spawn
、send
和receive
。spawn
创建一个并行进程,send
向某个进程发送消息,receive
则是接收消息。
Erlang进程意外退出时,会产生一个退出信号,所有与濒死进程链接
的进程都会收到这个信号。默认情况下,接收方会一并退出并将信号传播给与它链接的其他进程,直到所有直接或间接链接在一起的进程系统退出为止。
可以改写退出信号默认的传播行为,通过设置trap_exit进程标记,可以令进程不再服从外来的退出信号,而是将其捕捉,这种情况下,进程收到信号后,会先将其转为一条格式为{'EXIT',pid,Reason}的消息,该消息描述了哪个进程处于什么原因而发生故障。这类会捕捉信号的进程成为系统进程,可用于汇报故障,以及重启故障的子系统,这类进程也称为监督者。
OTP
使用OTP时,需要实现行为模式(类似于java里面的即接口),每种行为模式都有自己需要实现的函数和调用规范,比如gen_server
行为模式,实现模块里面需要实现init/1、handle_call/3、handle_cast/2、handle_info/2、terminate/2、code_change/3
回调函数。
%% 最精简的gen_server行为模式实现模块
-module(...).
-behaviour(gen_server).% 行为模式实现的名称
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
terminate/2, code_change/3]).
-record(state,{}).
init([]) ->
{ok,#state{}}
handle_call(_Request,_Form,State) ->
Reply=ok,
{reply,Reply,State}.
handle_cast(_Msg,State) ->
{noreply,State}.
handle_info(_Info,State) ->
{noreply,State}.
terminate(_Reason,State) ->
ok.
code_change(_OldVsn,State,_Extra) ->
{ok,State}.
实现了行为模式后,还需要一个容器(功能类似于J2EE里面的容器,比如Tomcat)。容器是一个进程,它执行的是某个库模块中的代码,并且会调用与行为模式实现相对应的回调模块来处理应用相关的逻辑,该库模块的名称与对应的行为模式的名称一致。
库模块包含了行为模式中的通用代码,其中包括新容器的启动函数,比如对于gen_server
行为模式而言,这部分代码就位于Erlang/OTP库中stdlib部分的gen_server模块中。当你调用gen_server:start(... , foo , ...)
时,就会创建一个新的以foo为回调模块的gen_server容器。我们将新容器进程的启动称作行为模式的实例化。
行为模式实现模块的典型布局
首部
以%%%
开头表示是文件注释,@开头是EDoc标注,EDoc可以直接通过源码标注生成文档(与JavaDoc类似),是随标准Erlang/OTP发行版一同发布的文档生成工具。
%%%-------------------------------------------------------------------
%%% @author Martin & Eric <erlware-dev@googlegroups.com>
%%% [http://www.erlware.org]
%%% @copyright 2008-2010 Erlware
%%% @doc RPC over TCP server. This module defines a server process that
%%% listens for incoming TCP connections and allows the user to
%%% execute RPC commands via that TCP stream.
%%% @end
%%%-------------------------------------------------------------------
-module(...).
指令,与文件名对应-behaviour(...)
指令,它告知编译器这个模块是哪个行为模式的一个实现,之后必须实现该行为模式的所有函数,否则编译时会报错。export()
指令,通常要写两个,一个是自己的API,一个是行为模式接口要求导出的函数。接着就是一些声明和预处理定义。
API
API一般只是对库模块函数进行一个封装。使用API可以隐藏底层使用的行为模式,使用者不需要调用库模块函数来启动容器,只需要调用API即可。
%% 调用start_link(Port)时,会启动一个容器,进而再调用init方法
start_link(Port) ->
gen_server:start_link({local, ?SERVER}, ?MODULE, [Port], []).
init([Port]) ->
{ok, LSock} = gen_tcp:listen(Port, [{active, true}]),
{ok, #state{port = Port, lsock = LSock}, 0}.
行为模式接口
行为模式所需的回调函数。
内部函数
API和行为模式接口所需的辅助函数。