作者
Raimo Niskanen <raimo(at)erlang(dot)org>
狀態
最終版本/27.0 已在 OTP 27 版本中實作;正規表示式符號 (`~r` 和 `~R`) 尚未實作
類型
標準追蹤
建立於
2023 年 9 月 25 日
Erlang 版本
OTP-27.0

EEP 66:字串字面值的符號 #

摘要 #

此 EEP 建議為字串字面值使用符號,非常類似 Elixir 符號。主要原因是為了促進其他建議的語言功能,其中許多功能以 符號 的名義存在於 Elixir 中,例如

  • 二進位字串:unicode:unicode_binary()
  • 正規表示式語法
  • 字串分隔符號的選擇
  • 逐字字串
  • 字串內插語法,或變數內插

理由 #

許多關於 摘要 中功能的現有建議,會在一般 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 程式語言是根據傳統的語彙分析器+剖析器+編譯器模型建構的。

語彙分析器,又稱掃描器或詞法分析器,會掃描原始碼字元序列,並將其轉換為一連串的符記 (Token),例如原子、變數、字串、整數、保留字、標點符號或運算子: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 字母、數字、_@。符號類型可以是空的。

符號類型定義了如何解釋 符號 語法糖。建議的符號類型為

  • «»:vanilla(預設(空名稱))符號

    建立一個字面 Erlang unicode:unicode_binary()。它是以 UTF-8 編碼的二進位表示的字串,相當於對 字串內容 應用 unicode:characters_to_binary/1字串分隔符號 和跳脫字元會像它們在一般字串或三引號字串中一樣工作。

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

    一般字串會遵循跳脫序列,但三引號字串是逐字的,因此 «~"» 等於 «~b"»,但 «~"""» 等於 «~B"""»,如下所述。

    以 UTF-8 二進位建立字串的簡單方法據說是 Erlang 中第一個也是最需要的遺失字串功能。這個符號就是這樣做的。

  • bunicode:unicode_binary()

    建立一個字面 UTF-8 編碼二進位,處理字串內容中的跳脫字元。字串內插等其他功能將需要另一個符號類型或使用 符號後綴

    在 Elixir 中,這對應於 ~s 符號,也就是 字串

  • Bunicode:unicode_binary(),逐字。

    建立一個字面 UTF-8 編碼二進位,具有逐字的字串內容。當找到結束分隔符號時,內容就會結束。無法跳脫結束分隔符號。

    在 Elixir 中,這對應於 ~S 字元符號,它是一個字串

  • sstring()

    建立一個字面 Unicode 碼點列表,並處理字串內容中的跳脫字元。其他功能,例如字串插值,將需要另一個字元符號類型或使用字元符號後綴

    在 Elixir 中,這對應於 ~c 字元符號,它是一個字元列表

  • Sstring(),逐字輸出。

    建立一個字面 Unicode 碼點列表,並逐字輸出字串內容。當找到結束分隔符時,內容結束。沒有辦法跳脫結束分隔符。

    在 Elixir 中,這對應於 ~C 字元符號,它是一個字元列表

  • r:正規表示式。

    此 EEP 建議暫時不實作正規表示式。目前仍不清楚應如何與 re 模組整合,以及與僅使用 SB 字元符號類型相比,是否值得投入精力。

    目前最好的想法是,此字元符號會建立一個字面項 {re,RE::unicode:charlist(),Flags::[unicode:latin1_char()]},這是一個未編譯的正規表示式,帶有編譯標誌,適用於 re 模組中(尚未實作)的函式。RE 元素是字串內容,而 Flags 元素是字元符號後綴

    請參閱關於此建議項類型背後原因的正規表示式章節。

    首先找到結束分隔符,並且在字串內容中,根據正規表示式規則處理跳脫字元序列。

    正規表示式字元符號的主要優點是避免了常規 Erlang 字串所需的額外 \ 跳脫。

    在引號中尋找名稱\數字,例如:"foo\17"

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

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

    其他優點是可能的工具和程式庫整合功能,例如讓 re 模組識別此元組格式,以及讓程式碼載入器預先編譯它們。

帶有其他未知字元符號類型的字元符號前綴應在詞法分析器或剖析器中產生「非法字元符號前綴」錯誤。另一種可能性是在編譯鏈中進一步傳遞它們,使剖析轉換能夠對它們執行動作,但該功能可以稍後添加,並且通常應避免使用剖析轉換,因為它們通常是難以發現問題的來源。

這些建議的字元符號類型是根據相應的 Erlang 類型命名的。 Elixir 中的字元符號類型是根據 Elixir 類型命名的。因此,例如,Erlang 中的 ~s 字元符號前綴會建立一個 Erlang string(),這是一個 Unicode 碼點列表,但在 Elixir 中,~s 字元符號前綴會建立一個 Elixir 字串,這是一個 UTF-8 編碼的二進位。

語言內的一致性應該比語言之間的一致性更重要,而且語言之間的字串類型不同已經是一個眾所皆知的怪癖。

字串分隔符 #

緊接在字元符號前綴之後的是字串起始分隔符。特定的起始分隔符字元具有相應的結束分隔符字元。

允許的起始-結束分隔符字元配對為:() [] {} <>

以下字元是起始分隔符,它們本身就是結束分隔符:/ | ' " ` #

也允許使用三引號分隔符,也就是說;如 EEP 64 中所述,使用 3 個或更多雙引號 " 字元的序列。

對於給定的字元符號類型原始字元符號除外),除了找到結束分隔符之外,使用的字串分隔符不會影響如何解譯字串內容。

但是,對於三引號字串,從概念上講,結束分隔符不會出現在字串的內容中,因此解譯字串內容不會干擾尋找結束分隔符。

建議的分隔符集合與Elixir 中的相同,外加 `#。它們是 ASCII 中通常用於括號或文字引用的字元,以及那些感覺像全高垂直線的字元,例外情況是:\ 太常被用於字元跳脫,加上 # 太有用,不能包含,因為在許多情況下(Shell 腳本、Perl 正規表示式)它是一個註解字元,很容易在字串內容中避免。

即使 Latin-1 是 Erlang 中定義的字元集,但 ASCII 仍然是程式語言的共同點。只有西歐鍵盤和程式碼頁面才能產生 127 以上的 Latin-1 字元。

允許在變數名稱和未加引號的原子中使用 127 以上的 Latin-1 字元,但使用此類字元的程式設計人員應注意,對於非 Latin-1 使用者,程式碼將無法正確讀取。另一方面,如果引誘程式設計人員使用例如存在於 Latin-1 鍵盤上的引號字元,但對於其他程式設計人員而言,它將完全不同,那將會很糟糕。因此,不應將 « » 之類的字元用於一般語法元素。

字串內容 #

在起始和結束字串分隔符之間,所有字元都是字串內容。

在三引號字串中,所有字元都是逐字輸出,但會像 EEP 64 中所述的那樣,照常剝離縮排和開頭和結尾的換行符。

在具有單字元字串分隔符的字串中,會像往常一樣為常規 Erlang 字串和加引號的原子處理以 \ 作為前綴的常規 Erlang 跳脫序列。

特定的字元符號類型可以有自己的字元跳脫規則,這可能會影響尋找結束分隔符

字元符號後綴 #

緊接在字串內容之後的是字元符號後綴,它可能是空的。

字元符號後綴與字元符號前綴中的字元符號類型一樣,由名稱字元組成。

字元符號後綴可能會指示如何解譯特定字元符號類型的字串內容。例如,對於 ~R 字元符號前綴(正規表示式),字元符號後綴會被解譯為簡短形式的編譯選項,例如「i」,這會使正規表示式不區分大小寫。例如「~R/^from: /i」。

詞法分析器可能必須執行的事項,例如如何處理跳脫字元規則,不應受字元符號後綴的影響,因為詞法分析器在看到字元符號後綴時,已經掃描過字串內容

如果字元符號類型不允許字元符號後綴,則應在詞法分析器或剖析器中產生「非法字元符號後綴」錯誤。

正規表示式 #

正規表示式字元符號「~R"expression"flags」應轉換為對工具/程式庫有用的東西。至少有兩種方法:未編譯的正規表示式已編譯的正規表示式

未編譯的正規表示式 #

正規表示式字元符號的值選擇為一個元組 {re,RE,Flags}

使用此表示法,可以使用接受此元組格式的函式來擴充 re 模組,該格式將正規表示式與編譯標誌捆綁在一起。這些函式是 re:compile/1,2re:replace/3,4re: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"]}

由於字元符號值表示未編譯的正規表示式,因此使用者可以選擇何時使用 re:compile/1,2 編譯它,或直接在例如 re:run/2,3 中使用它。

可以實作最佳化,使編譯器知道在將正規表示式字元符號(這是一個字面值)傳遞給 re:run/2,3 之類的函式時,可以發出程式碼,以便程式碼載入器(現在缺少的功能)在載入時編譯正規表示式,並改為將預先編譯的正規表示式傳遞給 re:run/2,3

為了確保此最佳化的安全,除了字元符號值中的選項之外,不允許其他編譯選項影響例如將選項作為第三個參數的 re:run/3。如果 re:run/3 會因任何編譯選項而失敗(僅允許執行階段選項),或者如果選項引數是要包含在預先編譯中的字面值,那麼此類最佳化是安全的。

已編譯的正規表示式 #

另一種可能性是,正規表達式Sigil的值是一個已編譯的正規表達式;re:mp()類型。

然後它可以像上面那樣使用,除了作為re:compile/1,2的參數之外。預先編譯將會是一個硬性要求,因為運行的 Erlang 程式碼必須看到已編譯的正規表達式。

而且我們仍然需要決定另一個用於 re:compile/1,2 的 sigil 類型,它是一個未編譯正規表達式的語法糖。如果沒有它,可以使用 ~S sigil,但它不會有編譯標誌作為後綴,所以對於已編譯和未編譯的正規表達式,這些標誌不能以相同的方式給出。

因此,未編譯 #

由於無論如何我們都需要一個 Sigil 作為未編譯正規表達式的語法糖,並且可以使用它進行預先編譯優化,因此這個 EEP 建議正規表達式 Sigil 應該表示一個帶有編譯標誌的未編譯正規表達式。

與 Elixir 的比較 #

在 Elixir 中沒有 Vanilla Sigil (空的 Sigil Type)。

這個 EEP 建議將以下字串分隔符添加到 Elixir 已有的集合中:# `

字串和二進制Sigil Type在語言之間有不同的命名,以保持語言(Erlang)內名稱的一致性:Elixir 中的 ~s 在 Erlang 中是 ~b,而 Elixir 中的 ~c 在 Erlang 中是 ~s,因此 ~s 的意思不同,因為字串是不同的東西。

當 Elixir 允許在字串內容中使用跳脫序列時,它也允許字串插值。這個 EEP 建議不要在建議的 Sigil Type 中實現字串插值。

當 Elixir 不允許在字串內容中使用跳脫序列時,它仍然允許跳脫結束分隔符。這個 EEP 建議這樣的字串應該是真正的逐字字串,沒有可能跳脫結束分隔符。

在語言中實作的跳脫序列略有不同;Elixir 允許跳脫換行符,並且有一個 Erlang 沒有的跳脫序列 \a

在 Elixir 的 ~S heredocs 和 Erlang 的三引號字串之間,換行符的處理方式也略有不同。請參閱 EEP 64

關於正規表達式 sigils,~R,特別是它們的Sigil Suffix的細節,仍需要在 Erlang 中決定。此外,仍然存在關於是否跳脫結束分隔符的問題。

尚未決定如何在 Erlang 中實作字串插值,甚至是是否實作,但很可能會使用Sigil Suffix或新的Sigil Type

參考實作 #

PR-7684 根據這個 EEP 實作了 ~s~S~b~B~ (vanilla) Sigil。

詞法分析器在字串文字之前產生一個 sigil_prefix 符號,在之後產生一個 sigil_suffix 符號。解析器合併並將它們轉換為正確的輸出項。

另一種方法是(例如)為整個字串產生一個 sigil_string 符號,然後在解析器中處理它。這需要在詞法分析器中保留更多的狀態,介於 sigil 前綴字串的各部分之間,因此需要更多的詞法分析器重寫。

版權 #

本文件置於公共領域或 CC0-1.0-Universal 許可證之下,以較寬容者為準。