作者
Richard A. O'Keefe <ok(at)cs(dot)otago(dot)ac(dot)nz>
状态
草案
类型
标准跟踪
创建时间
2008年7月9日
Erlang 版本
OTP_R12B-4

EEP 13: -enum 声明 #

摘要 #

Erlang 程序经常需要处理使用与 Erlang 无关的数据格式的数据流。因此,OTP 支持 ASN.1 和 CORBA 以及其他接口技术。二进制数据流通常包含“符号”值,这些值在原始描述中由某种枚举声明表示,通常字面上是 C 的 “enum” 声明。

此 EEP 提议为 Erlang 提供 “-enum” 声明,以便在接口一侧的原子和另一侧的整数之间进行方便的映射,尤其是在位语法中。

这用一种更清晰地表达程序员意图的方式取代了预处理器的一些用法。

规范 #

添加了一种新的声明形式、四个新的 guard BIF 和一个新的位语法类型说明符。

声明 #

'-' 'enum' '(' identifier-and-size ',' '{' enum-binding
    {',' enum-binding}* ')' '.'

其中 identifier-and-size 是

identifier

或者

identifier : size

或者

identifier / type-specifier-list

或者

identifier : size / type-specifier-list

并且 enum-binding 是

identifier '=' constant-integer-expression

或者

identifier

size 和 type-specifier-list 与位语法中的相同,除了 type-specifier-list 可能不包含 Type。如果缺少 size,它将是与整数值兼容的 [8,16,32,64] 中的第一个,如后文所述。如果存在 size,则它必须是与整数值兼容的整数。如果存在 signedness,则必须与整数值一致。

示例 #

-enum(colour, {red,orange,yellow,green,blue}).
-enum(fruit:32,  {quandong,lime,banana,orange,apple}).

左括号后的标识符称为“枚举标识符”,绑定绑定的标识符称为“枚举常量”。

-include-if 处理之后,对于任何标识符,最多应该有一个 enum 声明。该标识符不得为以下之一:

integer | float | binary | bytes | bitstring | bits

这种声明仅在 此 EEP 中定义的构造中才有意义;唯一受影响的现有符号是位语法。

在一个 enum 声明中,一个枚举常量不能在两个或多个绑定中绑定。

如果第一个绑定没有 integer-constant-expression,则相当于出现了“= 0”。如果稍后的绑定没有 integer-constant-expression,则相当于出现了“= N”,其中 N 比前一个绑定的整数值大 1。

在一个 enum 声明中,一个整数值不能在两个或多个绑定中使用,无论是隐式还是显式。

内置函数 #

is_enum_atom(Atom, Enumeration_Identifier) #

  • 当 Enumeration_Identifier 是一个声明为枚举标识符的原子,并且 Atom 是该声明中的枚举常量之一时,返回 true
  • 否则返回 false

如果 Enumeration_Identifier 是一个字面原子,则可以用作 guard 测试,如果它没有 enum 声明,则会出现编译时错误。

is_enum_integer(Integer, Enumeration_Identifier) #

  • 当 Enumeration_Identifier 是一个声明为枚举标识符的原子,并且 Integer 是一个用作该声明中绑定值之一的整数时,返回 true
  • 否则返回 false

如果 Enumeration_Identifier 是一个字面原子,则可以用作 guard 测试,如果它没有 enum 声明,则会出现编译时错误。

enum_to_atom(Integer, Enumeration_Identifier) #

  • is_enum_integer(Integer, Enumeration_Identifier) -> 时,
    Enumeration_Identifier 的声明中绑定到 Integer 的枚举常量

  • 否则以 badarg 退出。

如果 Enumeration_Identifier 是一个字面原子,则可以在 guard 表达式中使用,如果它没有 enum 声明,则会出现编译时错误。

enum_to_integer(Atom, Enumeration_Identifier) #

  • is_enum_atom(Atom, Enumeration_Identifier) -> 时,
    Atom 在 Enumeration_Identifier 的声明中绑定到的整数值

  • 否则以 badarg 退出。

如果 Enumeration_Identifier 是一个字面原子,则可以在 guard 表达式中使用,如果它没有 enum 声明,则会出现编译时错误。

所有这四个函数都应花费 O(1) 时间,并且在运行时不分配存储。

位语法扩展 #

位语法段中的 Type 也可以是 Enumeration_Identifier,对应的 Value 将是一个原子。正在匹配或构造的位字符串中的值是或将是绑定到原子的整数;因此,Size、Endianness、Signedness 和 Unit 的解释与 integer Type 相同。

在构造位字符串时,

    V / Enumeration_Identifier ...
or  V : Size / Enumeration_Identifier ...

的行为如同

    enum_to_integer(V, Enumeration_Identifier) / integer ...
or  enum_to_integer(V, Enumeration_Identifier) : Size / integer ...

已写入,但有一个例外,现在描述如下。

如果 enum 声明中的所有整数值都是非负的,则令 k 为最小整数,使得 2^k 大于所有这些整数。如果某些整数是负的,则令 k 为最小整数,使得 2^(k-1) 大于所有这些整数,并且 -(2^(k-1)) 小于或等于所有这些整数。那么,枚举值的段的大小必须至少为 k 位,无论实际值是多少。发现需要绕过此限制的程序员可以手动执行枚举常量<->整数转换;此限制的作用是防止意外的错误指定。enum 声明中给出的大小必须至少为 k。如果位语法中没有给出大小,则将使用 enum 声明中给出(或默认)的大小。

当在模式匹配中使用这样的段时,其行为如同

  • 首先提取一个整数,就像 Type 是 integer 一样,
  • 然后该值像使用 enum_to_atom 一样转换为原子,
  • 最后将该原子与出现的任何模式匹配。

预计显式原子值 V 的情况将在编译时完全转换,因此与使用宏和 /integer 相比没有开销。

动机 #

这是受到对 PADS 和其他数据描述语言的思考的启发。想象一下,一个 C 程序正在执行类似以下的操作:

enum seriousness {
    not_serious = 'N',
    hospitalised = 'H',
    life_threatening = 'L',
    congenital_abnormality = 'C',
    persisting_disability = 'P',
    intervention_required = 'I',
    death = 'D'
};
struct Message {
    char tag;                       /* a seriousness */
    union {
        int   number_of_days;       /* H */
        float extent_of_disability; /* C or P */
        char  procedure_code[5];    /* I */
    } supplement;
};

(Message 结构已大大简化。)

现在想象一下匹配它。

-define(NOT_SERIOUS, $N).
-define(HOSPITALISED, $H).
-define(LIFE_THREATENING, $L).
-define(CONGENITAL_ABNORMALITY, $C).
-define(PERSISTING_DISABILITY, $P).
-define(INTERVENTION_REQUIRED, $I).
-define(DEATH, $D).

decode_message(B0) ->
    case B0
      of <<?NOT_SERIOUS, B1/binary>> ->
            {{not_serious}, B1}
       ; <<?HOSPITALISED, NDays:32, B1/binary>> ->
            {{hospitalised,NDays}, B1}
       ; <<?LIFE_THREATENING, B1/binary>> ->
            {{life_threatening}, B1}
       ; <<?CONGENITAL_ABNORMALITY, Extent/float, B1/binary>> ->
            {{congenital_abnormality,Extent}, B1}
       ; <<?PERSISTING_DISABILITY, Extent/float, B1/binary>> ->
            {{persisting_abnormality,Extent}, B1}
       ; <<?INTERVENTION_REQUIRED, Code:5/bytes, B1/binary>> ->
            {{intervention_required,Code}, B1}
       ; <<?DEATH, B1/binary>> ->
            {{death}, B1}
    end.

这存在许多问题。

  • 你必须使用宏;模式中不允许使用函数。
  • 没有将这些宏作为一个组链接在一起的机制。
  • 因此,没有帮助来检查你是否正在使用正确的宏。
  • 没有词语将它们与原始枚举联系起来。
  • 如果大小不是 8,则必须在每个模式中重复。
  • 如果 Endianness 不是 big,则必须在每个模式中重复。
  • 如果大小错误,那就太糟糕了。
  • 如果使用了错误列表中的宏,那就太糟糕了。
  • 除非它恰好在两者中具有相同的值,否则你不能将相同的枚举常量名称用于多个枚举。
  • 如果你在计算中传递宏,它们看起来就像跟踪器和调试器中的数字一样;它们没有运行时符号值。

现在这是使用 -enum 的版本。

-enum(seriousness : 8, {
    not_serious = $N,
    hospitalised = $H
    life_threatening = $L,
    congenital_abnormality = $C,
    persisting_disability = $P,
    intervention_required = $I,
    death = $D
}).

decode_message(B0) ->
    case B0
      of <<not_serious/seriousness,
          B1/binary>> ->
            {{not_serious}, B1}
       ; <<hospitalised/seriousness,
           NDays:32, B1/binary>> ->
            {{hospitalised,NDays}, B1}
       ; <<life_threatening/seriousness,
           B1/binary>> ->
            {{life_threatening}, B1}
       ; <<congenital_abnormality/seriousness,
           Extent/float, B1/binary>> ->
            {{congenital_abnormality,Extent}, B1}
       ; <<persisting_disability/seriousness,
            Extent/float, B1/binary>> ->
            {{persisting_abnormality,Extent}, B1}
       ; <<intervention_required/seriousness,
            Code:5/bytes, B1/binary>> ->
            {{intervention_required,Code}, B1}
       ; <<death/seriousness,
           B1/binary>> ->
            {{death}, B1}
    end.

很幸运的是,此功能还提供了一种通过单个 guard 测试接受一组原子或整数的方法。让我们重构前面的示例,首先提取严重性,然后匹配主体,但是这次,每个形状只有一个主体。

-enum(seriousness, {
    not_serious = $N,
    hospitalised = $H
    life_threatening = $L,
    congenital_abnormality = $C,
    persisting_disability = $P,
    intervention_required = $I,
    death = $D
}).
-enum(no_more_info, {
    not_serious = $N,
    life_threatening = $L,
    death = $D
}).
-enum(extent_of_impairment, {
    congenital_abnormality = $C,
    persisting_disability = $P
}).

decode_message(<<Seriousness/seriousness, B0/binary>>) ->
    if is_enum_atom(Seriousness, no_more_info) ->
       {{Seriousness}, B0}
     ; is_enum_atom(Seriousness, extent_of_impairment) ->
       <<Extent/float, B1/binary>> = B0,
       {{Seriousness,Extent}, B1}
     ; Seriousness =:= hospitalised ->
       <<NDays:32, B1/binary>> = B0,
       {{Seriousness,NDays}, B1}
     ; Seriousness =:= intervention_required ->
       <<Code:5/bytes, B1/binary>> = B0,
       {{Seriousness,Code}, B1}
    end.

原理 #

由于这应该使其易于转换来自 C 或 PADS 或类似形式的描述,因此 enum 声明看起来像 C enum 声明。

由于大小、符号性和字节序可能需要在多个位置使用,因此将它们全部放入声明中是有意义的,这样它们就不必重复(因此也不能错误地重复)。

新的 BIF 中参数的顺序被选择为与 is_record/2 中的参数顺序匹配,以便 Erlang 程序员熟悉它们。

需要新的 BIF 来解释扩展的位语法。它们名称中唯一的缩写是 enum,它与声明中的关键字完全匹配。

新的 BIF 还可以用于通过源到源转换来实现扩展的位语法;不需要对位语法机制进行实际更改。

向后兼容性 #

使用任何四个新 BIF 的代码都将受到影响。Erlang/OTP 源代码中最接近提及任何这些原子的地方是使用的 enum_to_int。可以使用交叉引用工具找到确实使用这些 BIF 的代码。

一种简单的方法是说 BIF is_enum_atom/2is_enum_integer/2enum_to_atom/2enum_to_integer/2 在模块中的作用域内,当且仅当该模块中存在 -enum 声明时,在这种情况下,现有代码将完全不受影响。

对位语法的影响是,以前的非法形式(其中 Type 不是现有数字类型或位字符串类型之一,或者 Value 是一个原子)变为合法,但前提是已获得适当的 -enum 声明的许可。

参考实现 #

没有。但是,我们可以草拟一个。四个新的 BIF 都是简单的表查找,Erlang 编译器已经必须能够为索引子句选择生成这种表查找。因此,可以在 guard 中安全地调用它们。由于位语法中的 Type 只能是字面原子时才是枚举名称,因此编译器知道它是一个枚举名称,因此构造函数

<<... V : S / T X ...>>

可以翻译为

( V1 = enum_to_integer(V, X), <<... V1 : S / integer X ...>>)

并且模式

<<... V : S / T X ...>>

可以翻译为

<<... V' : S / integer X ...>>

通过添加

V =:= enum_to_atom(V', T)

如果 V 出现在模式中的其他位置或将在上下文中绑定,则将其添加到 guard 中,或者

   V = enum_to_atom(V', T)
if V would not otherwise become bound.

无论如何,都应该允许这样的绑定,但在这种情况下,它是完全安全的,因为它具有 O(1) 的时间复杂度,并且不需要任何动态存储分配(例如,与算术不同)。

版权 #

本文档已放入公共领域。