作者
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」運算式和列表推導式中的項。

但是,分號(`;`)可能會在識別語句中的模式結尾或分隔運算式時造成混淆。例如:

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、recieve、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-通用許可證之下,以更寬容的許可證為準。