Erlang 快速入门
Erlang 编程语言
2015-08-21
在很多语言中,我们都能看到函数式范型、动态类型、类型系统的身影,而 Erlang 在此基础上发展出了一些自成一派的特点:
- 轻量级进程。Erlang 的进程既不是操作系统层面的进程,也不是线程,而是由 Erlang 虚拟机进行管理调度的无状态的进程。建立一个进程的成本非常低,在博主机器上(2.4 GHz Intel Core i5,8 GB 1600 MHz DDR3)平均只需要 2~4 us,而建立 80 万个进程也只花费了几秒的时间,并且据官方说法,建立一个进程的内存占用不到 400 字。
- 消息原语。Erlang 直接在语言层面支持进程间通讯,并且在内部对并发进行了同步处理,用户不需要再关心多进程并发会不会发生同步问题,大大降低了在进程间传递消息的难度。例如,一句话就能搞定发消息:Pid ! Message.,收消息也就多个匹配和处理的过程:receive Message -> handle(Message) end.。
- 快速失败。如果发生了运行时错误,发生错误的进程会立刻停止执行,并借由消息机制传递错误,使其他进程能够帮助处理错误,或者干脆重启发生错误的进程。
- 代码热更新。如果是其它语言,当代码发生变动需要重新部署时,比如紧急修复了一个 bug,你需要停止正在运行的服务器,编译后再重新运行新程序。但是使用 Erlang,你不需要停机,不需要停机,只需要利用消息机制通知服务器,更换模块即可,Erlang 虚拟机会自动加载新模块,达到“热更新”。
- 分布式。Erlang 为分布式集群的实现提供了相当易用的函数,在集群的不同节点间通信与进程间通信的实现大同小异。如果熟悉了 Erlang 的消息机制,那么使用 Erlang 实现简单的分布式集群的学习成本近乎于零。
Erlang 的以上特性让它特别适合于实现高可靠、高性能的服务器。然而 Erlang 并不是一门新手友好的语言,且不说它属于函数式范型,光是它那受 Prolog 影响颇深的语法,也让人望而生畏。所以本文虽是快速入门,但也需要读者有一定的功力,熟悉至少一门静态类型语言和动态类型语言。否则,强行修炼,必将走火入魔。本文中涉及到的源代码可以在 Gist 获取。
环境
安装
1 2 3 4 |
# Linux sudo apt-get install erlang # OSX brew install erlang |
---|---|
官方的 Erlang Shell 是不带语法高亮的,如果需要高亮,可以安装 kjell。
运行
1 2 3 4 5 |
erl #查看erl 手册 erl-man erl #查看 lists 模块手册erl-man lists |
---|---|
在 erl 中按下 CTRL+G 中断正在运行的程序,如果没有运行的程序,那么再输入 q 退出 erl(连按两次 CTRL+C 也能退出),或者输入 h 查看可使用的命令。
1 2 3 4 5 6 7 8 9 10 11 |
1> User switch command —> h c [nn] - connect to job i [nn] - interrupt job k [nn] - kill job j - list alljobs s [shell] - start local shell r [node [shell]] - start remote shell q - quit erlang ? | h - this message |
---|---|
为了与命令行的 shell 相区分,下文使用 erl 代指 Erlang Shell。
运行环境差异
语法
注释
1 2 3 |
% % 在 Erlang 中表单行注释% Erlang 没有多行注释 %% 用几个 % 来注释只是风格问题 |
---|---|
变量
1 2 3 4 5 6 7 8 |
% 变量必须以大写字母或下划线开头 % 英文句号表示语句的结束 Num = 42. % 变量只有绑定和未绑定两种状态 % 变量在第一次匹配时被绑定 % 已被绑定的变量不能再改变它的值 % Num = 1. |
---|---|
模式匹配
1 2 3 4 5 |
% = 并不是赋值,而是模式匹配1 = 1. % 模式匹配的意思是:先计算右边的值,再将结果与左边进行匹配 Answer = 42. 42 = Answer. |
---|---|
数据类型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
% 浮点数 Pi = 3.14. % Atom 以小写字母开始(所以变量必须以大写字母开始),可以由字母、数字、 _ 或 @ 组成Bar =for_example@bar. % 被单引号括起来的也是atom Foo =’?!2333...’. % true 和`false只是约定用来做布尔运算的 atom<br />true andfa**l**s**e.**<br />% 被花括号括起来的是**元**组<br />Point= {po**i**n**t,**1, 2}.<br />% 被中括号括起来的是**列**表<br />Nums = [1,**2**,** 3**].<br />% 被双引号括起来的是**字**符**串**<br />Word = "Hi"**.**<br />% 字符串实际上是由整**数**组**成的**列表<br />[72, 105]="**Hi**".<br />% 字符以 $`开头,表示该字符对应的数字$\n. % 10 $a. % 97 |
---|---|
详见官方文档。
布尔运算
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
% and 类似于函数调用 and(ExprA, ExprB) false and (ok == io:format(“hi “)). % hi false % andalso 和其他语言一样,具有短路性质。当 ExprA== fal**se 时,不计算 `ExprB`false andalso (ok == io:format(“hi “)). % false % 不具有短路性质的逻辑或 true or (ok == io:format(“hi “)). % hi true % 具有短路性质的逻辑或 true orelse (ok == io:format(“hi “)). % tru**e % 逻辑非 not true. % 值相等吗? 1 == 1.0. % 值相等且类型一致吗? 1 =:= 1.0. % 值不相等吗? 1 /= 1. % 值不相等或类型不相等吗? 1 =/= 1.0. % 大于 1 >= 0. % 不是 ‘<=’ 哦! 1 =< 2. |
---|---|
数学运算
1 2 3 4 5 |
5/2. % 2.5 5div 2. % 2 5rem 2. % 1 %语法 Base#Valu**e** 用于表示其他进制的数(2<=Base<=36)2#101010 = 8#052= 16#2A. |
---|---|
元组匹配
1 2 |
% _ 是匿名变量,用来匹配任何值{, , Y} = Point. |
---|---|
列表操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
List= [1, 2, 3]. % [Head**|**Ta**i**l]= List 匹配列表的头和尾[Head|Tail]= List. % Head = 1, Tail = [2, 3]. hd(List)=:= Head. tl(List)=:= Tail. % [Head**|**Ta**i**l]** 还可以用来组成新列表ListPlus= [0|List]. % [0, 1, 2, 3] % 合并成新列表 [1, 2] ++ [3, 4]. % [1, 2, 3, 4] % 剔除列表元素 [2, 4, 2] -- [2]. % [4, 2] % 表达式`[F(X) || X<- L]` 产生了一个新列表 % 新列表的每个元素由列表 L 中的每个元素进行运算 F(X) 得到 Double =[2*X || X <- List]. % [2, 4,6] % 满足 X **r**em2 **=**= 0 的元素才计算 2*X DoubleEven = [2*X|| X <- List, X rem 2 == 0]. % [4] % 计算与原点的距离 Points =[{1, 1},{5, 12}, {3, 4}]. [math:sqrt(X*X+ YY) || {X, Y} <- Points]. % [1.4142135623730951,13.0,5.0] % 计算笛卡尔积 [{X,Y} || X <- [1, 2], Y <- [3, 4]]. % [{1,3},{1,4},{2,3},{2,4}] % 筛选 [Y ||{3, Y} <*- Points]. % [4] |
---|---|
比特语法
1 2 3 4 5 6 7 8 9 |
% 被 << 和 >> 括起来的值会被转换为二进制数据Color = <<16#010203:24>>. <> = Color. <<”hello, world”>>. % binaries 的遍历操作和列表类似 % 只不过是使用 <= 而不是 <- (所以小于等于采用=< )[X || <> <= <<1,2,3,4>>]. % [1,2,3,4] << <> || <> <= <<1,2,3,4>> >>. % <<1,2,3,4>> |
---|---|
详见 bit syntax。
模块
模块必须存储在后缀为 .erl 的文件中,且只能由模块属性和函数定义组成,下面以 test.erl 为例说明如何使用模块。
1 2 3 4 5 6 7 |
% 模块属性以 - 开头% module 属性是必须的,且参数必须与除去后缀的文件名一致 -module(test). % import 属性用来导入其他模块的函数,然后才能在模块中使用 % -import(Module, [Function1/Arity, …, FunctionN/Arity]). % 其中 Arity 是函数的参数数目 |
---|---|
模块必须编译才能使用 1,编译成功会生成 test.beam 文件。编译有多种方式,如:
- 使用 erlc 进行编译:erlc test.erl
- 在 erl 中执行 c(test).
模块在第一次引用时被自动加载,所以在调用模块中的函数时,不需要运行 import 之类的语句(python 就需要)。2
函数
匿名函数
表达式 fun(X) -> Expression end. 返回函数作为表达式的值。
1 2 3 4 5 6 7 8 |
% Erlang 对缩进不敏感,所以也可以写成多行 Foo = fun() -> do_nothing_but_return_a_atom end. % 任何函数都有返回值,最后一个表达式的值会被当作返回值 Foo(). % 匿名函数可以在 erl 中执行 4 =:= fun(X) -> X*X end (2). |
---|---|
函数定义
函数不能在 erl 中定义,所以我们将函数定义写在 mymethod.erl 模块中。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
% mymethod.erl -module(mymethod). % 要想在外部调用模块中的函数,首先得将函数导出 % -export([Function1/Arity, …, FunctionN/Arity]). -export([hi/0]). % 调试的时候可以使用下面的语句导出所有函数 % -compile(export_all). % 不需要 end hi() -> “hello, world”. |
---|---|
函数名是一个 atom,函数定义的形式如下:
1 2 3 4 5 6 7 8 |
% 函数头部 function(Arg1, Arg2, …, Arg3) -> % 函数体 Expression1, Expression2, … % 表达式的结果作为返回值 ExpressionN. |
---|---|
其他语言中的 if … else … 可以通过函数分句和模式匹配来实现:
1 2 3 4 5 |
defall_the_same(a, b, c): if a == b == c: return True else: return False |
---|---|
1 2 3 4 5 6 |
% 按函数分句的先后顺序进行模式匹配 % 找到第一个参数匹配的分句时,执行该分句下的表达式 % 分号表分句的结束,句号表整个函数的结束 allthe_same(X, X,X) -> true; % `` 是匿名变量 allthe_same(, ,) -> false. |
---|---|
有时候需要匹配一定范围内的值,这时候模式匹配就略显不足了。
1 2 3 4 |
is_adult(1) -> false; … is_adult(17) -> false; is_adult(_) -> true. |
---|---|
所以 Erlang 有 guard(断言)。Guard 以 when 关键字开头,可出现在函数头部或表达式中。
1 2 |
is_adult(Age) whenAge<18-> false; is_adult(_) -> true. |
---|---|
Guard 可以由一系列 guard 表达式组成。
1 2 3 4 5 6 7 |
% 逗号在 guard 中的作用类似于 and istriangle(A, B, C) whenA+B > C, B+C> A, A+C > B->true; is_triangle(, , ) -> false. % 分号在 guard 中的作用类似于 or isnum(X) when is_integer(X); is_float(X)->true; is_num() -> false. |
---|---|
下面给出几个例子帮助你熟悉函数定义:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
bro(Girlfriend) whenGirlfriend== girl -> io:format(“fall in love with ~p~n”, [Girlfriend]); % 若参数未被使用,编译时会发出警告:`Warning: variable ‘Girl’ is unused` % 如果参数名以下划线开头,则不会警告 bro(_Girl) -> io:format(“cheat!~n”). % 参数数目不同但同名的函数没有任何关系 bro() -> io:format(“I‘m asingle dog~n”). % 根据参数的模式匹配执行不同的分句 yo(brother) -> io:format(“Hi, man!~n”); yo(friend) -> io:format(“How are you?~n”); yo(People) -> io:format(“Are you \“~p\“?~n”, [People]). % 计算列表的和 sum(L) -> sum(L, 0).% 句号 sum([], Result) -> Result; % 分号 sum([H|T], Result) -> sum(T, H+Result). % 又是句号,为什么? |
---|---|
在 erl 中编译并运行。
1 2 3 4 5 6 7 8 9 10 11 12 |
% 编译当前目录下的 mymethod.erl 模块c(mymethod). % 可以使用 cd 切换目录% cd(“/path/to/where/you/saved/the-module/“). % 函数调用的形式是: Module:Function(A**r**guments). % erl 会寻找 Module.beam 文件中 **F**unction 的定义mymethod:hi(). % 内建函数(BIFs: built-in functions)会被自动导入,不需要指出模块名 date(). % seq 并没有被自动导入,但你可以直接使用lists:seq(1,4). % [1,2,3,4] |
---|---|
查看更多内建函数点这里。
if 与 case 表达式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
% if 类似于 guard,并且语法和 guard 一致hiif(X) -> % if 也有返回值Result = if % 必须匹配所有的逻辑,否则会 crash X > 0 -> positive; X == 0 -> zero; % 匹配剩下的所有可能 true -> negative end, io:format(“if expression result is’~p’~n”, [Result]). % case 类似于函数头部,其余部分和 if 几乎一样hi_case(X) -> Result = case X of X when X > 0 -> positive; X when X == 0 -> zero; % 匹配剩下的所有可能 -> negative end, io:format(“case expression resultis ‘~p’~n”, [Result]). |
---|---|
Record
Record 是一种类似于 C 语言中结构体的数据结构,它会在编译期间被转换成元组。record 定义不能出现在 erl 中,但是可以定义在 .erl 或 .hrl 中,这里我们定义在 bar.hrl 中。
1 2 3 4 |
% bar.hrl % record 将元组中的元素绑定到特定的名称 -record(point, {x = 0, y}). % 本质是元组 {point, X = 0, Y = undefine**d} ** |
---|---|
然后在 erl 中使用 record。
1 2 3 4 5 6 7 8 9 10 |
% 使用函数 rr (read records)导入 record 的定义rr(“bar.hrl”). % 创建 record P0 = #point{}. % #point{x =0,y = undefined} P1 = #point{y = 0}. % #point{x =0,y = 0} % 在 P1 的基础上创建 recordP2 = P1#point{x = 1}. % #point{x =1,y = 0} % 读取 record 的成员 P2#point.x + P2#point.y. % 1 |
---|---|
当然,你也可以在模块中使用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
% bar.erl -module(bar). -include(“bar.hrl“). -compile(export_all). distance(P) when is_number(P#point.x), is_number(P#point.y) -> math:sqrt(P#point.xP#point.x + P#point.y**P#point.y). % record 在function clause 中的匹配很违背直觉 % 仅匹配 `#point.y == 1`,而不管 #poin**t.x 是不是 0 test(#point{y = 1}) -> io:fwrite(“x=? y=1~n”); % 仅匹配 #poi**nt.x =**= 1 ,而不管 `#point.y` 是什么值test(#point{x = 1}) -> io:fwrite(“x=1 y=?~n”). test() -> P1 = #point{x = 1, y= 0}, P2 = {point, 1, 0}, case distance(P1) =:= distance(P2) of true -> ok; false -> error(“Oh My God! This is impossible!”) end, test(#point{x = whatever, y = 1}), % x=?y=1 test(#point{x = 1, y= 1}), % x=?y=1 test(#point{x = 1, y= whatever}). % x=1y=? |
---|---|
详见官方文档。
宏定义
宏定义的语法如下:
1 2 |
-define(Const, Replacement). -define(Func(Var1,…,VarN),Replacement). |
---|---|
我们新建一个 mymath.erl 文件实验宏定义。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
% mymath.erl -module(mymath). -compile(export_all). % 自定义的宏 -define(ONE,1). -define(ADD(X, Y), X+Y). test() -> io:format(“predefined macros: ~n”), % 预定义的宏 io:format(“~p ~p ~p ~p ~p~n“, [?MODULE, ?MODULE_STRING,?FILE, ?LINE,?MACHINE]), % ?MACRO 调用宏io:format(“one=~p add(1,2)=~p~n”, [?ONE, ?ADD(1, 2)]). |
---|---|
然后在 erl 中编译运行。
1 2 3 4 5 |
c(mymath). mymath:test(). % predefined macros: % mymath “mymath””mymath.erl” 8 ‘BEAM’ % one=1 add(1,2)=3 |
---|---|
详见官方文档。
异常
捕获异常的语法如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
tryExpressionof % guards 是可选的 SuccessfulPattern1 [Guards] -> Expression1; SuccessfulPattern2 [Guards] -> Expression2 catch TypeOfError:ExceptionPattern1 -> Expression3; TypeOfError:ExceptionPattern2 -> Expression4 % after 语句在 tr**y.**..catch 语句之后执行after Expr3 end. |
---|---|
我们新建 catcher.erl 文件,对 try…catch 语句进行实验。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
% catcher.erl -module(catcher). -compile(export_all). % throw , exi**t , e**rror 三者都能产生异常do_something(throw) -> throw(lol); % exception throw: lol do_something(exit) -> exit(lol);% exception exit: lol do_something(error) -> error(lol); % exception error: lol do_something(X) -> X. normal_catcher(X) -> try do_something(X) of Result-> io:format(“do_something(~p) => ~p~n”, [X, Result]) catch throw:E -> io:format(“catch throw: ~p~n”, [E]); exit:E -> io:format(“catch exit: ~p~n”, [E]); error:E -> io:format(“catch error: ~p~n”, [E]) end. all_catcher(X)-> try do_something(X) of Result-> io:format(“do_something(~p) => ~p~n”, [X, Result]) catch % 省略错误类型 % 默认为throw 类型 E -> io:format(“catch you: ~p~n”, [E]) after io:format(“after `try…catch`~n“) end. simple_catcher(X) -> % 将异常转换为一个描述异常的元组 % 如果没有异常,则返回表达式的值 catch do_somethin**g(X). |
---|---|
进程与消息
Erlang 的每个进程都有一个消息队列保存收到的消息,而 receive 语句是用来从消息队列中提取消息的。receive 会遍历消息队列,直到找到能够匹配的消息,将其从消息队列中移除,并执行相应的 receive 处理逻辑。3
receive 的语法如下:
1 2 3 4 5 6 7 8 |
receive Pattern1 [Guards1] -> Expressions1; Pattern2 [Guards2] -> Expressions2 % after语句是可选的,意为: % 如果 `Timeout 毫秒后没收到消息,执行 Expressions3`after Timeout -> Expressions3 end. |
---|---|
发送消息的语法很简单:
1 | Pid!Message. |
---|---|
我们在 erl 中直观的感受一下接发消息的过程。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
% self() 获取当前进程的 pid% process_info(Pid) 用来查看进程运行时信息% 返回结果中的 messages 字段就是消息队列的内容process_info(self()). % 向 Erlang shell 进程发送一个消息 hi self() ! {self(), “a_more_complex_case”}. self() ! hi. self() ! [i, am, a, list]. % 现在消息队列中有两个消息了 process_info(self()). receive hi -> io:format(“get hi~n”) end. receive Msg -> io:format(“get: ~p~n”, [Msg]) end. % 被 receive 取完后,消息队列就空了 process_info(self()). |
---|---|
下面新建一个 m.erl 文件实验一些更复杂的例子。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
% m.erl -module(m). -export([start_echo/0, starthurry/0]). echo() -> receive {From, Msg}-> io:format(“~p => ~p: ~p~n”,[From, self(),Msg]), From ! Msg, % 循环接收消息 echo() % 如果10秒内没收到消息,就执行 after 语句 after 10000 -> io:format(“quit echo process~n”) end. % after**0 的逻辑类似于:% if is_not_empty(message_queue) % receive() % else % after() do_hurry() -> receive Msg -> io:format(“message: ‘~p’~n”,[Msg]), dohurry() after 0 -> io:format(“no more message, quit~n”) end. % 等待10秒后从消息队列中取出所有消息 hurry(Wait) -> timer:sleep(Wait), do_hurry(). start_echo() -> % 调用 spawn** 启动新进程,并返回一个pid(进程标识符)% 在 **erl 中调用为 `spawn(Function)`,在模块中为 `spawn(fun Function/0)`spawn(fun echo/0). start_hurry() -> spawn(fun() -> hurry(10**000) end). |
---|---|
然后在 erl 中运行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
% 启动新进程,将新进程 pid 与 E 绑定E = m:start_echo(). % Erlang shell 进程发送消息 “hi”给 **E** 进程E ! {self(), “hi”}. % 接收 echo 传回的消息receive Msg -> io:format(“receivedfrom echo:~p~n”, [Msg]) end. % 如果10秒没有再发消息给 E 进程,它会执行afterH = m:start_hurry(). H ! {self(), “hello”}. H ! yo. H ! [hi, bro]. |
---|---|