Erlang学习基本语法一

Erlang 语言学习记录

基础部分

变量和原子

1
2
3
4
5
6
7
8
9
Eshell V10.3.5.19  (abort with ^G)
1> 123456 * 223344.
27573156864
2> X = 123.
123
3> X * 2.
246
4> X = 999.
** exception error: no match of right hand side value 999

一旦定义了变量 X = 123, 那么 X 永远就是123, 不允许改变。其中 = 不是一个赋值操作符,它实际上是一个模式匹配操作符


请注意Erlang的变量以大写字母开头。所以X、This和A_long都是变量。以小写字母开头的名称(比如monday或friday)不是变量,而是符号常量,它们被称为原子(atom)。

1
2
5> abc=123.
** exception error: no match of right hand side value 123

进程、模块和编译

在shell里编译并运行Hello World

1
2
3
4
5
6
// hello.erl
-module(hello).
-export([start/0]).

start() ->
io:format("Hello world~n").

编译并运行它,操作如下:

1
2
3
4
5
6
7
Eshell V10.3.5.19  (abort with ^G)
1> c(hello).
{ok,hello}
2> hello:start().
Hello world
ok
3> halt().

c(hello)命令编译了hello.erl文件里的代码。{ok, hello}的意思是编译成功。 现在代码已准备好运行了。第2行里执行了hello:start()函数。 第3行里停止了Erlang shell。

在Erlang shell外编译

1
2
3
$ erlc hello.erl
$ erl -noshell -s hello start -s init stop
Hello world

erlc从命令行启动了Erlang编译器。编译器编译了hello.erl里的代码并生成了一个名为hello.beam的目标代码文件。

$erl -noshell … 命令加载了hello模块并执行hello:start()函数。随后,它执行了init:stop(), 这个表达式终止了Erlang会话。

并发

要在两台机器之间传输文件,需要两个程序:第一台机器上运行的客户端和第二台机器上运行的服务端。为了实现这一点,我们将制作两个模块:afile_clientafile_server

文件服务器进程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// afile_server.erl
-module(afile_server).
-export([start/1, loop/1]).

start(Dir) -> spawn(afile_server, loop, [Dir]).

loop(Dir) ->
receive
{Client, list_dir} ->
Client ! {self(), file:list_dir(Dir)};
{Client, {get_file, File}} ->
Full = filename:join(Dir, File),
Client ! {self(), file:read_file(Full)}
end,
loop(Dir).

答疑解惑:不用担心最后的自身调用,这不会耗尽栈空间。Erlang对代码采用了一种所谓“尾部调用”的优化,意思是此函数的运行空间是固定的。 这是用Erlang编写循环的标准方式,只要在最后调用自身即可。

注意:loop函数永远不会返回。在顺序编程语言里,必须要极其小心避免无限循环,因为只有一条控制线,如果这条线卡在循环里就有麻烦了。Erlang则没有这个问题。服务器只是一个在无限循环里处理请求的程序,与我们想要执行的其他任务并行运行。

这段程序意思:如果接收到{Client, list_dir}消息,就应该回复一个文件列表;如果接收到的消息是{Client, {get_file, File}},则回复这个文件。作为模式匹配过程的一部分,Client变量在接收消息时会被绑定。

  • 回复给谁

    所有接收的消息都包含变量Client,它是发送请求进程的进程标识符,也是应该回复的对象。如果想要得到一条消息的回复,最后说明一下回复应该发给谁。

  • self()的用法

    服务端发送的回复包含了参数self()(在这个案例里self()是服务端的进程标识符)。这个标识符被附在消息中,使客户端可以检查收到的消息来自的服务端,而不是其他某个进程。

  • 模式匹配被用于选择消息

    接收语句内部有两个模式。可以这样编写:

    1
    2
    3
    4
    5
    6
    7
    reveive
    Pattern1 ->
    Actions1;
    Pattern2 ->
    Actions2 ->
    ...
    end
Erlang编译器和运行时系统会正确推断出如何在接收到消息时运行适当的代码。不需要编写任何的if-then-else或switch语句来设定该做什么。

现在可以在shell里编译和测试这段代码;

1
2
3
4
5
6
7
8
9
10
11
12
13

Eshell V10.3.5.19 (abort with ^G)
1> c(afile_server).
{ok, afile_server}
2> FileServer = afile_server:start(".").
<0.80.0>
3> FileServer ! {self(), list_dir}.
{<0.78.0>,list_dir}
4> receive X -> X end.
{<0.80.0>,
{ok,["hello.erl","shop1.erl","file.svg","nav_conf.svg",
"afile_server.beam","world.erl","hello.beam",
"afile_server.erl"]}}
1
2
1> c(afile_server).
{ok, afile_server}
编译afile_server.erl文件所包含的afile_server模块。编译成功返回值是{ok, afile_server}。
1
2
2> FileServer = afile_server:start(".").
<0.80.0>
`afile_server:start(Dir)`调用`spawn(afile_server, loop, [Dir])`。这就创建出一个新的并行进程来执行函数`afile_server:loop(Dir)`并返回一个进程标识符,可以用它来与此进程通信。 `<0.80.0>`是文件服务器进程的进程标识符。它的显示方式是尖括号内由句号分隔的3个整数。
1
2
3> FileServer ! {self(), list_dir}.
{<0.78.0>,list_dir}
这里给文件服务器进程发送了一条`{self(), list_dir}`消息。Pid ! Message的返回值被规定为Message,因此shell打印出`{self(), list_dir}`的值,即`{<0.78.0>,list_dir}`。 `<0.78.0>`是Erlang shell自身的进程标识符,它被包括在消息内,告知文件服务器应该回复给谁。
1
2
3
4
5
4> receive X -> X end.
{<0.80.0>,
{ok,["hello.erl","shop1.erl","file.svg","nav_conf.svg",
"afile_server.beam","world.erl","hello.beam",
"afile_server.erl"]}}
`receive X -> X end`接收文件服务器发送的回复。它返回元组`{<0.80.0>, {ok,...}}`。该元组的第一个元素是文件服务器的进程标识符。第二个参数是`file:list_dir(Dir)`函数的返回值,它在文件服务器进程的接收循环里得出。

客户端代码

文件服务器通过一个名为afile_client的客户端模块进行访问。这个模块的主要目的是为了隐藏底层通信协议的细节。客户端代码的用户可以通过调用此客户端模块导出的lsget_file函数来传输文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// afile_client.erl
-module(afile_client).
-export([ls/1, get_file/2]).

ls(Server) ->
Server ! {self(), list_dir},
receive
{Server, FileList} ->
FileList

end.
get_file(Server, File) ->
Server ! {self(), {get_file, File}},
receive
{Server, Content} ->
Content
end.

如果对比afile_clientafile_server的代码,就会发现是一种美妙的对称性。

现在重启shell并重新编译所有代码,展示客户端和服务器如果共同工作。

1
2
3
4
5
6
7
8
9
10
11
12
13
Eshell V10.3.5.19  (abort with ^G)
1> c(afile_server).
{ok,afile_server}
2> c(afile_client).
{ok,afile_client}
3> FileServer = afile_server:start(".").
<0.90.0>
4> c(afile_client).
{ok,afile_client}
5> afile_client:get_file(FileServer, "missing").
{error,enoent}
6> afile_client:get_file(FileServer, "afile_server.erl").
{ok,<<"-module(afile_server).\n-export([start/1, loop/1]).\n\nstart(Dir) -> spawn(afile_server, loop, [Dir]).\n\nloop(Di"...>>}

顺序编程

Erlang shell里编辑命令

快捷键可用的命令(注意^Key的意思是应该按下Ctrl+Key):

命令 说明
^A 行首
^D 删除当前字符
^E 行尾
^F或右箭头键 向前的字符
^B或左箭头键 向后的字符
^P或上箭头键 前一行
^N或下箭头键 下一行
^T 调换最近两个字符的位置
Tab 尝试扩展当前模块或函数的名称

整数运算

1
2
3
4
1> 2 + 3 * 4.
14
2> (2 + 3) * 4.
20

Erlang可以用任意长度的整数执行整数运算。在Erlang里,整数运算是精确的,因此无需担心运算溢出或无法用特定字长来表示某个整数。

1
2
3> 123456789 * 987654321 * 112233445566778899 * 998877665544332211.
13669560260321809985966198898925761696613427909935341

也可以用十六进制和三十二进制的计数法:

1
2
4> 16#cafe * 32#sugar.
1577682511434

变量

1
2
3
4
5
6
1> X = 123456789.
123456789
2> X.
123456789
3> X * X * X * X.
232305722798259244150093798251441

可以把某个命令的结果保存在变量里。
在第1行给变量X指派一个值,shell在下一行里打印出了这个变量的值。请注意所有变量名都必须以 大写字母 开头。

如果想知道一个变量的值,只需要输入变量名。 X既然已经有了一个值,就可以直接使用它。 但是,如果视图给变量X指派一个不同的值,就会得到一条错误消息。

1
2
4> X = 1234.
** exception error: no match of right hand side value 1234
  • 首先,X不是一个变量,不是你习惯的Java和C等语言里的概念。
  • 其次,=不是一个赋值操作符,而是一个模式匹配操作符。

    Erlang的变量不会变

    Erlang的变量是一次性赋值变量。顾名思义,它们只能被赋值一次。已被指派一个值的变量称为绑定变量,否则称为未绑定变量。

    变量的作用域是它定义时所处的词汇单元。因此,如果X被用再一条单独的函数子句之内,它的值就不会“逃出”这个子句。没有同一函数的不同子句共享全局或私有变量这种说法。 如果X出现在许多不同的函数里,那么所有这些X的值都是不相干的。

    浮点数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    1> 5/3.
    1.6666666666666667
    2> 4/2.
    2.0
    3> 5 div 3.
    1
    4> 5 rem 3.
    2
    5> 4 div 2.
    2

第1行的行尾数字是整数3。句号表示表达式的结尾,而不是小数点。如果我想写的是浮点数,我会写作3.0。

当你用/给两个整数做除法时,结果会自动转换为浮点数。因此,5/3的值是1.6666666666666667。

第2行,尽管4能被2整除,但是结果仍然是一个浮点数,而不是整数。要从除法里获取整数结果,我们必须使用操作符div和rem。

N div M是让N除以M然后舍去余数。 N rem M 是N除以M后剩下的余数。

Erlang在内部使用64位的IEEE 754-1985浮点数,因此使用浮点数的程序会存在和C等语言一样的浮点数取整与精度问题。

原子

在Erang里,原子被用于表示常量值。原子是全局性的,不需要宏定义或包含文件就能实现。

原子以小写字母开头,后接一串字母、数字、下划线(_)或者at(@)符号,例如red、december、cat、meters、yards、joe@somehost和a_long_name

原子还可以放在单引号(‘)内。可以用这种引号形式创建以大写字母开头(否则会被解释成变量)或包含字母数字以外字符的原子,例如’Monday’、’Tuesday’、’+’、’*’和’an atom with spaces’。 甚至可以给无需引号的原子加上引号,因此’a’和a的意思完全一致。在某些语言里,单引号和双引号可以互换使用。Erlang里不是这样。单引号的用法如前面所示,双引号用于给字符串字面量(string literal)定界。

一个原子的值就是它本身。所以,如果输入一个原子作为命令,Erlang shell就会打印出这个原子的值。

1
2
1> hello.
hello

元组

创建元组的方法是用大括号把想要表达的值括起来,并用逗号分隔它们。举个例子,如果想要表达某人的姓名和身高,就可以用{joe, 1.82}。这个元组包含了一个原子和一个浮点数。

1
P = {10, 45}

这样就创建了一个元组并把它绑定到变量P上。 与C结构不同,元组里的字段没有名字。因为这个元组只包含一对整数,所以必须记住它的用途是什么。一种常用的做法是将原子作为元组的第一个元素,用它来表示元组是什么。因此,我们会写成{point, 10, 45}而不是{10, 45}, 这就使程序的可理解性大大增加了。这种给元组贴标签的方式不是语言所要求的,而是一种推荐的编程风格。

元组还可以嵌套。如想要表示某个人一些情况(名字、身高、鞋码和眼睛颜色),就可以像下面这样写:

1
2
3
4
5
1> Person = {person, {name, joe}, {height, 1.82}, {footsize, 42}, {eyecolour, brown}}.
{person,{name,joe},
{height,1.82},
{footsize,42},
{eyecolour,brown}}

请注意我们是如何将原子同时用于标明字段和(在name和eyecolour里)作为字段值的。

创建元组

元组会在声明它们时自动创建,不再使用时则被销毁。

Erlang使用一个垃圾收集器来回收所有未使用的内存,这样就不必担心内存分配的问题了。如果在构建新元组时用到变量,那么新的元组会共享该变量所引用数据结构的值。下面是一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
7> Person = {person, {name, joe}, {height, 1.82}, {footsize, 42}, {eyecolour, brown}}.
{person,{name,joe},
{height,1.82},
{footsize,42},
{eyecolour,brown}}
8> F = {firstName, joe}.
{firstName,joe}
9> L = {lastName, armstrong}.
{lastName,armstrong}
10> P = {person, F, L}.
{person,{firstName,joe},{lastName,armstrong}}
11> {true, Q, 23, Costs}.
* 1: variable 'Q' is unbound

如果试图用未定义的变量创建数据结构,就会得到一个错误。 最后一句,变量Q就是未定义。

提取元组的值

如果想要从某个元组里提取一些值,就会使用模式匹配操作符=。

1
2
1> Point = {point, 10, 45}.
{point,10,45}

如想把Point里的字段提取到变量X和Y里,可以这样写:

1
2
3
4
5
6
2> {point, X, Y} = Point.
{point,10,45}
3> X.
10
4> Y.
45

在命令2里,X绑定了10,Y绑定了45。根据规定,表达式Lhs=Rhs的值是Rhs,因此shell打印出{point, 10, 45}

如你所见,等号两侧的元组必须有相同数量的元组,而且两侧的对应元素必须绑定为相同的值。

现在假设输入了这样的语句:

1
2
5> {point, C, C} = Point.
** exception error: no match of right hand side value {point,10,45}

模式{point, C, C}{point, 10, 45}不匹配,因为C不能同时是10和45。因此,这次模式匹配失败,系统打印出了错误信息。

下面这个例子里,模式{point, C, C} 则是匹配的:

1
2
3
4
5
6
7> Point1 = {point, 25, 25}.
{point,25,25}
8> {point, C, C} = Point1.
{point,25,25}
9> C.
25

如果有一个复杂的元组,就可以编写一个与该元组形状(结构)相同的模式,并在待提取值的位置加入未绑定变量来提取该元组的值。

1
2
1> Person = {person, {name, joe, armstrong}, {footsize, 42}}.
{person,{name,joe,armstrong},{footsize,42}}

现在将编写一个模式来提取此人姓名中的名(first name)。

1
2
3
4
2> {_,{_,Who,_},_} = Person.
{person,{name,joe,armstrong},{footsize,42}}
3> Who.
joe

请注意,在上面的例子中,将_作为占位符,用于表示不感兴趣的那些变量。符号_被称为匿名变量。与正规变量不同,同一模式里的多个_不必绑定相同的值。

列表

假设需要表示一个图形。此图形由三角形和正方形组成,就可以用一个列表来表示它。

1
1> Drawing = [{square, {10,10},10}, {triangle,{15,10},{25,10},{30,40}},...]

这个图形列表里的每个元素都是固定大小的元组(例如{square, Point, Side}或者{triangle, Point1, Point2, Point3}), 但这个图形本身可能包含任意数量的事物,因此用列表来表示。

列表里的各元素可以是任何类型,因此可以编写下面的例子:

1
2
1> [1+7,hello,2-2,{cost, apple, 30-20}, 3].
[8,hello,0,{cost,apple,10},3]

列表的第一个元素被称为列表头(head)。假设把列表头去掉,剩下的就被称为列表尾(tail)。

举例,如果有一个列表[1,2,3,4,5],那么列表头就是整数1,列表尾则是列表[2,3,4,5]。请注意列表头可以是任何事物,但列表尾通常仍然是个列表。

定义列表

如果T是一个列表,那么[H|T]也是一个列表,它的头是H,尾是T。竖线(|)把列表的头与尾分隔开。[] 是一个空列表。

可以给T的开头添加不止一个元素,写法是[E1, E2, …, En | T]。

举个例子,如果一开始定义ThingsToBuy如下:

1
2
3
4
5
6
7
2> ThingsToBuy = [{apples, 10}, {pears, 6}, {milk, 3}].
[{apples,10},{pears,6},{milk,3}]

// 那么就可以这样扩展列表

3> ThingsToBuy1 = [{oranges, 4}, {newspaper, 1} | ThingsToBuy].
[{oranges,4},{newspaper,1},{apples,10},{pears,6},{milk,3}]

提取列表元素

当我们在商店里,手上拿着购物单ThingsToBuy1时,所做的第一件事就是把这个列表拆成头和尾。

1
2
4> [Buy1|ThingsToBuy2] = ThingsToBuy1.
[{oranges,4},{newspaper,1},{apples,10},{pears,6},{milk,3}]

操作成功,绑定如下:

Bug1 = {oranges, 4}, ThingsToBuy2 = [{newspaper,1},{apples,10},{pears,6},{milk,3}]。于是我们先去买橙子(oranges),然后可以继续拆出下一对商品。

1
2
5> [Buy2, Buy3 | ThingsToBuy3] = ThingsToBuy2.
[{newspaper,1},{apples,10},{pears,6},{milk,3}]

操作成功后 Buy2 = {newspaper, 1}, Buy3 = {apples, 10}, 而ThingsToBuy3 = [{pears,6},{milk,3}]。

字符串

严格说,Erlang里没有字符串。要在Erlang里表示字符串,可以选择一个由整数组成的列表或者一个二进制型。当字符串表示为一个整数列表时,列表里的每个元素都代表了一个Unicode代码点(codepoint)。

可以用字符串字面量来创建这样的一个列表。字符串字面量其实就是用双引号(“)围起来的一串字符。例如:

1
2
1> Name = "Hello".
"Hello"

“Hello”其实只是一个列表的简写,这个列表包含了代表字符串里各个字符的整数字符代码。

注意:在一些编程语言里,字符串可以用单引号或双引号定界。而在Erlang里,必须使用双引号。

1
2
3
4
5
6
2> [1, 2, 3].
[1,2,3]
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]

在表达式2里,列表[1,2,3]在打印时未做转换。这是因为1、2和3不是可打印字符。

在表达式3里,列表里的所有项目都是可打印字符,因此它被打印成字符串字面量。

表达式4和表达式3差不多,区别在于这个列表以1开头,而1不是可打印字符。因此,这个列表在打印时未做转换。

不需要知道代表某个字符的是哪个整数,可以把“美元符号语法”用于这个目的。举个例子,$a实际上就是代表字符a的整数,以此类推。

1
2
3
4
5> I = $s.
115
6> [I-32, $u, $r, $p, $r, $i, $s, $e].
"Surprise"

用列表来表示字符串时,它里面的各个整数都代表Unicode字符。必须使用特殊的语法才能输入某些字符,在打印列表时也要选择正确的格式惯例。

1
2
3
4
5
1> X = "a\x{221e}b".
[97,8734,98]
2> io:format("~ts~n",[X]).
a∞b
ok

在第1行里,我们创建了一个包含三个整数的列表。第一个整数97是字符a的ASCII和Unicode编码。\x{221e} 这种记法的作用是输入一个代表Unicode无穷大字符的十六进制整数(8734)。最后,98是字符b的ASCII和Unicode编码。shell用列表记法([97,8734,98])将它打印出来,这是因为8734不是一个可打印的Latin1字符编码。在第2行里,我们用一个格式化I/O语句打印出这个字符串,里面使用了代表无穷大字符的正确字符图案。

如果shell将某个整数列表打印成字符串,而你其实想让它打印成一列整数,那就必须使用格式化的写语句,就像下面这样:

1
2
3
4
5
1> X = [97, 98, 99].
"abc"
2> io:format("~w~n", ["abc"]).
[97,98,99]
ok

总结:

模式=单元 结果
{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=”cat”
[A,B,C|T] = [a,b,c,d,e,f] 成功:A=a, B=b, C=c, T=[d,e,f]
多谢您的大力支持