Erlang学习基本语法二

Erlang 语言学习记录

基础部分

模块与函数

模块是Erlang的基础代码单元。模块保存在扩展名为.erl的文件里,而且必须先编译才能运行模块里的代码。编译后的模块以.beam作为扩展名。

geometry.erl

1
2
3
4
5
-module(geometry).
-export([area/1]).

area({rectangle, Width, Height}) -> Width * Height;
area({square, Side}) -> Side * Side.

文件的第一行是模块声明。声明里的模块名必须与存放该模块的主文件名相同。

第二行是导出声明。Name/N这种记法是指一个带有N个参数的函数Name,N被称为函数的元数(arity)。export的参数是由Name/N项目组成的一个列表。因此,-export([area/1])的意思是带有一个参数的函数area可以在此模块之外调用。

未从模块里导出的函数只能在模块内调用。已导出函数就相当于面向对象编程语言里的公共方法,未导出函数则相当于私有方法。

area函数有两个子句。这些子句由一个分号隔开,最后的子句以句号加空白结束。每条子句都有一个头部和一个主体,两者用箭头(->)分隔。头部包含一个函数名,后接零个或更多个模式,主体则包含一列表达式,它们会在头部里的模式与调用参数成功匹配时执行。这些子句会根据它们在函数定义里出现的顺序进行匹配。

1
2
3
4
5
6
1> c(geometry).
{ok,geometry}
2> geometry:area({rectangle, 10, 5}).
50
3> geometry:area({square, 3}).
9

在第1行给出了命令c(geometry),它的作用是编译geometry.erl文件里的代码。编译器返回了{ok,geometry},意思是编译成功,而且geometry模块已被编译和加载。编译器会在当前目录创建一个名为geometry.beam的目标代码模块。在第2行和第3行调用了geometry模块里的函数。请注意,需要给函数名附上模块名,这样才能准确标明想调用的是哪个函数。

目录和代码路径

Erlang shell有许多内建命令可供查看和修改当前的工作目录。

  • pwd() 打印当前工作目录。
  • ls() 列出当前工作目录里所有的文件名。
  • cd(Dir) 修改当前工作目录至Dir。

添加测试代码

geometry1.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
-module(geometry1).
-export([test/0, area/1]).

test() ->
12 = area({rectangle, 3, 4}),
144 = area({square, 12}),
tests_workd.

area({rectangle, Width, Height}) -> Width * Height;
area({square, Side}) -> Side * Side.

1> c(geometry1).
{ok,geometry1}
2> geometry1:test().
tests_workd
```
`12 = area({rectangle, 3, 4})` 这行代码是一项测试。如果`area({rectangle, 3, 4})`没有返回12, 模式匹配就会失败,我们会得到一条错误消息。执行geometry1:test()并看到结果是tests_workd时,就可以确定test/0主体里的所有测试都成功通过了。


注意:

- 逗号(,)分隔函数调用、数据构造和模式中的参数。
- 分号(;)分隔子句。我们能在很多地方看到子句,例如函数定义,以及caseiftry..catch和receive表达式。
- 句号(.)(后接空白)分隔函数整体,以及shell里的表达式。


#### 购物案例

假如有一个购物列表:

[{oranges,4},{newspaper,1},{apples,10},{pears,6},{milk,3}]

我们想要知道购物花了多少钱。要计算它,需要知道购物列表里每一项的价格。假设此信息将在一个名为shop的模块中计算,它的定义如下:

>shop.erl

-module(shop).
-export([cost/1]).

cost(oranges) -> 5;
cost(newspaper) -> 8;
cost(apples) -> 2;
cost(pears) -> 9;
cost(milk) -> 7.

1
函数cost/15个子句组成。每个子句的头部都包含一个模式(在这个案例里只是一个非常简单的原子)。当执行shop:cost(X)时,系统会尝试将X与各子句中的模式相匹配。如果发现了匹配,就会执行->右侧的代码。

1> c(shop).
{ok,shop}
2> shop:cost(apples).
2
3> shop:cost(oranges).
5
4> shop:cost(socks).
** exception error: no function clause matching shop:cost(socks) (shop.erl, line 4)

1
2
3
4
5
我们在第1行编译了shop.erl文件里的模块。在第2行和第3行,查询了apples和oranges的价格(结果中的25是单价)。在第4行查询了socks(袜子)的价格,但因为没有子句与其匹配,所以得到了一个模式匹配错误,系统打印了一条包含文件名和出错行号的错误消息。

假设购物列表 Buy = [{oranges,4},{newspaper,1},{apples,10},{pears,6},{milk,3}], 我们想要计算列表里所有项目的总价值。

>shop1.erl

-module(shop1).
-export([total/1]).

total([{What, N}|T]) -> shop:cost(What) * N + total(T);
total([])

1
拿它来测试下:

1> c(shop1).
{ok,shop1}
2> shop1:total([]).
0
3> shop1:total([{milk, 3}]).
21
4> shop1:total([{pears,6}, {milk, 3}]).
75

1
2
3
4
5
2行返回0是因为total/1的第二个子句是total([])->0

3行,shop1:total([{milk, 3}])与第一个子句相匹配,从而绑定了What = milk、N = 3以及T=[].

接下来,函数的主体代码被执行,所以要执行的表达式是:

shop:cost(milk) * 3 + total([]);

1
2
3
4

4行,与total/1的第一个子句相匹配,绑定了What = pears、N = 6 以及 T = [{milk, 3}]。

执行的是:

shop:cost(pears) * 6 + total([{milk}, 3])

简化后就是:

9 * 6 + total([{milk}, 3])

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


在这个案例中,Head是模式{What, N}。 当子句1匹配了一个非空列表,就会从中取出列表头,用它做一些事,然后调用自身继续处理列表尾。当列表缩减至空列表([])时,就会匹配子句2。

#### fun: 基本的抽象单元

Erlang是一种函数式编程语言。此外,函数式编程语言还表示函数可以被用作其他函数的参数,也可以返回函数。操作其他函数的函数被称为**高阶函数**,而在Erlang中用于代表函数的数据类型被称为`fun`。

- 对列表里的每个元素执行相同的操作。在这个案例里,将fun作为参数传递给lists:map/2和lists:filter/2等函数。
- 创建自己的控制抽象。这一技巧极其有用。例如,Erlang没有for循环,但我们可以轻松创建自己的for循环。创建控制抽象的优点是可以让它们精确实现我们想要的做法,而不是依赖一组预定义的控制抽象,因为它们的行为可能不完全是我们想要的。
- 实现可重入解析代码、解析组合器或惰性求值器等事物。在这个案例里,我们编写返回fun的函数。这种技术很强大,但可能会导致程序难以调试。

funs是“匿名的”函数。这样称呼它们是因为它们没有名字。

1> Double = fun(X) -> 2 * X end.

#Fun<erl_eval.6.128620087>
2> Double(2).
4

1
fun可以有任意数量的参数。可以编写一个函数来计算直角三角形的斜边,

3> Hypot = fun(X, Y) -> math:sqrt(X X + Y Y) end.

#Fun<erl_eval.12.128620087>
4> Hypot(3,4).
5.0
5> Hypot(3).
** exception error: interpreted function with arity 2 called with one argument

1
2
3
4

如果参数的数量不正确,将会得到一个错误。

fun可以有多个不同的子句。如下一个转换华氏与摄氏温度的函数:

10> TempConvert = fun({c, C}) -> {f, 32 + C 9 / 5};
10> ({f,F}) -> {c, (F - 32)
5 / 9}
10> end.

#Fun<erl_eval.6.128620087>
11> TempConvert({c, 100}).
{f,212.0}
12> TempConvert({f, 212}).
{c,100.0}
13> TempConvert({c,0}).
{f,32.0}

1
2
3
4

> 注意,第10行的表达式占据了多行。输入这个表达式时,每输入一个新行,shell就会重复10>这个提示符,意思是这个表达式还不完整,shell想得到更多的输入。

#### 以fun作为参数的函数

1> L = [1,2,3,4].
[1,2,3,4]
2> lists:map(fun(X) -> 2 * X end, L).
[2,4,6,8]

1
2
3
4

另外还有函数:lists:filter(P, L), 它返回一个新的列表,内含L中所有符合条件的元素(条件是对元素E而言P(E)true)。

定义一个函数Even(X),如果X是偶数就返回true

3> Even = fun(X) -> (X rem 2) =:= 0 end.

#Fun<erl_eval.6.128620087>

1
这里的`X rem 2`会计算出X除以2后的余数,`=:=`用来测试是否相等。现在可以测试Even, 然后将 它用作map和filter的参数了。

4> Even(8).
true
5> Even(7).
false
6> lists:map(Even, [1,2,3,4,5,6,8]).
[false,true,false,true,false,true,true]
7> lists:filter(Even, [1,2,3,4,5,6,8]).
[2,4,6,8]

1
2
3
4
5
map和filter等函数能在一次调用里对整个列表执行某种操作,我们把它们称为`一次一列表式`操作。使用一次一列表式操作让程序变得更小,而且易于理解。之所以易于理解是因为我们可以把对整个列表的每一次操作看做程序的单个概念性步骤。否则,就必须将对列表元素的每一次操作视为程序的单独步骤了。

#### 返回fun的函数

函数不仅可以使用fun作为函数,还可以返回fun

1> Fruit = [apple, pear, orange].
[apple,pear,orange]

1
现定义一个MakeTest(L)函数,将事物列表(L)转变成一个测试函数,用来检查它的参数是否在列表L中。

2> MakeTest = fun(L) -> (fun(X) -> lists:member(X, L) end) end.

#Fun<erl_eval.6.128620087>
3> IsFruit = MakeTest(Fruit).

#Fun<erl_eval.6.128620087>

1
如果X是列表L中的成员,lists:member(X, L)就返回true, 否则返回false。 构建完测试函数后,我们来试试它。

4> IsFruit(pear).
true
5> IsFruit(apple).
true
6> IsFruit(dog).
false

1
也可以把它用作lists:filter/2的参数。

7> lists:filter(IsFruit, [dog, orange, cat, apple, bear]).
[orange,apple]

1
2
3
4

##### 自己创建一个For循环的函数

> lib_misc.erl

for(Max, Max, F) -> [F(Max)];
for(I, Max, F) -> [F(I)|for(I+1, Max, F)].

1
2
3
4

执行for(1,10,F)会创建列表[F(1), F(2), ..., F(10)]。

现已经有了一个简单的for循环。我们可以用它生成一个从110的整数列表。

1> c(lib_misc).
{ok,lib_misc}
2> lib_misc:for(1, 10, fun(I) -> I end).
[1,2,3,4,5,6,7,8,9,10]
3> lib_misc:for(1, 10, fun(I) -> I*I end).
[1,4,9,16,25,36,49,64,81,100]

1
2
3
4

##### 对列表所有元素求和

> mylists.erl

-module(mylists).
-export([sum/1]).

sum([H|T]) -> H + sum(T);
sum([]) -> 0.

1
2


1> c(mylists).
{ok,mylists}
2> L = [1,3,10].
[1,3,10]
3> mylists:sum(L).
14

1
2

现添加map的方法。

map(_, []) -> [];
map(F, [H|T]) -> [F(H)|map(F,T)].

1
2
第一句说明如何处理空列表。让任何函数映射一个空列表的元素只会得到一个空列表。
第二句规定了如何处理带有头H和尾T的列表。只要构建一个头为F(H)、尾为map(F, T)的新列表即可。

1> c(mylists).
{ok,mylists}
2> L = [1,2,3,4,5].
[1,2,3,4,5]
3> mylists:map(fun(X) -> 2X end, L).
[2,4,6,8,10]
4> mylists:map(fun(X) -> X
X end, L).
[1,4,9,16,25]

1
2
3
4

##### 改进版的shop2

> shop2.erl

-module(shop2).
-export([total/1]).
-import(lists, [map/2, sum/1]).

total(L) ->
sum(map(fun({What, N}) -> shop:cost(What) * N end, L)).

1
2

通过观察其中步骤,了解此函数的工作方式。

1> Buy = [{oranges, 4}, {newspaper, 1}, {apples, 10}, {pears, 6}, {milk, 3}].
[{oranges,4},{newspaper,1},{apples,10},{pears,6},{milk,3}]
2> L1 = lists:map(fun({What, N}) -> shop:cost(What) * N end, Buy).
[20,8,20,54,21]
3> lists:sum(L1).
123

1
2
3
4
5
6
7
8
9

- `-import(lists, [map/2, sum/1])` 声明的意思是map/2函数是从lists模块里导入的,后面的也一样。这就意味着我们可以用map(Fun, ...)来代替lists:map(Fun, ...)的写法了。 cost/1没有在导入声明中声明过,因此必须使用“完全限定”的名称shop:cost。
- `-export([total/1])` 声明的意思是total/1函数可以在shop2模块之外调用。只有从一个模块里导出的函数才能在该模块之外调用。

#### 列表推导

`列表推导`可以无需使用fun、map或filter就能创建列表的表达式。它使程序变的更短,更易理解。

过去的用法:

1> L = [1,2,3,4,5].
[1,2,3,4,5]
2> lists:map(fun(X) -> 2*X end, L).
[2,4,6,8,10]

1
而用列表推导的方式:

1> L = [1,2,3,4,5].
[1,2,3,4,5]
2> [2*X || X <- L ].
[2,4,6,8,10]

1
2

`[ F(X) || X <- L ]` 标记的意思是“由F(X)组成的列表(X从列表L中提取)”。因此,`[2*X || X <- L ]` 的意思就是“由2*X组成的列表(X从列表L中提取)”。

4> Buy = [{oranges, 4}, {newspaper, 1}, {apples, 10}, {pears, 6}, {milk, 3}].
[{oranges,4},{newspaper,1},{apples,10},{pears,6},{milk,3}]
5> [{Name, 2 * Number} || {Name, Number} <- Buy].
[{oranges,8},{newspaper,2},{apples,20},{pears,12},{milk,6}]

1
2
3
上面例子为,为原始列表里每一项的数字加倍。

请注意, `||` 符号右侧的元组`{Name, Number}`是一个模式,用于匹配列表Buy里的各个元素。左侧的元组`{Name, 2*Number}`则是一个构造器。

6> [{shop:cost(A), B} || {A, B} <- 7 buy]. [{5,4},{8,1},{2,10},{9,6},{7,3}]> [shop:cost(A) B || {A, B} <- 8 buy]. [20,8,20,54,21]> lists:sum([shop:cost(A) B || {A, B} <- Buy]).
123

1
2

最后,如果想把它作为一个函数,如下写:

total(L) ->
lists:sum([shop:cost(A) * B || {A, B} <- L]).

1
2

列表推导最常规的形式是下面这种表达式:

[X || Qualifier1, Qualifier2, …]

1
2
3
4
5
6
7
X是任意一条表达式,后面的限定符可以是生成器、位串生成器或过滤器。

- 生成器 的写法是Pattern <- ListExpr,其中的ListExp必须是一个能够得出列表的表达式。
- 位串生成器的写法是BitStringPattern <= BitStringExpr,其中的BitStringExpr必须是一个能够得出位串的表达式。
- 过滤器 既可以是判断函数(即返回truefalse的函数),也可以是布尔表达式。

如下为,列表推导里的生成器部分起着过滤器的作用:

1> [X || {a, X} <- [{a, 1}, {b, 2}, {c, 3}, {a, 4}, hello, “wow”]].
[1,4]

1
2
3
4
5


##### 快排算法QuickSort

> lib_misc.erl

qsort([]) -> [];
qsort([Pivot|T]) ->
qsort([X || X <- T, X < Pivot])
++ [Pivot] ++
qsort([X || X <- t, x>= Pivot]).

1
2

注意,这里的++是中缀插入操作符。展示这段代码是为了表现它的优雅,而不是效率。这样使用++一般不认为是良好的编程实践做法。

1> c(lib_misc).
{ok,lib_misc}
2> L=[23,6,2,9,27,400,78,45,61,82,14].
[23,6,2,9,27,400,78,45,61,82,14]
3> lib_misc:qsort(L).
[2,6,9,14,23,27,45,61,78,82,400]

1
为了了解它是如何工作的,我们将一步步展示执行的过程。先从一个列表L开始,对它调用qsort(L)。下面这一步匹配qsort的子句2, 产生了如下绑定:Piovt -> 23 和 T -> [6,2,9,27,400,78,45,61,82,14]:

4> [Pivot|T] = L.
[23,6,2,9,27,400,78,45,61,82,14]

1
现在将T分成两个列表,一个包含T里所有小于Pivot(中位数)的元素,另一个包含所有大于或者等于Pivot的元素。

5> Smaller = [X || X <- T, X < Pivot].
[6,2,9,14]
6> Bigger = [X || X <- t, x>= Pivot].
[27,400,78,45,61,82]

1
2

现在排序Smaller和Bigger,并将它们与Pivot合并。

qsort([6,2,9,14]) ++ [23] ++ qsort([27,400,78,45,61,82])
= [2,6,9,14] ++ [23] ++ [27,45,61,78,82,400]
= [2,6,9,14,23,27,45,61,78,82,400]

1
2
3
4
5
6
7


#### 毕达哥拉斯三元数组

pythag(N)函数会生成一个包含所有整数{A, B, C}组合的列表,其中A<sup>2</sup>+B<sup>2</sup>=C<sup>2</sup>并且各条边之和小于等于N。

>lib_misc.erl

pythag(N) ->
[{A,B,C} ||
A <- lists:seq(1, N),
B <- lists:seq(1, N),
C <- lists:seq(1, N),
A + B + C =< N,
A A + B B =:= C*C
].

1
简单解释:lists:seq(1, N)返回一个包含1到N所有整数的列表。因此, A<-lists:seq(1, N)的意思是A提取从1到N的所有可能值。

1> c(lib_misc).
{ok,lib_misc}
2> lib_misc:pythag(16).
[{3,4,5},{4,3,5}]
3> lib_misc:pythag(30).
[{3,4,5},{4,3,5},{5,12,13},{6,8,10},{8,6,10},{12,5,13}]

1
2
3
4

#### 回文构词

>lib_misc.erl

perms([]) -> [[]];
perms(L) -> [[H | T] || H <- L, T < perms(L–[H])].

1
2


1> c(lib_misc).
{ok,lib_misc}
2> lib_misc:perms(“123”).
[“123”,”132”,”213”,”231”,”312”,”321”]
3> lib_misc:perms(“cats”).
[“cats”,”cast”,”ctas”,”ctsa”,”csat”,”csta”,”acts”,”acst”,
“atcs”,”atsc”,”asct”,”astc”,”tcas”,”tcsa”,”tacs”,”tasc”,
“tsca”,”tsac”,”scat”,”scta”,”sact”,”satc”,”stca”,”stac”]

1
2
3
4

`X -- Y` 是列表移除操作符,它从X里移除Y中的元素。

假设想要计算字符串“cats”的所有排列形式。首先,分离字符串的第一个字符,也就是c,并计算字符串移除c后的所有排列形式。“cats”移除c后是字符串“ats”,而“ats”的全部排列形式是以下字符串:["ats","ast","tas","tsa","sat","sta"]。 接下来,把c附加到所有字符串的开头,形成["cats","cast","ctas","ctsa","csat","csta"]。然后继续分离第二个字符并重复这一算法,以此类推。

[[H | T] || H <- L, T < perms(L–[H])]
`

它的意思是穷尽一切可能从L里提取H,然后穷尽一切可能从perms(L – [H])(即列表L移除H后的所有排列形式)里提取T,最后返回[H|T]。

多谢您的大力支持