作者
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 而不是调用本地函数。

将接收转换为 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 中找到。

版权 #

本文档已置于公共领域。