Erlang基础语法教程:从模块定义到并发编程完全指南

系统讲解Erlang基础语法,涵盖模块定义、函数导出、模式匹配、递归、列表元组、列表推导式、匿名函数与进程消息传递,适合Erlang入门者系统学习函数式编程与并发编程。

Erlang是一门诞生于爱立信实验室的函数式编程语言,专为构建高并发、高可用和分布式系统而设计。它以轻量级进程消息传递和**“let it crash"容错哲学**闻名于世,被WhatsApp、RabbitMQ、Discord等知名产品广泛采用。

本教程将从零开始系统讲解Erlang基础语法,涵盖模块定义、函数编写、模式匹配、递归、数据结构、列表推导式、匿名函数以及进程通信等核心概念。每个知识点都配有可运行的代码示例和详细说明,帮助你快速建立Erlang编程思维,为后续的并发编程和OTP框架学习打下坚实基础。

目录

  1. 模块定义
  2. 导出函数
  3. 函数定义
  4. 模式匹配
  5. 条件表达式
  6. 循环和递归
  7. 列表和元组
  8. 列表推导式
  9. 匿名函数
  10. 进程和消息传递
  11. 接收消息
  12. 完整示例:Hello World程序
  13. 总结
  14. 延伸阅读

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/2add/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 函数中,参数 XY 通过位置进行模式匹配——调用 add(1, 2) 时,X 匹配 1Y 匹配 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提供了 ifcase 和守卫(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是一门纯函数式编程语言,没有 forwhile 等传统循环结构,所有循环都通过递归来实现。这是Erlang入门时需要适应的重要思维转变。

基本递归

factorial(0) ->
    1;
factorial(N) when N > 0 ->
    N * factorial(N - 1).

factorial 函数利用函数多子句守卫(guard) 计算阶乘:当 N0 时返回 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)].

这个函数生成一个包含从 1N 每个数字平方的列表。X <- lists:seq(1, N) 是生成器,表示 X 依次取 1N 的值。

带过滤条件

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:maplists: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/0greet/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 构成签名,支持多子句和守卫
模式匹配= 是匹配操作符,变量单次赋值不可变
条件表达式caseif 和守卫三种条件判断机制
递归Erlang没有传统循环,用尾递归实现高效迭代
列表与元组列表是链表(头操作O(1)),元组是定长异构结构
列表推导式简洁的列表构建语法,支持过滤和多生成器
匿名函数fun 关键字定义,支持闭包和多子句模式匹配
并发与消息spawn 创建轻量进程,! 发送消息,receive 接收

掌握这些基础知识后,你就具备了进一步学习Erlang/OTP框架的能力。OTP提供了 gen_serversupervisorapplication 等行为模式,将上述基础概念封装为可复用的并发编程模板,是构建生产级Erlang应用的标准方式。

延伸阅读

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页