作者
Tom Davies <todavies5(at)gmail(dot)com>
状态
草案
类型
标准跟踪
创建于
2023年6月1日

EEP 62: 字符串插值语法 #

摘要 #

本 EEP 提议了一种新的字符串插值语法,允许将表达式嵌入到字符串常量中,以使构造复合字符串更具可读性。

例如,新语法

bf"A utf-8 binary string: ~2 + 2~"

将求值为

<<"A utf-8 binary string: 4"/utf8>>

特性概述 #

此提案在两个轴上增加了四种字符串插值:UTF-8 二进制字符串或 Unicode 代码点列表,以及面向用户或面向开发人员的格式化。

结果是具有插值值的四种通用语法类别

% binary format
<<"A utf-8 binary string: 4"/utf8>> =
  bf"A utf-8 binary string: ~2 + 2~"

% list format
"A unicode codepoint list string: 4" =
  lf"A unicode codepoint list string: ~2 + 2~"

% binary debug
<<"A utf-8 binary string: {4, foo, [x, y, z]}"/utf8>> =
  bd"A utf-8 binary string: ~{2 + 2, foo, [x, y, z]}~"

% list debug
"A unicode codepoint list string: {4, foo, [x, y, z]}" =
  ld"A unicode codepoint list string: ~{2 + 2, foo, [x, y, z]}~"

任意表达式都可以嵌套在字符串插值替换中,包括变量、函数调用、宏,甚至进一步的字符串插值表达式。

设计 #

为什么同时需要列表字符串和二进制字符串? #

在标准库的 string 模块中,字符串由 unicode:chardata() 表示,即代码点列表、带有 UTF-8 编码代码点的二进制字符串(UTF-8 二进制字符串)或两者的混合。

考虑到这一点,面向列表和面向二进制的字符串插值语法都接受任何类型的插值值,但是插值的用户决定他们是想生成 unicode:char_list() 还是 unicode:unicode_binary(),这取决于他们使用哪种插值(bf"..."bd"..." 用于创建二进制字符串,或 lf"..."ld"..." 用于创建列表)。

列表字符串对于向后兼容性和便利性最有用。二进制字符串对于内存紧凑性和 IO 最有用。

为什么需要面向用户和面向开发人员的字符串? #

开发人员通常希望格式化字符串有两种相似但不同的情况:在记录/调试时,以及在向用户显示数据时。

在记录或调试时,最重要的特性通常是:可以打印任何类型的项,并且它应该无损地往返,并且可以被开发人员明确地读取。这些属性的示例是,例如,保留运行时类型信息,例如在格式化字符串时保持引号引用,并以完整范围和分辨率打印浮点数。

在向用户显示时,最重要的特性通常是:它们始终是人类可读的且格式清晰。这些属性的示例是,例如,逐字格式化字符串,不带引号,并且不保留任何 Erlang 特性(例如,我们不希望打印 Erlang 元组,因为它们对于普通应用程序消费者来说意义不大),因此我们宁愿得到 badarg 错误来促使开发人员做出明确的格式化决定。

为什么没有格式化选项? #

让我们考虑一下之前介绍的两个用例

  • 记录/调试:通常,您希望即发即弃,将您关心的任何值传递给格式化程序,然后让它明确地打印该值,这意味着无需调整格式化选项:bd"~Timestamp~: ~Query~ returned ~Result~"
  • 向用户显示:通常,您希望严格控制格式化,并且可能希望以模块化和可重用的方式进行。在这种情况下,将您的格式化决定分解为一个函数,并插值该函数的结果可能是最好的方法:bf"You account balance is now ~my_app:format_balance(Currency, Balance)~"

值得注意的是,此设计和实现中的任何内容都不会阻止未来引入诸如 bf"float: ~.2f(MyFloat)~" 之类的格式化选项,就像使用 io_lib:format 等一样。但是,现有的标准库函数可以提供类似的功能,例如 bf"float: ~float_to_binary(MyFloat, [{decimals, 2}, compact])~",并且可以分解为它们自己的可重用函数。

为什么不使用 Elixir 的语法? #

Elixir 使用 #{...} 在字符串中引入插值表达式,并且重用该语法可能很方便。不幸的是,这与 Erlang 的 Map 语法冲突。Elixir 的 Map 使用 %{...},因此它没有该冲突。

实现概述 #

要解析插值字符串,扫描器会跟踪一些关于我们当前是否在插值字符串中的额外状态,此时它会启用对 ~ 作为插值表达式分隔符的识别,并生成新的标记,这些标记表示插值字符串的各种组件。

在编译和 Shell 求值的早期,插值字符串被转换为对 io_lib 模块中函数的调用,因此不会影响编译或求值的后续阶段。

参考实现 #

PR #7343

向后兼容性 #

新的字符串插值语法以前不是有效的语法,因此支持新语法的工具应该完全向后兼容现有源代码。

新语法将生成对标准库中新的二进制构造函数的调用,因此使用此新特性编译的 BEAM 文件将与早期版本不兼容。

版权 #

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