作者
Raimo Niskanen <raimo(at)erlang(dot)org>
状态
最终版/27.0 已在 OTP 27 版本中实现;正则表达式的标记(`~r` 和 `~R`)尚未实现
类型
标准跟踪
创建于
2023-09-25
Erlang 版本
OTP-27.0

EEP 66:字符串字面量的标记 #

摘要 #

本 EEP 提议为字符串字面量引入标记,非常类似于 Elixir 标记。主要原因是促进其他建议的语言特性,其中许多特性在 Elixir 中以 标记 的形式存在,例如

  • 二进制字符串:unicode:unicode_binary()
  • 正则表达式语法
  • 字符串分隔符的选择
  • Verbatim 字符串
  • 字符串插值语法,或变量插值

理由 #

许多关于 摘要 中特性的现有建议都在普通 Erlang 字符串之前使用前缀,例如

u"For UTF-8 encoded binary strings"

bf"For UTF-8 encoded binary with interpolation formatting: ~foo()~"

本 EEP 建议对字面字符串使用与 Elixir 中 标记 相同或非常相似的语法,以避免简单前缀带来的语法问题,并避免这些兄弟语言在没有充分理由的情况下偏离太多。

~"For UTF-8 encoded binary strings"

设计决策 #

在以下文本中,双角引号用于标记源代码字符,以提高清晰度。例如:点字符(句点):«.»。

Erlang 语言结构(词法分析器和解析器) #

Erlang 编程语言是根据传统的词法分析器+解析器+编译器模型构建的。

词法分析器,又名扫描器,又名词法分析器,扫描源代码字符序列并将其转换为标记序列,例如原子、变量、字符串、整数、保留字、标点符号或运算符:atomVariable"string"123case:++

解析器接收标记序列,并根据 Erlang 语法构建解析树,即 AST(抽象语法树)。然后,编译器将此 AST 编译为可执行(虚拟机)代码。

词法分析器 #

词法分析器很简单。它源自工具 lex,该工具尝试在输入上使用一组正则表达式,当其中一个匹配时,它会成为一个标记并从输入中删除。不断重复。

词法分析器不再那么简单,但它不会保留太多状态,并且只会在输入中向前看固定数量的字符。

例如,从开始状态,如果词法分析器看到一个 ' 字符,它会切换状态以扫描带引号的原子。这样做时,它会转换转义序列,例如 \n(转换为 ASCII 10),当它看到一个 ' 字符时,它会生成一个原子标记并返回到开始状态。

简单前缀的问题 #

所有这些简单前缀都必须在词法分析器中成为单独的标记:«bf"» 将构成带插值语法的二进制字符串的开始标记。«bf"""»、«b"»、«b"""» 等也是如此。

词法分析器必须知道前缀字符的所有组合,并为每个组合发出不同的标记。

今天,字符序列 «b»、«f»、«"» 被扫描为原子 bf 的标记,后跟字符串开始标记 "。该组合在解析器中失败,因此今天在语法上是无效的,这使得简单前缀成为可能的语言扩展。

简单前缀方法必须向前扫描多个字符,以区分原子后跟字符串开始与带前缀的字符串开始,并且它将是一个不同的字符数,具体取决于目前已找到哪些原子字符。这相当混乱。

此外,我们很可能希望具有选择 字符串分隔符 的功能,特别是对于正则表达式,例如

re(^"+.*/.*$)

在所需的分隔符中,有 /< >。当前有效的代码 «b<X» 表示原子 b 小于 X,相反,它必须被解释为带前缀的字符串开始 b<,其中 X 是第一个字符串内容字符。

对于 / 字符,我们遇到了类似的问题,例如 «b/X»,今天这将是一个运行时错误,但如果我们也想要大写字母前缀,那么 «B/X» 今天是完全有效的,但会变成字符串开始。

简单字符串前缀可能存在更多问题:«#bf{» 今天是以 bf 命名的记录的开始,并被扫描为标点符号 #、原子 bf 和分隔符 {,解析器将其解析为记录开始。

使用简单前缀字符,词法分析器必须重写以将 «#bf» 识别为新的记录标记,这种重写可能会导致记录处理中出现意外更改。例如,今天,«# bf {» 也是有效的记录开始,因此为了兼容,词法分析器必须允许在新记录标记内,在 # 和原子字符之间存在空格甚至换行符,这将非常丑陋……

出于其他原因,即函数调用括号是可选的,Elixir 选择使用 ~ 字符作为字符串前缀的开始,他们称之为“标记”。

为此功能设置不同的起始字符可以简化标记化和解析。

标记 #

一般而言,标记 是变量的前缀,指示其类型,例如 Basic 或 Perl 中的 $I,其中 $ 是标记,I 是变量。

在这里,我们将标记定义为字符串字面量的前缀(也可能是后缀),指示应如何解释它。标记是一种语法糖,会转换为某些 Erlang 项或表达式。

标记字符串字面量由以下部分组成

  1. 标记前缀~ 后跟一个可能为空的名称。
  2. 字符串分隔符 内的 字符串内容
  3. 标记后缀,一个可能为空的名称字符序列。

标记转换 #

标记由词法分析器和解析器提前转换为其他项或表达式。解析和编译中的后续步骤会找出转换结果是否有效。

模式和表达式 #

转换后的项在何处有效取决于它被转换为什么。例如,如果标记转换为其他字面项,则它在模式中将有效。

如果标记已成为包含函数调用的内容,则它仅在一般表达式中有效,而不在模式中有效。

字符串连接 #

相邻的字符串由解析器连接,例如 «"abc" "def"» 连接为 "abcdef"

标记看起来像带有前缀(也可能是后缀)的字符串,但可能会转换为字符串以外的内容,因此它不能进行字符串连接。

因此,«~s"abc" "def"» 应该是非法的,并且任何类型的标记和任何其他项以任何顺序组成的任何其他序列也应该是非法的。

标记前缀 #

标记前缀以波浪号字符 ~ 开头,后跟标记类型,标记类型是由允许作为变量或原子中第二个或更高字符的字符序列组成的名称。简而言之,ISO Latin-1 字母、数字、_@。标记类型可能为空。

标记类型定义应如何解释 标记 语法糖。建议的标记类型为

  • «»:普通(默认(空名称))标记

    创建一个字面的 Erlang unicode:unicode_binary()。它是一个表示为 UTF-8 编码的二进制字符串,等效于对 字符串内容 应用 unicode:characters_to_binary/1字符串分隔符 和转义字符的工作方式与普通字符串或三引号字符串相同。

    因此,«~"abc\d"» 等效于 «<<"abc\d"/utf8>>»,«~'abc"d'» 等效于 «<<"abc\"d"/utf8>>»。

    普通字符串会识别转义序列,但三引号字符串是 verbatim,因此 «~"» 等效于 «~b"»,但 «~"""» 等效于 «~B"""»,如下所述。

    一种以 UTF-8 二进制形式创建字符串的简单方法据说是 Erlang 中第一个也是最需要的缺失字符串功能。这个标记就是这样做的。

  • bunicode:unicode_binary()

    创建一个字面的 UTF-8 编码的二进制文件,处理字符串内容中的转义字符。诸如字符串插值之类的其他功能将需要另一个标记类型或使用 标记后缀

    在 Elixir 中,这对应于 ~s 标记,一个字符串

  • Bunicode:unicode_binary(),verbatim。

    创建一个字面的 UTF-8 编码的二进制文件,带有 verbatim 字符串内容。当找到结束分隔符时,内容结束。没有办法转义结束分隔符。

    在 Elixir 中,这对应于 ~S 标记,一个 字符串

  • sstring()

    创建一个字面的 Unicode 代码点列表,处理字符串内容中的转义字符。诸如字符串插值之类的其他功能将需要另一个标记类型或使用 标记后缀

    在 Elixir 中,这对应于 ~c 标记,一个 字符列表

  • Sstring(),verbatim。

    创建一个字面的 Unicode 代码点列表,带有 verbatim 字符串内容。当找到结束分隔符时,内容结束。没有办法转义结束分隔符。

    在 Elixir 中,这对应于 ~C 标记,一个 字符列表

  • r:正则表达式。

    本 EEP 提议暂不实现正则表达式。目前尚不清楚如何与 re 模块集成,以及与仅使用 SB Sigil 类型相比,这样做是否值得。

    到目前为止,最好的想法是,此 sigil 创建一个字面量项 {re,RE::unicode:charlist(),Flags::[unicode:latin1_char()]},这是一个未编译的正则表达式,带有编译标志,适用于 re 模块中(尚未实现的)函数。 RE 元素是 字符串内容,而 Flags 元素是 Sigil 后缀

    请参阅关于此提议的项类型背后的理由的正则表达式部分。

    首先找到结束分隔符,并在字符串内容中,根据正则表达式规则处理字符转义序列。

    正则表达式 Sigil 的主要优点是避免了常规 Erlang 字符串所需的额外的 \ 转义。

    在引号中查找诸如 "foo\17" 之类的 name\number。

    今天:re:run(Subject, "^\\s*\"[a-z]+\\\\\\d+\"", [caseless,unicode])

    Sigil: re:run(Subject, ~r/^\s*"[a-z]+\\\d+"/iu)

    其他优点包括可能的工具和库集成功能,例如使 re 模块识别此元组格式,并使代码加载器对其进行预编译。

带有其他未知 Sigil 类型的Sigil 前缀应在词法分析器或解析器中引发错误“非法的 sigil 前缀”。另一种可能性是在编译链中进一步传递它们,使解析转换可以对它们起作用,但是该功能可以稍后添加,并且通常应避免使用解析转换,因为它们通常是难以发现问题的根源。

这些提议的 Sigil 类型根据相应的 Erlang 类型命名。Elixir 中的 Sigil 类型根据 Elixir 类型命名。因此,例如,Erlang 中的 ~s Sigil 前缀会创建一个 Erlang string(),这是一个 Unicode 代码点列表,但在 Elixir 中,~s Sigil 前缀会创建一个 Elixir String,这是一个 UTF-8 编码的二进制文件。

语言内部的一致性应该比语言之间的一致性更重要,并且语言之间的字符串类型不同已经是众所周知的怪癖。

字符串分隔符 #

紧随Sigil 前缀之后的是字符串开始分隔符。特定的开始分隔符字符具有对应的结束分隔符字符。

允许的开始-结束分隔符字符对是:() [] {} <>

以下字符是开始分隔符,它们本身就是结束分隔符:/ | ' " ` #

还允许使用三引号分隔符,即:如 EEP 64 中所述的 3 个或更多双引号 " 字符的序列。

对于给定的Sigil 类型原始 Sigil 除外),使用的字符串分隔符不会影响字符串内容的解释方式,除了找到结束分隔符之外。

但是,对于三引号字符串,从概念上讲,结束分隔符不会出现在字符串的内容中,因此解释字符串内容不会干扰查找结束分隔符。

提议的分隔符集与 Elixir 中的相同,加上 `#。 它们是 ASCII 中通常用于括号或文本引用的字符,以及那些感觉像全高垂直线的字符,除了: \ 太常用于字符转义,以及 # 在许多上下文中(shell 脚本,Perl 正则表达式)都是注释字符,在字符串内容中很容易避免,因此非常有用,不能不包括。

即使 Latin-1 是定义 Erlang 的字符集,但 ASCII 仍然是编程语言的共同点。只有西欧键盘和代码页才有可能生成高于 127 的 Latin-1 字符。

高于 127 的 Latin-1 字符允许在变量名和未加引号的原子中使用,但是使用它们的程序员应该意识到,对于非 Latin-1 用户,代码将无法正确读取。另一方面,引诱程序员使用例如碰巧存在于 Latin-1 键盘上的引号字符,但对于其他程序员而言将完全不同,这将是不好的。因此,像 « » 这样的字符应被用于一般的语法元素。

字符串内容 #

在开始和结束字符串分隔符之间,所有字符都是字符串内容。

在三引号字符串中,所有字符都是逐字的,但是按照 EEP 64 中的描述,会像往常一样删除缩进和前导和尾随换行符。

在使用单字符字符串分隔符的字符串中,通常会像 Erlang 常规字符串和带引号的原子一样,遵守以 \ 开头的 Erlang 转义序列。

特定的Sigil 类型可以具有其自己的字符转义规则,这可能会影响查找结束分隔符

Sigil 后缀 #

紧随字符串内容之后的是 Sigil 后缀,该后缀可能为空。

Sigil 后缀与Sigil 前缀中的 Sigil 类型一样,由名称字符组成。

Sigil 后缀可以指示如何解释特定Sigil 类型的字符串内容。 例如; 对于 ~R Sigil 前缀(正则表达式),Sigil 后缀被解释为简短的编译选项,例如使正则表达式字符不区分大小写的“i”。 例如 “~R/^from: /i”。

词法分析器可能需要执行的操作,例如如何处理转义字符规则,不应受 Sigil 后缀的影响,因为词法分析器在看到 Sigil 后缀时已经扫描了字符串内容

如果Sigil 类型不允许使用 Sigil 后缀,则应在词法分析器或解析器中生成错误“非法的 sigil 后缀”。

正则表达式 #

正则表达式 sigil «~R"expression"flags» 应转换为对工具/库有用的东西。 至少有两种方法; 未编译的正则表达式编译的正则表达式

未编译的正则表达式 #

正则表达式Sigil的值被选择为元组 {re,RE,Flags}

使用此表示形式,可以为 re 模块增强功能,使其接受此元组格式,该格式将正则表达式与编译标志捆绑在一起。 这些函数是 re:compile/1,2re:replace/3,4 re:run/2,3re:split/2,3。 这些函数应将 Flags 的字符转换为 re:compile_option()

调用尚未实现的 re:run/3 的示例

1> re:run("ABC123", ~r"abc\d+"i, [{capture,first,list}]).
{match,["ABC123"]}

由于Sigil值表示未编译的正则表达式,因此用户可以选择何时使用 re:compile/1,2 对其进行编译,或者直接在例如 re:run/2,3 中使用它。

可以实现一种优化,使编译器意识到,当将正则表达式Sigil(它是字面量)传递给诸如 re:run/2,3 之类的函数时,可以为代码加载器(现在缺少的功能)发出代码,以便在加载时编译正则表达式,并将预编译的正则表达式传递给 re:run/2,3

为了确保此优化安全,除了 Sigil 值中的编译选项之外,不能允许其他编译选项影响例如将选项作为第三个参数的 re:run/3。如果 re:run/3 会因任何编译选项(仅允许运行时选项)而失败,或者如果选项参数是包含在预编译中的字面量,则此类优化是安全的。

编译的正则表达式 #

另一种可能性是,正则表达式Sigil的值是已编译的正则表达式; re:mp() 类型。

然后可以像上面一样使用它,除了作为 re:compile/1,2 的参数。预编译将是一个硬性要求,因为正在运行的 Erlang 代码必须看到已编译的正则表达式。

我们仍然必须决定另一种 sigil 类型,该类型用于 re:compile/1,2,它是未编译的正则表达式的语法糖。 如果没有它,可以使用 ~S sigil,但这不会将编译标志作为后缀,因此无法以相同的方式为编译的正则表达式和未编译的正则表达式提供这些标志。

因此,未编译 #

由于无论如何我们需要一个Sigil,它是未编译的正则表达式的语法糖,并且可以对此进行预编译优化,因此此 EEP 建议正则表达式 Sigil 应表示带有编译标志的未编译正则表达式。

与 Elixir 的比较 #

Elixir 中没有原始 Sigil(空的Sigil 类型)。

本 EEP 提议将以下字符串分隔符添加到 Elixir 拥有的集合中:# `

字符串和二进制Sigil 类型在语言之间的命名方式不同,以使名称在语言内部(Erlang)保持一致:Elixir 中的 ~s 在 Erlang 中为 ~b,Elixir 中的 ~c 在 Erlang 中为 ~s,因此 ~s 的含义不同,因为字符串是不同的东西。

当 Elixir 允许在字符串内容中使用转义序列时,它也允许字符串插值。此 EEP 建议在建议的Sigil 类型实现字符串插值。

当 Elixir 不允许在字符串内容中使用转义序列时,它仍然允许转义结束分隔符。此 EEP 建议此类字符串应是真正逐字的,不能转义结束分隔符。

在两种语言中实现的转义序列略有不同。 Elixir 允许转义换行符,并且具有 Erlang 没有的转义序列 \a

Elixir 中的 ~S heredocs 和 Erlang 中的三引号字符串之间在处理换行符的方式上也略有不同。 请参阅 EEP 64

关于正则表达式标记 ~R 的细节,特别是它们的标记后缀,在 Erlang 中仍待决定。此外,关于是否需要转义结束分隔符的问题也尚未解决。

Erlang 中如何甚至是否实现字符串插值尚未决定,但很可能使用标记后缀或新的标记类型

参考实现 #

PR-7684 根据此 EEP 实现了 ~s~S~b~B~ (原始) 标记。

词法分析器在字符串字面量之前生成一个 sigil_prefix 标记,并在之后生成一个 sigil_suffix 标记。解析器将它们合并并转换为正确的输出项。

另一种方法是为整个字符串生成一个 (例如) sigil_string 标记,然后在解析器中处理它。这将需要在词法分析器中保留更多状态,用于标记前缀字符串的各个部分,因此需要更多词法分析器重写。

版权 #

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