作者
Raimo Niskanen <raimo(at)erlang(dot)org> , Kiko Fernandez-Reyes <kiko(at)erlang(dot)org>
状态
最终/27-w 在 OTP 27 版本中实现,在 OTP 26.1 版本中带有警告
类型
标准跟踪
创建于
2023年6月7日
Erlang 版本
OTP-27.0
历史记录
https://erlangforums.com/t/feature-heredocs-triple-quoted-text/2638/26 https://github.com/erlang/otp/pull/7451 https://erlangforums.com/t/triple-quoted-strings-adjacent-strings-without-white-space/3017

EEP 64: 三引号字符串 #

摘要 #

此 EEP 提议引入三引号字符串,并定义它们的语义。主要好处是以简单而有用的方式(即带有缩进)允许使用多行字符串,类似于其他语言,例如Elixir

它们的第一个用例是在包含 Markdown 或类似格式文本的模块内文档属性,其中逐字文本是可取的,因为任何文档文本格式都有其自己的转义序列概念,这将与 Erlang 的转义序列冲突。

基本原理 #

如今(2023 年 6 月),编写多行字符串很麻烦,并且可以说很难看。它们可能包含转义序列,并且没有缩进的概念

foo () ->
    case bar() of
         ok ->
             X = "First line
Second line with \"\\*not emphasized\\* Markdown\"
Third line",
             {ok, X}
    end.

内容的缩进无法遵循周围代码的缩进,并且 * 必须进行双重转义才能在实际内容中获得 \* 字符序列。

正如 EEP 59 中建议的那样,在文档属性中,缩进问题并不是那么明显,因为文档属性本身的缩进不多

-doc "
First line
Second line with \"\\*not emphasized\\* Markdown\"
Third line".

但是,带有缩进并且不必引用反斜杠肯定看起来更好

-doc """
    First line
    Second line with "\*not emphasized\* Markdown"
    Third line
    """.

考虑此 EEP 的主要原因是为了文档属性,其中不必担心转义序列是此 EEP 最具吸引力的特性。然而,引入新的字符串格式还需要定义它在 Erlang 代码中的行为方式。

仅在属性中允许的字符串格式会非常奇怪,并且此 EEP 中建议的格式在 Erlang 代码中也会很有用。

设计决策 #

属性是源代码中的 Erlang 形式,它由 - 标记、原子、一个值项和一个句点(点)组成。值项可以括在括号中(对于文档属性来说这不是很有趣)。

-doc "  Badly formatted
documentation paragraph
/-\\
\\-/".

文档属性应具有字符串作为其内容项,这里我们希望使用我们新的更方便的三引号字符串而不是普通字符串

-doc """
      Better formatted
    documentation paragraph
    /-\
    \-/
    """.

逐字字符串 #

我们希望字符串是逐字的,因为这样可以确保它们不会与任何文档文本格式冲突。在 Markdown(和 AsciiDoc)中,使用反斜杠字符 (\) 会发生冲突,因为它在普通的 Erlang 字符串中具有含义,因此如果字符串接受转义序列,则每个反斜杠都需要转义为双反斜杠。

但这在许多情况下不是一个大问题。 Elixir 也使用 Markdown 作为文档格式,并且大多忽略了这个问题。但在某些模块中,这一个问题,例如正则表达式模块,在那里使用 Sigils 来避免引用所有反斜杠。

我们也可以像 Elixir 一样选择两者,但那样我们必须先实现 Sigils 或类似的东西,然后才能获得足够有用的三引号字符串。

因此,如果我们必须选择一种格式,它应该是逐字的格式,因为这是更通用的格式,在代码中也是如此;在代码中,您只是想出于某种目的定义一个字符串,这是无法预见的。

这不会对 Sigils 关上大门。我们可以声明,我们只是选择三引号字符串的默认值是逐字的,而普通字符串的默认值是转义的。虽然我们没有和 Elixir 相同的默认值有点烦人,但语言中字符串之间还有其他更烦人的细微差异。请参阅 与 Elixir 的比较

因为字符串是逐字的,并且我们想使用它们在代码中定义任何字符串,所以我们不能限制在末尾始终有一个换行符。因此,应剥离最后一个换行符(在字符串结束行之前的行上的换行符)。如果需要结束换行符,则很容易添加换行符。

Elixir 不会剥离最后一个换行符,并通过转义换行符来回避这个问题。如果您不想要最后一个换行符,则在最后一行上放置一个反斜杠。但这在他们的逐字字符串中不起作用。因此,您必须在带有结束换行符的逐字字符串和必须转义反斜杠之间做出选择。

具有固定结束标记的逐字字符串的一个稍微令人讨厌的后果是,无法创建包含此类结束标记的字符串。因此,作为一种可能的扩展,此 EEP 建议不仅允许使用三引号字符串,还允许使用 3 个或更多引号的字符串。然后可以选择不属于字符串一部分的开始和结束标记,并且可以创建任何字符串。这对于一个非常罕见的极端情况来说不是一个很漂亮的解决方案。

三引号字符串扫描器标记 #

三引号字符串必须是扫描器识别为字符串的标记,这使其适合作为文档属性值项。它以三个双引号开头和结尾:"""

选择双引号 ",因为普通的 Erlang 字符串使用它们,这只是一个新的变体。由于使用双引号,三引号字符串应像普通字符串一样生成字符列表(Unicode 代码点)。

如果三引号字符串产生 UTF-8 二进制文件会更方便,但这对于双引号来说会是一个令人惊讶的功能,并且文档构建过程可以通过将字符列表转换为所需的二进制块来解决此问题。

在源代码中,三引号字符串在二进制文件中有效,因此生成 Unicode 二进制文件非常简单

X = <<"""
    Line 1
    Line 2
    """/utf8>>

2 + 7 个字符(“<<” + “/utf8>>”)的开销并不耗尽,因为我们的目标是多行字符串。

作为未来的扩展,有人提议使用 Sigils(前缀)来处理专用字符串,例如正则表达式、插值变量(PR-7343)、Unicode 二进制字符串等。例如:X = ~u"Tschüß" 用于 UTF-8 编码的二进制文件。

三引号字符串开头 #

在开头 """ 之后,只允许使用空白符直到行尾。

作为一种可能的未来扩展,我们可能会在此处允许不应作为字符串内容一部分的文本,但可以作为例如编辑器/漂亮打印机中语法突出显示和缩进处理的提示。

-doc """ md
    Markdown content
    * Bullet list
    """.

扫描器不需要对开头 """ 后面的行中的字符进行任何特殊处理,除非它不应搜索结束 """

稍后一步会将字符串内容中的字符(包括换行符)剥离。

如果这些字符中的任何一个不是空白符,则会报告语法错误。

三引号字符串结尾 #

所有字符都按原样(逐字)收集,并成为字符串内容。

三引号字符串以换行符开头,后跟可选的空白符,然后是 """。这完成了扫描器标记。

稍后一步使用结尾行上的空白符作为字符串缩进的定义,并从字符串中的每一行剥离该特定的空白符序列,并剥离结尾行之前的换行符。

如果任何行没有以定义的缩进开头,要么是因为行太短,要么是前缀不同,则会报告语法错误。但是,为了方便起见并遵守编辑器约定;空行可能是完全空的,而不是缩进的,但如果它以不是换行符的空白符开头,则它们必须是定义的缩进。

要求所有行(空行除外)必须具有完全相同的缩进字符,这是一个简单的解决方案,无需定义如何进行缩进空白符(制表符与空格)规范化,并且似乎也是一个合理的要求。

CRLF 和空白符 #

字符 CR - Unicode 代码点值 13,LF - 代码点 10,以及空白符 - 与今天 Erlang 扫描器中定义的相同,由扫描器像往常一样处理,除非结尾行之前的行以 CR LF 结尾,则 CR 也被解释为换行符的一部分,并与 LF 一起剥离。这对于具有 CR LF 换行符的系统来说很方便。

除此之外,字符串内的 CRLF 和空白符会按原样传递。

这意味着以下文本中将普通字符串用作匹配参考的示例假设源代码只有 LF 换行符,例如

"""

X
""" = "\nX"

如果源代码有 CR LF 换行符,则该示例将变为

"""

X
""" = "\r\nX"

此示例在两种情况下均有效,但可能更难阅读

"""

X
""" = "
X"

开头和结尾换行符 #

以上规则会剥离一个开头换行符和一个结尾换行符。这是一个简单的约定,它还可以控制字符串的内容

示例 1

"""

  X

""" = "\n  X\n"

示例 2

"""
X
""" = "X"

示例 3

"""
 
""" = ""

请注意,以下可能是一个语法错误;由于起始行和最后内容行的末尾换行符都会被删除,因此多行字符串可能太短,导致内容被视为空。但更方便的做法是将该换行符视为被双重删除,从而允许将其视为空字符串。

"""
""" = ""

缩进 #

以上规则有助于使内容缩进以符合周围的代码。结束行决定缩进。

示例 1

"""
This string
is not indented
""" =
    "This string\nis not indented"

示例 2

"""
    This string
    is indented
    """ =
    "This string\nis indented"

示例 3

"""
      This indented string
    has an indented first line
    """ =
    "  This indented string\nhas an indented first line"

"""

示例 4

foo() ->
    X =
        """
          This indented string
        has an indented first line

        and an empty line that is not indented
        """,
    %% That content line 3 is empty instead of indented
    %% is only visible if you "touch" the text
    %% with the cursor or the mouse
    X =
        "  This indented string\n"
        "has an indented first line\n"
        "\n"
        "and an empty line that is not indented".

示例 5

"""
This is a syntax error (incorrect indentation)
    """

示例 6

""" This is a syntax error
(non-white-space on start line)
"""

示例 6

"""
This is an incomplete string so the scanner will search forward
for the end, and the shell will block waiting for more lines,
since these quote characters are not a valid string ending: """

向后不兼容 #

这在今天有效

X = """
    X
    """

它等效于

X = "" "
    X
    " ""

这等效于

X = "
    X
    "

这等效于

X = "\n    X\n"

但是,使用建议的三引号字符串,第一个代码片段将等效于

X = "X"

此外,这在今天也有效

X = """ xxx
  X
    """

但根据此 EEP,它将是两个语法错误

  1. 起始行在 """ 之后有非空白字符。
  2. 第一行内容缩进不正确。

还有许多其他类似的构造也将导致语法错误。

  • 不太可能有人故意在源代码中使用 """ 来表示与另一个字符串连接的空字符串。
  • 今天允许的与 """ 的大多数组合都会导致语法错误。只有少数组合会略微改变行为(字符串内容)。
  • 用户可以简单地在其源代码中 grep 搜索 """。 通过宏创建相同的序列将更难找到; 最糟糕的问题不是新的语法错误(很难错过),而是行为的改变。 而行为的改变将是略微不同的字符串内容。

允许在下一章中建议的 3 个或更多引号的字符串可能会导致更大的向后不兼容性。这是一个例子

X = """"
    ++ foo() ++
    """"

该代码在今天有效,并且将在 foo() 的返回值之前添加空字符串,并在之后添加空字符串。 当引入 3 个或更多引号的字符串时,它将变为

X = "++ foo() ++"

不过,代码示例确实很奇怪。

因此,应该很少有人会遇到此 EEP 中建议的实际向后不兼容问题。

但是,为了帮助用户找到意外使用 3 个或更多引号的字符串,应该在当前的 Erlang/OTP 版本中尽早引入编译器警告,并且此 EEP 可能会在下一个版本中实现。

不幸的是,这样的警告可能比此 EEP 更难实现,因为扫描器无法发出警告。解析器也无法发出警告。

可以通过让扫描器发出一个特殊的虚拟令牌来实现警告,该令牌会被解析器剥离并忽略。然后,预处理器可以查看扫描的令牌流并发出警告。这样,解析器输出就不会发生变化,因此预处理器和编译器的其余部分以及例如解析转换都不会受到影响。

引用 """ #

使用上述规则,不可能在三引号字符串中行的开头有 """

这将是允许的

-doc """
    A triple-quoted string starts with: """
    and ends with: """
    """.

只要 """ 不在行的开头。不幸的是,这是一个谎言,因为根据此 EEP,结尾应该在行的开头……

可以在 Erlang 代码中解决

X = """
    A triple-quoted string starts with: """
    and ends with:
    
    """ "\"\"\"".

这很丑陋。

我们可以忽略这个怪癖,因为它只有在放置在行的开头时 """ 才会成为问题,或者我们可以使用 GitHub Flavored Markdown技巧来允许 3 个或更多起始字符和匹配的结束字符,因此这将是有效的

X = """"
    A triple-quoted string starts with: """
    and ends with:
    """
    """"

与 Elixir 的比较 #

Elixir 有三引号字符串,并将其命名为heredocs

它们使用 """ 分隔,如本建议中所示,并且对于起始行、结束行和缩进具有几乎相同的规则。以下是已知的差异

  • 最后一个行尾不会被删除。
  • 它们生成 UTF-8 编码的二进制文件,就像 Elixir" 引号字符串一样。
  • 它们接受转义序列。
  • 换行符可以转义。
  • 有一个转义序列 "\a"
  • 它们不允许超过 3 个 " 字符作为字符串的开头,这是此 EEP 中的一个建议
  • 它们接受 Sigils,这允许字符串是逐字的,但这样就无法避免最后的换行符。

此 EEP 建议的三引号字符串与 Elixirheredocs 类似,没有插值和转义

~S"""
Heredoc without interpolation and escaping
"""

Elixir 字符串有一个最后的换行符,此 EEP 建议应始终删除,因为在没有转义序列的情况下,无法删除它。

版权 #

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