Erlang是一门诞生于爱立信实验室的函数式编程语言,专为构建高并发、高可用和分布式系统而设计。它以轻量级进程、消息传递和**“let it crash"容错哲学**闻名于世,被WhatsApp、RabbitMQ、Discord等知名产品广泛采用。
本教程将从零开始系统讲解Erlang基础语法,涵盖模块定义、函数编写、模式匹配、递归、数据结构、列表推导式、匿名函数以及进程通信等核心概念。每个知识点都配有可运行的代码示例和详细说明,帮助你快速建立Erlang编程思维,为后续的并发编程和OTP框架学习打下坚实基础。
目录
1. 模块定义
每个Erlang源文件(.erl文件)都对应一个模块,使用 -module() 属性声明模块名。模块名必须与文件名一致,这是Erlang编译器强制的约定。
-module(example).
这行代码声明当前文件是一个名为 example 的模块,必须保存为 example.erl。
注意:
-module()必须是文件中的第一个属性(忽略注释),且必须以句号.结尾。Erlang中所有属性声明和表达式都以.结束,这是语法的基本规则。
2. 导出函数
使用 -export() 属性指定模块中可以被外部调用的函数。未导出的函数只能在模块内部使用,类似于其他语言中的 private 方法。
-export([start/0, add/2]).
这表示 example 模块导出了两个函数:
start/0:无参数函数(arity 为 0)add/2:接受两个参数的函数(arity 为 2)
Erlang中函数签名由函数名和参数个数(arity) 共同决定,所以 add/2 和 add/3 是完全不同的函数,可以各自独立导出。
3. 函数定义
Erlang函数定义的格式为:函数名(参数列表) -> 函数体.。函数名和变量名的命名规则是:以小写英文字母开头,由字母、数字和下划线组成。
start() ->
io:fwrite("Starting the example~n").
这里定义了一个名为 start 的函数(arity 为 0),使用 io:fwrite/1 将文本输出到控制台。~n 是Erlang的换行转义符。
函数可以有多个子句(clauses),用分号 ; 分隔。Erlang会按顺序尝试匹配每个子句:
greet("Alice") ->
io:fwrite("Welcome back, Alice!~n");
greet(Name) ->
io:fwrite("Hello, ~s!~n", [Name]).
这个 greet/1 函数有两个子句:当参数为 "Alice" 时走第一个分支,其他情况走第二个分支。这是Erlang函数式编程中非常优雅的模式。
4. 模式匹配
模式匹配是Erlang的核心机制之一,远比简单的变量赋值强大。Erlang中的 = 是模式匹配操作符,而非赋值操作符。Erlang变量是单次赋值的——一旦绑定,值不可改变(这也是函数式编程的重要特征)。
add(X, Y) ->
X + Y.
在 add 函数中,参数 X 和 Y 通过位置进行模式匹配——调用 add(1, 2) 时,X 匹配 1,Y 匹配 2。
更复杂的模式匹配示例:
% 元组解构
{ok, Value} = {ok, 42}. % Value 绑定为 42
% 列表解构(Head | Tail 模式)
[Head | Tail] = [1, 2, 3, 4]. % Head = 1, Tail = [2, 3, 4]
% 匹配失败会抛出 badmatch 异常
% {ok, X} = {error, "not found"}. % 报错!** exception error: no match
模式匹配在Erlang中无处不在:函数参数、case 表达式、receive 语句、列表推导式等都依赖它。掌握模式匹配是学好Erlang的关键一步。
5. 条件表达式
Erlang提供了 if、case 和守卫(guards)三种条件判断机制。
case 表达式
case 结合模式匹配进行条件分支,是最常用的条件结构:
is_positive(N) ->
case N > 0 of
true -> true;
false -> false
end.
case 表达式中每个分支都是一个模式匹配子句,可以使用 _ 作为通配符匹配所有未覆盖的情况:
classify(N) ->
case N of
0 -> zero;
_ when N > 0 -> positive;
_ -> negative
end.
if 表达式
if 表达式使用守卫序列进行判断,不需要匹配特定的值:
grade(Score) ->
if
Score >= 90 -> excellent;
Score >= 70 -> good;
Score >= 60 -> pass;
true -> fail % true 作为 catch-all 分支
end.
注意:
if表达式必须至少有一个分支能匹配,否则会抛出if_clause运行时异常。通常用true作为最后的兜底分支。
6. 循环和递归
Erlang是一门纯函数式编程语言,没有 for、while 等传统循环结构,所有循环都通过递归来实现。这是Erlang入门时需要适应的重要思维转变。
基本递归
factorial(0) ->
1;
factorial(N) when N > 0 ->
N * factorial(N - 1).
factorial 函数利用函数多子句和守卫(guard) 计算阶乘:当 N 为 0 时返回 1(基准情况),否则递归调用自身。
尾递归优化
在实际的Erlang教程和工程实践中,推荐使用尾递归以避免栈溢出:
factorial(N) ->
factorial(N, 1).
factorial(0, Acc) ->
Acc;
factorial(N, Acc) when N > 0 ->
factorial(N - 1, N * Acc).
尾递归版本引入一个累加器 Acc,每次递归调用是函数的最后一个操作,BEAM虚拟机会将其优化为常数栈空间的循环,与命令式语言的 while 循环效率相当。
列表递归遍历
递归也常用于遍历和转换列表:
double([]) ->
[];
double([Head | Tail]) ->
[Head * 2 | double(Tail)].
这个函数将列表中每个元素乘以 2。[Head | Tail] 是Erlang中最经典的模式匹配之一,将列表拆分为头部和尾部。
7. 列表和元组
列表(List)和元组(Tuple)是Erlang中最常用的两种数据结构,理解它们的区别对写出高效的Erlang代码至关重要。
列表(List)
列表用方括号 [] 表示,适合存储同构的、长度可变的数据序列。列表是链表结构,头部操作(添加/删除)是 O(1),尾部追加是 O(N)。
list_example() ->
Numbers = [1, 2, 3, 4],
% 头部添加元素(O(1) 操作)
Extended = [0 | Numbers], % [0, 1, 2, 3, 4]
% 列表拼接
Combined = [1, 2] ++ [3, 4], % [1, 2, 3, 4]
% 获取列表长度
Length = length(Numbers), % 4
Extended.
常用的列表操作函数(lists 模块):
lists:map(fun(X) -> X * 2 end, [1, 2, 3]). % [2, 4, 6]
lists:filter(fun(X) -> X > 2 end, [1, 2, 3, 4]). % [3, 4]
lists:foldl(fun(X, Acc) -> X + Acc end, 0, [1, 2, 3]). % 6
元组(Tuple)
元组用花括号 {} 表示,适合存储固定数量、异构的数据组合。元组的随机访问是 O(1),常用于表示结构化记录。
tuple_example() ->
Person = {person, "Alice", 30},
% 通过 element/2 获取元素(从 1 开始计数)
Name = element(2, Person), % "Alice"
% 通过 setelement/3 修改元素
Updated = setelement(3, Person, 31), % {person, "Alice", 31}
Updated.
最佳实践:通常使用原子(atom)作为元组的第一个元素来标记元组的类型,例如
{ok, Value}和{error, Reason}是Erlang中最惯用的返回值模式。
8. 列表推导式
列表推导式(List Comprehension)是Erlang中一种简洁且强大的构建列表的方法,语法格式为 [表达式 || 生成器, 过滤器]。
基本用法
squared_list(N) ->
[X * X || X <- lists:seq(1, N)].
这个函数生成一个包含从 1 到 N 每个数字平方的列表。X <- lists:seq(1, N) 是生成器,表示 X 依次取 1 到 N 的值。
带过滤条件
even_squares(N) ->
[X * X || X <- lists:seq(1, N), X rem 2 == 0].
只保留偶数的平方。X rem 2 == 0 是过滤条件,rem 是Erlang的取模运算符。
多生成器
pairs() ->
[{X, Y} || X <- [1, 2, 3], Y <- [a, b]].
% 结果: [{1,a},{1,b},{2,a},{2,b},{3,a},{3,b}]
多个生成器构成笛卡尔积,后面的生成器变化更快(类似嵌套循环)。
位串推导式
Erlang还支持位串推导式(Binary Comprehension),用于处理二进制数据,这在处理网络协议和文件IO时非常有用:
% 将二进制数据中的每个字节加 1
<< <<(B + 1)>> || <<B>> <= <<1, 2, 3>> >>.
% 结果: <<2, 3, 4>>
9. 匿名函数
匿名函数(Anonymous Function,也叫 Fun)是Erlang函数式编程的重要组成部分,常用于高阶函数(如 lists:map、lists:filter)和并发编程中的 spawn。
定义和调用
Double = fun(X) -> X * 2 end.
Double(5). % 返回 10
定义了一个接受参数 X 并返回 X * 2 的匿名函数,将其绑定到变量 Double。
注意:Erlang中变量名必须以大写字母开头,所以
Double而非double。这是Erlang与大多数语言的重要区别——小写开头的是原子(atom),大写开头的才是变量。
在高阶函数中使用
% 使用 fun 进行列表变换
lists:map(fun(X) -> X + 1 end, [1, 2, 3]). % [2, 3, 4]
% 使用 fun 进行列表过滤
lists:filter(fun(X) -> X > 2 end, [1, 2, 3, 4]). % [3, 4]
% 使用 fun 进行折叠(归约)
lists:foldl(fun(X, Acc) -> X + Acc end, 0, [1, 2, 3, 4]). % 10
多子句匿名函数
匿名函数也支持模式匹配的多子句写法:
Classifier = fun
(0) -> zero;
(N) when N > 0 -> positive;
(N) when N < 0 -> negative
end.
Classifier(5). % positive
Classifier(-3). % negative
Classifier(0). % zero
闭包
Erlang的匿名函数支持闭包,可以捕获定义时作用域中的变量:
make_adder(N) ->
fun(X) -> X + N end.
Add5 = make_adder(5),
Add5(10). % 返回 15
10. 进程和消息传递
Erlang的并发模型基于Actor模型:每个进程是一个独立的执行单元,进程之间通过异步消息传递通信,不共享内存。这是Erlang最强大的特性之一,也是它被广泛用于电信和分布式系统的原因。
创建进程
spawn_process() ->
spawn(fun() -> io:fwrite("This is a new process~n") end).
spawn/1 接受一个无参 Fun,在新进程中执行它,返回新进程的 PID(进程标识符)。Erlang的进程极其轻量——创建开销仅为几百字节内存,一台普通机器可以轻松运行数百万个进程。
发送消息
使用 ! 操作符向进程发送消息:
Pid = spawn(fun() ->
receive
{From, Msg} ->
From ! {self(), echo, Msg}
end
end),
Pid ! {self(), hello}. % 向 Pid 发送消息
! 是异步发送操作,发送方不会阻塞。self() 返回当前进程的 PID。
链接与监控
Erlang提供了进程链接(link)和监控(monitor)机制来实现容错:
% 链接进程:一个崩溃,另一个也会收到退出信号
spawn_link(fun() -> error(oops) end).
% 监控进程:单向通知,被监控进程崩溃时收到 'DOWN' 消息
Ref = erlang:monitor(process, Pid).
这构成了OTP Supervisor 树的基础——“let it crash"哲学的核心实现机制。
11. 接收消息
receive 语句用于从进程邮箱中接收消息。每个Erlang进程都有一个私有的消息邮箱,receive 会按顺序扫描邮箱中的消息并进行模式匹配。
process_messages() ->
receive
{sender, Message} ->
io:fwrite("Received message: ~p~n", [Message]);
stop ->
io:fwrite("Stopping...~n"),
ok;
_ ->
io:fwrite("Unknown message~n"),
process_messages() % 递归继续接收
end.
带超时的 receive
receive 支持 after 子句设置超时时间(毫秒),这在等待响应时非常实用:
wait_for_reply() ->
receive
{reply, Data} ->
{ok, Data}
after 5000 ->
{error, timeout}
end.
这个函数最多等待 5 秒,如果超时则返回 {error, timeout}。
递归实现消息循环
在实际应用中,服务器进程通常通过递归来持续接收消息:
server_loop(State) ->
receive
{From, get} ->
From ! {self(), State},
server_loop(State);
{set, NewState} ->
server_loop(NewState);
stop ->
ok
end.
这种递归消息循环是Erlang/OTP中 gen_server 行为模式的基本原理。
完整示例:Hello World程序
下面是一个完整的Erlang程序,综合运用了本教程中介绍的多个语法元素,包括模块定义、函数导出、模式匹配、控制台输出以及如何从Erlang shell编译和运行模块。
%% 文件名: hello.erl
%% 定义模块名为 hello
-module(hello).
%% 导出函数,使得其他模块可以调用 start/0 和 greet/1
-export([start/0, greet/1]).
%% start/0 函数作为程序的入口点
start() ->
%% 打印 "Hello, World!" 到控制台
io:fwrite("Hello, World!~n"),
%% 调用 greet/1 函数,传入不同的名字
greet("Alice"),
greet("Bob"),
greet("Erlang").
%% greet/1 函数使用模式匹配处理不同参数
greet("Erlang") ->
io:fwrite("Hello, ~s! Welcome to functional programming!~n", ["Erlang"]);
greet(Name) ->
io:fwrite("Hello, ~s!~n", [Name]).
编译和运行
在Erlang shell中编译并运行:
1> c(hello).
{ok,hello}
2> hello:start().
Hello, World!
Hello, Alice!
Hello, Bob!
Hello, Erlang! Welcome to functional programming!
ok
逐行解释
| 代码 | 说明 |
|---|---|
-module(hello). | 声明模块名为 hello,必须与文件名 hello.erl 一致 |
-export([start/0, greet/1]). | 导出 start/0 和 greet/1 两个函数供外部调用 |
start() -> | 定义入口函数,arity 为 0 |
io:fwrite("Hello, World!~n") | 使用 io:fwrite/1 输出字符串,~n 为换行符 |
greet("Alice") | 调用 greet/1,传入字符串参数 |
greet("Erlang") -> | greet/1 的第一个子句,参数匹配 "Erlang" |
greet(Name) -> | greet/1 的兜底子句,Name 匹配任意值 |
io:fwrite("...~s...~n", [Name]) | 使用 ~s 格式化占位符输出字符串参数 |
这个例子展示了Erlang的基本语法,包括模块定义、函数导出、函数多子句、模式匹配、控制台输出以及Erlang shell的编译和运行流程。
总结
本文系统介绍了Erlang的基础语法和核心概念,让我们回顾一下关键知识点:
| 概念 | 要点 |
|---|---|
| 模块与导出 | 每个 .erl 文件是一个模块,-export 控制公开接口 |
| 函数定义 | 函数名 + arity 构成签名,支持多子句和守卫 |
| 模式匹配 | = 是匹配操作符,变量单次赋值不可变 |
| 条件表达式 | case、if 和守卫三种条件判断机制 |
| 递归 | Erlang没有传统循环,用尾递归实现高效迭代 |
| 列表与元组 | 列表是链表(头操作O(1)),元组是定长异构结构 |
| 列表推导式 | 简洁的列表构建语法,支持过滤和多生成器 |
| 匿名函数 | fun 关键字定义,支持闭包和多子句模式匹配 |
| 并发与消息 | spawn 创建轻量进程,! 发送消息,receive 接收 |
掌握这些基础知识后,你就具备了进一步学习Erlang/OTP框架的能力。OTP提供了 gen_server、supervisor、application 等行为模式,将上述基础概念封装为可复用的并发编程模板,是构建生产级Erlang应用的标准方式。
延伸阅读
- Erlang官方文档:Erlang顺序编程指南,是最权威的语法参考
- Learn You Some Erlang:最受欢迎的Erlang免费在线教程,风格幽默且内容深入
- Erlang/OTP设计原则:OTP框架的设计理念和最佳实践
- Erlang编程规范:列表推导式和位串推导式的进阶用法
- Joe Armstrong的博客:Erlang语言创造者的技术思考,理解语言设计背后的哲学
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。