Erlang程序设计
第三章 变量
在Erlang里,变量就像数学里的那样。当关联一个值与一个变量时,所下的是一种断言, 也就是事实陈述。这个变量具有那个值,仅此而已。
变量的作用域是它定义时所处的语汇单元。因此,如果X被用在一条单独的函数子句 之内,它的值就不会“逃出”这个子句。没有同一函数的不同子句共享全局或私有变量这种说法。 如果X出现在许多不同的函数里,那么所有这些X的值都是不相干的。
Erlang里没有可变状态,没有共享内存,也没有锁。这让程序并行变得简单了。
模式匹配例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{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]
f()命令让shell忘记现有的任何绑定。
第四章 模块与函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
% 模块声明,模块名必须与文件名相同
-module(geometry).
% 导出声明,Name/N代表带有N个参数的函数Name,N被称为函数的arity(元数)
% export的参数是一个函数列表,表示这些函数可以被外部调用
% 未从模块导出的函数相当于OOPL(面向对象编程语言)中的私有函数,只能在模块内部调用
-export([area/1, test/0]).
% area函数有两个字句,采用分号分隔,每个字句都有一个头部和一个主题
% 头部包含函数名,后接零个或更多模式
% 主体包含一列表达式,会在头部里的模式与调用参数成功匹配时执行,会按照子句的定义顺序进行匹配
area({rectangle, Width, Height}) ->
Width * Height;
area({square, Side}) ->
Side * Side.
% test函数用于测试area函数
test() ->
12 = area({rectangle, 3, 4}),
144 = area({square, 12}),
tests_worked.
c(geomerty)
命令可以编译上面的代码,编译成功后会在当前目录生成一个名为geometry.beam
的目标代码模块。
如果你碰巧选择了与系统模块相冲突的模块名,那么编译模块时会得到一条奇怪的消息,说不能加载位于固定目录(sticky directory)的模块。只需重命名那个模块,然后删除编译模块时生成的所有.beam文件就可以了。
Erlang shell有许多内建命令可供查看和修改当前的工作目录。
- pwd()打印当前工作目录。
- ls()列出当前工作目录里所有的文件名。
- cd(Dir)修改当前工作目录至Dir。
- 逗号(,)分隔函数调用、数据构造和模式中的参数。
- 分号(;)分隔子句。我们能在很多地方看到子句,例如函数定义,以及case、if、try..catch和receive表达式。
- 句号(.)(后接空白)分隔函数整体,以及shell里的表达式。
Erlang是一种函数式编程语言。此外,函数式编程语言还表示函数可以被用作其他函数的参数,也可以返回函数。操作其他函数的函数被称为高阶函数(higher-order function),而在Erlang中用于代表函数的数据类型被称为fun。
1
2
3
4
5
L = [1, 2, 3, 4],
% 将会输出[2, 4, 6, 8]
lists:map(fun(X) -> X * 2 end, L).
map和filter等函数能在一次调用里对整个列表执行某种操作,我们把它们称为一次一列表(list-at-a-time)式操作。使用一次一列表式操作让程序变得更小,而且易于理解。之所以易于理解是因为我们可以把对整个列表的每一次操作看作程序的单个概念性步骤。否则,就必须将对列表元素的每一次操作视为程序的单独步骤了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
1> Fruit = [apple, pear, orange].
[apple,pear,orange]
2> MakeTest = fun(L) -> (fun(X) -> lists:member(X, L) end) end.
#Fun<erl_eval.42.39164016>
3> IsFruit = MakeTest(Fruit).
#Fun<erl_eval.42.39164016>
4> IsFruit(pear).
true
5> IsFruit(apple).
true
6> IsFruit(dog).
false
记住,括号里的东西就是返回值。
MakeTest(Fruit)
执行后返回fun(X) -> lists:member(X, Fruit) end
,即一个关于X
的函数,Fruit
是前面的fun
的参数。IsFruit(apple)
,即是计算lists:member(apple, Fruit)
的值。
定义自己的控制抽象:
1
2
3
4
5
6
7
8
9
10
11
-module(lib_misc).
-export([for/3, test_for/0]).
for(Max, Max, F) -> [F(Max)];
for(I, Max, F) -> [F(I) | for(I + 1, Max, F)].
test_for() ->
[2, 4, 6, 8] = for(1, 4, fun(X) -> X * 2 end),
tests_worked.
自定义sum
与map
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
-module(mylists).
-export([sum/1, test_sum/0, map/2, test_map/0]).
sum([H | T]) -> H + sum(T);
sum([]) -> 0.
map(F, [H | T]) -> [F(H) | map(F, T)];
map(_, []) -> [].
test_sum() ->
10 = sum([1, 2, 3, 4]),
tests_worked.
test_map() ->
[2, 4, 6, 8] = map(fun(X) -> X * 2 end, [1, 2, 3, 4]),
tests_worked.
编写程序时,我的做法是“编写一点”然后“测试一点”。我从一个包含少量函数的小模 块开始,先编译它,然后在shell里用一些命令测试它。当我觉得满意后,就会再编写一些函数, 编译它们,测试它们,以此类推。
通常,我并不完全确定程序需要什么样的数据结构,通过测试那些样本代码,我就能看 出所选的数据结构是否合适。
我倾向于让程序“生长”,而不是事先就完全想好要如何编写它们,这样就不会在出现问 题时才发现犯了大错。最重要的是,这样做很有趣。我能立即获得反馈,而且只要在程序里输 入就能知道我的想法是否有效。
一旦弄清楚如何在shell里做某些事情,我通常就会转而编写makefile和一些代码,重现我 在shell里学到的内容。
1
2
3
4
5
6
1> L = [1,2,3,4,5].
[1,2,3,4,5]
2> [2*X || X <- L].
[2,4,6,8,10]
上面的方式叫做列表推导,这种[F(X) || X <- L]
的写法,表示一个由F(X)
组成的列表,而X
是从L
当中提取的。