强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

Erlang/OTP 完全指南 / 05 - 模式匹配

第 05 章:模式匹配

模式匹配(Pattern Matching)是 Erlang 编程的灵魂。本章深入讲解函数子句、case 表达式、守卫(Guard)和递归中的模式匹配。


5.1 模式匹配基础

5.1.1 = 操作符

在 Erlang 中,= 不是赋值,而是模式匹配(Pattern Match):

%% 匹配成功:左侧模式匹配右侧值
X = 42.                %% X 绑定为 42
{A, B} = {1, 2}.       %% A=1, B=2
[H|T] = [1,2,3].       %% H=1, T=[2,3]

%% 匹配失败:抛出 badmatch 异常
X = 43.
%% ** exception error: no match of right hand side value 43

%% 两边都已绑定时,检查是否相等
X = 42.   %% ok(X 已经是 42)

%% 模式匹配可以解构任意复杂的数据
#{name := Name, age := Age} = #{name => "Alice", age => 25}.
%% Name="Alice", Age=25

5.1.2 匹配的规则

规则示例
字面量必须完全相等42 = 42 ✅, 42 = 43
未绑定变量匹配任意值X = 42 → X=42
已绑定变量检查相等X = X ✅(如果 X 已绑定)
_ 匹配任意值(不绑定){_, Y} = {1, 2}
结构必须匹配{A, B} = {1, 2, 3}
列表用 | 分割头部和尾部`[H

5.2 函数子句

5.2.1 多子句函数

%% area/1 的多个子句
area({circle, R}) ->
    math:pi() * R * R;
area({rect, W, H}) ->
    W * H;
area({triangle, B, H}) ->
    B * H / 2.
1> area({circle, 5}).
78.53981633974483
2> area({rect, 4, 6}).
24
3> area({triangle, 3, 8}).
12.0
4> area({square, 4}).
** exception error: no function clause matching area({square,4})

5.2.2 函数子句匹配顺序

函数子句从上到下依次匹配,第一个匹配成功的子句被执行:

%% 子句顺序很重要!
classify(X) when is_number(X), X > 0 -> positive;
classify(X) when is_number(X), X < 0 -> negative;
classify(0) -> zero;
classify(_) -> not_a_number.

5.2.3 参数解构

%% 解构元组参数
handle({connect, Host, Port}) ->
    io:format("Connecting to ~s:~p~n", [Host, Port]);
handle({disconnect, Reason}) ->
    io:format("Disconnected: ~p~n", [Reason]);
handle({data, Data}) ->
    io:format("Received: ~p~n", [Data]).

%% 解构列表参数
process([]) ->
    empty;
process([Single]) ->
    {one_element, Single};
process([First, Second]) ->
    {two_elements, First, Second};
process([H|T]) ->
    {head, H, tail_length, length(T)}.

%% 解构嵌套结构
process_message({chat, #{from := From, to := To, body := Body}}) ->
    io:format("~s -> ~s: ~s~n", [From, To, Body]);
process_message({system, Level, Msg}) ->
    io:format("[~p] ~s~n", [Level, Msg]).

5.3 守卫(Guard)

5.3.1 基本守卫

%% when 关键字引入守卫
is_positive(X) when X > 0 -> true;
is_positive(_) -> false.

%% 多条件守卫(逗号 = 且)
in_range(X, Min, Max) when is_number(X), X >= Min, X =< Max ->
    true;
in_range(_, _, _) ->
    false.

%% 分号 = 或
is_adult({person, _, Age}) when Age >= 18; Age < 0 ->
    true;
is_adult(_) ->
    false.

5.3.2 守卫表达式

守卫表达式说明
X > Y大于
X < Y小于
X >= Y大于等于
X =< Y小于等于
X =:= Y等于(严格)
X =/= Y不等于(严格)
X == Y等于(自动转换类型)
X /= Y不等于(自动转换类型)
is_atom(X)类型检查
is_integer(X)类型检查
is_list(X)类型检查
is_map(X)类型检查
is_pid(X)类型检查
is_function(X, N)检查是否是 N 元函数
is_record(X, Tag, Size)检查 record
element(N, Tuple)取元组第 N 个元素
hd(List)列表头
tl(List)列表尾
length(List)列表长度
abs(X)绝对值
round(X)四舍五入
trunc(X)截断
size(X)元组或二进制大小
bit_size(X)位串大小
byte_size(X)二进制字节数

5.3.3 守卫限制

⚠️ 注意:守卫中不能调用自定义函数,只能使用 BIF 和比较操作

%% ❌ 错误:守卫中不能调用自定义函数
is_valid(X) when my_module:check(X) -> true.

%% ✅ 正确:在函数体内调用
handle(X) ->
    case my_module:check(X) of
        true -> do_something(X);
        false -> error
    end.

%% 守卫中不能使用 and/or,使用逗号和分号代替
%% ❌ when X > 0 and X < 10
%% ✅ when X > 0, X < 10

5.3.4 =:= 与 == 的区别

%% =:= 严格相等(推荐)
1 =:= 1.0.    %% false
1 == 1.0.     %% true(类型转换)

%% 最佳实践:始终使用 =:= 和 =/=
%% 避免类型转换带来的隐式行为

5.4 case 表达式

5.4.1 基本用法

%% case 语法
describe(Number) ->
    case Number of
        0 -> "zero";
        1 -> "one";
        2 -> "two";
        _ -> "many"
    end.

%% case 可以匹配任意模式
process(Result) ->
    case Result of
        {ok, Data} ->
            io:format("Success: ~p~n", [Data]);
        {error, Reason} ->
            io:format("Error: ~p~n", [Reason]);
        timeout ->
            io:format("Operation timed out~n")
    end.

5.4.2 case 中的守卫

classify_temperature(Temp) ->
    case Temp of
        T when T < 0 -> freezing;
        T when T < 15 -> cold;
        T when T < 25 -> comfortable;
        T when T < 35 -> warm;
        _ -> hot
    end.

5.4.3 变量作用域规则

⚠️ 关键规则:case 的每个分支必须绑定相同的变量集合

%% ❌ 错误:不同分支绑定不同变量
case X of
    {a, Y} -> Y;       %% 绑定 Y
    {b, Z} -> Z         %% 绑定 Z(Y 未绑定)
end.
%% 编译错误:unsafe variable 'Y'

%% ✅ 正确:在 case 前预绑定
Y = undefined,
Z = undefined,
case X of
    {a, Y} -> Y;
    {b, Z} -> Z
end.

%% ✅ 更好的方式:使用嵌套函数或明确赋值
handle(X) ->
    Result = case X of
        {a, Val} -> Val;
        {b, Val} -> Val
    end,
    Result.

5.5 if 表达式

5.5.1 基本用法

%% if 表达式(类似其他语言的 if-else)
abs_value(X) ->
    if
        X >= 0 -> X;
        true   -> -X     %% true 是"默认分支"(相当于 else)
    end.

%% 多条件
grade(Score) ->
    if
        Score >= 90 -> 'A';
        Score >= 80 -> 'B';
        Score >= 70 -> 'C';
        Score >= 60 -> 'D';
        true        -> 'F'
    end.

5.5.2 if 的守卫条件

%% if 的每个分支都是守卫条件
check(X) ->
    if
        is_atom(X)    -> "atom";
        is_integer(X) -> "integer";
        is_list(X)    -> "list";
        true          -> "unknown"
    end.

💡 提示:if 表达式不常用,大多数情况下 case + 模式匹配更清晰。


5.6 递归中的模式匹配

5.6.1 列表处理

%% 求列表长度(递归)
my_length([]) -> 0;
my_length([_H|T]) -> 1 + my_length(T).

%% 列表求和
sum([]) -> 0;
sum([H|T]) -> H + sum(T).

%% 列表反转
reverse([]) -> [];
reverse([H|T]) -> reverse(T) ++ [H].
%% 注意:这种写法效率低(O(n²)),实际使用 lists:reverse/1

5.6.2 列表过滤

%% 过滤偶数
filter_even([]) -> [];
filter_even([H|T]) when H rem 2 =:= 0 ->
    [H | filter_even(T)];
filter_even([_H|T]) ->
    filter_even(T).

%% 通用过滤器
filter(_Pred, []) -> [];
filter(Pred, [H|T]) ->
    case Pred(H) of
        true  -> [H | filter(Pred, T)];
        false -> filter(Pred, T)
    end.

%% 使用
1> filter(fun(X) -> X > 3 end, [1,2,3,4,5]).
[4,5]

5.6.3 嵌套结构匹配

%% 递归处理树结构
-type tree() :: {node, tree(), tree()} | {leaf, term()}.

sum_tree({leaf, Value}) ->
    Value;
sum_tree({node, Left, Right}) ->
    sum_tree(Left) + sum_tree(Right).

%% 使用
1> Tree = {node, {leaf, 1}, {node, {leaf, 2}, {leaf, 3}}}.
2> sum_tree(Tree).
6

5.7 高级模式匹配技巧

5.7.1 嵌套模式匹配

%% 同时匹配多层结构
handle_event({click, {button, Id}, #{x := X, y := Y}}) ->
    io:format("Button ~p clicked at (~p, ~p)~n", [Id, X, Y]).

%% 匹配带默认值的 map
process(#{name := Name, age := Age, role := Role}) ->
    io:format("~s (~p): ~s~n", [Name, Age, Role]);
process(#{name := Name, age := Age}) ->
    process(#{name => Name, age => Age, role => "user"}).

5.7.2 = 操作符在模式中的使用

%% 在模式中使用 = 绑定整个子结构
handle({person, _} = Person) ->
    %% Person 绑定了整个 {person, _} 元组
    process_person(Person).

%% 更复杂的例子
analyze([{tag, Tag} = Entry | Rest]) ->
    %% Entry 绑定整个 {tag, Tag} 元组
    io:format("Tag: ~p, Full entry: ~p~n", [Tag, Entry]),
    analyze(Rest);
analyze([]) ->
    ok.

5.7.3 二进制模式匹配

%% 解析二进制协议
%% 格式: [版本:8][类型:8][长度:16][数据:binary]
parse_packet(<<Version:8, Type:8, Length:16, Data:Length/binary>>) ->
    {ok, #{version => Version, type => Type, data => Data}};
parse_packet(_) ->
    {error, invalid_packet}.

%% 解析 IPv4 地址
parse_ipv4(<<A:8, B:8, C:8, D:8>>) ->
    io_lib:format("~p.~p.~p.~p", [A, B, C, D]).

%% HTTP 状态行解析
parse_status_line(<<"HTTP/", Version:5/binary, " ", Code:3/binary, " ", Reason/binary>>) ->
    {Version, binary_to_integer(Code), binary_to_list(Reason)}.

5.7.4 Map 模式匹配

%% Map 部分匹配(只匹配需要的 key)
greet(#{name := Name}) ->
    io:format("Hello, ~s!~n", [Name]).

%% 带守卫的 Map 匹配
check_access(#{role := admin}) ->
    full_access;
check_access(#{role := user, level := Level}) when Level >= 5 ->
    limited_access;
check_access(#{role := user}) ->
    read_only;
check_access(_) ->
    no_access.

%% 使用
1> greet(#{name => "Alice", age => 25}).
Hello, Alice!
ok
2> check_access(#{role => admin}).
full_access

5.8 实战:命令解析器

%% command_parser.erl
-module(command_parser).
-export([parse/1, execute/1]).

-type command() ::
    {say, string()} |
    {move, atom()} |
    {attack, string()} |
    {look, string() | all} |
    quit.

-spec parse(string()) -> {ok, command()} | {error, string()}.
parse(Input) ->
    case string:lexemes(string:trim(Input), " ") of
        ["say" | Words] ->
            {ok, {say, string:join(Words, " ")}};
        ["move", Direction] ->
            case parse_direction(Direction) of
                {ok, Dir} -> {ok, {move, Dir}};
                error     -> {error, "Invalid direction: " ++ Direction}
            end;
        ["attack", Target] ->
            {ok, {attack, Target}};
        ["look"] ->
            {ok, {look, all}};
        ["look", Target] ->
            {ok, {look, Target}};
        ["quit"] ->
            {ok, quit};
        [] ->
            {error, "Empty command"};
        _ ->
            {error, "Unknown command"}
    end.

-spec execute(command()) -> string().
execute({say, Message}) ->
    "You say: " ++ Message;
execute({move, north}) -> "You move north. A dark corridor.";
execute({move, south}) -> "You move south. An open field.";
execute({move, east})  -> "You move east. A river blocks your path.";
execute({move, west})  -> "You move west. A mountain rises ahead.";
execute({attack, Target}) ->
    "You attack " ++ Target ++ "!";
execute({look, all}) ->
    "You see: a sword, a shield, and a potion.";
execute({look, Target}) ->
    "You examine: " ++ Target;
execute(quit) ->
    "Goodbye!".

-spec parse_direction(string()) -> {ok, atom()} | error.
parse_direction("north") -> {ok, north};
parse_direction("south") -> {ok, south};
parse_direction("east")  -> {ok, east};
parse_direction("west")  -> {ok, west};
parse_direction(_)       -> error.
$ erl
1> c(command_parser).
{ok, command_parser}
2> {ok, Cmd} = command_parser:parse("say hello world").
3> command_parser:execute(Cmd).
"You say: hello world"
4> {ok, Cmd2} = command_parser:parse("move north").
5> command_parser:execute(Cmd2).
"You move north. A dark corridor."

5.9 实战:JSON 解析器(简化版)

%% mini_json.erl
-module(mini_json).
-export([parse/1]).

%% 简化的 JSON 解析器(演示模式匹配)
parse(<<${, Rest/binary>>) ->
    parse_object(Rest, #{});
parse(<<$[, Rest/binary>>) ->
    parse_array(Rest, []);
parse(<<$", Rest/binary>>) ->
    parse_string(Rest, []).

parse_object(<<$}, Rest/binary>>, Acc) ->
    {Acc, Rest};
parse_object(<<$\s, Rest/binary>>, Acc) ->
    parse_object(Rest, Acc);
parse_object(<<$,, Rest/binary>>, Acc) ->
    parse_object(Rest, Acc);
parse_object(<<$", Rest/binary>>, Acc) ->
    {Key, Rest2} = parse_string(Rest, []),
    {Value, Rest3} = parse_value(skip_whitespace(Rest2)),
    parse_object(Rest3, Acc#{Key => Value}).

parse_array(<<$], Rest/binary>>, Acc) ->
    {lists:reverse(Acc), Rest};
parse_array(<<$\s, Rest/binary>>, Acc) ->
    parse_array(Rest, Acc);
parse_array(<<$, , Rest/binary>>, Acc) ->
    parse_array(Rest, Acc);
parse_array(Data, Acc) ->
    {Value, Rest} = parse_value(skip_whitespace(Data)),
    parse_array(Rest, [Value | Acc]).

parse_string(<<$", Rest/binary>>, Acc) ->
    {lists:reverse(Acc), Rest};
parse_string(<<C, Rest/binary>>, Acc) ->
    parse_string(Rest, [C | Acc]).

parse_value(<<$", Rest/binary>>) -> parse_string(Rest, []);
parse_value(<<${, Rest/binary>>) -> parse_object(Rest, #{});
parse_value(<<$[, Rest/binary>>) -> parse_array(Rest, []).

skip_whitespace(<<$\s, Rest/binary>>) -> skip_whitespace(Rest);
skip_whitespace(<<$\n, Rest/binary>>) -> skip_whitespace(Rest);
skip_whitespace(<<$\t, Rest/binary>>) -> skip_whitespace(Rest);
skip_whitespace(Data) -> Data.

5.10 注意事项

⚠️ 常见陷阱

陷阱说明
变量作用域case 分支必须绑定相同的变量
=:= vs ==始终使用 =:= 避免隐式类型转换
子句顺序函数子句从上到下匹配,顺序很重要
守卫副作用守卫不能有副作用(不能调用自定义函数)
未处理的模式确保所有可能的情况都被覆盖

💡 最佳实践

  1. 优先使用模式匹配 + 函数子句代替 case
  2. 使用守卫的逗号 ,(且)和分号 ;(或)
  3. 始终包含通配符 _ 分支处理意外情况
  4. 二进制模式匹配是处理网络协议的利器
  5. 利用 = 在模式中绑定子结构

5.11 扩展阅读


上一章:04 - 变量与类型 下一章:06 - 函数进阶