脚踏实地学 Erlang —— 基础篇


一、Erlang Shell

返回目录

  1. 启动 Erlang Shell

    erl
    
    参数说明
    -noshell不显示 Erlang Shell,执行任务时使用

    windows系统

    erl  %% 可以使用重定向标准输入输出 或 管道
    werl %% 支持编辑快捷键,显示正确的编码字符
    
  2. 全局函数/命令

    命令说明
    help().打印可用的shell函数
    h().打印先前输入过的命令
    v(N).取出第N号提示符对应的计算结果
    f()让 shell 忘记现有的任何绑定
    cd(Dir).更改当前目录(Dir是双引号字符串)
    ls().打印目录内容
    pwd().打印当前目录路径
    i().打印当前系统的运行时信息
    memory().打印内存使用信息
    q().
    init:stop().
    有序的方式关停,并退出 Erlang Shel
    halt().
    erlang:halt().
    立即退出 Erlang Shel

    Erlang Shell 里不能输入附注("-"开头的语句)

  3. 快捷键

    命令说明
    Ctrl + C调出BREAK菜单
    Ctrl + G调出用户开关命令菜单,
    输入h 显示可用命令
    回复控制:i 回车 c 回车
    Ctrl + A行首
    Ctrl + D删除当前字符
    Ctrl + E行尾
    Ctrl + F 或→向前的字符
    Ctrl + B 或←向后的字符
    Ctrl + P 或 ↑前一行
    Ctrl + N 或 ↓下一行
    Ctrl + T调换最近两个字符的位置
    Tab尝试扩展当前模块或函数的名称

二、操作符

返回目录

操作符说明
%行注释
()优先运算符
=赋值, 匹配
,元素分隔符
;句子分隔符
.语句结束符

算数操作符

返回目录

操作符描述参数类型优先级
+ X数字1
- X数字1
X * Y数字2
X / Y浮点除数字2
bnot X按位取反整数2
X div Y整除整数2
X rem Y取余整数2
X band Y按位与整数2
X + Y数字3
X - Y数字3
X bor Y按位或整数3
X bxor Y按位异或整数3
X bsl N把 X 向左算术位移 N 位整数3
X bsr N把 X 向右算术位移 N 位整数3

逻辑操作符

返回目录

操作符说明
not B1逻辑非
B1 and B2逻辑与
B1 andalso B2短路逻辑与
B1 or B2逻辑或
B1 orelse B2短路逻辑或
B1 xor B2逻辑异或

B1和B2都必须是布尔值或者执行结果为布尔值的表达式

  • andalso 与 and 的区别
    % 当 X 为 0 时,f(X) 里的关卡会失败,但 g(X) 里的关卡会成功
    f(X) when (X == 0) or (1/X > 2) -> ...
    g(X) when (X == 0) orelse (1/X > 2) -> ...
    

比较操作符

返回目录

允许不同变量进行比较操作

操作符说明
< =<小于 小于等于
> >=大于 大于等于
=:=
==
等于(区分整型和浮点型)
等于(不区分数字类型)
=/=
/=
不等于(区分整型和浮点型)
不等于(不区分数字类型)

数据类型之间的大小顺序:
number < atom < reference < fun < port < pid < tuple < list < bit string

三、变量

返回目录

  1. 变量的首字母大写,变量的值不可改变
  2. 定义变量
    1> One.  %% 使用未绑定的变量,报错
    * 1: variable 'One' is unbound
    2> One = 1. %% 变量 One 的值为 1
    1
    3> Un = Uno = One = 1. %% 变量Un、Uno、One 的值为 1
    1
    4> Two = One + One. %% 变量 Two 的值为 2
    2
    5> Two = 2. %% 变量 Two 的值为 2
    2
    6> Two = Two + 1. %% 变量 Two 的值 为 3,报错,值不能改变
    ** exception error: no match of right hand side value 3
    

模式匹配

返回目录

用模式匹配来代替赋值操作

模式 = 单元结果
_能重复匹配任何数据
{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]

四、数据类型

返回目录

基础数据类型:

类型说明备注
Integer整型
Float浮点型
Atom原子型
bool布尔型
Tuple元组型
List列表
String字符串
fun函数型
Union
binary二进制型
bitstring二进制串
record记录
map映射组

内部类型:

类型说明备注
any()
none()
pid()进程id
port()端口
reference()
[]
Atom
binary()
float()
Fun
Integer
Tuple
Union
UserDefined
ref

整型 integer

返回目录

  1. 定义整数
    1> N10 = 15.          %% 十进制
    
  2. 进制整数,用 base#value 表示2 ~ 36进制数,超出9的数用 abc…z 等字符代替。
    1> N2 = 2#00101010.   %% 二进制
    2> N8 = 8#123756322.  %% 八进制
    3> N16 = 16#af6bfa23. %% 十六进制
    
  3. 用 ASCII 编码表示整数或字符,格式:$char ???
    1> $a.   %% 字符 a 的ASCII 编码是 97
    2> $1.   %% 字符 1 的ASCII 编码是 49
    3> $\n.  %% 换行符 的ASCII 编码是 10
    4> $\^c. %% Ctrl+C 的ASCII 编码是 3
    

浮点型 float

返回目录

采用 IEEE 754-1985 格式的 64 位双精度浮点数。可以表示绝对值在 10-323 到 10308 范围内的实数。由五部分组成:

  1. 可选的正负号
  2. 整数部分
  3. 小数点
  4. 小数部分
  5. 可选的指数部分
  • 定义浮点数
    1> F1 = 1.0.
    2> F2 = 3.1415926.
    3> F3 = -2.3e+6.
    4> F4 = 23.56E-27.
    

原子 atom

返回目录

  1. 原子被用于表示常量值,是全局性的。
  2. 原子以小写字母开头,后接一串字母、数字、下划线 (_) 或at (@) 符号
  3. 用单引号 包裹原子名,用来创建除字母和数字以外字符的原子(含首字母大写、特殊符号、句子等)
  4. 原子值就是它本身,cat 与 ‘cat’ 等价
  5. 原子在32位系统中占4个字节,在64位系统中占8个字节。
  6. 原子都会被引用到一个原子表中,该表不会被垃圾回收。当内存耗尽或原子个数超过 1 048 577时,系统会崩溃。所以不要动态创建原子,
  • 定义原子
    1> cat.
    cat
    2> 'Cat'.
    'Cat'
    3> cat =:= 'cat'.
    true
    

布尔型 bool

返回目录

Erlang 中没有布尔型,true 和 false 都是原子

元组 tuple

返回目录

元组会在定义时自动创建,不再使用时则被销毁

  1. 定义元组
    1> P1 = {10, 45}.                              %% 声明元组
    2> P2 = {point, 10, 45}.                       %% 增加可读性
    3> P3 = {person, {name, joe}, {height, 1.82}}. %% 元组嵌套
    
  2. 提取元组的值
    1> {X, Y} = {10, 45}.               %% X为10,Y为45
    2> {point, X, Y} = {point, 10, 45}. %% X为10,Y为45
    3> {point, C, C} = {point, 25, 25}. %% C为25,如值不同则报错
    4> {_, {_, Who}, _} = P3.           %% Who为joe。_为通配符
    

列表 list

返回目录

列表的第一个元素叫表头(head),其余的叫表尾(tail)。注意列表头可以是任何事物,但列表尾通常仍然是个列表。

举例:T是个列表,[H|T]也是个列表,头是H,尾是T。竖线(|)把头与尾隔开。[]是个空列表。

访问列表头是一种非常高效的操作,先处理表头,再处理表尾。

  1. 定义列表

    1> Drawing = [{square,{10,10},10}, {triang,{15,10},{25,10},{30,40}}].
    2> ThingsToBuy = [{apple,10}, {pears,6}, {milk,3}].
    %% 多表头
    3> ThingsToBuy1 = [{oranges,4}, {newspaper,1} | ThingsToBuy].
    
  2. 提取列表的元素

    1> [Buy1 | ThingsToBuy2] = ThingsToBuy1.
    %% Buy1 = {oranges, 4}
    %% ThingsToBuy2 = [{newspaper,1}, {apples,10}, {pears,6}, {milk,3}]
    
    %% 多表头
    2> [Buy2, Buy3 | ThingsToBuy3] = ThingsToBuy2.
    %% Buy2 = {newspaper,1}
    %% Buy3 = {apples,10}
    %% ThingsToBuy3 = [{pears,6}, {milk,3}]
    

    如果有一个非空列表 L,那么表达式 [X | Y] = L (X, Y都是未绑定变量)会提取列表头作为X,列表尾作为Y。

  3. 列表元素操作
    ++ 可以把列表粘合起来,-- 可以删除列表中的元素。

    1> [1, 2, 3] ++ [4, 5].
    [1, 2, 3, 4, 5]
    2> [1, 2, 3, 4, 5] -- [1, 2, 3].
    [4, 5]
    3> [2, 4, 2] -- [2, 4].
    [2]
    

    两个操作符都是右结合的。所以都要从右向左进行操作。

    1> [1, 2, 3] -- [1, 2] -- [3].
    [3]
    2> [1, 2, 3] -- [1, 2] -- [2].
    [2, 3]
    

    数据量大时不推荐使用,效率低

列表推导 list comprehension

返回目录

列表推导(list comprehension)是无需使用fun、map或filter就能创建列表的表达式。它让 程序变得更短,更容易理解。

构造自然顺序的列表

  1. 总是向列表头添加元素。
  2. 从输入列表的头部提取元素,然后添加到输出列表的头部,形成的结果与输入列表顺序相反的输出列表。
  3. 如果顺序很重要,调用lists:reverse/1函数(高度优化过)
  4. 避免违反以上的建议。
  1. 普通形式 [ expression || Pattern <- ListExpr ]

    英文中文说明
    expression表达式用于生成新的元素
    Pattern模式用于匹配 list 中的各个元素
    ListExpr列表用生成新列表的源列表

    示例:

    1> [ 2*X || X <- [1,2,3,4,5] ].
    [2,4,6,8,10]
    
    2> map(F,L) -> [F(X) || X <- L].
    %% 等价于 map(F, [H | T]) -> [F(H) | map(F, T)].
    

    从 [1,2,3,4,5] 中按顺序挨个取出一个元素匹配给 X,然后用 2*X 计算出新列表相应顺序的元素值。[2,4,6,8,10]

  2. 常见形式 [ X || Qualifier1, Qualifier2, …]

    英文中文说明
    X表达式任意一条表达式
    Qualifier限定符可以是 generator 或 bitstring、和 filter

    带过滤器的示例:

    1> [X || X <- [1,2,3,4], X rem 2 =:= 0].
    [2, 4]
    

    Qualifier

    英文中文形式说明
    generator生成器Pattern <- ListExprListExpr:必须是能得出列表的表达式
    bitstring位串生成器BitStringPattern <= BitStringExprBitStringExpr 必须是能得出 bitstring 的表达式
    filter过滤器既可以是判断函数(返回true或false),也可以是布尔表达式列表推导里的生成器部分起着过滤器的作用

    多个生成器的示例:

    1> [X+Y || X <- [1, 2], Y <- [3, 4]].
    [4, 5, 5, 6] %% 会执行 1+3、1+4、2+3、2+4 的操作
    

    生成器启动过滤的作用的示例:

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

二进制型 binary

返回目录

二进制型节省空间,用来保存大量的原始数据(大型字符串、文件内容)

  1. 总位数是 8 的倍数。
  2. 定义二进制型:<<E1, E2, ..., En>>
    1> B1 = <<5, 10, 20>>.  %% 整数(0~255)
    <<5,10,20>>
    2> B2 = <<"hello">>.    %% 字符串
    <<"hello">>
    3> B3 = <<65, $B, 67>>. %% 内容是的 ASCII 码
    <<"ABC">>
    

位语法

返回目录

用来定义二进制型位串

  1. 格式:<<E1, E2, ..., En>>Ei为其中某一元素。
    Ei = Value |
    Value:Size |
    Value/TypeSpecifierList |
    Value:Size/TypeSpecifierList

  2. Value

    范围类型示例
    定义时字符串
    整数(0~255)
    浮点数
    二进制型
    已绑定变量
    表达式
    <<“example”>>
    <<11, 22, 33>>
    <<>>
    <<>>
    A = 12. <<A, A>>.
    <<>>
    模式匹配未绑定变量
    字符串
    整数(0~255)
    浮点数
    二进制型
    已绑定变量
    <<X1, X2>> = <<-44>>
    <<>>
    <<>>
    <<>>
    <<>>
    <<>>
  3. Size 所占位数

    范围类型示例
    定义时整数
    表达式
    模式匹配整数
    已绑定变量

    Size 的默认值会根据 Value 的类型变化。在模式匹配里,默认值只对最后那个元素有效。

    
    
    类型默认值
    整数8
    浮点数64
    二进制型是其大小
  4. TypeSpecifierList 类型指定列表
    形式:End-Sign-Type-Unit;顺序任意,都可省略(为默认值)

    形式描述说明
    End字节序列big:高位优先,网络字节顺序(network byte order)。默认值
    little:低位优先
    native:在运行时根据机器的 CPU 来确定
    Sign符号类型unsigned 无符号,默认值
    signed:有符号
    只用于模式匹配
    Type数据类型integer,默认值
    float
    binary | bytes
    bitstring | bits
    utf8
    utf16
    utf32
    Unit数据单位写法:unit:1|2|…256
    integer、float、bitstring 的默认值是1
    binary 的默认值为 8
    utf8、utf16、utf32 类型无需定义

    这个值的所占空间 = Size * Unit

待整理的示例:

%% 存储24位真彩色
1> Color = 16#F09A29.
15768105

2> pixel = <<Color:24>>.
<<240, 154, 41>>

3> Pixels = <<213, 45, 132, 64, 76, 32, 76, 0, 0, 234, 32, 15>>.
<<213,45,132,64,76,32,76,0,0,234,32,15>>

4> <<Pix1:24, Pix2:24, Pix3:24, Pix4:24>> = Pixels.
<<213,45,132,64,76,32,76,0,0,234,32,15>>

5> <<R:8, G:8, B:8>> = <<Pix1:24>>.
<<213,45,132>>

6> R.
213

7> <<R1:8, Rest/binary>> = Pixels.
<<213,45,132,64,76,32,76,0,0,234,32,15>>

8> R1.
213

%% 获取 Size
<<Size:4, Data:Size/binary>> = <<1,2,3,4,5,6>>

M = <<X:3, Y:7, Z:6>> %M的类型是binary 因为数据的总长度是16位,可以被8整除
M = <<X:2, Y:7, Z:6>> %M的类型是bitstring 因为总位数是15

1> Red = 2.
2> Green = 61.
3> Blue = 20.
% 打包颜色, 定义一个16位数据的双字节二进制型
4> Mem = <<Red:5, Green:6, Blue:5>>.
<<23,180>>
% 解包颜色
5> <<R1:5, G1:6, B1:5>> = Mem.

位串:bitstring

返回目录

位串(bitstring)的模式匹配是位级操作,这样我们就能在单次操作里打包和解包 位的序列。

这对编写需要操作位级数据的代码来说极其有用,例如没有按照8位边界对齐的数据或者可变长度数据,它们的数据长度用位而不是字节来表示。

1> B1 = <<1:8>>.
<<1>>
2> byte_size(B1).
1
3> is_binary(B1).
true
4> is_bitstring(B1).
true

5> B2 = <<1: 17>>.
<<0,0,1:1>>
6> is_binary(B2).
false
7> is_bitstring(B2).
true
8> byte_size(B2).
3  %% 字节大小
9> bit_size(B2).
17 %% 位大小

因为文件和套接字使用的单位是字节,所以不能把一个位串写入文件或套接字。

位推导 bit comprehension

返回目录

提取出字节里各个单独的位,
遍历二进制型并生成列表或二进制型。

1> B = <<16#5f>>.
<<"_">> %% 包含单个字节的二进制型
2> [X || <<X:1>> <= B].
[0,1,0,1,1,1,1,1]   %% 包含字节里各个位的列表
3> << <<X>> || <<X:1>> <= B >>.
<<0,1,0,1,1,1,1,1>> %% 包含这些位的二进制型

字符串 string

返回目录

Erlang 用列表和二进制型表示字符串。

  1. 定义字符串

    1> Name = "Kate".
    "Kate"
    

    在 shell 中,如果列表内的所有整数都代表可打印字符(ASCII 码),那么就会将其打印成字符串字面量,否则,打印成列表。

    2> [83, 117, 114, 112, 114, 105, 115, 101].
    "Surprise"
    
    3> [1, 83, 117, 114, 112, 114, 105, 115, 101].
    [1,83,117,114,112,114,105,115,101]
    
  2. $char 格式输出字符的 ASCII 码

    1> $a.
    97
    2> $1.
    49
    3> $\n.
    10
    4> $\^c.
    3
    5> I = $s.
    115
    6> [I-32, $u, $r, $p, $r, $i, $s, $e].
    "Surprise"
    
  3. 输出数字或 Unicode 字符

    1> X = [97,98,99].
    "abc"
    2> io:format("~w~n",["abc"]).
    [97,98,99]
    
    %% 输出 Unicode字符
    3> X1 = "a\x{221e}b".
    [97,8734,98]
    4> io:format("~ts~n", [X1]).
    a∞b
    
  4. 字符集
    从 Erlang R16B 开始,Erlang 的源代码文件都假定采用UTF-8 字符集编码。在这之前用的是 ISO-8859-1(Latin-1)字符集。这意味着所有 UTF-8 可打印字符都能在源代码文件里使用,无需使用任何转义。

    Erlang内部没有字符数据类型。字符串其实并不存在,而是由整数列表来表示。用整数列表表示 Unicode 字符串是毫无问题的。

  5. 转义字符

    转义字符含义整数编码
    \b退格符8
    \d删除符127
    \e换行符27
    \f换页符12
    \n换行符10
    \r回车符13
    \s空格符32
    \t水平制表符9
    \v垂直制表符11
    \x{…}十六进制字符
    ^a…^z
    或 ^A…^Z
    Ctrl+A 至 Ctrl+Z1 至 36
    \’单引号39
    \"双引号34
    \\反斜杠92
    \CC的 ASCII编码(C是一个字符)(一个整数)
  6. 各种字符串的区别

    表现方式占用空间匹配/操作比较
    列表字符串简单线性时间
    二进制字符串复杂线性时间
    原子有上限只能比较常量时间(快)

五、模块 module

返回目录

  • 模块是 Erlang 的基本代码单元。模块文件的扩展名为 .erl
  • 必须先编译才能运行模块里的代码;编译后的模块以 .beam 作为扩展名
  • 模块中定义函数和属性。
  • 属性是元数据,用来描述模块自身。定义属性用-Name(Attribute).的形式。
    -module(Name).模块名:必须定义的第一条属性,其名称必须与文件名相同,首字母小写,是一个原子(atom)。
    -author(Name).模块的作者。
    -vsn(VersionNumber).模块版本号,默认自动生成
  • 语句都必须在方法内才能被执行。
  • 模块不能环形依赖(A引用B,B又引用A)
  1. 创建模块文件 geometry.erl

    -module(geometry).              %% 定义模块
    -export([area/1]).              %% 导出方法
    -import(other, [fn1/1,fn2/2]).  %% 导入模块 other 的方法 fn1 和 fn2;
                                    %% /n 表示有n个参数
    
    %% 定义方法:计算长方形的面积
    %% 首先该方法进行匹配:使得 Width=10、Height=5
    %% 然后再执行 -> 后的代码 Width * Height; 计算得 50
    area({rectangle, Width, Height}) ->
      Width * Height;
      
    %% 方法重载,用分号分隔
    area({circle, Radius}) ->
      3.14159 * Radius * Radius;
    
    %% 结束用句号结尾
    area({square, Side}) ->
      Side * Side. 
    

    第一行 -module(geometry).为声明 geometry 模块。

    第二行 -export([area/1]).为导出方法,area 为方法名,/1 表示该方法有一个参数。

    第三行 -import(other, [fn1/1,fn2/2]).为导入 other 模块中的方法 fn1/1 和 fn2/2。

  2. 在 Erlang Shell 中编译并执行模块代码
    用内置函数 c/1对 geometry.erl 进行编译。并生成 geometry.beam 文件

    %% 1. 编译模块
    1> c(geometry).
    {ok,geometry}
    
    %% 2. 带调试信息的编译模块
    %% debug_info 开启调试信息,建议一直开启
    %% export_all 导出模块内所有方法
    2> c(geometry, [debug_info, export_all]).
    
    %% 2.1 也可以在模块内添加属性达到同样的效果
    %% -compile([debug_info, export_all]).
    

    不进入 Erlang Shell,直接编译模块

    erlc geometry.erl
    

    编译成本地代码(需平台的支持),传闻快20%,但是 .beam 文件不能跨平台使用。

    hipe:c(Module, OptionsList).
    # 或在 Erlang Shell 中执行:
    1> c(Module, [native]).
    
  3. 调用模块中的方法:Module:Function(Arguments)

    2> geometry:area({rectangle, 10, 5}).
    50
    
  4. 测试代码(生产环境还是用功能全的测试代码)

    -module(geometry).
    -export([area/1]).
    
    test() ->
        12 = area({rectangle, 3, 4}),
        144 = area({square, 12}),
        tests_wored.
    
    area({rectangle, Width, Height}) -> Width * Height;
    area({square, Side}) -> Side * Side.
    

返回目录

  1. 在模块里定义宏:-define(MACRO, some_value).
    %% 常量宏
    -define(HOUR, 3600). %单位是秒
    %% 函数宏
    -define(sub(X,Y), X-Y). %两个数相减
    
  2. 在编译时定义宏c(MODULE, [{d, Macro, Value},...])Value 可以省略
    1> c(main, [{d, 'DEBUGMODE'}]).
    
  3. 使用宏:?MACRO
    %% Erlang 内置的宏
    ?MODULE % 模块名
    ?FILE   % 文件名
    ?LINE   % 当前行号
    
  4. 判断宏:-ifdef(MACRO).-else.endif.
    -ifdef(DEBUGMODE).
    -define(DEBUG(S), io:format("dbg:"++S)).
    -else.
    -define(DEBUG(S), ok).
    -endif
    

元数据

返回目录

模块属性是描述模块自身的元数据。

  1. 查看模块的元数据 Module:module_info().
    %% 编译模块
    1> c(main).
    
    %% 查看模块 main 的信息。
    2> main:moudle_info().
    [{module,main},
    {exports,[{run,3},
              {sum,1},
              {map,2},
              {module_info,0},
              {module_info,1}]},
    {attributes,[{vsn,[215390944566671778967536641503511308425]}]},
    {compile,[{version,"7.5.4"},
              {options,[native]},
              {source,"/Users/lixu/example/erl_demo/main.erl"}]},
    {native,true},
    {md5,<<119,213,217,218,169,99,126,49,30,250,134,13,165,
            26,146,174>>}]
    
  2. 查看模块中的指定信息 Module:module_info/1.
    3> mian:module_info(attributes). 
    [{vsn,[215390944566671778967536641503511308425]}]
    

头文件

返回目录

  • 扩展名为 .hrl 的文件是同名模块的头文件

六、函数 fun

返回目录

  1. 定义函数
    模块 里定义函数

    hypot(X, Y) ->          %% 定义 命名函数 
      math:sqrt(X*X + Y*Y). %% 最后一句是函数的返回值
    

    模块shell 里定义匿名函数

    1> Double = fun(X) -> 2*X end. %% 定义 匿名函数
    2> Double(5).                  %% 调用 匿名函数
    

    函数中的语句用逗号 , 分隔,用 . 结尾

  2. 函数重载
    Erlang Shell 中定义重载函数(参数个数相同,值不同)

    %% 转换华氏度与摄氏度
    1> TempConvert = fun({c, C}) -> {f, 32+ C*9/5};
    1>                  ({f, F}) -> {c, (F-32)*5/9} %% 省略了fun
    1>               end.
    #Fun<erl_eval.7.126501267>
    
    2> TempConvert({c, 100}).
    {f,212.0}
    
    3> TempConvert({f, 212}).
    {c,100.0}
    

    模块 中定义重载函数

    %% 导出模块内的方法(函数)
    -export([abc/1, abc/2]).
    
    abc({sum, A, B}) -> A * B.
    abc({total, A, B}) -> A + B.      %% 参数个数相同
    abc({sum, A, B}, C) -> A + B + C. %% 参数个数不同
    

    定义模块的方法:用分号 ; 间隔同名函数的不同实现。最后以 . 结尾。

    函数中没有显式的返回语句,用最后一句表达式的值当返回值。

模式匹配

返回目录

  1. 函数的参数使用模式匹配重载函数
    %% 根据性别区分敬语
    greet(male, Name) ->
    	io:format("Hello, Mr. ~s", [Name]);
    greet(female, Name) ->
    	io:format("Hello, Mrs. ~s", [Name]);
    greet(_, Name) ->
    	io:format("Hello, ~s!", [Name]).
    
  2. 函数的参数可以使用 = 来匹配元组的内部元素同时,还匹配整个元组。
    valid_time({ Date = { Y, M, D }, Time = { H, Min, S} }) ->
      io:format("The Date tuple (~p) says today is: ~p/~p/~p, ~n", [Date, Y, M, D]),
      io:format("The Time tuple (~p) indicates: ~p:~p:~p. ~n", [Time, H, Min, S]);
    valid_time(_) ->
      io:format("Stop feeding me wrong data!~n").
    
  3. 模式匹配的局限性
    a.可能匹配到结构相同,但类型错误的值。
    b.不确定参数的取值范围
    这时候就需要 关卡 来解决来。

关卡 guard

返回目录

关卡是模式匹配的一种扩展。

可以对某个模式里的变量执行简单的测试和比较。

  1. 示例:求两个数的最大值

    is_positive(X) when is_integer(X), X > 0 -> true;
    is_positive(_) -> false.
    

    当 X 为整型时,并且X 大于 0 时句 1 匹配,返回 true。
    当句1不匹配时,句2匹配,返回 false。

  2. 使用在函数的头部使用关卡(由 when 引入);或在支持表达式的任何地方使用。

  3. 当关卡被用作表达式时,执行的结果是 truefalse

  4. 关卡由一系列关卡表达式组成,用逗号 “,” 分割。只有GuardExpr1, GuardExpr2, …,GuardExprN 等所有的关卡表达式都为 true 时才为 true

  5. 合法的关卡表达式是所有合法Erlang表达式的一个子集。关卡不能调用用户定义的函数,来确保关卡表达式的执行是无副作用的。

  6. 下列语法形式是合法的关卡表达式:

    合法的关卡表达式说明
    原子 true
    其它常量(各种数据结构和已绑定变量),在关卡表达式里都为 false
    调用后面表1里的关卡判断函数和表2里的内置函数
    数据结构比较参考表6
    算术表达式参考表3
    布尔表达式参考8.7
    短路布尔表达式参考8.23
  7. 关卡序列是指单一或一系列的关卡,用分号“;”分隔。对于关卡序列G1; G2; …; Gn,只要其中一个关卡的值为 true,它的值就为 true

  8. 关卡示例:

    %% 判断两个值的大小
    max(X, Y) when X > Y -> X; %% 当 X 大于 Y 时,返回 X
    max(_, Y) -> Y.            %% 如 X 不大于 Y,则返回 Y
    
    %% 当X是一个整数、并且 X 大于 Y,并且 Y 小于 6 时匹配
    f(X,Y) when is_integer(X), X > Y, Y < 6 -> ...
    
    %% T是一个包含六个元素的元组,并且T中第三个元素的绝对值大于5
    is_tuple(T), tuple_size(T) =:= 6, abs(element(3, T)) > 5
    
    %% 元组 X 的第 4 个元素与列表 L 的列表头相同
    element(4, X) =:= hd(L)
    
    %% (X 等于 cat) 或者 (X 等于 dog)
    X =:= dog; X =:= cat
    
    %% (X 是整数,并且 X 大于 Y) 或者 Y 的绝对值小于 23
    is_integer(X), X > Y; abs(Y) < 23
    
    %% A 大于等于 -1 并且 A 加 1 小于 B
    A >= -1.0 andalso A+1 > B
    
    %% 如果 L 是个原子,那么判断 L 是个列表 并且 L 的长度大于 2
    is_atom(L) orelse ( is_list(L) andalso length(L) > 2)
    


表1 返回关卡

判断函数说明
is_atom(X)X是一个原子
is_binary(X)X是一个二进制型
is_constant(X)X是一个常量
is_float(X)X是一个浮点数
is_function(X)X是一个fun
is_function(X, N)X是一个带有N个参数的fun
is_integer(X)X是一个整数
is_list(X)X是一个列表
is_map(X)X是一个映射组
is_number(X)X是一个整数或浮点数
is_pid(X)X是一个进程标识符
is_pmod(X)X是一个参数化模块的实例
is_port(X)X是一个端口
is_reference(X)X是一个引用
is_tuple(X)X是一个元组
is_record(X,Tag)X是一个类型为Tag的记录
is_record(X,Tag,N)X是一个类型为Tag、大小为N的记录


表2 返回关卡

内置函数说明
abs(X)X的绝对值
byte_size(X)X的字节数,X必须是一个位串或二进制型
element(N, X)X里的元素N,注意:X必须是一个元组
float(X)将X 转换成一个浮点数,X必须是一个数字
hd(X)列表 X 的列表头
length(X)列表X的长度
node()当前的节点
node(X)创建X的节点,X可以是一个进程、标识符、引用或端口 round(X)
self()当前进程的进程标识符
size(X)X的大小,它可以是一个元组或二进制型
trunc(X)将X去掉小数部分取整,X必须是一个数字
tl(X)列表X的列表尾
tuple_size(T)元组T的大小

if 表达式

返回目录

  1. if 表达式的语法:

    if  Guard1 -> Expr_seq1;
        Guard2 -> Expr_seq1;
        ...
        GuardN -> Expr_seqN 
    end, %% 多个 if 用逗号分隔
    if  ...
    end. %% 用英文句号结束
    

    如果 Guard1 的值为 true,那么执行 Expr_seq1,if 的值 为 Expr_seq1 的值

    如果 Guard1 的值为 false,那么执行 Guard2。以此类推直到某个关卡成功为止

    if 表达式必须至少有一个关卡的执行结果为 true。否则就会发生异常错误。

  2. 为了避免异常错误,在 if 表达式的最后添加一个 true 关卡。如想让异常错误发生,就不添加。

    if  A > 0 -> do_this()
        true -> true %% 避免if表达式没有值而报错
    end
    

case … of 表达式

返回目录

  1. case … of 表达式的语法:

    case Expression of
        Pattern1 [when Guard1] -> Expr_seq1;
        Pattern2 [when Guard2] -> Expr_seq2;
        ...
    end
    

    Expression 表达式;Pattern 模式匹配;Guard 关卡;Expr_seq 要执行的表达式

    首先 Expression 被执行,其值为 Value。

    随后 Value 轮流与 Pattern1(可选关卡)、Pattern2等模式进行匹配,直到匹配成功

    发现匹配,相应的表达式序列就会执行,而表达式的结果就是 case 表达式的值

    如果所有模式都不匹配,就会发生异常错误( exception )

    filter( P, [H | T] ) ->
        case P(H) of
            true -> [ H | filter(P, T) ];
            false -> filter(P, T)
        end
    filter( P, [] ) -> [].
    
  2. case 不是必须的。下面只用模式匹配来定义 filter

    filter( P, [H | T] ) -> filter( P(H), H, P, T );
    filter( P, [] ) -> [].
    
    filter1(true, H, P, T) -> [H | filter(P, T)];
    filter1(false, H, P, T) -> filter(P, T)
    

    这个定义比较丑。必须建立额外的函数 filter1,并向 filter/2 传递所有参数

七、高阶函数

返回目录

函数说明
Lambda 函数匿名函数
高阶函数操作其它函数的函数
  1. 函数作为高阶函数参数
    %% 定义一个函数
    1> Fn = fun(X) -> 2*X end.
    
    %% 定义一个高阶函数,
    %% 函数 F 作为 参数 和 返回值
    2> HFn = fun(F,X) -> F(X) end.
    
    %% 调用高阶函数,
    3> HFn(Fn, 9).
    18
    
    匿名函数参数
    1> lists:map( fun(X) -> 2*X end ,  [1, 2, 3, 4]).
    [2,4,6,8]
    
  2. 函数作为高阶函数返回值
    1> Fruit = [apple, pear, orange].
    2> Test = fun(L) -> (fun(X) -> lists:member(X, L) end) end.
    3> IsFruit = Test(Fruit). 
    
    4> IsFruit(pear).
    true
    5> IsFruit(dog).
    false
    
    就是把返回值替换成函数即可

实现 for 循环

返回目录

  1. 定义 for 循环

    %% 模块名称
    -module(my_lib).
    
    %% 导出模块内的方法
    -export([for/3]).
    
    %% 实现 for 循环
    for(Max, Max, F) -> [F(Max)];
    for(I, Max, F) -> [F(I) | for(I+1, Max, F)].
    
  2. Erlang Shell 中调用 for 循环

    %% 编译
    1> c(my_lib).
    
    %% 调用
    2> my_lib:for(1, 10, fun(I) -> I end).
    [1,2,3,4,5,6,7,8,9,10]
    3> my_lib:for(1, 10, fun(I) -> I*I end).
    [1,4,9,16,25,36,49,64,81,100]
    
  3. 在模块中使用 for 循环

    %% 模块名称
    -module(main).
    
    %% 导出模块内的方法
    -export([run/3]).
    
    %% 导入模块 my_lib 的 for 方法
    -import(my_lib, [for/3]).
    
    %% 必须在模块中的函数内调用引入的方法。
    run(I, Max, F) ->
      my_lib:for(I, Max, F).
    
  4. 重入解析代码(reentrant parsing code)

  5. 解析组合器(parser combinator)

  6. 惰性求值器(lazy evaluator)

记录 record

返回目录
记录其实就是元组的另一种形式
使用记录,可以给元组里的各个元素关联一个名称

  1. 应该在下列情形里使用记录:
    • 当你可以用一些预先确定且数量固定的原子来表示数据时;
    • 当记录里的元素数量和元素名称不会随时间而改变时;
    • 当存储空间是个问题时,典型的案例是你有一大堆元组,并且每个元组都有相同的结构。
  2. 语法,不是shell命令,只能用在源代码模块里使用。
    记录能保存在 .erl 文件(源代码)和 .hrl 文件(包含文件,类似c语言的 .h 头文件)
    -record(Name, {
        % 以下两个键带有默认值
        key1 = Default1,
        key2 = Default2,
        ...
        % key3 = undefined
        key3,
        ...
    }).
    
  3. 举例,
    1. 定义记录。记录一旦被定义,就可以创建该记录的实例了
      % records.hrl
      -record(todo, {status = reminder, who = joe, text}).
      
    2. 创建和更新记录
      % 在 shell 里读取 包含记录的 .hrl 文件
      1> rr("records.hrl").
      [todo]
      % 创建记录,所有的键都是原子,而且必须与记录定义里所用的一致
      2> #todo{}.
      #todo{status = reminder, who = joe, text=undefined}
      3> X1 = #todo{ status = urgent, text = "Fix errata in book"}.
      #todo{status = urgent, who = joe, text=undefined}
      % 创建的是原始记录的副本
      4> X2 = #todo{ status = done }.
      #todo{status = done, who = joe, text = "Fix errata in book"}
      % 提取记录字段
      5> #todo{who = W, test = Txt} = X2.
      #todo{status = done, who = joe, text = "Fix errata in book"}
      6> W.
      joe
      7> Txt.
      "Fix errata in book"
      % 只提取单个字段,用 . 语法
      8> X2#todo.text
      "Fix errata in book"
      
      1. 在函数里模式匹配记录
      clear_status(#todo{status=S, who=W} = R) ->
          % 在此函数内部, S 和 W 绑定了记录里的字段值
          % R是整个记录
          R#todo{status=finished}
          % ...
      
      1. 要匹配某个类型的记录
      do_something(X) when is_record(X, todo) ->
          %%  ...
      
      1. 忘掉记录的定义,变换元组
      9> X2
      #todo{status=done,who=joe,text="Fix errata in book"}
      10> rf(todo). % 忘掉记录todo的定义
      11> X2.
      {todo,done,joe,"Fix errata in book"}
      

映射组 map

返回目录

键-值对的关联性集合
键可以是任意的Erlang数据类型
比元组占用更多的存储空间,查找起来也更慢

  1. 映射组适合以下的情形:

    • 当键不能预先知道时用来表示键-值数据结构;
    • 当存在大量不同的键时用来表示数据;
    • 当方便使用很重要而效率无关紧要时作为万能的数据结构使用;
    • 用作“自解释型”的数据结构,也就是说,用户容易从键名猜出值的含义;
    • 用来表示键-值解析树,例如XML或配置文件;
    • 用JSON来和其他编程语言通信。
    • 键和值可以是任何有效的Erlang数据类型
    • 映射组在系统内部是作为有序集合存储的,打印时总是使用各键排序后的顺序,与映射组的创建方式无关
  2. 语法

    % 创建映射组
    Map = #{ Key1 => Val1, Key2 => Val2, ... KeyN => ValN }.
    % 更新映射组
    NewMap = OldMap#{K1 := V1, ...,Kn := Vn}.
    
  3. 举例

    % 创建映射组
    1> F1 = #{ a => 1, b => 2 }.
    #{a => 1,b => 2}
    2> Facts = #{
        {wife, fred} => "Sue",
        {age, fred} => 45,
        {daughter, fred} => "Mary",
        {likes, jim} => ["Man"]
    }.
    % 替换映射组
    3> F2 = F1#{ c => xx }. % 新建键值 c
    #{a => 1,b => 2,c => xx}
    4> F3 = F1#{ c := 3 }. % 错误,变量 F1 里没有 c 这个 key 
    5> F3 = F2#{ c := 6 }. % ok,F3 的 c 值为 6
    % 取值
    6> #{ c := X } = F3. % 单字段提取 X = 6
    7> #{ d := Y } = F3. % 错误,匹配不到右边的值
    8> #{ Z => 1547}. % 错误,变量 Z 未绑定
    
  4. 模式匹配映射组字段
    可以在函数的头部使用包含模式的映射组,前提是映射组里所有的键都是已知的

    % 定义一个 count_characters(Str) 函数
    % 让它返回一个映射组,内含字符串里各个字符的出现次数
    % 这个例子运行不起来 以后在考究
    count_characters(Str) ->
        count_characters(Str, #{}).
    
    count_characters([H | T], #{ H => N } = X) ->
        count_characters(T, X#{ H := N+1 });
    count_characters([H | T], X) ->
        count_characters(T, X#{ H => 1 });
    count_characters([], X) ->
        X.
    

    执行

    1> count_characters("hello").
    #{101=>1,104=>1,1-8=>2,111=>1}
    
  5. 方法
    maps:new() -> #{} %返回一个空的映射组
    erlang:is_map(M) %如果M是映射组返回true否则返回false。可以用在关卡测试或函数主体中。
    map:to_list(M) -> [{K1, V1}, … ,{Kn, Vn}] %把映射组M里的所有键和值转换成一个键值列表,键值在生成的列表里严格按照升序排列。
    maps:from_list([{K1, V1}, …, {Kn, Vn}]) -> M %把一个包含键值对的列表转换成一个映射组M。如果同样的键不止一次的出现,就使用列表里第一键所关联的值,后续的值都会被忽略。
    maps:size(Map) -> numberOfEntries %返回映射组里的条目数量。
    maps:is_key(Key, Map) -> boolean()%如果映射组包含一个键未key的项就返回true,否则返回false。
    maps:get(Key, Map) -> val %返回映射组里与Key关联的值,否则抛出一个异常错误。
    maps:find(Key, Map) -> {ok, Value} | error。%返回映射组与Key关联的值,否则返回error。
    maps:keys(Map) -> [Key1, …, KeyN] %返回映射组所含的键列表,按升序排序
    maps:remove(Key, M) -> M1%返回一个新映射组M1,除了键未Key的项(如果有的话)被移除外,其他与M一致。
    maps:without([Key1, …, KeyN], M) -> M1 %返回一个新映射组M1,它是M的复制,但移除了带有[Key1,…, KeyN]列表里这些键的元素。
    maps:difference(M1, M2) -> M3 %M3是M1的复制,但移除了那些与M2里的元素具有相同键的元素
    %他的行为类似于下面的定义
    maps:difference(M1, M2) ->
    maps:without(maps:keys(M2), M1).
    6. 映射组排序
    映射组在比较时首先会比大小,然后再按照键的排序比较键和值。
    如果A和B是映射组,那么当maps:size(A) < maps:size(B)时A < B。
    如果A和B是大小相同的映射组,那么当maps:to_list(A) < maps:to_list(B)时A < B。
    举个例子,A = #{age => 23, person => “jim”}小于B = # {email => “sue@somplace.com”, name => “sue”}。这是因为A的最小键(age)比B的最小键(email)更小。
    当映射组与其他Erlang数据类型相比较时,因为我们认为映射组比列表或元组“更复杂”,所以映射组总是会大于列表或元组
    映射组可以通过io:format里的~p选项输出,并用io:read或file:consult读取。
    7. 以JSON为桥梁
    有两个内置函数可以让映射组和JSON数据相互转换

    内置函数说明
    maps:to_json(Map) -> Bin把一个映射组转换成二进制型,它包含用JSON表示的该映射组请。注意,不是所有的映射组都能转换成JSON数据类型。映射组里所有的值都必须是能用JSON表示的对象。例如,值不能包含的对象有fun、进程标识符和引用等。如果有任何的键或值不能用JSON表示,maps:to_json就会失败。
    maps:from_json(Bin) -> Map把一个包含JSON数据的二进制型转换成映射组。
    maps:safe_from_json(Bin) -> Map把一个包含JSON数据的二进制型转换成映射组。Bin里的任何原子必须在调用此内置函数前就已存在,否则就会抛出一个异常错误。这样做是为了防止创建大量的新原子。出于效率的原因,Erlang不会垃圾回收(garbage collect)原子,所以连续不断地添加新原子会(在很长一段时间后)让Erlang虚拟机崩溃。

    上面两种定义里的Map都必须是json_map()类型的实例
    - type json_map() = [{json_key(), json_value()}].
    - type json_key() = atom() | binary() | io_list() 并且
    - type json_value() = integer() | binary() | float() | atom() | [json_value] | json_map()
    JSON对象与Erlang值的映射关系如下。
    - SON的数字用Erlang的整数或浮点数表示。
    - JSON的字符串用Erlang的二进制型表示。
    - JSON的列表用Erlang的列表表示。
    - JSON的true和false用Erlang的原子true和false表示。
    - JSON的对象用Erlang的映射组表示,但是有限制:映射组里的键必须是原子、字符串或二进制型,而值必须可以用JSON的数据类型表示。

    当来回转换JSON数据类型时,应当注意一些特定的转换限制。Erlang对整数提供了无限的精度。所以,Erlang会很自然地把映射组里的某个大数转换成JSON数据里的大数,而解码此JSON数据的程序不一定能理解它。

-----------------------------

列表推导

Quicksort 快速排序

返回目录

  1. 定义快速排序

    -module(my_lib).
    -export([qsort/1]).
    
    qsort( [] ) -> [];
    qsort( [ Pivot | T ] ) ->
        qsort( [ X || X <- T, X < Pivot ] )
        ++ [ Pivot ] ++
        qsort( [ X || X <- T, X>= Pivot ] ).
    

    ++ 操作符表示连接列表,代码优雅,但效率不行。

    调用

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

    1、参数 [Povot | T] 产生绑定

    #伪代码
    [ Pivot | T ] 
    = Pivot = 23, T = [6,2,9,27,400,78,45,61,82,14]
    

    2、将列表 T 分成两个表,一个包含 T 里所有小于 Pivot 的元素,另一个包含所有大于等于 Pivot 的元素:

    #伪代码
    Smaller = [ X || X <- T, X < Pivot ].
    = [6,2,9,14] 
    Bigger = [ X || X <- T, X >= Pivot ].
    = [27,400,78,45,61,82]
    

    3、递归排序 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]
    

毕达哥拉斯三元数组

返回目录

pythag(N) ->
    [ {A,B,C} ||
        A <- lists:seq(1, N), %% A 提取列表[1,..N]里元素
        B <- lists:seq(1, N), %% B 提取列表[1,..N]里元素
        C <- lists:seq(1, N), %% C 提取列表[1,..N]里元素
        A+B+C =< N,           %% 条件1:A 加 B 加 C 小于等于 N
        A*A+B*B =:= C*C       %% 条件2:并且 A方 加 B方 等于 C方
    ].

回文构词

返回目录

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

1、从 L 里 提取 H,然后从 perms( L – [H] )里提取 T,最后返回 [H | T]
2、H <- L 相当于一个for循环,在每次循环中又执行一次 T <- perms( L – [ H ] ) ,相当于循环嵌套,又相当于递归

位语法

返回目录

  1. 寻找MPEG音频数据里的同步点
    MPEG音频数据是由许多的帧组成的。每一帧都有它自己的帧头,后面跟着音频信息。由于没有文件头,所以原则上可以把一个MPEG文件切成多个片段,而每一段都可以独立播放。任何读取MPEG流的软件都应当能找到头帧,然后同步MPEG数据。
    MPEG头以一段11位的帧同步(frame sync)信息开头,它包含11个连续的1位,后面跟着描述后续数据的信息:AAAAAAAA AAABBCCD EEEEFFGH IIJJKLMM
    AAAAAAAAAAA 同步字(11位,全为1)
    BB 2位的MPEG音频版本ID
    CC 2位的层描述
    D 1位保护位

    知道了从A到M的值,就能计算出一个MPEG帧的总长度
    为了找到同步点,首先假设我们成功定位到了某个MPEG头的起始位置,然后试着计算该帧的长度。之后可能会发生以下某一种情况。
    - 假设正确,因此当向前跳过帧长后,会找到另一个MPEG头。
    - 假设错误。可能没有定位到标记MPEG头起始位置的位序列(由11个连续的1组成)上,或者因为字的格式不正确而无法计算帧长。
    - 假设错误,但定位到的若干字节音乐数据碰巧和MPEG头的起始位置很相似。在这种情况下,可以计算帧长,但是当向前跳过这段距离后,我们无法找到新的MPEG头。
    为了确保万无一失,我们将寻找三个连续的MPEG头。同步的程序如下所示:
    
    
  2. 构建微软通用对象文件格式(Microsoft Common Object File Format,简称COFF)的二进制数据文件。打包和解包二进制数据文件(例如COFF)的典型做法是使用二进制型和二进制模式匹配
    
    
  3. 解包一个IPv4数据报(datagram)
    
    

实现简单的处理列表的方法

list 递归求和

返回目录

  1. 定义递归求和函数 sum/1
    %% 模块名称
    -module(my_lib).
    
    %% 导出模块内的方法
    -export([sum/1]).
    
    sum( [H | T] ) -> H + sum(T); %% 递归调用
    sum( [] ) -> 0.               %% 为空,返回 0
    
    两个 sum 的顺序不重要,因为两种情况是互斥的。
  2. 在 Erlang Shell 调用
    %% 编译
    1> c(my_lib).
    
    %% 调用
    2> my_lib:sum([1,2,3,4,5]).
    15
    

遍历 list

返回目录

  1. 定义遍历 List 函数 map/2
    %% 模块名称
    -module(my_lib).
    
    %% 导出模块内的方法
    -export([map/2]).
    
    %% 两个参数,一个处理函数,一个要处理的 List
    map(_, []) -> [];                 %% 空 List 返回空 List
    map(F, [H|T]) -> [F(H)|map(F,T)]. %% 递归调用
    
  2. 在 Erlang Shell 中调用
    1> c(my_lib).
    2> my_lib:map(fun(X) -> 2*X end, [1,2,3,4,5]).
    [2,4,6,8,10]
    

计算总和

返回目录

  1. 文件 shop.erl
    -module(shop).
    -export([cost/1]).
    
    cost(oranges) -> 5;
    cost(newspaper) -> 8;
    cost(apple) -> 2;
    cost(pears) -> 9;
    cost(milk) -> 7;
    
  2. 文件 shop1.erl
    -module(shop1).
    -export([totle/1]).
    
    total([{What, N} | T]) -> shop:cost(What) * N + total(T);
    total([]) -> 0.
    
  3. 执行 shop1.erl
    $ erl
    1> c(shop).
    {ok,shop}
    2> c(shop1).
    {ok,shop1}
    3> shop1:total([]).
    0
    4> shop1.total([{milk, 3}]).
    21
    
    语句匹配所得值为:What = milk,N = 3,T = []
    执行语句 shop:cost(What) * N + total(T)。
    即:shop:cost(milk) => 7 * 3 + total([]) => 0 = 21
    5> shop1:total([{pears, 6}, {milk, 3}]).
    75
    
    语句匹配所得值为:What = pears,N = 6,T = [{milk, 3}]
    6> Buy = [{oranges, 4}, {newspaper, 1}, {apple, 10}, {pears, 6}, {milk, 3}].
    7> shop1:total(Buy).
    123
    

归集器 accumulator

返回目录

```erlang
% lib_misc.erl
% 不推荐,这样会遍历2次列表
odds_and_evens1(L) ->
    Odds = [X || X <- L, (X rem 2) =:= 1],
    Evens = [X || X <- L, (X rem 2) =:= 0],
    {Odds, Evens}.
    
% 推荐,只遍历列表1次,把奇偶数分别添加到适合的列表里。
odds_and_evens2(L) -> odds_and_evens_acc(L, [], []).
odds_and_evens_acc([H|T], Odds, Evens) ->
    case (H rem 2) fo
        1 -> odds_and_evens_acc(T, [H|Odds], Evens);
        0 -> odds_and_evens_acc(T, Odds,  [H|Evens]);
    end;
odds_and_evens_acc([], Odds, Evens) ->
    { lists:reverse(Odds), lists:reverse(Evens) }. % 得出得结果是反得,需要逆转
```
执行
```shell
1> lib_misc:odds_and_evens1([1,2,3,4,5,6]).
{[1,3,5], [2,4,6]}
2> lib_misc:odds_and_evens2([1,2,3,4,5,6]).
{[1,3,5], [2,4,6]}
```

apply

返回目录

此内置函数能调用某个模块里的某个函数,并向它传递参数
与直接调用函数的区别在于模块名和/或函数名可以是动态计算得出的
尽量避免使用 apply
```apply(Mod, Func, [Arg1, Arg2, ..., ArgN])```动态调用
```Mod:Func(Arg1, Arg2, ..., ArgN)```效果相同,静态调用
1. 所有的Erlang内置函数也可以通过apply进行调用,
    ```shell
    1> apply(erlang, atom_to_list, [hello]).
    ```
2. Mod 参数不必是一个原子,也可以是一个元组
```{Mod, P1, P2, ..., Pn}:Func(A1,A2,...An)```等同于
```Mod:Func(A1,A2,...,An,{Mod,P1,P2,...,Pn})```

元数

返回目录

一个函数的元数(arity)是该函数所拥有的参数数量。
同一模块里的两个名称相同、元数不同的函数是完全不同的函数。
Erlang程序员经常将名称相同、元数不同的函数作为辅助函数使用。
```erlang
% 元数为 1
sum(L) -> sum(L, 0). % 累加列表 L 里的所有元素
% 元数为 2 辅助函数,不导出来隐藏它们
sum([], N) -> N;
sum([H|T], N) -> sum(T, H+N).
```
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值