Erlang 函数:fun() 与匿名函数
在 Erlang 编程中,函数不仅是代码的执行单元,更是传递逻辑的核心载体。除了我们在模块中定义的命名函数外,Erlang 还提供了一种极其强大的“匿名函数”机制,通常通过 fun 关键字来创建。这种函数没有固定的名称,可以像普通数据一样在变量间传递、赋值或在运行时动态生成。
本文将直接带你掌握 fun 的定义、使用以及“闭包”特性的实际应用。
1. 基础定义:创建与调用
匿名函数最直观的特点是“即用即定义”,不需要预先在模块头部声明。
定义 一个简单的加法匿名函数。在 Erlang Shell 中输入以下代码:
Add = fun(A, B) -> A + B end.
这段代码做了两件事:创建了一个接收两个参数并返回其和的函数,然后将这个函数实体赋值给变量 Add。
调用 这个匿名函数。Erlang 中调用匿名函数的语法非常独特,必须在变量名后加上一个点 .,然后接括号:
Add(10, 20).
执行后,终端将输出 30。
2. 函数作为参数:高阶函数的应用
匿名函数最常见的用途是作为参数传递给高阶函数,例如列表操作函数 lists:map 或 lists: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 调用的流程,特别是带有守卫和多子句的情况,可以参考以下逻辑流:
在编写复杂的匿名函数时,必须确保至少有一个子句能够匹配输入,否则程序会运行时崩溃。
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/1 或 is_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 都提供了极高的灵活性和表达力。

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