作者
Serge Aleynikov <saleyn(at)gmail(dot)com>
状态
草案
类型
标准跟踪
创建于
2021年6月9日
Erlang 版本
OTP-24.0

EEP 57:扩展模式语法以支持可替代匹配 #

摘要 #

目前,在 case、receive 表达式、函数子句、lambda、try-of 和 try-catch、列表和位字符串推导式中的生成器以及函数参数中定义模式匹配时,Erlang 只支持表达式中的单个匹配。

本提案扩展了语法,允许多个可替代匹配共享同一个主体或与右侧表达式进行匹配。

规范 #

目前,case、receive、try-of 表达式中的匹配语法类似于这样

case I of
  1 -> less_than_three;
  2 -> less_than_three;
  3 -> less_than_ten;
  _ -> other
end.

应该修改为允许

case I of
  1 | 2 -> less_than_three;
  3     -> less_than_ten;
  _     -> other
end.

同样,函数或表达式匹配看起来像这样

foo(1) -> ok;
foo(2) -> ok;
foo(N) -> other.

更一般地,case、receive、try-of 表达式、lambda 或模式匹配中的模式应从以下内容扩展

case Expr of
    Pattern1 [when GuardSeq1] ->
        Body1;
    ...;
    PatternN [when GuardSeqN] ->
        BodyN
end

receive
    Pattern1 [when GuardSeq1] ->
        Body1;
    ...;
    PatternN [when GuardSeqN] ->
        BodyN
after ExprT ->
    BodyT
end

try Exp [of
    Pattern1 [when GuardSeq1] ->
        Body1;
    ...;
    PatternN [when GuardSeqN] ->
        BodyN
    ]
catch
    Class1:ExceptionPattern1[:Stacktrace] [when ExceptionGuardSeq1] ->
        ExceptionBody1;
    ...;
    ClassN:ExceptionPatternN[:Stacktrace] [when ExceptionGuardSeqN] ->
        ExceptionBodyN
end

Res = ExprF(Expr1,...,ExprN)

以支持以下语法

case Expr of
    Pattern1A |
    Pattern1B |
    ...
    Pattern1N [when GuardSeq1] ->
        Body1;
    ...;
    PatternNA |
    PatternNB |
    ...
    PatternNN [when GuardSeqN] ->
        BodyN
end

receive
    Pattern1A |
    Pattern1B |
    ...
    Pattern1N [when GuardSeq1] ->
        Body1;
    ...;
    PatternNA |
    PatternNB |
    ...
    PatternNN [when GuardSeqN] ->
        BodyN
after ExprT ->
    BodyT
end

try Exp [of
    Pattern1A |
    Pattern1B |
    ...
    Pattern1N [when GuardSeq1] ->
        Body1;
    ...;
    PatternNA |
    PatternNB |
    ...
    PatternNN [when GuardSeqN] ->
        BodyN
    ]
catch
    Class1:ExceptionPattern1A|
           ExceptionPattern2A| ...
           ExceptionPatternNA[:StackTrace] [when ExceptionGuard1A] ->
        ExceptionBodyE1;
    ClassN:ExceptionPattern1N|
           ExceptionPattern2N| ...
           ExceptionPatternNN[:StackTrace] [when ExceptionGuardNN] ->
        ExceptionBodyEN
end

Res1 | Res2 | ... | ResN =
  ExprF(Expr1, Expr2, ... , ExprN)

ExprF1(Expr1A1 | Expr1A2 | ... | Expr1AN, ...,
       Expr1N1 | Expr1N2 | ... | Expr1NN) [when GuardSeq1] -> Body1;
ExprFN(ExprNA1 | ExprNA2 | ... | ExprNAN, ...,
       ExprNN1 | ExprNN2 | ... | ExprNNN) [when GuardSeq1] -> Body1

只有当变量在所有可替代模式中都已绑定时,才可以在保护式中使用该变量。也就是说,允许以下情况

case X of
    {A, 0} | {0, A} when A > 0 -> ok
end

{A, 1} | {A, 2} = foo()

bar({A, 1} | {A, 2}) -> true

而不允许以下情况

case X of
    {A, 0} |
    {0, B} when B > 0 -> ok
end

{A, 1} | {B, 2} = foo()

bar({A, 1} | {B, 2}) -> true

上面显示的例子会产生如下形式的编译错误

Error: alternative patterns must have the same variables defined

| 的使用变得模糊时,例如在与列表中的 cons 组合使用时,优先级会赋予传统的 cons 语法,并且可以使用括号来消除可替代匹配表达式的歧义

case X of
  [3 | T]     -> {ok, T};
  [1 | 2 | T] -> error;   % Compiler error: ambiguous use of pipe symbol
  [(1|2) | T] ->
    % Alternative matches, of lists' head being 1 or 2
    {ok, T}
end

[(1|2) | T] = check_head_version(L)

可替代模式的分隔符的选择 #

在本规范中,我们建议使用管道符 | 作为可替代模式的分隔符,原因如下

  1. 它已经以类似的原因用于类型规范中(例如 -spec f(foo|bar) -> ok.),并且这种自然的分隔符选择将是一致的。
  2. 它对于来自其他语言的人来说,很熟悉,表示“或”条件。

一个可能的替代方案是使用 ; 分隔符,它目前在保护式、“if”表达式和列表推导式中具有类似于 or 条件分隔项的含义。

但是,分号 (;) 可能会在语句中识别模式的结尾或分隔表达式时造成混淆。例如

case Expr of
  a ->
    true = foo();  % <-- is this the end of the body or a syntax error
                   %     which should be a comma, with the body
                   %     returning 'b'?

  b;               % <-- is this the alternative pattern or a pattern
                   %     with a missing body?
  C when is_integer(C) ->
    false
end

此外,; 用于分隔 if、case、receive、try-of-catch、fun 和函数中的子句。如果将其选为可替代模式的分隔符,则可能会造成更多混淆。

另一个可能的替代方案是允许跳过 if、case、receive、try-of 和 fun 表达式中的主体

case Expr of
  a ->
  b ->
  c ->
    true;
  _ ->
    false
end

但是,将相同的原则应用于表示函数调用返回中的可替代模式会导致更多混淆

a -> b -> c = foo()

嵌套可替代模式 #

处理可替代模式有两种方法

  1. 仅允许顶层交替(例如 Expr1 | Expr2 | ... | ExprN
  2. 允许无限嵌套(例如 {a|{b|c, d}} | e | {f|g, [(h|i), (j|k) | [(l|m)]]}

虽然支持嵌套模式是可取的,但根据实现的复杂性,可能决定不支持无限嵌套。更具体地说,编译器实现嵌套模式的复杂性可能超过此功能的好处。

重叠模式 #

在可替代模式重叠的情况下,将使用自然的从左到右的评估顺序,类似于 case 语句中模式的顺序。例如

{a, X} | {X, b} = foo()

此示例等效于

case foo() of
  Term = {a, X} -> Term;
  Term = {X, b} -> Term;
  Term -> error({badmatch, Term})
end

原理 #

本提案的实现将导致更方便的方式来编写冗余匹配子句,并消除代码重复。

在现有 OTP 代码的许多地方都可以找到可能有用的示例。例如 ssl_logger.erl

case logger:compare_levels(Level, debug) of
    lt ->
        ?LOG_DEBUG(#{direction => Direction,
                     protocol => Protocol,
                     message => Message},
                   #{domain => [otp,ssl,Protocol]});
    eq ->
        ?LOG_DEBUG(#{direction => Direction,
                     protocol => Protocol,
                     message => Message},
                   #{domain => [otp,ssl,Protocol]});
    _ ->
        ok
end.

case logger:compare_levels(Level, notice) of
    lt ->
        ?LOG_NOTICE(Report);
    eq ->
        ?LOG_NOTICE(Report);
    _ ->
        ok
end.

此外,这种语法扩展导致函数规范定义与其实现之间的一致性

-spec f(foo|bar) -> ok.
f(foo|bar) -> ok.

向后兼容性 #

任何使用 case 和 receive 表达式的旧实现的现有代码将继续像今天一样工作并产生相同的结果。

参考实现 #

作为实现建议,可以通过复制包含管道符的替代模式来重写 AST,因此 X of lt | gt -> ok end 的情况变为 case X of lt -> ok; gt -> ok end。对于匹配表达式,它可以将 lt | gt = compare(A, B) 重写为 case 语句 case compare(A, B) of lt -> lt; gt -> gt end,嵌套的可替代模式可以使用保护式进行匹配重写,例如:函数参数匹配 foo(lt | gt, a | b) -> true 将变为

foo(lt | gt, a | b) -> true

变为

foo(A, B) ->
  case A of
    _ when A =:= lt; A =:= gt ->
    case B of
      _ when B =:= a; B =:= b ->
            . . .
      end
  end

编译器重写列表推导式中的可替代模式提出了额外的挑战。实现建议包括以下内容

[X || {a,X} | {b,X} <- L]

变为

[case I of
    {a, X} | {b, X} -> X
 end
 || I <- L,
 case I of
    {a, _} | {b, _} -> true;
    _               -> false
 end
]

[X || {X} <-
    [case Item of
         {a,X0} | {b,X0} ->
             {X0}; % Set of matched variables
         _ ->
             nomatch
     end || Item <- L]]

版权 #

本文档已置于公共领域或 CC0-1.0 通用许可之下,以更宽松的为准。