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

Erlang/OTP 完全指南 / 06 - 函数进阶

第 06 章:函数进阶 — 匿名函数、高阶函数与列表推导

函数是 Erlang 中的一等公民(First-class Citizen)。本章学习匿名函数(fun)、高阶函数(Higher-order Function)和列表推导(List Comprehension)。


6.1 匿名函数(Fun)

6.1.1 基本语法

%% 基本匿名函数
Square = fun(X) -> X * X end.
Square(5).  %% 25

%% 多参数
Add = fun(X, Y) -> X + Y end.
Add(3, 4).  %% 7

%% 多子句匿名函数
Abs = fun(X) when X >= 0 -> X;
         (X) -> -X
      end.
Abs(-5).  %% 5

6.1.2 闭包(Closure)

匿名函数可以捕获外部变量(形成闭包):

%% 闭包捕获外部变量
Multiplier = fun(N) ->
    fun(X) -> X * N end
end.

Triple = Multiplier(3).
Triple(10).  %% 30

FiveTimes = Multiplier(5).
FiveTimes(10).  %% 50

%% 每个闭包独立持有自己的变量
Greet = fun(Name) ->
    fun() -> io:format("Hello, ~s!~n", [Name]) end
end.

HelloAlice = Greet("Alice").
HelloBob = Greet("Bob").
HelloAlice().  %% Hello, Alice!
HelloBob().    %% Hello, Bob!

6.1.3 函数引用

%% 引用已有模块函数
F = fun io:format/2.
F("Hello~n", []).

%% 简写语法(& 操作符,Erlang 不原生支持,Elixir 支持)
%% 在 Erlang 中使用 fun 模块:函数/参数个数
lists:map(fun erlang:abs/1, [-1, -2, 3]).  %% [1, 2, 3]

%% 函数引用也可以用在 export 中
-export([handle/1]).

%% 将函数传递给其他函数
lists:foreach(fun io:format/1, ["Hello\n", "World\n"]).

6.2 高阶函数

6.2.1 什么是高阶函数?

接受函数作为参数或返回函数的函数:

%% 接受函数作为参数
apply_twice(F, X) ->
    F(F(X)).

apply_twice(fun(X) -> X + 1 end, 5).  %% 7

%% 返回函数
add(N) ->
    fun(X) -> X + N end.

Add5 = add(5).
Add5(10).  %% 15

6.2.2 lists 模块中的高阶函数

函数作用示例
lists:map(F, List)对每个元素应用函数lists:map(fun(X) -> X*2 end, [1,2,3])[2,4,6]
lists:filter(F, List)过滤元素lists:filter(fun(X) -> X > 2 end, [1,2,3,4])[3,4]
lists:foldl(F, Acc, List)左折叠lists:foldl(fun(X,S) -> X+S end, 0, [1,2,3])6
lists:foldr(F, Acc, List)右折叠类似 foldl,但从右往左
lists:any(F, List)任一满足lists:any(fun(X) -> X > 3 end, [1,2,3])false
lists:all(F, List)全部满足lists:all(fun(X) -> X > 0 end, [1,2,3])true
lists:foreach(F, List)遍历(无返回值)lists:foreach(fun(X) -> io:format("~p~n", [X]) end, [1,2])
lists:sort(F, List)自定义排序lists:sort(fun(A,B) -> A > B end, [3,1,2])[3,2,1]
lists:partition(F, List)分区lists:partition(fun(X) -> X > 2 end, [1,2,3,4]){[3,4],[1,2]}
lists:dropwhile(F, List)删除前缀满足条件的lists:dropwhile(fun(X) -> X < 3 end, [1,2,3,4])[3,4]
lists:takewhile(F, List)取前缀满足条件的lists:takewhile(fun(X) -> X < 3 end, [1,2,3,4])[1,2]

6.2.3 map 详解

%% 基本用法
lists:map(fun(X) -> X * 2 end, [1, 2, 3]).
%% [2, 4, 6]

%% 提取嵌套字段
Users = [{alice, 25}, {bob, 30}, {charlie, 22}],
Names = lists:map(fun({Name, _}) -> Name end, Users).
%% [alice, bob, charlie]

%% 字符串处理
Lines = ["  hello  ", "  world  "],
Trimmed = lists:map(fun string:trim/1, Lines).
%% ["hello", "world"]

%% 链式处理
Result = lists:map(fun(X) -> X * 2 end,
           lists:map(fun(X) -> X + 1 end, [1, 2, 3])).
%% [4, 6, 8]

6.2.4 foldl 详解

foldl 是最强大的列表操作,几乎所有列表操作都可以用 foldl 实现:

%% 求和
lists:foldl(fun(X, Acc) -> X + Acc end, 0, [1, 2, 3, 4, 5]).
%% 15

%% 求最大值
lists:foldl(fun(X, Max) -> max(X, Max end), 0, [3, 1, 4, 1, 5]).
%% 5

%% 计数
lists:foldl(fun(X, Count) when X > 3 -> Count + 1;
               (_, Count) -> Count
            end, 0, [1, 2, 3, 4, 5]).
%% 2

%% 构建 Map
lists:foldl(fun({K, V}, Acc) -> Acc#{K => V} end,
            #{},
            [{name, "Alice"}, {age, 25}, {city, "Beijing"}]).
%% #{name => "Alice", age => 25, city => "Beijing"}

%% 分组
group_by(Fun, List) ->
    lists:foldl(fun(Item, Acc) ->
        Key = Fun(Item),
        Group = maps:get(Key, Acc, []),
        Acc#{Key => [Item | Group]}
    end, #{}, List).

group_by(fun(X) -> X rem 2 end, [1,2,3,4,5,6]).
%% #{0 => [6,4,2], 1 => [5,3,1]}

6.2.5 filter 与 partition

%% 过滤偶数
Evens = lists:filter(fun(X) -> X rem 2 =:= 0 end, [1,2,3,4,5,6]).
%% [2, 4, 6]

%% 过滤有效用户
ValidUsers = lists:filter(fun(#{active := A}) -> A end, Users).

%% 分区:满足条件的和不满足的
{Pos, Neg} = lists:partition(fun(X) -> X >= 0 end, [-1, 2, -3, 4, -5]).
%% {[2, 4], [-1, -3, -5]}

%% 反过滤
Reject = fun(Pred, List) ->
    lists:filter(fun(X) -> not Pred(X) end, List)
end.

6.3 列表推导(List Comprehension)

6.3.1 基本语法

%% [表达式 || 生成器, ..., 守卫]
%% 语法:[Expr || Generator, ..., Guard]

%% 基本用法
[X * 2 || X <- [1, 2, 3, 4, 5]].
%% [2, 4, 6, 8, 10]

%% 带过滤器
[X || X <- [1,2,3,4,5,6], X rem 2 =:= 0].
%% [2, 4, 6]

%% 多个生成器
[{X, Y} || X <- [1,2], Y <- [a,b]].
%% [{1,a},{1,b},{2,a},{2,b}]

%% 多个条件
[{X, Y} || X <- [1,2,3,4,5], Y <- [1,2,3,4,5],
           X + Y =:= 6, X < Y].
%% [{1,5},{2,4}]

6.3.2 生成器类型

%% 列表生成器
[X || X <- [1, 2, 3]].

%% 二进制生成器
<< <<(X * 2)>> || <<X>> <= <<1, 2, 3>> >>.
%% <<2, 4, 6>>

%% Map 生成器
[{K, V} || {K, V} := #{name => "Alice", age => 25}].
%% [{name, "Alice"}, {age, 25}]

%% 范围生成器(整数范围)
[X || X <- lists:seq(1, 10)].
%% [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
[X * X || X <- lists:seq(1, 5)].
%% [1, 4, 9, 16, 25]

6.3.3 实用示例

%% 质数判断(简单版)
is_prime(N) when N < 2 -> false;
is_prime(2) -> true;
is_prime(N) ->
    not lists:any(fun(X) -> N rem X =:= 0 end,
                  lists:seq(2, trunc(math:sqrt(N)))).

%% 生成质数列表
Primes = [X || X <- lists:seq(2, 100), is_prime(X)].
%% [2,3,5,7,11,13,17,19,23,29,31,37,41,43,47,53,59,61,67,71,73,79,83,89,97]

%% 字符串处理
Words = ["Hello", "World", "Erlang"],
Upper = [string:uppercase(W) || W <- Words].
%% ["HELLO", "WORLD", "ERLANG"]

%% 嵌套列表展平
Nested = [[1, 2], [3, 4], [5]],
Flat = [X || Sub <- Nested, X <- Sub].
%% [1, 2, 3, 4, 5]

%% 文件过滤
Files = ["data.txt", "image.png", "log.txt", "doc.pdf"],
TxtFiles = [F || F <- Files, filename:extension(F) =:= ".txt"].
%% ["data.txt", "log.txt"]

%% 矩阵转置
transpose([[]|_]) -> [];
transpose(Matrix) ->
    [lists:map(fun hd/1, Matrix) | transpose(lists:map(fun tl/1, Matrix))].

transpose([[1,2,3],[4,5,6],[7,8,9]]).
%% [[1,4,7],[2,5,8],[3,6,9]]

%% 笛卡尔积
cartesian(A, B) ->
    [{X, Y} || X <- A, Y <- B].

cartesian([1,2], [a,b,c]).
%% [{1,a},{1,b},{1,c},{2,a},{2,b},{2,c}]

6.4 管道操作(模拟)

Erlang 没有原生的管道操作符,但可以用以下模式模拟:

%% 方式一:嵌套调用(可读性差)
Result = lists:map(fun(X) -> X * 2 end,
           lists:filter(fun(X) -> X > 3 end,
             lists:map(fun(X) -> X + 1 end, [1,2,3,4,5]))).

%% 方式二:中间变量(推荐)
Step1 = lists:map(fun(X) -> X + 1 end, [1,2,3,4,5]),
Step2 = lists:filter(fun(X) -> X > 3 end, Step1),
Result = lists:map(fun(X) -> X * 2 end, Step2).
%% [8, 10, 12]

%% 方式三:自定义管道函数
pipe(Input, Funs) ->
    lists:foldl(fun(F, Acc) -> F(Acc) end, Input, Funs).

pipe([1,2,3,4,5], [
    fun(Xs) -> lists:map(fun(X) -> X + 1 end, Xs) end,
    fun(Xs) -> lists:filter(fun(X) -> X > 3 end, Xs) end,
    fun(Xs) -> lists:map(fun(X) -> X * 2 end, Xs) end
]).
%% [8, 10, 12]

6.5 函数组合

%% 函数组合
compose(F, G) ->
    fun(X) -> F(G(X)) end.

%% 使用
Double = fun(X) -> X * 2 end.
AddOne = fun(X) -> X + 1 end.

DoubleThenAddOne = compose(AddOne, Double),
DoubleThenAddOne(5).  %% 11 (5*2+1)

AddOneThenDouble = compose(Double, AddOne),
AddOneThenDouble(5).  %% 12 ((5+1)*2)

%% 管道组合(从左到右)
pipe2(F, G) ->
    fun(X) -> G(F(X)) end.

6.6 柯里化(模拟)

Erlang 不原生支持柯里化,但可以模拟:

%% 柯里化模拟
curry2(F) ->
    fun(X) -> fun(Y) -> F(X, Y) end end.

Add = fun(X, Y) -> X + Y end.
CurriedAdd = curry2(Add).
Add5 = CurriedAdd(5).
Add5(10).  %% 15

%% 更实用的版本
curry3(F) ->
    fun(X) -> fun(Y) -> fun(Z) -> F(X, Y, Z) end end end.

6.7 实战:数据处理器

%% data_processor.erl
-module(data_processor).
-export([process_sales/1, top_n/2, summarize/1]).

-type sale() :: #{product := string(), amount := float(), region := atom()}.
-type summary() :: #{total := float(), count := integer(), avg := float()}.

%% 模拟销售数据
-spec process_sales([sale()]) -> #{atom() => summary()}.
process_sales(Sales) ->
    %% 按地区分组
    ByRegion = group_by(fun(#{region := R}) -> R end, Sales),
    %% 计算每个地区的汇总
    maps:map(fun(_Region, RegionSales) -> summarize(RegionSales) end, ByRegion).

-spec summarize([sale()]) -> summary().
summarize(Sales) ->
    Total = lists:foldl(fun(#{amount := A}, Acc) -> A + Acc end, 0, Sales),
    Count = length(Sales),
    #{total => Total, count => Count, avg => Total / Count}.

-spec top_n([sale()], integer()) -> [sale()].
top_n(Sales, N) ->
    Sorted = lists:sort(fun(#{amount := A}, #{amount := B}) -> A >= B end, Sales),
    lists:sublist(Sorted, N).

%% 内部函数
group_by(Fun, List) ->
    lists:foldl(fun(Item, Acc) ->
        Key = Fun(Item),
        Group = maps:get(Key, Acc, []),
        Acc#{Key => [Item | Group]}
    end, #{}, List).
$ erl
1> c(data_processor).
{ok, data_processor}
2> Sales = [
   #{product => "Laptop", amount => 999.99, region => north},
   #{product => "Phone", amount => 699.99, region => south},
   #{product => "Tablet", amount => 449.99, region => north},
   #{product => "Watch", amount => 299.99, region => south}
].
3> data_processor:process_sales(Sales).
#{north =>
      #{avg => 724.99, count => 2, total => 1449.98},
  south =>
      #{avg => 499.99, count => 2, total => 999.98}}

6.8 实战:事件处理器

%% event_handler.erl
-module(event_handler).
-export([new/0, add_handler/3, handle_event/2]).

-type handler() :: {atom(), fun()}.
-type handlers() :: [handler()].

-spec new() -> handlers().
new() -> [].

-spec add_handler(atom(), fun(), handlers()) -> handlers().
add_handler(EventType, Fun, Handlers) ->
    [{EventType, Fun} | Handlers].

-spec handle_event(term(), handlers()) -> ok.
handle_event(Event, Handlers) ->
    lists:foreach(
        fun({Type, Fun}) ->
            case Event of
                {Type, Data} -> Fun(Data);
                _ -> ok
            end
        end,
        Handlers).

6.9 注意事项

⚠️ 常见陷阱

陷阱说明
闭包捕获变量引用闭包捕获变量的值,不是引用(不可变)
列表推导不是惰性的所有元素立即计算,大数据需注意内存
++ 效率低列表拼接 O(n),优先使用 IO list 或 cons
foldl vs foldrfoldl 更高效(尾递归),优先使用 foldl
过多嵌套嵌套的 map/filter/foldl 难以阅读,拆分使用中间变量

💡 最佳实践

  1. 优先使用 lists:foldl/3 而非手写递归
  2. 简单映射用列表推导,复杂逻辑用 lists:map/filter
  3. 复杂数据管道用中间变量,保持可读性
  4. 闭包避免捕获大对象,防止内存泄漏
  5. 善用模式匹配在函数参数中解构

6.10 扩展阅读


上一章:05 - 模式匹配 下一章:07 - 列表深入