作者
Jesse Gumm <ja(at)gumm(dot)io>
状态
已接受
类型
标准跟踪
创建于
2024-10-08

EEP 72:保留字和变量作为记录名称,以及定义语法的增强 #

摘要 #

本 EEP 放宽了对记录名称的一些限制,使其在用保留字 (#if 而不是 #'if') 或首字母大写的单词(目前将被视为变量的术语,例如 #Hello 而不是 #'Hello')命名时,不再需要用引号括起来。

本 EEP 还建议在记录定义中添加一种新的类似记录的语法(也采用上述语法更改),以便以下记录定义有效且相同

-record('div', {a :: integer(), b :: integer()}).
-record #div{a :: integer(), b :: integer()}.

后者是拟议的新语法。以下内容也将有效且相同,因为括号在属性中是可选的,并且原子即使不是强制的也可以用引号括起来

-record 'div', {a :: integer(), b :: integer()}.
-record #'div'{a :: integer(), b :: integer()}.
-record(#'div'{a :: integer(), b :: integer()}).
-record(#div{a :: integer(), b :: integer()}).

用法语法动机 #

记录名称是原子。因此,当前 Erlang 语法要求记录名称与该语言对原子的其余使用保持一致。

Erlang 中的所有原子都可以用单引号表示。一些例子

例如

'foo'.
'FOO'.
'foo-bar'.

但是,方便的是,简单的原子(所有字母数字、下划线 (_) 或 at 符号 (@),第一个字符是小写字母,而不是 20 多个保留字之一),在所有上下文中都可以在不加必要的引号的情况下调用。一些例子

foo.
foo_Bar.
'foo-bar'. % still quoted since the term has a non-atomic character in it.

方便的是,这也意味着用简单原子命名的记录可以被调用和使用,而无需引用这些原子。例如

-record(foo, {a, b}).
-record(bar, {c}).

go() ->
    X = #foo{a = 1, b = 2},
    Y = X#foo{a = something_else},
    Z = #bar{c = Y#foo.a},
    ...

不幸的是,这也意味着用任何不符合“简单原子”模式的名称命名的记录,在定义和使用时都必须用引号括起来。例如

-record('div', {a, b}).
-record('SET', {c}).

go() ->
    X = #'div'{a = 1, b = 2},
    Y = X#'div'{a = something_else},
    Z = #'SET'{c = Y#'div'.a},
    ...

虽然这种方法与语言中原子用法一致,但对于保留字和首字母大写的原子,如果您需要用保留字(或首字母大写的术语)命名记录,这会使记录语法感觉不一致。在这种情况下,几乎可以保证用户不会使用名为“if”、“receive”、“fun”等的记录,即使可能完全有充分的理由使用这样的名称。Nitrogen Web Framework 中想到的最常见的用例。由于 HTML 有一个 div 标签,Nitrogen(使用 Erlang 记录表示 HTML 标签)自然应该有一个 #div 记录,但是,由于 'div' 是一个保留字(整数除法运算符),因此使用记录 #panel 来避免程序员必须调用 #'div',这感觉不自然且笨拙。

此外,ASN.1 和 Corba 等应用程序都有很大程度上依赖大写记录名称的命名约定,因此它们目前也必须用引号括起来。您可以在 Erlang 的 asn1 应用程序的模块中看到这一点。(之前的链接指向 asn1 中的一些记录定义,但您可以在 asn1 应用程序中的多个模块中看到用法散布。)

用法语法规范 #

本 EEP 通过以下方式简化了上述示例

  1. 允许在记录名称中使用不带引号的保留字和变量,以及
  2. 简化定义,使记录定义和记录用法之间的语法更加一致。

通过本 EEP 的更改,上述代码变为

-record('div', {a, b}).
-record('SET', {c}).

go() ->
    X = #div{a = 1, b = 2},
    Y = X#div{a = something_else},
    Z = #SET{c = Y#div.a},
    ...

定义语法动机 #

虽然用法语法规范中更新的示例使记录的使用更加清晰,但仍然存在另一个可以相对容易解决的不一致之处。这就是记录定义仍然需要引用记录名称,如上面的示例所示(为方便起见在此重复)

-record('div', {a, b}).

go() ->
    X = #div{a = 1, b = 2},
    Y = X#div{a = something_else},
    Z = Y#div.a,
    ...

因此,尽管记录定义需要被视为 'div',但记录用法不再需要引用的术语 'div',这肯定会让 Erlang 初学者想知道为什么 'div' 在定义中需要引用,而其他看起来像原子的术语则不需要。

定义语法规范 #

方便的是,有一个相当简单的解决方案,那就是允许将记录用法语法也用作记录定义。

因此,本 EEP 还添加了一种新的记录定义语法,从而改进了一般记录用法和记录定义之间的对称性。

那么,上面的示例可以完全像下面这样

-record #div{a, b}.

go() ->
    X = #div{a = 1, b = 2},
    Y = X#div{a = something_else},
    Z = Y#div.a,
    ...

实现 #

要更新使用记录的语法,我们可以安全地增强解析器,将其现有的 '#' atom '{' ... '}''#'atom '.' atom 记录处理更改为 '#' record_name '{' ... '}''#' record_name '.' atom,并将 record_name 定义为 atomvarreserved_word

要更新记录定义语法,我们可以简单地对 attribute 非终结符进行一些新的修改,以允许 '#' record_name 作为 record 属性的名称,而不是像通用属性一样使用 atom

向后兼容性 #

由于本 EEP 仅添加了新语法,因此绝大多数现有代码库仍然可以工作,但使用新语法分析代码的 AST/代码分析工具可能会出现异常。

如果您的代码使用新的语法规则,则可能需要更新语法突出显示和代码完成工具以支持新的语法。

更广泛的关注点和讨论点 #

虽然新的定义语法在记录用法周围创建了一定程度的对称性,但要实现完全的对称性是不可能的,因为记录始终可以作为它实际标记的原子元组来处理。问题在于在哪里划清界限,即记录的真实性质在哪里显示,以及我们应该如何努力隐藏它。这些是剩余的关注点和不一致之处

辅助记录函数 #

其他处理记录的函数(如 is_record/2record_info/1)目前不受本 EEP 中任何语法更改的约束,因此,如果记录名称不是简单原子,仍然需要引用它们。例如:is_record(X, div) 仍然会是一个语法错误。所以仍然没有真正的 100% 对称性。请注意,与其使用 is_record(X, 'div') 保护,不如使用 #div{} 匹配可能更常用,因为它更简洁且被认为更易读。

两种定义语法? #

本 EEP 引入了一种新的记录定义语法,这可能会让一些人想知道为什么该语言有两种相当不同的定义记录的语法。由于获取、设置、匹配等语法的用法(例如 #rec{a=x,y=b})比定义更常见,因此定义语法自然应该反映用法。

为了获得更高的对称性,Erlang 的类型系统中用于定义记录的语法也与新提议的定义语法相匹配。

因此,我认为将现有的用法和类型语法与定义系统共享可能会成为默认/首选方式,而原始语法仍然是为了向后兼容。

参考实现 #

参考实现以 GitHub 上的拉取请求的形式提供

https://github.com/erlang/otp/pull/7873

版权 #

本文档已置于公共领域。