Erlang入门教程 - 顺序式编程

这里写图片描述
Getting Started with Erlang User’s Guide Version 9.0


1. Erlang Shell

大多数操作系统有一个命令行解释器或者shell,UNIX和Linux有很多,Window有一个。Erlang有它自己的shell可以在上面直接写小段的erlang代码,并求值观察发生了什么[1]

在操作系统中的shell或者命令解释器中输入erl就可打开Erlang Shell。你将会看到类似下面的内容:

% erl
Erlang R15B (erts-5.9.1) [source] [smp:8:8] [rq:8] [async-threads:0] [hipe] [kernel-poll:false]

Eshell V5.9.1  (abort with ^G)
1>

在shell中输入“2+5.”然后按下回车。注意你要在代码后面加上一个点”.”来告诉shell你完成了输入,然后才能回车。

1> 2 + 5.
7
2>

如上所示,Erlang shell会标出行数表明可以输入(1> 2>),它准确的说明了2+5等于7.如果你在shell输入有误,你可以用backspace键删除,这和其他shell一样。这里有一份关于在shell中编辑命令更详细的指南[2]。

(注意下面例子中shell给出的行数顺序乱了。这是因为写教程和代码测试是分开进行的)

下面是稍微复杂一点的计算

2> (42 + 77) * 66 / 3.
2618.0

和平时的算数运算一样,这里使用了括号,乘号“*”,除号“/”[3]。

可以按下Ctrl+C终止erlang system和erlang shell,会产生如下输出

BREAK: (a)bort (c)ontinue (p)roc info (i)nfo (l)oaded
       (v)ersion (k)ill (D)b-tables (d)istribution
a
%

输入“a”离开erlang system。

另一个终止erlang system的方法是输入halt():

扫描二维码关注公众号,回复: 1763051 查看本文章
3> halt().
% 

2. 模块和函数

一个编程语言如果只能在shell中运行代码那它就没什么太大作用。所以这里有一个erlang程序,用你喜欢的文本编辑器输入并保存为tut.erl文件。文件名tut.erl是非常重要的。如果你很幸运你的编辑器支持Erlang语言模式,那你就能更舒服的输入代码。但是没有它也能完美工作。这是你要输入进文件的代码:

-module(tut).
-export([double/1]).

double(X) ->
    2 * X.

不难猜到这个程序把数值翻倍。前两行代码会在后面讨论。让我们编译这个程序。可以在erlang shell中输入如下命令完成编译,其中c表示compile(编译)

3> c(tut).
{ok,tut}

{ok,tut}表示编译OK,如果它说“error”就表示你输入的代码有误。附加的错误消息告诉你哪里错了以便你修改代码然后重新编译程序。

现在运行这个程序

4> tut:double(10).
20

正如我们所期待的,10翻倍后是20。

现在让我们回到前两行代码。erlang程序写在文件里,每个文件都包含一个erlang模块。模块的第一行代码是模块名。[5]

-module(tut).

因此,这个模块是tut。注意行末的点“.”。存储模块的文件名字必须和模块名一样但不需要.erl扩展名。在这里文件的名字应该是tut.erl。当使用另一个模块的函数时,要使用module_name:function_name(arguments)语法。所以下面的代码意思是调用tut模块的double函数并传入实参“10”.

4> tut:double(10).

第二行表示tut包含函数double,它有一个参数(本例中是X):

-export([double/1]).

同时第二行也表示这个函数可以在该模块外被调用。关于这点稍后讨论。再次提醒不要漏掉行末的点。

现在举个复杂点的例子,计算一个数的阶乘。比如4的阶乘4*3*2*1等于24。

tut1.erl文件中输入下面代码:

-module(tut1).
-export([fac/1]).

fac(1) ->
    1;
fac(N) ->
    N * fac(N - 1).

所以这是一个模块,模块名为tut,包含一个函数fac,函数接受一个参数N

第一部分表示1的阶乘是1:

fac(1) ->
    1;

注意这个部分结尾用分号“;”表示下面还有其他部分。

第二个部分表示N的阶乘是N乘以N-1的阶乘:

fac(N) ->
    N * fac(N - 1).

还要注意这个部分结尾用点“.”表示下面没有了。

编译文件

5> c(tut1).
{ok,tut1}

现在计算4的阶乘

6> tut1:fac(4).
24

在这里传给tut模块中的函数fac实参4。

一个函数可以有很多参数,让我们扩展模块tut1写一个函数计算两个数的乘积:

-module(tut1).
-export([fac/1, mult/2]).

fac(1) ->
    1;
fac(N) ->
    N * fac(N - 1).

mult(X, Y) ->
    X * Y.

注意也需要一行-export指明导出该模块的另一个接受两个参数的函数mult

编译:

7> c(tut1).
{ok,tut1}

试试使用新函数mult

8> tut1:mult(3,4).
12

例中实参是整型,函数代码中形参N,X,Y被称为变量。变量必须以大写字母开头[6]。比如NumberShortSizeAge

3. 原子

在Erlang中原子是另一种数据类型。原子以小写字母开头[7],比如charles, centimeter, inch。原子只是一个简单的名字,没有任何其他内容。它们不像变量一样还有值。

tut2.erl中输入下面代码。它可以在inch和centimeter之间来回转换。

-module(tut2).
-export([convert/2]).

convert(M, inch) ->
    M / 2.54;

convert(N, centimeter) ->
    N * 2.54.

编译:

9> c(tut2).
{ok,tut2}

测试:

10> tut2:convert(3, inch).
1.1811023622047243
11> tut2:convert(7, centimeter).
17.78

结果引入小数不解释,希望你能自己搞懂。

我们来看看如果给convert输入centimeterinch之外的东西会发生什么:

12> tut2:convert(3, miles).
** exception error: no function clause matching tut2:convert(3,miles) (tut2.erl, line 4)

convert函数的两个部分叫做clauses。如上所示,miles没有在函数的任何clause中。Erlang system不能匹配任何clause所以返回错误消息function_clause。shell格式化后的错误消息很nice,除此之外错误消息也会保存在shell历史中,可以调用命令v/1输出。

13> v(12).
{'EXIT',{function_clause,[{tut2,convert,
                                [3,miles],
                                [{file,"tut2.erl"},{line,4}]},
                          {erl_eval,do_apply,5,[{file,"erl_eval.erl"},{line,482}]},
                          {shell,exprs,7,[{file,"shell.erl"},{line,666}]},
                          {shell,eval_exprs,7,[{file,"shell.erl"},{line,621}]},
                          {shell,eval_loop,3,[{file,"shell.erl"},{line,606}]}]}}

4.Tuples

现在tut2的代码是很糟糕的编程风格,考虑:

tut2:convert(3, inch).

3是英寸吗?还是将要被转换为英寸的厘米?Erlang有一个方法可以将东西分组使之更容易理解。它们就是用花括号“{”和“}”围起来的tuple

所以{inch,3}意思是3英寸,{centimeter,5}意思是5厘米。现在让我们重新写一个英寸厘米互转的程序。
tut3.erl中输入下面的代码:

-module(tut3).
-export([convert_length/1]).

convert_length({centimeter, X}) ->
    {inch, X / 2.54};
convert_length({inch, Y}) ->
    {centimeter, Y * 2.54}.

编译并测试:

14> c(tut3).
{ok,tut3}
15> tut3:convert_length({inch, 5}).
{centimeter,12.7}
16> tut3:convert_length(tut3:convert_length({inch, 5})).
{inch,5.0}

注意16行那里5英寸先转换为厘米然后又转回英寸。换句话说,一个函数的结果可以作为另一个函数的参数。考虑16行是怎么工作的。首先{inch,5}作为一个参数传递给convert_length,然后{inch,5}匹配convert_length的第一个clauseconvert_length({centimeter,X})。可以看出{centimeter,X}不匹配{inch,5}。当前匹配失败,然后尝试下一个clauseconvert_length({inch,Y})。该匹配成功,Y绑定为5。

Tuples可以不止两个部分,事实上你想让它有多少它就能有多少,当然前提是每个部分必须是在erlang中有效的term。举个例子,要表示世界各城市的温度:

{moscow, {c, -10}}
{cape_town, {f, 70}}
{paris, {f, 28}}

Tuples有一个固定项数。每一项都称为element。在{moscow,{c,-10}}中,element 1是moscow,element 2是{c,-10}。这里c表示Celsius,f表示Fahrenheit

5. 列表

鉴于已经有tuple将元素分组,现在也需要一个工具表示一列元素。Erlang中列表由方括号“[”“]”表示。比如要表示世界各城市的温度可以:

[{moscow, {c, -10}}, {cape_town, {f, 70}}, {stockholm, {c, -4}},
 {paris, {f, 28}}, {london, {f, 36}}]

注意如果列表太长一行没法写完,没关系,Erlang允许我们在合理的地方换行。

在列表中获取一部分可以使用“|”,下面例子是最好的解释:

17> [First |TheRest] = [1,2,3,4,5].
[1,2,3,4,5]
18> First.
1
19> TheRest.
[2,3,4,5]

使用“|”分隔列表中的第一个元素和剩余元素。First绑定为1,TheRest绑定为[2,3,4,5]

另一个例子:

20> [E1, E2 | R] = [1,2,3,4,5,6,7].
[1,2,3,4,5,6,7]
21> E1.
1
22> E2.
2
23> R.
[3,4,5,6,7]

在这里可以看到使用“|”获取列表的头两个元素。如果你尝试获取更多元素,就会返回一个错误。注意列表没有元素这个特殊情况,“[]”:

[1,2]
25> A.
1
26> B.
2
27> C.
[]

在上面的例子中,使用的是新变量名而不是复用之前的变量名:First,TheRest,E1,E2,R,A,B,C。这是因为一个变量名在相同作用域内只能绑定一次值。关于这点后面讨论。

下面的例子显示了如何获取列表的长度。在tut4.erl中输入下面代码:

-module(tut4).

-export([list_length/1]).

list_length([]) ->
    0;    
list_length([First | Rest]) ->
    1 + list_length(Rest).

编译并测试:

28> c(tut4).
{ok,tut4}

29> tut4:list_length([1,2,3,4,5,6,7]).
7

解释:

list_length([]) ->
    0;

一个空列表的长度显然是0。

list_length([First | Rest]) ->
    1 + list_length(Rest).

包含第一个元素First和剩余元素Rest的列表的长度是1 + Rest长度
(致高级用户:这不是尾递归,关于这个函数有一个更好的实现)

通常,tuple用于其它语言的“record”或者“struct”所用的地方。列表用于表示大小变化的事物,即用于其它语言的链表所用的地方。

Erlang没有字符串类型。作为代替,字符串可以表示为Unicode字符列表。这意味着例如列表[97,98,99]等价于“abc”。Erlang shell“机智”的猜测你给的列表的意思,并给出它认为最合适的构型的输出,比如:

30> [97,98,99].
"abc"

6. Maps

Maps是一堆键值关联对的集合。键值对使用 “#{” 和”}”进行封装。要创建一个key关联value 42可以这么做:

> #{ "key" => 42 }.
#{"key" => 42}

让我们通过下面这个使用有趣特性的例子直接跳到一个深度。

下面的例子显示了如何通过maps引用颜色和α通道进而计算α混合(alpha blending)。在color.erl文件中输入下面代码:

-module(color).

-export([new/4, blend/2]).

-define(is_channel(V), (is_float(V) andalso V >= 0.0 andalso V =< 1.0)).

new(R,G,B,A) when ?is_channel(R), ?is_channel(G),
                  ?is_channel(B), ?is_channel(A) ->
    #{red => R, green => G, blue => B, alpha => A}.

blend(Src,Dst) ->
    blend(Src,Dst,alpha(Src,Dst)).

blend(Src,Dst,Alpha) when Alpha > 0.0 ->
    Dst#{
        red   := red(Src,Dst) / Alpha,
        green := green(Src,Dst) / Alpha,
        blue  := blue(Src,Dst) / Alpha,
        alpha := Alpha
    };
blend(_,Dst,_) ->
    Dst#{
        red   := 0.0,
        green := 0.0,
        blue  := 0.0,
        alpha := 0.0
    }.

alpha(#{alpha := SA}, #{alpha := DA}) ->
    SA + DA*(1.0 - SA).

red(#{red := SV, alpha := SA}, #{red := DV, alpha := DA}) ->
    SV*SA + DV*DA*(1.0 - SA).
green(#{green := SV, alpha := SA}, #{green := DV, alpha := DA}) ->
    SV*SA + DV*DA*(1.0 - SA).
blue(#{blue := SV, alpha := SA}, #{blue := DV, alpha := DA}) ->
    SV*SA + DV*DA*(1.0 - SA).

编译并测试:

> c(color).
{ok,color}
> C1 = color:new(0.3,0.4,0.5,1.0).
#{alpha => 1.0,blue => 0.5,green => 0.4,red => 0.3}
> C2 = color:new(1.0,0.8,0.1,0.3).
#{alpha => 0.3,blue => 0.1,green => 0.8,red => 1.0}
> color:blend(C1,C2).
#{alpha => 1.0,blue => 0.5,green => 0.4,red => 0.3}
> color:blend(C2,C1).
#{alpha => 1.0,blue => 0.38,green => 0.52,red => 0.51}

This example warrants some explanation:

-define(is_channel(V), (is_float(V) andalso V >= 0.0 andalso V =< 1.0)).

首先定义一个宏is_channel用于guard测试。为了方便起见这里使用它减少语法的杂糅。关于宏的更多信息,参见Preprocessor[8]。

new(R,G,B,A) when ?is_channel(R), ?is_channel(G),
                  ?is_channel(B), ?is_channel(A) ->
    #{red => R, green => G, blue => B, alpha => A}.

函数new/4创建一个map并且让键red,green,blue,alpha关联一个初始值。 在这里, 将宏?is_channel作用于每个参数,确保只有0.0到1.0之间的浮点值才被允许,只有在创建新map的时候才允许使用=>运算符。

使用new/4创建一个color,然后将colors传递给blend/2,它就能计算出混合后的颜色。

首先blend/2做的事是计算混合后的α通道

alpha(#{alpha := SA}, #{alpha := DA}) ->
    SA + DA*(1.0 - SA).

使用:=运算符获取参数中键alpha所关联的value。参数中的其他键会被忽略掉,只要求键alpha。
函数red/2, blue/2,green/2也是如此:

red(#{red := SV, alpha := SA}, #{red := DV, alpha := DA}) ->
    SV*SA + DV*DA*(1.0 - SA).

这里不同点是这几个函数获取参数中的两个键,其它的被忽略。

最后,让我们在blend/3中返回混合后的颜色:

blend(Src,Dst,Alpha) when Alpha > 0.0 ->
    Dst#{
        red   := red(Src,Dst) / Alpha,
        green := green(Src,Dst) / Alpha,
        blue  := blue(Src,Dst) / Alpha,
        alpha := Alpha
    };

Dst使用新通道值进行更新。更新一个已有的键关联的值的语法是使用:=运算符。

7. 标准模块和帮助手册

Erlang有很多标准模块帮你做一些事情。比如,io模块包含了很多用于格式化输入/输出的函数。要查找关于标准模块的信息,可以在shell或者命令提示符中使用erl -man命令。试试在操作系统shell上输入命令:

% erl -man io
ERLANG MODULE DEFINITION                                    io(3)

MODULE
     io - Standard I/O Server Interface Functions

DESCRIPTION
     This module provides an  interface  to  standard  Erlang  IO
     servers. The output functions all return ok if they are suc-
     ...

如果在你的系统上不能正常工作,文档也以HTML的形式随Erlang/OPT release一起发布了。你可以在网页上阅读这些HTML文档,也可以下载www.erlang.se(商业Erlang)www.erlang.org(开源Erlang) 的PDF来读。比如Erlang/OTP release R9B的文档:

http://www.erlang.org/doc/r9b/doc/index.html

8. 在终端上输出

如果上面这些例子能格式化输出一些内容就爽了,所以下面这个例子教我们一种的方法,使用io:format函数输出。像其他函数一样,你也可以在shell中测试io:format函数。

31> io:format("hello world~n", []).
hello world
ok
32> io:format("this outputs one Erlang term: ~w~n", [hello]).
this outputs one Erlang term: hello
ok
33> io:format("this outputs two Erlang terms: ~w~w~n", [hello, world]).
this outputs two Erlang terms: helloworld
ok
34> io:format("this outputs two Erlang terms: ~w ~w~n", [hello, world]).
this outputs two Erlang terms: hello world
ok

函数format/2(即格式化两个参数)接受两个列表参数。第一个列表总是用引号”“包含的。它是什么样就输出什么样,除了~w有点特别,它表示一个占位符,第二个列表参数将会代替这些占位符。每个 ~n都被替换为一个新行。如果一切顺利,函数io:format/2 本身返回一个原子atom。像其他Erlang函数一样,如果有错误发生它就会crash。这不是Erlang的缺陷,这是有意为之。Erlang有一个复杂的错误处理机制,这点会在后面说明。 作为一个练习,试试让io:format crash,应该很简单吧。 但是请注意虽然io:format 函数crashe了,Erlang shell却不会crash。

9 更复杂的例子

现在给出一个更复杂的例子总结你到目前为止学到的东西。假设你现在有一个世界各城市的温度的列表。列表中有一些城市用的华氏度,有一些用的摄氏度。首先让我们把他们全部转化为摄氏度,然后漂亮的输出这些数据。

%% This module is in file tut5.erl

-module(tut5).
-export([format_temps/1]).

%% Only this function is exported
format_temps([])->                        % No output for an empty list
    ok;
format_temps([City | Rest]) ->
    print_temp(convert_to_celsius(City)),
    format_temps(Rest).

convert_to_celsius({Name, {c, Temp}}) ->  % No conversion needed
    {Name, {c, Temp}};
convert_to_celsius({Name, {f, Temp}}) ->  % Do the conversion
    {Name, {c, (Temp - 32) * 5 / 9}}.

print_temp({Name, {c, Temp}}) ->
    io:format("~-15w ~w c~n", [Name, Temp]).
35> c(tut5).
{ok,tut5}
36> tut5:format_temps([{moscow, {c, -10}}, {cape_town, {f, 70}},
{stockholm, {c, -4}}, {paris, {f, 28}}, {london, {f, 36}}]).
moscow          -10 c
cape_town       21.11111111111111 c
stockholm       -4 c
paris           -2.2222222222222223 c
london          2.2222222222222223 c
ok

在说明程序是如何运行之前,注意代码中的注释。Erlang的注释以%字符开头直到行末。同时也要注意 -export([format_temps/1]). 只包含format_temps/1函数,其他函数都是局部函数,它们在tut5模块外面不可见。

还要注意在shell中测试程序的时候,如果一行太长就展开成两行。

当初次调用format_temps时,City 绑定值{moscow,{c,-10}}Rest绑定列表剩余部分。所以调用的是print_temp(convert_to_celsius({moscow,{c,-10}}))

这个函数调用把convert_to_celsius({moscow,{c,-10}})的结果传递给print_temp 。当像这样函数嵌套调用时,执行顺序是从里到外。也就是说,convert_to_celsius({moscow,{c,-10}})先被求值,然后print_temp({moscow,{c,-10}})再被求值。函数convert_to_celsius的工作类似于前面例子的convert_length

print_temp简单的调用io:format。注意~-15w表示在左边输出15个空白之后再输出占位符w实际表示的值。[9]

现在再对列表的剩余部分使用format_temps(Rest)。它所做的事类似于其他语言中的循环结构。(是的,这是递归,但是别担心)。所以同样的format_temps再次被调用。这时City的值绑定为{cape_town,{f,70}},如此反复直至列表为空,匹配第一个clauseformat_temps([])。这个clause简单的返回原子ok,程序结束。

10. 模式匹配,Guard,变量作用域

像这样如果能找出列表中最高/最低气温是很有用的。在扩展程序以寻找气温极值之前,让我们看看在函数中寻找列表最大值:

-module(tut6).
-export([list_max/1]).

list_max([Head|Rest]) ->
   list_max(Rest, Head).

list_max([], Res) ->
    Res;
list_max([Head|Rest], Result_so_far) when Head > Result_so_far ->
    list_max(Rest, Head);
list_max([Head|Rest], Result_so_far)  ->
    list_max(Rest, Result_so_far).
37> c(tut6).
{ok,tut6}
38> tut6:list_max([1,2,3,4,5,7,4,3,2,1]).
7

首先注意有两个函数名字一样,list_max(指list_max([Head|Rest])list_max([], Res))。然而,它们接受不同数目的参数。在Erlang中它们被视作完全不同的函数。
当你需要区分这些函数的时候,你直接写 Name/Arity 即可,Name是函数名,Arity是函数参数个数,在这是list_max/1,list_max/2

在这个例子中你遍历一个列表然后“带走”一个值,即Result_so_far。list_max/1简单的假定列表的最大值是表头元素然后把表头元素和列表剩余部分传递给list_max/2进行调用。按上面代码给出的参数将会调用list_max([2,3,4,5,7,4,3,2,1],1)。如果你尝试给list_max/1一个空列表或者给它一个不是列表的值,将引发一个错误。Erlang的哲学是不在函数中处理这类错误,而是在其他地方处理。关于这点后面会讨论。

list_max/2, 当Head > Result_so_far时使用Head代替Result_so_far。在->后使用when表示你只在when后面的测试为true时才使用这个函数(clause)。这类测试叫做guard。如果guard为false(即guard失败),就转而尝试下一个函数(clause)。在这里,如果Head不大于Result_so_far,那它一定小于或等于。这意味着下面函数(clause)不需要guard。

在guards中有这么一些有用的运算符:

  • < 小于
  • > 大于
  • == 等于
  • >= 大于等于
  • =< 小于等于
  • /= 不等于

要修改上面的程序让它找出列表中的最小值,你只需要把>改成<。(但那样做最好也把函数名改成list_min)。

之前提到过一个变量在它的作用域内只能绑定一次值。但如你所见Result_so_far绑定了很多次值。这没问题,因为每次你调用list_max/2都会创建一个新的作用域,然后Result_so_far就在那个新作用域中进行绑定。

另一个创建变量并绑定值的方法是使用匹配运算符=。当你写下M = 5,一个名为M的变量创建了并绑定值5。如果,在相同的作用域,你写下M =6,就会返回错误。在shell中试试:

39> M = 5.
5
40> M = 6.
** exception error: no match of right hand side value 6
41> M = M + 1.
** exception error: no match of right hand side value 6
42> N = M + 1.
6

匹配运算符在分离Erlang 一个项并创建新项时是非常有用的。

43> {X, Y} = {paris, {f, 28}}.
{paris,{f,28}}
44> X.
paris
45> Y.
{f,28}

在这里X绑定为paris,Y绑定为{f,28}

如果你尝试再使用其它城市,就会返回一个错误。

46> {X, Y} = {london, {f, 36}}.
** exception error: no match of right hand side value {london,{f,36}}

变量也可用于改善程序可读性。举个例子,在上面的list_max/2函数中,你可以这么写:

list_max([Head|Rest], Result_so_far) when Head > Result_so_far ->
    New_result_far = Head,
    list_max(Rest, New_result_far);

这样可能更清晰一些。

11. List进阶

记住”|”运算符可以用于获取列表的头元素:

47> [M1|T1] = [paris, london, rome].
[paris,london,rome]
48> M1.
paris
49> T1.
[london,rome]

“|”运算符也可以用于向列表头添加一个元素:

50> L1 = [madrid | T1].
[madrid,london,rome]
51> L1.
[madrid,london,rome]

现在举个例子说明它是如何工作的——反转列表:

-module(tut8).

-export([reverse/1]).

reverse(List) ->
    reverse(List, []).

reverse([Head | Rest], Reversed_List) ->
    reverse(Rest, [Head | Reversed_List]);
reverse([], Reversed_List) ->
    Reversed_List.
52> c(tut8).
{ok,tut8}
53> tut8:reverse([1,2,3]).
[3,2,1]

考虑一下Reversed_List是如何构造出来的。它最开始是空列表”[]”,然后列表头元素被取走并添加到Reversed_List里,如下所示:

reverse([1|2,3], []) =>
    reverse([2,3], [1|[]])

reverse([2|3], [1]) =>
    reverse([3], [2|[1])

reverse([3|[]], [2,1]) =>
    reverse([], [3|[2,1]])

reverse([], [3,2,1]) =>
    [3,2,1]

lists模块包含了很多操纵列表的函数,比如反转列表。所以在写一个列表操纵的函数之前最好先检查一下是否它早已存在。

现在让我们回到城市温度的例子,但是使用更结构化的方法。首先让我们把列表转化成只包含摄氏度:

-module(tut7).
-export([format_temps/1]).

format_temps(List_of_cities) ->
    convert_list_to_c(List_of_cities).

convert_list_to_c([{Name, {f, F}} | Rest]) ->
    Converted_City = {Name, {c, (F -32)* 5 / 9}},
    [Converted_City | convert_list_to_c(Rest)];

convert_list_to_c([City | Rest]) ->
    [City | convert_list_to_c(Rest)];

convert_list_to_c([]) ->
    [].

测试这个函数:

54> c(tut7).
{ok, tut7}.
55> tut7:format_temps([{moscow, {c, -10}}, {cape_town, {f, 70}},
{stockholm, {c, -4}}, {paris, {f, 28}}, {london, {f, 36}}]).
[{moscow,{c,-10}},
 {cape_town,{c,21.11111111111111}},
 {stockholm,{c,-4}},
 {paris,{c,-2.2222222222222223}},
 {london,{c,2.2222222222222223}}]

解释:

format_temps(List_of_cities) ->
    convert_list_to_c(List_of_cities).

在这里format_temps/1调用convert_list_to_c/1。而convert_list_to_c/1取列表List_of_cities头元素,如果是摄氏度就什么也不做如果是华氏度还需要转化为摄氏度。 “|”运算符用于把转化后的元素添加到转换后的列表中,像这样:

[Converted_City | convert_list_to_c(Rest)];

或者:

[City | convert_list_to_c(Rest)];

这个操作将会持续到到达列表尾部,即列表为空。

convert_list_to_c([]) ->
    [].

当列表完成转换后,添加一个输出的函数:

-module(tut7).
-export([format_temps/1]).

format_temps(List_of_cities) ->
    Converted_List = convert_list_to_c(List_of_cities),
    print_temp(Converted_List).

convert_list_to_c([{Name, {f, F}} | Rest]) ->
    Converted_City = {Name, {c, (F -32)* 5 / 9}},
    [Converted_City | convert_list_to_c(Rest)];

convert_list_to_c([City | Rest]) ->
    [City | convert_list_to_c(Rest)];

convert_list_to_c([]) ->
    [].

print_temp([{Name, {c, Temp}} | Rest]) ->
    io:format("~-15w ~w c~n", [Name, Temp]),
    print_temp(Rest);
print_temp([]) ->
    ok.
56> c(tut7).
{ok,tut7}
57> tut7:format_temps([{moscow, {c, -10}}, {cape_town, {f, 70}},
{stockholm, {c, -4}}, {paris, {f, 28}}, {london, {f, 36}}]).
moscow          -10 c
cape_town       21.11111111111111 c
stockholm       -4 c
paris           -2.2222222222222223 c
london          2.2222222222222223 c
ok

还必须添加一个函数寻找城市最高气温最低气温。下面的程序不是最佳解决方案,因为它遍历了列表四次。但最好是先力求清晰和正确,并且只有在需要时才改善程序效率。

-module(tut7).
-export([format_temps/1]).

format_temps(List_of_cities) ->
    Converted_List = convert_list_to_c(List_of_cities),
    print_temp(Converted_List),
    {Max_city, Min_city} = find_max_and_min(Converted_List),
    print_max_and_min(Max_city, Min_city).

convert_list_to_c([{Name, {f, Temp}} | Rest]) ->
    Converted_City = {Name, {c, (Temp -32)* 5 / 9}},
    [Converted_City | convert_list_to_c(Rest)];

convert_list_to_c([City | Rest]) ->
    [City | convert_list_to_c(Rest)];

convert_list_to_c([]) ->
    [].

print_temp([{Name, {c, Temp}} | Rest]) ->
    io:format("~-15w ~w c~n", [Name, Temp]),
    print_temp(Rest);
print_temp([]) ->
    ok.

find_max_and_min([City | Rest]) ->
    find_max_and_min(Rest, City, City).

find_max_and_min([{Name, {c, Temp}} | Rest], 
         {Max_Name, {c, Max_Temp}}, 
         {Min_Name, {c, Min_Temp}}) ->
    if 
        Temp > Max_Temp ->
            Max_City = {Name, {c, Temp}};           % Change
        true -> 
            Max_City = {Max_Name, {c, Max_Temp}} % Unchanged
    end,
    if
         Temp < Min_Temp ->
            Min_City = {Name, {c, Temp}};           % Change
        true -> 
            Min_City = {Min_Name, {c, Min_Temp}} % Unchanged
    end,
    find_max_and_min(Rest, Max_City, Min_City);

find_max_and_min([], Max_City, Min_City) ->
    {Max_City, Min_City}.

print_max_and_min({Max_name, {c, Max_temp}}, {Min_name, {c, Min_temp}}) ->
    io:format("Max temperature was ~w c in ~w~n", [Max_temp, Max_name]),
    io:format("Min temperature was ~w c in ~w~n", [Min_temp, Min_name]).
58> c(tut7).
{ok, tut7}
59> tut7:format_temps([{moscow, {c, -10}}, {cape_town, {f, 70}},
{stockholm, {c, -4}}, {paris, {f, 28}}, {london, {f, 36}}]).
moscow          -10 c
cape_town       21.11111111111111 c
stockholm       -4 c
paris           -2.2222222222222223 c
london          2.2222222222222223 c
Max temperature was 21.11111111111111 c in cape_town
Min temperature was -10 c in moscow
ok

12. If 和Case

函数find_max_and_min可以找出气温极值。一个新的结构,if, 在这里引入,也可以完成这项工作。If的工作机制如下:

if
    Condition 1 ->
        Action 1;
    Condition 2 ->
        Action 2;
    Condition 3 ->
        Action 3;
    Condition 4 ->
        Action 4
end

注意在end之前没有”;”,Conditions类似于guards,即测试成功或者失败。Erlang从上面开始测试直到找到一个条件(Condition)测试成功。然后它求值(执行)条件后跟着的Action并且忽略其它的条件和行为(Action)。如果没有条件匹配,就会引发运行时异常。通常有一个条件总是能通过测试即true原子。它通常位于if的最后一个条件,对应的action执行其他条件都测试失败后该做的事情。

下面短小的程序显示了if是如何工作的:

-module(tut9).
-export([test_if/2]).

test_if(A, B) ->
    if 
        A == 5 ->
            io:format("A == 5~n", []),
            a_equals_5;
        B == 6 ->
            io:format("B == 6~n", []),
            b_equals_6;
        A == 2, B == 3 ->                      %That is A equals 2 and B equals 3
            io:format("A == 2, B == 3~n", []),
            a_equals_2_b_equals_3;
        A == 1 ; B == 7 ->                     %That is A equals 1 or B equals 7
            io:format("A == 1 ; B == 7~n", []),
            a_equals_1_or_b_equals_7
    end.

测试这个程序:

60> c(tut9).
{ok,tut9}
61> tut9:test_if(5,33).
A == 5
a_equals_5
62> tut9:test_if(33,6).
B == 6
b_equals_6
63> tut9:test_if(2, 3).
A == 2, B == 3
a_equals_2_b_equals_3
64> tut9:test_if(1, 33).
A == 1 ; B == 7
a_equals_1_or_b_equals_7
65> tut9:test_if(33, 7).
A == 1 ; B == 7
a_equals_1_or_b_equals_7
66> tut9:test_if(33, 33).
** exception error: no true branch found when evaluating an if expression
     in function  tut9:test_if/2 (tut9.erl, line 5)

tut9:test_if(33,33)没有使任何条件测试成功。这引发了一个运行时错误,在这里shell漂亮的格式化了错误消息。

case是Erlang的另一个结构。回忆一下convert_length函数是这样写的:

convert_length({centimeter, X}) ->
    {inch, X / 2.54};
convert_length({inch, Y}) ->
    {centimeter, Y * 2.54}.

同样的程序也可以这样写:

-module(tut10).
-export([convert_length/1]).

convert_length(Length) ->
    case Length of
        {centimeter, X} ->
            {inch, X / 2.54};
        {inch, Y} ->
            {centimeter, Y * 2.54}
    end.
67> c(tut10).
{ok,tut10}
68> tut10:convert_length({inch, 6}).
{centimeter,15.24}
69> tut10:convert_length({centimeter, 2.5}).
{inch,0.984251968503937}

caseif都有返回值,也就是说,在上面的case例子中返回{inch,X/2.54}或者{centimeter,Y*2.54}。case的行为也可以使用guards修改。下面的例子解释了这一点。给它指定的年月它告诉我们该月有多少天。

-module(tut11).
-export([month_length/2]).

month_length(Year, Month) ->
    %% All years divisible by 400 are leap
    %% Years divisible by 100 are not leap (except the 400 rule above)
    %% Years divisible by 4 are leap (except the 100 rule above)
    Leap = if
        trunc(Year / 400) * 400 == Year ->
            leap;
        trunc(Year / 100) * 100 == Year ->
            not_leap;
        trunc(Year / 4) * 4 == Year ->
            leap;
        true ->
            not_leap
    end,  
    case Month of
        sep -> 30;
        apr -> 30;
        jun -> 30;
        nov -> 30;
        feb when Leap == leap -> 29;
        feb -> 28;
        jan -> 31;
        mar -> 31;
        may -> 31;
        jul -> 31;
        aug -> 31;
        oct -> 31;
        dec -> 31
    end.
70> c(tut11).
{ok,tut11}
71> tut11:month_length(2004, feb).
29
72> tut11:month_length(2003, feb).
28
73> tut11:month_length(1947, aug).
31

13. 内置函数(BIFs)

BIFs是指出于某些原因内置于Erlang虚拟机中的函数。BIFs通常是以实用性为出发点实现的,所以不可能低效实现。一些BIFs可以直接使用函数名进行调用(通常情况下应该是模块名:函数名(module_name:function_name)),只是因为它们默认属于erlang模块。比如,调用BIF函数trunc等价于erlang:trunc

如下所示,首先检查是否闰年。如果一个年份能整除400,那它就是闰年。为了决定是否闰年,首先将年份除以400然后使用BIF函数trunc截去小数部分。然后再次乘以400,然后看是否再次返回相同的值。比如,2004年:

2004 / 400 = 5.01
trunc(5.01) = 5
5 * 400 = 2000

2000现在不同于2004,所以2004不能被400整除。2000年:

2000 / 400 = 5.0
trunc(5.0) = 5
5 * 400 = 2000

显然,2000是一个闰年。

(注意这里的内容涉及12. If和Case的例子)接着的两个trunc调用同样类似于上面的测试,检测一个年份是否为闰年。第一个if返回leapnot_leap,并将结果绑定到Leap变量上。这个变量会用于后面一堆case语句中的feb case的guard部分,来告诉我们这个年份的二月有多长。

这个例子展示了trunc函数的使用。用erlang的运算符rem会更容易,rem会返回被除数除以除数后的余数,如下所示:

74> 2004 rem 400.
4

可以代替:

trunc(Year / 400) * 400 == Year ->
    leap;

它也可以这样写:

Year rem 400 == 0 ->
    leap;

在Erlang中有很多像trunc一样的内置函数。只有极少数内置函数可以用于guard中,你不能在guard中使用自定义的函数[10] (致高级读者:这是为了确保guard没有副作用(译注:关于副作用可以参见我的博客文章http://blog.csdn.net/racaljk/article/details/76020371 ,小广告..))
让我们在shell中玩玩下面这些函数:

75> trunc(5.6).
5
76> round(5.6).
6
77> length([a,b,c,d]).
4
78> float(5).
5.0
79> is_atom(hello).
true
80> is_atom("hello").
false
81> is_tuple({paris, {c, 30}}).
true
82> is_tuple([paris, {c, 30}]).
false

它们都可以在guard中使用。接下来几个内置函数就不能了:

83> atom_to_list(hello).
"hello"
84> list_to_atom("goodbye").
goodbye
85> integer_to_list(22).
"22"

这三个内置函数所做的转换用Erlang实现将会非常困难(或者说不可能)。

14. 高阶函数

Erlang,像大多数函数式编程语言一样,也有高阶函数。下面就是一个例子,使用shell:

86> Xf = fun(X) -> X * 2 end.
#Fun<erl_eval.5.123085357>
87> Xf(5).
10

定义一个函数将一个数值翻倍,并把这个函数赋值(译注:原文assign)给一个变量。因此Xf(5)将返回10。对于列表操作有两个有用的函数:foreachmap,定义如下:

foreach(Fun, [First|Rest]) ->
    Fun(First),
    foreach(Fun, Rest);
foreach(Fun, []) ->
    ok.

map(Fun, [First|Rest]) -> 
    [Fun(First)|map(Fun,Rest)];
map(Fun, []) -> 
    [].

这两个函数由标准模块lists提供。foreach接受一个列表和一个函数参数,将函数作用于列表的每一个元素。map创建一个新列表,并将函数作用于列表每个元素后的返回值添加到新列表。 回到shell,使用map和高阶函数将列表每个元素加三:

88> Add_3 = fun(X) -> X + 3 end.
#Fun<erl_eval.5.123085357>
89> lists:map(Add_3, [1,2,3]).
[4,5,6]

让我们(再次)打印城市列表的气温:

90> Print_City = fun({City, {X, Temp}}) -> io:format("~-15w ~w ~w~n",
[City, X, Temp]) end.
#Fun<erl_eval.5.123085357>
91> lists:foreach(Print_City, [{moscow, {c, -10}}, {cape_town, {f, 70}},
{stockholm, {c, -4}}, {paris, {f, 28}}, {london, {f, 36}}]).
moscow          c -10
cape_town       f 70
stockholm       c -4
paris           f 28
london          f 36
ok

现在定义一个函数,把它们的气温全部转换为摄氏度:

-module(tut13).

-export([convert_list_to_c/1]).

convert_to_c({Name, {f, Temp}}) ->
    {Name, {c, trunc((Temp - 32) * 5 / 9)}};
convert_to_c({Name, {c, Temp}}) ->
    {Name, {c, Temp}}.

convert_list_to_c(List) ->
    lists:map(fun convert_to_c/1, List).
92> tut13:convert_list_to_c([{moscow, {c, -10}}, {cape_town, {f, 70}},
{stockholm, {c, -4}}, {paris, {f, 28}}, {london, {f, 36}}]).
[{moscow,{c,-10}},
 {cape_town,{c,21}},
 {stockholm,{c,-4}},
 {paris,{c,-2}},
 {london,{c,2}}]

convert_to_c函数和之前一样,但是这里用fun引用它。

lists:map(fun convert_to_c/1, List)

当一个函数在别处定义,然后使用fun引用,需要写出函数名/函数参数个数(记住斜杠后面的数字是函数参数个数)。所以就在lists:map(fun convert_to_c/1, List)中如是写道。如上所示,convert_list_to_c变得更短更容易理解了。

标准模块lists也包含了一个排序函数sort(Fun, List),其中Fun接受两个参数。这个函数判断如果第一个参数小于第二个参数就返回true,否则false
convert_list_to_c中添加排序:

-module(tut13).

-export([convert_list_to_c/1]).

convert_to_c({Name, {f, Temp}}) ->
    {Name, {c, trunc((Temp - 32) * 5 / 9)}};
convert_to_c({Name, {c, Temp}}) ->
    {Name, {c, Temp}}.

convert_list_to_c(List) ->
    New_list = lists:map(fun convert_to_c/1, List),
    lists:sort(fun({_, {c, Temp1}}, {_, {c, Temp2}}) ->
                       Temp1 < Temp2 end, New_list).
93> c(tut13).
{ok,tut13}
94> tut13:convert_list_to_c([{moscow, {c, -10}}, {cape_town, {f, 70}},
{stockholm, {c, -4}}, {paris, {f, 28}}, {london, {f, 36}}]).
[{moscow,{c,-10}},
 {stockholm,{c,-4}},
 {paris,{c,-2}},
 {london,{c,2}},
 {cape_town,{c,21}}]

可以看到排序中使用了这么一个函数:

fun({_, {c, Temp1}}, {_, {c, Temp2}}) -> Temp1 < Temp2 end,

这里引入了匿名变量”_”的概念。这是获取一个变量值但是忽略它的简写形式。它可以在任何合适的地方使用,不仅仅是在fun中。如果Temp1小于Temp2那么Temp1 < Temp2返回true。

Erlang 顺序编程(Erlang Sequential Programming)全文完。

引用

【1】http://erlang.org/doc/man/shell.html
【2】http://erlang.org/doc/apps/erts/tty.html
【3】http://erlang.org/doc/reference_manual/expressions.html
【4】http://erlang.org/doc/apps/tools/erlang_mode_chapter.html
【5】http://erlang.org/doc/reference_manual/modules.html
【6】http://erlang.org/doc/reference_manual/expressions.html
【7】http://erlang.org/doc/reference_manual/data_types.html
【8】http://erlang.org/doc/reference_manual/macros.html
【9】http://erlang.org/doc/man/io.html#fwrite-1
【10】http://erlang.org/doc/reference_manual/expressions.html

猜你喜欢

转载自blog.csdn.net/u013524455/article/details/76022010