作者
Björn Gustavsson <bjorn(at)erlang(dot)org>
狀態
已接受/23.0 實作於 OTP 版本 23
類型
標準追蹤
建立時間
2020-01-28
Erlang 版本
OTP-23.0
發布歷史
2020-01-28

EEP 52:允許在映射和二進位匹配中使用鍵和大小表達式 #

摘要 #

本 EEP 提議擴展二進位匹配,允許區段大小為守衛表達式,並擴展映射匹配,允許鍵為守衛表達式。

規格 #

我們提議在二進位匹配中,二進位區段的大小可以是一個守衛表達式。以下是一個範例

example1(<<Size:8,Payload:((Size-1)*8)/binary,Rest/binary>>) ->
   {Payload,Rest}.

允許使用與守衛中相同的表達式,除了不允許使用舊式的類型測試 (例如 list/1tuple/1)。除非表達式僅由單個數字或單個變數組成,否則必須將其括在括號中。表達式中使用的任何變數都必須事先綁定,或在與表達式相同的二進位模式中綁定。也就是說,以下範例是不合法的

illegal_example2(N, <<X:N,T/binary>>) ->
    {X,T}.

如果任何區段中的大小表達式無法成功求值或求值為非整數值,則二進位模式將無法匹配。例如

example3(<<X:(1/0)>>) -> X;
example3(<<X:not_integer>>) -> X;
example3(_) -> no_match.

第一個子句將不匹配,因為 1/0 的求值失敗。第二個子句將不匹配,因為大小求值為一個原子。

在目前的映射匹配語法中,映射模式中的鍵必須是單個值或字面值。如果映射中的鍵是複雜的術語,則會導致不自然的程式碼。例如

example4(M, X) ->
    Key = {tag,X},
    #{Key := Value} = M,
    Value.

我們提議映射模式中的鍵可以是一個守衛表達式。這將允許先前的範例像這樣編寫

example5(M, X) ->
    #{{tag,X} := Value} = M,
    Value.

鍵表達式中使用的所有變數都必須事先綁定。因此,以下範例是不合法的

illegal_example6(Key, #{Key := Value}) -> Value.

動機 #

目前映射鍵的限制令人驚訝。例如,允許使用像 {a,b} 這樣的字面元組作為鍵,但不允許使用帶有像 {a,Var} 這樣的變數的元組。

在二進位匹配中,始終可以使用 unit: 修飾符將匹配出的數字乘以一個小常數。提議的擴展使得在更多情況下可以在同一個二進位模式中匹配標頭和有效負載。

原理 #

為什麼允許守衛表達式? #

我們確實考慮過只允許使用算術運算符進行術語構造和表達式。我們改用守衛表達式的原因有兩個

  • 很容易解釋和理解允許哪些表達式作為區段大小和映射鍵,因為允許在守衛中使用相同類型的表達式。

  • 在計算二進位區段的大小時,守衛 BIF 的子集在實踐中可能很有用。例如:ceiling/1round/1byte_size/1bit_size/1map_get/2。我們不希望在大小表達式中出現任意允許的 BIF 列表,因此唯一合乎邏輯的方法是允許完整的守衛表達式。

為什麼荒謬的大小表達式不是編譯錯誤? #

顯然永遠不會求值為整數的大小表達式不會導致編譯錯誤(但可能會導致警告)。例如

example6(Bin, V) ->
    <<X:(is_list(V))>> = Bin,
    X.

原因是合法 Erlang 程式的規則應該簡單且明確,以幫助產生 Erlang 程式的人員和工具。

為什麼非簡單的大小表達式需要括號? #

與構造二進位時需要括號的原因相同,即沒有括號語言文法會模稜兩可,因為二進位模式使用字元 :/- 時的含義與語言的其餘部分不同。

向後相容性 #

在 OTP 22 及更早版本中使用擴展的表達式區段大小和映射鍵會導致編譯錯誤。因此,不會影響現有的原始碼。

但是,Core Erlang 的語義發生了變化,可能需要更新語言編譯器或產生 Core Erlang 程式碼的工具。

有兩個主要的變化

  • 在 Core Erlang 中,不再允許二進位模式在同一個二進位模式中綁定和使用變數。

  • 為了完全支援接收中的二進位匹配,必須將接收降低為更基本的操作。

Core Erlang 中的二進位匹配 #

在 Erlang 中,可以在二進位模式中綁定一個變數,並在同一模式中稍後用作區段的大小

foo(<<Sz:16,X:Sz>>) -> X.

在 OTP 22 及更早版本中,轉換為 Core Erlang 很簡單

'foo'/1 =
    fun (_0) ->
        case _0 of
          <#{#<Sz>(16,1,'integer',['unsigned'|['big']]),
             #<X>(Sz,1,'integer',['unsigned'|['big']])}#> when 'true' ->
              X
          <_1> when 'true' ->
              %% Raise function_clause exception.
              .
              .
              .
        end

雖然轉換很簡單,但所有 Core Erlang 傳遞都需要處理在同一範圍中綁定和使用變數。如果我們允許表達式作為區段大小,那將變得更加複雜。

在 OTP 23 中,區段大小表達式中使用的所有變數都必須已在封閉環境中綁定。先前的範例必須使用巢狀大小寫重新編寫,如下所示

'foo'/1 =
    fun (_0) ->
          case _0 of
              <#{#<Sz>(16,1,'integer',['unsigned'|['big']]),
               #<_2>('all',1,'binary',['unsigned'|['big']])}#> when 'true' ->
                  case _2 of
                     <#{#<X>(Sz,1,'integer',['unsigned'|['big']])}#> when 'true' ->
                         X
                     <_3> when 'true' ->
                         %% Raise function_clause exception.
                         .
                         .
                         .
                    end
               <_4> when 'true' ->
                    %% Raise function_clause exception.
                    .
                    .
                    .
              end

但是,從範例中可以看出,重複了引發 function_clause 例外的程式碼。程式碼重複在這種簡單的範例中沒有什麼大不了的,但在二進位匹配子句之後接著許多其他子句的函式中就會有問題。為了避免程式碼重複,我們必須使用帶有 letrec_goto 註釋的 letrec

'foo'/1 =
    fun (_0) ->
        ( letrec
              'label^0'/0 =
                  fun () ->
                        case _0 of
                          <_1> when 'true' ->
                                %% Raise function_clause exception.
                                .
                                .
                                .
                        end
          in  case _0 of
                <#{#<Sz>(16,1,'integer',['unsigned'|['big']]),
                   #<_2>('all',1,'binary',['unsigned'|['big']])}#> when 'true' ->
                    case _2 of
                      <#{#<X>(Sz,1,'integer',['unsigned'|['big']])}#> when 'true' ->
                          X
                      <_3> when 'true' ->
                            apply 'label^0'/0()
                    end
                <_4> when 'true' ->
                      apply 'label^0'/0()
              end
          -| ['letrec_goto'] )

letrec 被賦予註釋 letrec_goto 時,它將被特別轉換。 apply 操作將被轉換為 goto 而不是呼叫本機函式。

將 receive 轉換為 Core Erlang #

考慮以下範例

bar(Timeout) ->
    receive
        {tag,Msg} -> Msg
    after
        Timeout ->
            no_message
    end.

在 OTP 22 及更早版本中,轉換為 Core Erlang 很簡單

'bar'/1 =
    fun (Timeout) ->
        receive
          <{'tag',Msg}> when 'true' ->
              Msg
        after Timeout ->
          'no_message'

為了完全支援 OTP 23 中的二進位匹配,Erlang 中的 receive 現在已降低為 Core Erlang 中更基本的操作

'foo'/1 =
    fun (Timeout) ->
        ( letrec
              'recv$^0'/0 =
                  fun () ->
                      let <PeekSucceeded,Message> =
                          primop 'recv_peek_message'()
                      in  case PeekSucceeded of
                            <'true'> when 'true' ->
                                case Message of
                                  <{'tag',Msg}> when 'true' ->
                                      do  primop 'remove_message'()
                                          Msg
                                  <Other> when 'true' ->
                                      do  primop 'recv_next'()
                                            apply 'recv$^0'/0()
                                end
                            <'false'> when 'true' ->
                                let <TimedOut> =
                                    primop 'recv_wait_timeout'(Timeout)
                                in  case TimedOut of
                                      <'true'> when 'true' ->
                                          do  primop 'timeout'()
                                              'no_message'
                                      <'false'> when 'true' ->
                                          apply 'recv$^0'/0()
                                    end
                          end
          in  apply 'recv$^0'/0()
          -| ['letrec_goto'] )

從 OTP 23 中的 Core Erlang 程式碼進行編譯時,編譯器將接受使用 receive 建構的 Core Erlang 程式碼,並自動將其降低為更基本的操作。也就是說,對於上面的範例,來自 OTP 22 的 Core Erlang 轉換將被接受為 OTP 23 中編譯器的輸入。

以下是另一個 OTP 22 的 Core Erlang 程式碼將不被接受的範例。以下是 Erlang 程式碼

foobar() ->
    receive
        <<Sz:16,X:Sz>> -> X
    end.

在 OTP 22 中,這將被轉換為像這樣的 Core Erlang 程式碼

'foobar'/0 =
    fun () ->
        receive
          <#{#<Sz>(16,1,'integer',['unsigned'|['big']]),
             #<X>(Sz,1,'integer',['unsigned'|['big']])}#> when 'true' ->
              X
        after 'infinity' ->
          'true'

OTP 23 的編譯器不會接受這種轉換。 receive 必須降低為更基本的操作,並且必須使用巢狀大小寫重新編寫二進位匹配

'foobar'/0 =
    fun () ->
        ( letrec
              'recv$^0'/0 =
                  fun () ->
                      let <_5,_0> =
                          primop 'recv_peek_message'()
                      in  case _5 of
                            <'true'> when 'true' ->
                                ( letrec
                                      'label^0'/0 =
                                          fun () ->
                                                do  primop 'recv_next'()
                                                    apply 'recv$^0'/0()
                                  in  case _0 of
                                        <#{#<Sz>(16,1,'integer',['unsigned'|['big']]),
                                           #<_1>('all',1,'binary',['unsigned'|['big']])}#> when 'true' ->
                                            case _1 of
                                              <#{#<X>(Sz,1,'integer',['unsigned'|['big']])}#> when 'true' ->
                                                  do  primop 'remove_message'()
                                                      X
                                              <_2> when 'true' ->
                                                    apply 'label^0'/0()
                                            end
                                        <_3> when 'true' ->
                                              apply 'label^0'/0()
                                      end
                                  -| ['letrec_goto'] )
                            <'false'> when 'true' ->
                                  let <_4> =
                                      primop 'recv_wait_timeout'
                                          ('infinity')
                                  in  case _4 of
                                        <'true'> when 'true' ->
                                            do  primop 'timeout'()
                                                'true'
                                        <'false'> when 'true' ->
                                            apply 'recv$^0'/0()
                                      end
                          end
          in apply 'recv$^0'/0() )
          -| ['letrec_goto']

實作 #

實作可以在 PR #2521 中找到。

版權 #

本文件已置於公有領域。