本 EEP 提议添加一个新的单目运算符 ^
,用于显式地将模式中的变量标记为已绑定。这在 Elixir 中被称为“固定”,请参阅 Elixir 文档。
例如
f(X, Y) ->
case X of
{a, Y} -> ok;
_ -> error
end.
可以更明确地写成
f(X, Y) ->
case X of
{a, ^Y} -> ok;
_ -> error
end.
在 Elixir 中,这个运算符对于能够在模式中引用已绑定变量的值是绝对必要的,因为模式中的变量总是被视为新的隐藏实例(就像在 Erlang 的 fun 子句头中一样),除非明确固定。
在 Erlang 中,它们是可选的,但仍然是一个好主意,因为它们使程序在编辑和重构时更加健壮,并且还允许在 fun 子句头和推导式生成器模式中使用固定的变量。
Erlang 添加了一个新的单目运算符 ^
,称为“固定运算符”。它只能在模式中使用,并且只能用于变量。它的含义是,“固定”变量将在模式的封闭环境中解释,并且其值将在模式中的该位置使用。
在当前的 Erlang 中,如果变量已在封闭环境中绑定,则在普通的匹配结构中会自动发生此行为。在以下示例中
f(X, Y) ->
case X of
{a, Y} -> {ok, Y};
_ -> error
end.
模式中 Y
的使用被视为对函数参数 Y
的引用,而不是引入一个新变量,并且子句主体中的 Y
也是同一个参数。因此,在这种情况下,将模式变量注释为 ^Y
不会更改程序的行为,但会明确其意图
f(X, Y) ->
case X of
{a, ^Y} -> {ok, Y};
_ -> error
end.
对于 fun 表达式和列表推导式生成器模式,固定运算符使该语言更具表现力。考虑以下 Erlang 代码
f(X, Y) ->
F = fun ({a, Y}) -> {ok, Y};
(_) -> error
end,
F(X).
在这里,fun F
的子句头中出现的 Y
是一个新的变量实例,它隐藏了 f(X, Y)
的 Y
参数,并且 fun 子句将匹配该位置中的任何值。子句主体中的 Y
是在子句头中绑定的那个。但是,使用固定运算符,我们可以选择性地匹配外部作用域中绑定的变量
f(X, Y) ->
F = fun ({a, ^Y}) -> {ok, Y};
(_) -> error
end,
F(X).
在这种情况下,Y
没有新的绑定,并且 fun 子句主体中 Y
的使用是指函数参数。但也可以在同一模式中组合固定和隐藏
f(X, Y) ->
F = fun ({a, ^Y, Y}) -> {ok, Y};
(_) -> error
end,
F(X).
在这种情况下,固定的字段是指函数参数的值,但 Y
也有一个新的隐藏绑定到元组的第三个字段。fun 子句主体中的使用现在是指隐藏的实例。
列表推导式或二进制推导式中的生成器模式遵循与 fun 子句头相同的规则,因此使用固定,我们可以例如编写以下代码
f(X, Y) ->
[{b, Y} || {a, ^Y, Y} <- X].
其中 {b, Y}
中的 Y
是绑定到模式元组第三个元素的隐藏实例。
最后,添加了一个新的编译器标志 warn_unpinned_vars
,默认情况下禁用,如果启用,则编译器会发出有关在模式中使用未明确用 ^
运算符注释的所有已绑定变量的警告。这允许用户按模块逐步将其代码迁移到在其所有代码中使用显式固定。如果固定成为 Erlang 的常态,则可以默认启用此标志,并且最终,对于引用模式中已绑定的变量,可以严格要求固定运算符。
模式中变量的显式固定使程序更具可读性,因为代码的意图变得清晰。当已经在 Erlang 中使用已绑定的变量而没有任何注释时,任何阅读一段代码的人都必须首先仔细研究它,以了解在模式点会绑定哪些变量,然后他们才能判断任何模式变量是新绑定还是意味着相等断言。即使对于经验丰富的 Erlang 用户来说,在代码审查期间或在试图理解一段注释不充分的代码时,也容易错过这一点。
也许更重要的是,固定还使程序在编辑和重构时更加健壮。以我们之前的示例为例,并添加一个打印语句
f(X, Y) ->
io:format("checking: ~p", [Y]),
case X of
{a, Y} -> {ok, Y};
_ -> error
end.
假设有人将函数参数从 Y
重命名为 Z
,并更新了打印语句,但忘记更新 case 子句中的使用。如果没有显式固定注释,则会默默允许更改,但模式中的 Y
将被解释为一个新变量,它将匹配任何值,然后该值将在主体中使用。这会更改程序的行为。如果模式中的使用被注释为 ^Y
,编译器将生成错误“Y 未绑定”,并且会捕获错误。
当修改代码以添加功能或修复错误时,程序员可能希望为临时结果引入新变量。在一个长函数体中,这会带来引入新错误的风险。考虑以下情况
g(Stuff) ->
...
Thing = case ... of
{a, T} -> T;
_ -> 0
end,
...
{ok, [Thing|Stuff]}.
在这里,T
是一个新变量,显然只是一个临时变量,用于提取元组的第二个元素。但是,假设有人在函数主体中更高的地方添加了一个名为 T
的绑定,而没有注意到该名称已在使用中
g(Stuff) ->
...
T = q(Stuff) + 1,
io:format("~p", [p(T)]),
...
Thing = case ... of
{a, T} -> T;
_ -> 0
end,
...
{ok, [Thing|Stuff]}.
现在,仅当元组的第二个元素与先前定义的 T
的值完全相同时,case 开关的第一个子句才会匹配。同样,编译器会默默接受此更改,但如果已指示它警告所有在模式中使用未注释的已绑定变量,则会检测到此错误。
在 funs 和推导式中,固定还允许我们执行其他需要额外临时变量的操作。考虑以下代码
f(X, Y) ->
F = fun ({a, Y}) -> {ok, Y};
(_) -> error
end,
F(X).
由于 fun 的子句头中的 Y
是一个新的隐藏实例,因此该模式将匹配该位置中的任何值。为了仅匹配作为 Y
传递给 f
的值,必须添加子句保护,并且必须使用临时变量来访问外部 Y
f(X, Y) ->
OuterY = Y,
F = fun ({a, Y}) when Y =:= OuterY -> {ok, Y};
(_) -> error
end,
F(X).
我们可以改为重命名内部使用的 Y
以避免隐藏,但相等性测试仍然必须写成显式保护
f(X, Y) ->
F = fun ({a, Z}) when Z =:= Y -> {ok, Y};
(_) -> error
end,
F(X).
借助固定运算符,不再需要担心此类问题,我们可以简单地编写
f(X, Y) ->
F = fun ({a, ^Y}) -> {ok, Y};
(_) -> error
end,
F(X).
此外,在需要同时访问周围的 Y
定义和引入新的隐藏绑定的奇特情况下,可以使用固定轻松编写
f(X, Y) ->
F = fun ({a, ^Y, Y}) -> {ok, Y};
(_) -> error
end,
F(X).
但是在当前的 Erlang 中,需要两个单独的临时变量
f(X, Y) ->
OuterY = Y,
F = fun ({a, Temp, Y}) when Temp =:= OuterY -> {ok, Y};
(_) -> error
end,
F(X).
如前所述,这同样适用于推导式生成器中的模式。
添加一个新的且先前未使用的运算符 ^
不会影响现有代码的含义,并且除非使用 warn_unpinned_vars
显式启用,否则编译器不会为现有代码发出任何新的警告或错误。因此,此更改是完全向后兼容的。
该实现可以在 PR #2951 中找到。
本文档已放入公共领域。