文章目录

Erlang 函数:fun() 与匿名函数

发布于 2026-04-19 04:20:18 · 浏览 6 次 · 评论 0 条

Erlang 函数:fun() 与匿名函数

在 Erlang 编程中,函数不仅是代码的执行单元,更是传递逻辑的核心载体。除了我们在模块中定义的命名函数外,Erlang 还提供了一种极其强大的“匿名函数”机制,通常通过 fun 关键字来创建。这种函数没有固定的名称,可以像普通数据一样在变量间传递、赋值或在运行时动态生成。

本文将直接带你掌握 fun 的定义、使用以及“闭包”特性的实际应用。


1. 基础定义:创建与调用

匿名函数最直观的特点是“即用即定义”,不需要预先在模块头部声明。

定义 一个简单的加法匿名函数。在 Erlang Shell 中输入以下代码:

Add = fun(A, B) -> A + B end.

这段代码做了两件事:创建了一个接收两个参数并返回其和的函数,然后将这个函数实体赋值给变量 Add

调用 这个匿名函数。Erlang 中调用匿名函数的语法非常独特,必须在变量名后加上一个点 .,然后接括号:

Add(10, 20).

执行后,终端将输出 30

2. 函数作为参数:高阶函数的应用

匿名函数最常见的用途是作为参数传递给高阶函数,例如列表操作函数 lists:maplists:filter

假设有一个数字列表,你需要将列表中的每个数字翻倍。

准备 一个测试列表:

Numbers = [1, 2, 3, 4, 5].

使用 lists:map/2 函数,并传入一个匿名函数作为第一个参数:

Doubled = lists:map(fun(X) -> X * 2 end, Numbers).

变量 Doubled 的结果将是 [2, 4, 6, 8, 10]

这里 fun(X) -> X * 2 end 是一个临时的逻辑块,它被传递进 map 函数内部,对列表中的每一个元素执行了一次。


3. 深入理解:闭包与变量捕获

这是 Erlang 匿名函数最强大的特性:匿名函数可以“捕获”其定义作用域内存在的变量,并在函数定义体外继续使用这些变量。这被称为“闭包”。

定义一个外部变量 Base

Base = 10.

创建 一个匿名函数,该函数内部引用了变量 Base

AddBase = fun(X) -> X + Base end.

调用 这个函数:

AddBase(5).

输出结果为 15。虽然 AddBase 是在 Base 等于 10 的环境下定义的,但即便我们后续改变了 Base 的值(Erlang 中是单次赋值,所以通常是变量被绑定后的状态),该函数记住的永远是它定义那一刻 Base 的值。

如果你尝试重新绑定变量(虽然在同一个 Shell 作用域中不能直接赋值两次,但在不同函数层级中可以模拟),闭包依然保持其原始环境。这种特性常用于“函数工厂”,即生成函数的函数。

编写 一个生成函数的示例:

MakeMultiplier = fun(Factor) ->
    fun(Number) -> Number * Factor end
end.

生成 两个特定的乘法函数:

Triple = MakeMultiplier(3).
Quadruple = MakeMultiplier(4).

调用 它们:

Triple(5).   % 输出 15
Quadruple(5). % 输出 20

Triple 记住了 Factor 为 3 的环境,而 Quadruple 记住了 Factor 为 4 的环境。


4. fun 的多种形式:匿名 vs 命名引用

fun 关键字不仅可以定义匿名逻辑,还可以引用已存在的命名函数。这在需要将模块函数作为高阶函数参数时非常有用。

语法格式为 fun Module:Function/Arity

假设有一个模块 math_ops,其中包含导出函数 square/1

定义 引用形式的 fun:

SquareFun = fun math_ops:square/1.

使用 它:

SquareFun(4). % 等同于调用 math_ops:square(4)

为了区分这两种形式,请参考下表:

特性 匿名定义 fun(...) -> ... end 命名引用 fun Module:Func/Arity
定义位置 任何代码行内,即时定义 引用已存在于模块中的函数
灵活性 极高,可动态构建逻辑 较低,依赖现有函数
性能 每次调用会有轻微的额外开销 引用现有代码,通常效率更高
主要用途 临时逻辑、闭包、简短回调 传递标准库函数或模块核心逻辑

5. 复杂逻辑:Fun 中的模式匹配与 Guard

匿名函数不仅仅是简单的单行表达式,它们也支持复杂的模式匹配和守卫,这与命名函数的子句非常相似。

定义 一个包含多子句和守卫的匿名函数。该函数根据输入类型返回不同描述:

Describe = fun
    (X) when is_integer(X) -> "Integer: " ++ integer_to_list(X);
    (X) when is_float(X) -> "Float: " ++ float_to_list(X);
    (_) -> "Unknown type"
end.

执行 测试:

Describe(42).     % 输出 "Integer: 42"
Describe(3.14).   % 输出 "Float: 3.14000000000000012434" (浮点精度)
Describe(hello).  % 输出 "Unknown type"

注意,Fun 的多个子句之间用分号 ; 分隔,最后一个子句以 end 结束。

为了更直观地理解 Erlang 虚拟机如何处理 fun 调用的流程,特别是带有守卫和多子句的情况,可以参考以下逻辑流:

graph TD A["调用 Fun(Param)"] --> B{检查第一个子句守卫} B -- 通过 --> C["执行第一个子句逻辑"] B -- 失败 --> D{检查第二个子句守卫} D -- 通过 --> E["执行第二个子句逻辑"] D -- 失败 --> F{更多子句?} F -- 是 --> G["继续检查后续子句"] F -- 否 --> H["抛出 function_clause 错误"] C --> I["返回结果"] E --> I G --> B

在编写复杂的匿名函数时,必须确保至少有一个子句能够匹配输入,否则程序会运行时崩溃。


6. 命名函数中的递归限制与 Fun 自递归

通常我们使用命名函数进行递归。那么匿名函数可以递归调用自己吗?由于 fun 没有名字,直接实现递归比较困难,但在变量绑定后,可以通过变量名实现。

定义 一个阶乘的匿名函数:

Fact = fun
    (0) -> 1;
    (N) when N > 0 -> N * Fact(N - 1)
end.

上述代码在大多数现代 Erlang 版本(OTP 17+)中是有效的,因为编译器扩展了变量 Fact 的作用域,使得 fun 内部能够“看见”自己。但在旧版本中,这会报未定义变量错误。

为了兼容旧版本或更底层的实现,可以使用 Y-combinator 模式或者将 fun 作为参数传递给自己。

使用 自身作为参数的写法(最通用的自递归方式):

FactGen = fun
    (_, 0) -> 1;
    (Self, N) when N > 0 -> N * Self(Self, N - 1)
end.

% 调用时需要把自己传进去
Fact = fun(N) -> FactGen(FactGen, N) end.

运行 测试:

Fact(5). % 输出 120

这种方式确保了函数逻辑的纯粹性,不依赖于外部变量的名字绑定,但可读性稍差。在实际工程中,如果逻辑复杂且需要递归,建议优先使用命名函数。


7. 列表推导中的 Fun

在列表推导式中,fun 可以用来简化复杂的过滤或转换逻辑。

假设有一个用户列表,包含年龄和姓名:

Users = [{bob, 25}, {alice, 17}, {carl, 30}].

编写 一个匿名函数判断是否成年:

IsAdult = fun({_, Age}) -> Age >= 18 end.

应用 在列表推导式中过滤未成年人:

Adults = [Name || {Name, _} <- Users, IsAdult({Name, _})].

这种写法比直接在列表推导式中写复杂的布尔逻辑更清晰,也便于复用。


8. 常见错误与调试技巧

在使用 fun 时,新手常遇到两类错误。

1. 语法错误:遗漏 end 或分隔符

错误 示例:

BadFun = fun(X) -> X * 2.

原因:缺少 end
修正

GoodFun = fun(X) -> X * 2 end.

2. 运行时错误:Arity mismatch(参数个数不匹配)

错误 示例:

Double = fun(X) -> X * 2 end.
Double(1, 2).

原因Double 只接受 1 个参数,但传入了 2 个。
结果:报错 badarg 或 function_clause 错误。

调试 技巧:
当你不确定一个变量的类型是否为 fun 时,可以使用内置 guard is_function/1is_function/2

测试 变量类型:

IsFun = is_function(Double).           % 返回 true
IsFunWithArity = is_function(Double, 1). % 返回 true
IsFunWithArity2 = is_function(Double, 2).% 返回 false

在编写接收 fun 作为参数的通用函数时,务必加上 Arity 检查:

safe_apply(Fun, Args) when is_function(Fun, length(Args)) ->
    apply(Fun, Args);
safe_apply(_, _) ->
    {error, badarity}.

掌握 fun() 与匿名函数,是通往 Erlang 高级编程的必经之路。无论是利用闭包构建工厂函数,还是利用高阶函数处理数据列表,fun 都提供了极高的灵活性和表达力。

评论 (0)

暂无评论,快来抢沙发吧!

扫一扫,手机查看

扫描上方二维码,在手机上查看本文