作者
Cons T Åhs <cons(at)erlang(dot)org>
状态
草案
类型
标准跟踪
创建
2021-10-04
Erlang 版本
OTP-25.0

EEP 60:引入对实验性特性的支持 #

摘要 #

此 EEP 提供关于如何允许支持启用和禁用语言特性的建议。例如,这将允许用户在新的或提议的语言特性最终确定之前,试用、评论并提出更改建议。当引入新的向后不兼容特性时,它还可以避免使用新语言特性,从而使其可以在更慢的过渡中进行,例如,以逐个文件为基础。目前,这主要集中在语言本身的更改上,但它也可以用于运行时中的更改。

理由 #

其他语言支持尝试不同语言特性的可能性。请参阅 Kjell Winblad 的Erlang 实验性语言特性调查报告,了解一些示例。此报告作为附录逐字包含。

动机 #

我们希望通过添加新结构并删除或更改现有结构的语义来发展 Erlang/OTP。在将新特性最终确定为语言的一部分之前,允许用户在更大范围内尝试该特性,而无需运行 Erlang/OTP 的单独分支,这很方便。这将更容易测试新特性并促进反馈。在新特性成为 Erlang/OTP 的永久部分之前,应该可以选择使用它。

同样,应该可以使用新特性,尤其是更改语义或删除现有结构的特性。这样做的好处是,升级 OTP 时不会强制用户进行代码库的转换,而是可以以更及时的方式完成。

这也将导致一种统一的方式来记录和引入各个级别的实验性特性,即语言、运行时、应用程序和 API。

我们希望能够在编译期间和运行时控制启用或禁用的特性。可以通过编译器/运行时的选项以及正在编译的模块中的指令来实现此控制。当所需的特性不存在时,可以发出良好的错误消息。

可以在下图看到特性的生命周期。

  +--------------+       +----------+
  | Experimental ------> | Approved |
  +---.-------.--+       +-----.----+
      |       |                |
      |       |                |
      |       |                |
      |       |                |
+-----v----+  |          +-----v-----+
| Rejected |  +--------> | Permanent |
+----------+             +-----------+

除了启用和禁用特性的可能性之外,默认情况下可以启用特性。还将有多种方式来获取有关特性的信息(详情如下)。其中一些总结在下表中。

状态 默认 可控 可用
实验性 禁用
已批准 启用
永久 启用
已拒绝 禁用

注释

  • 可控意味着可以通过编译器选项和正在编译的模块中的指令启用或禁用。
  • 可以使用预处理器宏 FEATURE_AVAILABLEerl_features 模块中的函数来查看可用状态。

详细信息 #

需要将用于启用和禁用特性或获取有关特性的信息的修改添加到至少以下模块或通用区域

  • erlc - 选项处理
  • erl_scan - 关键字处理
  • epp - 指令处理和关键字集的更改
  • erl_parse(可能)- 特定特性的新语法规则
  • 预处理器
  • erl_lint - 特定特性的更改
  • erl_expand_records - 特定特性的更改
  • beam_asm
  • compile 模块中用于处理选项的函数
  • 模块中的 -feature(..). 指令
  • 运行时系统 - 选项处理和模块加载
  • 保存特性当前状态的特殊模块
  • erl_eval - 特定特性的更改
  • 解析转换

将添加一个新模块 erl_features,以提供有关特性的详细信息以及对特性处理的支持函数。

在下面,我们在示例中使用了一些不存在的特性。在这一点上,这些特性在没有进一步解释的情况下,是 maybe_expr(请参阅 EEP49)、module_aliasieee754float

erlc 的选项 #

编译器包装器 erlc 应扩展四个新选项,两个用于启用和禁用特性,两个用于获取有关特性的信息。前两个都以特性名称(原子)作为参数。允许多个实例。

  1. -enable-feature <特性名称> 打开选定的特性。
  2. -disable-feature <特性名称> 关闭选定的特性。
  3. -list-features 显示当前特性的列表和简短描述。
  4. describe-feature <特性名称> 显示特性的更长描述。

编译器将理解其他 + 样式选项。

  1. +'{enable_feature,<特性名称>}' – 请参阅上文。
  2. +'{disable_feature,<特性名称>}' – 请参阅上文。
  3. +warn_keywords – 为在某些现有特性中用作关键字的原子生成警告。
  4. +nowarn_keywords – 阻止如上所述的警告。
  • 默认情况下,警告现有可用特性中用作关键字的原子。
  • 对于启用和禁用特性,可以使用原子 all 来启用或禁用所有可用特性。

注意:前两个 + 选项的替代方法是具有三个元素的元组,即 {feature, enable | disable, <特性名称>}。这具有与 -feature 指令的格式更加一致的优点(请参阅下文)。

示例 #

  • erlc -enable-feature module_alias 表示在编译文件时使用特性 module_alias
  • erlc -enable-feature ieee754float -disable-feature module_alias 表示在编译文件时使用特性 ieee754float,但不使用特性 module_alias。实际上,使用 module_alias 的实例应该因此生成错误。

预处理器添加 #

添加预处理器宏以启用检查特定特性是否可用或已启用。我们添加两个预定义宏

  • FEATURE_AVAILABLE(F) – 当特性 F 在当前版本中可用时为 true。对于未知特性,这将为 false
  • FEATURE_ENABLED(F) – 当特性 F 在代码中的当前位置启用时为 true。对于未知特性,这将为 false

同时具有这两个宏的用例是,可以使用 FEATURE_AVAILABLE 来确定特性是否可用,如果可用,则启用它。这将更容易编写在较长时间内适用于多个 OTP 版本的代码。然后,可以将宏 FEATURE_ENABLED 用于具有替代实现的代码段。

示例 #

-if(?FEATURE_AVAILABLE(maybe_expr)).
%% Use the feature when available
-feature(enable, maybe_expr).
-endif.

-if(?FEATURE_ENABLED(maybe_expr)).
%% code that use the feature
-else.
%% alternative code not using the feature
-endif.

%% ..the above also allows simple negative tests
-if(not ?FEATURE_ENABLED(ieee754float)).
..
-endif.

compile 中函数的选项 #

采用 options 参数的 compile 中的函数(即 file/2forms/2noenv_file/2noenv_forms/2)应进行扩展,以便 {enable_feature, atom()}{disable_feature, atom()} 选项也被识别。

新的 -feature(enable|disable, <特性>) 指令 #

添加一个带有两个参数的新 -feature(..) 指令。仅允许在 -module(..) 声明之后和文件中到任何使用语法的指令的前缀中,例如,记录定义、-export(..). 或函数定义。允许预处理器指令、宏定义和包含,但是如果其中任何一个包含/导致上述任何一个,则前缀将结束。前缀概念扩展到涵盖包含的文件,这意味着前缀可以在包含的文件中处于活动状态和结束状态。

如果第一个参数是 enable (disable),则为正在编译的模块启用(禁用)第二个参数给定的特性。

允许多个 -feature 指令的实例。模块中的 -feature 指令的实例将优先于给编译器的选项。实际上,启用和禁用特性将具有后写获胜语义。

编译模块时,当特性已启用时,即使模块中没有实际使用该特性,该特性也将被视为已使用

运行时的选项 #

erlc 中用于编译模块的选项类似,在启动运行时时(例如,使用 erl),我们应该能够指定允许的特性。这意味着,当加载模块时,可能会由于使用(即,使用某个特性编译)我们不允许的特性而被拒绝。这样做的原因是,人们可能希望在测试和开发期间允许使用特性,但在生产中允许使用特性时要更加小心。

这些选项的名称应与 erlc 的选项相同,即 -enable-feature-disable-feature

启动后无法更改已启用特性的集合。

信息模块 #

名为 erl_features 的模块用于获取有关已知特性的状态的信息。

用于获取有关特性的信息的新函数

获取当前版本中可用的特性 #

features() -> [atom()]

获取有关给定特性的信息 #

feature_info(atom()) -> FeatureInfoMap
when
Description :: string(),
Type :: extension | backwards_incompatible_change,
FeatureInfoMap ::
   #{description := Description,
     short := Description,
     type := Type,
     keywords := [atom()],
     experimental => Release,
     approved => Release,
     permanent => Release,
     rejected => Release,
     status := experimental
             | approved
             | permanent
             | rejected
     }
Release :: non_neg_integer()

%% As above, but give the feature info for a given release
feature_info(atom(), Release) -> Result

键的描述

  • description - 特性的详细描述。
  • short - 简短的单行简介,描述特性。
  • type - 特性的性质,即保守扩展或向后不兼容的更改。
  • keywords - 特性引入的新关键字。
  • status - 特性的当前状态,每个状态都有一个对应的键,说明特性何时进入该状态。

    请注意,所有键 experimentalapprovedpermanentrejected 都不会出现,而只会显示上述生命周期图中当前状态之前的键。

这对内部和外部工具很有用,可以用于

  • 警告即将删除的特性的使用。
  • 当尝试使用不存在(或不再存在)的特性时给出错误
  • 告知编译器它需要使用哪些选项来激活某个实验性特性
  • 当不再需要时,自动删除或警告 -feature(..) 指令的实例
  • 自动提供有关两个版本之间已成为永久特性(或已批准)的信息。这可以用于向用户提供有关在进行升级之前需要更改的内容的信息。

erl_features 模块还将包含用于实际处理特性的支持函数,例如,动态关键字处理,但这些是与实现相关的,供内部使用,因此此处不再详细记录。

实现说明 #

  • 由于特性选项同时存在于编译器前端和运行时中,因此编译后的模块需要指示哪些特性已被允许(或使用)。这将通过在 beam 文件中名为 Meta 的新块中记录已使用的特性来实现。在运行时尝试加载时,将对照运行时中启用的特性检查已使用的特性。如果要加载的模块使用了未启用的特性,则将不允许加载。
  • 虽然有可能仅在前端实现新的语言特性,即本质上通过高级宏或解析转换,从而相当有把握地认为它不会对编译器或运行时的后续阶段产生任何影响(只要转换后的代码是正确的),但我们不会记录特性实现的这种细粒度级别。毕竟,我们可能会在稍后阶段更改实现。

示例 #

对新的 maybe .. else .. end 表达式(如 EEP49 中所述)的支持将使用特性机制来实现。这将使用特性名称 maybe_expr 完成,并且最初将具有 experimental 状态。这将为社区提供一个很好的机会来试用并提供反馈,然后再将其永久纳入该语言。

当编译使用 maybe 的模块时,需要启用特性 maybe_expr。这可以通过几种方式完成

  1. 使用 erlc 的选项,即 erlc -enable-feature maybe_expr
  2. 使用 + 选项的可能性,即 erlc +'{enable_feature,maybe_expr}'
  3. 使用正在编译的模块中的指令,即 -feature(enable, maybe_expr).

为了简化代码库的转换或允许在(早期)版本中使用该特性(该特性在这些版本中不可用),可以使用引入的宏。可以使用上述编译器选项启用特性。或者,使用以下代码,如果定义了 use_maybe,则可以启用该特性。

 -ifdef(use_maybe).
 -feature(enabled, maybe_expr).
 -endif.

 -if(?FEATURE_ENABLED(maybe_expr)).
 %% Code using the maybe expression
 foo(..) ->
    maybe
    X ?= ..
    end.
 -else.
 %% Alternative (old?) implementation not using maybe
 foo(..) ->
   ..
 -endif.

如果模块在启用 maybe_expr 的情况下编译,这将记录在 beam 文件中(在新的 Meta 块中)。要允许在运行时加载模块,必须使用 enable-feature 选项启用特性 maybe_expr

向后兼容性 #

不同 OTP 版本和具有特性的模块方面的一些可能场景

  • 使用启用并在 Meta 块中记录的特性编译的模块可以加载到(可能是较旧的)不知道该块存在的 OTP 版本中。可能还有其他因素阻止加载,例如,使用新的 BEAM 指令。
  • 包含 -feature 指令的模块无法由(可能是较旧的)不知道特性的 OTP 版本编译。由于格式(带有两个参数)与属性(一个参数)的格式不同,因此将生成错误。

参考实现 #

实现目前正在进行中。目前支持以下内容

  • erlc 的长选项
    • -enable-feature ..
    • -disable-feature ..
  • erlc+ 选项,例如,+'{enable_feature, maybe_expr}'
  • 理解使用 +warn_keywords+nowarn_keywordserlc。从 erl_lint 生成的警告。
  • 内联编译器指令
    • -feature(enable, ..).
    • -feature(disable, ..). 这些仅允许在文件的已定义前缀中使用。
  • 处理 compile:file/2 中的特性选项
  • FEATURE_AVAILABLEFEATURE_ENABLED,两者都是 1 元。当看到 -feature 指令的实例时,当启用特性的集合发生变化时,宏 FEATURE_ENABLED 会动态更改。
  • 动态更改关键字(保留字)集,即对启用/禁用特性的上述选项作出反应。
  • 检测和报告尝试使用当前未知特性的一些错误处理。
  • erl_features 模块的大部分,包括本文档中描述的函数和支持函数,用于处理关键字(保留字)集中的更改。
  • 在编译模块时,会将新块(当前名为 Meta)添加到 beam 文件中。因此,该块包含有关编译模块时使用的特性(如选项集所见,而不是特性是否实际存在)的信息。将来可以扩展该块以包含其他元信息。
  • 可以为 erl 提供选项来启用和禁用特性。使用未启用特性的模块将不会加载到系统中。
  • 可以给出关于现有特性的关键字的原子警告(使用 +warn_keywordserlc)。

未来工作 #

  • 提供一个指南,说明在哪里为新特性实现不同的支持,以及如何访问所需的特性设置。
  • 类似于 gcc-std=.. 选项,用于指定要编译的语言标准,可以向 erlc 等添加类似的选项。简而言之,这将是一种命名特定版本中默认启用的所有语言特性的集合的方式。使用 -lang=otp24 命名选项 -lang 意味着我们希望使用 OTP24 中默认启用的所有特性(并且仅使用这些特性)编译输入文件,即使 erlc 来自 OTP25 版本也是如此。因此,将不允许在 OTP25 中添加的任何特性。但是,如果添加 enable-feature 选项,例如,erlc -lang=otp24 -enable-feature module_alias,则允许。

参考资料 #

附录 #

这是 Kjell Winblad 的原始报告 报告。其中一些内容应复制到文档的其他位置,因为它提供了有关其他语言的良好背景并奠定了基础。

Erlang 实验性语言特性调查报告 #

Erlang 目前不支持选择性地使用并非 Erlang 语言正式组成部分的实验性语言特性。支持这样做可以帮助用户试用和试验对该语言的潜在扩展,而无需将此扩展添加到主语言中。本报告研究了在其他语言中选择性地包含实验性语言特性的支持情况,以及在 Erlang 中这种支持可能是什么样子。

在其他语言中选择性启用实验性语言特性的支持 #

Python #

Pyhton 支持使语言扩展或更改在成为强制性之前是可选的。Python 模块 __future__ 定义了几个特性名称,如下所示

FeatureName = _Feature(OptionalRelease, MandatoryRelease,
                       CompilerFlag)
  • OptionalRelease 是可以首次选择性启用特性的版本
  • MandatoryRelease 是特性成为/“计划成为”强制性的版本
  • CompilerFlag 是需要传递给编译模块函数以启用该特性的编译器标志

Python 有一个 特殊语句,需要将其放置在 python 模块的顶部附近,以在特定模块中启用语言特性。启用已成为强制性的特性的语句不起作用。

在 Python 中,特性名称永远不会从 __future__ 模块中删除,这意味着 __future__ 模块包含语言更改的历史记录。

Python 的未来导入语句和 __future__ 模块的一些优点是

  • 在使用可能发生向后不兼容更改的版本成为强制性之前,用户可以开始逐个模块地迁移代码。
  • 用户可以在默认启用语言扩展之前开始尝试使用它们
  • 可以在语言扩展成为强制性之前发现并修复语言扩展的问题(或者可以删除语言扩展)
  • 可以通过 __future__ 模块访问可通过编程访问的语言更改历史记录。工具可以使用此历史记录,例如,删除不再必要的 from __future__ import x 语句。

Ruby #

Ruby 没有对实验性特性提供特殊支持(实验性特性只是在文档中记录为实验性的)。有关更多信息,请参阅 此问题,该问题建议使用命令行标志来启用实验性特性。

Rust #

Rust 有一个 特殊语法,用于激活实验性语言特性。这是一个示例

#![feature(box_syntax)]

fn main() {
    let five = box 5;
}

此类特性在 Rust 的术语中称为不稳定的。它们可能随时更改或消失。

为当前编译单元(crate)激活该特性。

Haskell #

Haskell 可以使用文件头中的编译指示来激活某些语言特性

{-# LANGUAGE TemplateHaskell #-}

Java #

Java 允许用户测试计划用于更高版本的特性。这需要通过在编译 Java 文件时将编译标志传递给编译器来启用

javac --enable-preview --release 12 # 其他标志

上面的行可以在发布 Java 版本 12 之前的早期 Java 版本中启用计划用于 Java 版本 12 的语言特性。为了限制预览特性的使用,在使用 -enable-preview 标志编译 Java 程序时,还必须传递 -enable-preview。使用预览特性时,始终会打印警告消息。

当在 java 中为特定版本启用预览特性时,会获得该版本的所有预览特性。无法选择单个特性。

功能在被认为足够好、无需修改即可包含之前,不会作为预览功能发布。因此,对预览功能的更改相对较少,但可能会发生。

详情请参阅此处

Erlang 的建议 #

以下方法可用于激活实验性功能

  • 文件内的 -compile(). 指令,
  • 传递给 compile 模块中编译函数的选项之一,或者
  • 传递给 erlc 的编译标志。

用于启用实验性功能的选项/标志可以包含前缀和实验性功能的名称

示例

-compile([{enable_experimental, pinning_operator}]).

compile:file(File, [{enable_experimental,pinning_operator}])

erlc -enable_experimental_pinning_operator

在上述示例中,enable_experimental 是前缀,而 pinning_operator 是实验性功能的名称。

所有当前存在和过去存在的实验性功能都可以在一个特殊的模块中“记录”(类似于 Python)。假设这个模块名为 experimental_features。该模块可以是公开的,允许外部工具使用该模块,也可以是内部的,如果我们想能够更改其 API。

experimental_features 模块具有可用于获取实验性功能信息的函数

list_experimental_features() -> [atom()].

此函数返回所有当前存在和曾经存在的实验性功能的列表。

get_experimental_feature_info(FeatureName) -> Result when
FeatureName :: atom(),
description :: string(),
Type :: extension | backwords_incompatible_change,
Result :: missing | FeatureInfoMap,
FeatureInfoMap :: #{optional_release := ReleaseNr,
                status := experimental | %% May be removed
                                         %% or changed
                          {remove_planned, ReleaseNr,
                           AdditionalInfo :: string()} |
                          {inclusion_planned, ReleaseNr} |
                          {removed, ReleaseNr,
                           AdditionalInfo :: string()},
                          {included, ReleaseNr}
                 %% list of compiler options that needs
                 %% to be given to activate this feature
                 %% (this can be useful, for example, when
                 %% one experimental feature depend on another)
                compiler_options := list()
               },
ReleaseNr : {Major :: integer(),
             Minor :: integer(),
             Patch :: integer(),
             Label :: string()}.

此函数返回与给定功能名称相关的信息。

外部和内部工具可以使用从 experimental_features 模块获取的信息来

  • 警告有关使用将被删除的内容
  • 如果使用了已被删除的内容,则给出错误
  • 告知编译器应使用哪些选项来激活某个实验性功能
  • -compile() 指令不再需要时,自动从中删除 {enable_experimental,x} 元组。
  • 自动提供有关两个版本之间哪些功能成为强制性的信息(这可以向用户提供有关升级前需要更改的内容的信息)

防止在生产代码中过度使用实验性功能 #

类似于 Java,我们可以发出有关已编译模块中使用实验性功能的信息。当运行包含实验性功能的模块时,VM 可以使用此信息打印警告消息。我们还可以在运行使用实验性功能编译的代码时强制使用特殊标志。

版权 #

本文档放置在公共领域或 CC0-1.0-Universal 许可之下,以较宽松者为准。