作者
Raimo Niskanen <raimo(at)erlang(dot)org> , Kiko Fernandez-Reyes <kiko(at)erlang(dot)org>
狀態
最終版/27-w 已在 OTP 27 版本實作,並在 OTP 26.1 版本中提供警告
類型
標準追蹤
建立日期
2023-06-07
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
    """.

掃描器不需要對起始 """ 之後的行上的字元進行任何特殊處理,除了它不應搜尋結束的 """ 之外。

稍後的步驟會從字串內容中刪除直到並包括換行符的字元。

如果這些字元中的任何一個不是空白字元,則會回報語法錯誤。

三引號字串結束 #

所有字元都會按原樣收集(逐字),並成為字串內容。

三引號字串以換行符開始,後面接著可選的空白字元,然後是 """。這就完成了掃描器符號。

稍後的步驟使用結束行上的空白字元作為字串縮排的定義,並從字串中的每一行刪除該特定的空白字元序列,並刪除結束行之前的換行符。

如果任何一行沒有以定義的縮排開始,因為該行太短或字首不同,則會回報語法錯誤。為了方便起見並遵守編輯器慣例,但是;空行可能完全為空而不是縮排,但是如果它以非換行符的空白字元開頭,則它們必須是定義的縮排。

要求所有行(空行除外)必須具有完全相同的縮排字元是一種簡單的解決方案,不必定義應如何完成縮排空白字元(Tab 鍵與空格鍵)正規化,並且看起來也是一個合理的要求。

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. 第一個內容行具有不正確的縮排。

還有許多其他類似的結構也會出現語法錯誤。

  • 幾乎不可能有人故意在原始碼中使用 """ 來表示與另一個字串串接的空字串。
  • 大多數今天允許的與 """ 的組合都會導致語法錯誤。只有少數會出現微妙的行為變化(字串內容)。
  • 使用者可以簡單地在其原始碼中搜尋 """。建立相同的序列,例如透過巨集,會更難以尋找;最糟糕的問題不是新的語法錯誤(很難錯過),而是行為的改變。而行為的改變將是稍微不同的字串內容。

允許下一個章節中建議的 3 個或更多引號的字串可能會導致更大的向後不相容性。這是一個範例

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

該程式碼目前有效,它會在 foo() 的回傳值之前加上空字串,並在其後附加空字串。當引入 3 個或更多引號的字串時,它將會變成:

X = "++ foo() ++"

不過,這個程式碼範例真的很奇怪。

因此,應該不太可能有人會因為這個 EEP 中的建議而遇到真正的回溯不相容問題。

但是,為了幫助使用者找到意外使用 3 個或更多引號字串的情況,應該在當前的 Erlang/OTP 版本中盡早引入編譯器警告,而這個 EEP 可能會在下一個版本中實作。

不幸的是,實作這樣的警告可能會比這個 EEP 更複雜,因為掃描器無法發出警告。剖析器也無法發出。

可以透過讓掃描器發出一個特殊的虛擬 token,然後讓剖析器移除並忽略它來實作警告。然後預處理器可以查看掃描過的 token 流並發出警告。這樣一來,剖析器的輸出就不會改變,因此預處理器和編譯器的其餘部分以及例如剖析轉換等都不會受到影響。

""" 的引用 #

根據上述規則,不可能在三引號字串中讓 """ 出現在一行的開頭。

這樣是可以允許的

-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-通用許可證下,以較寬鬆者為準。