第 11 章 OCaml 程式語言

1 詞法慣例

空白

以下字元被視為空白:空格、水平製表符、歸位符、換行符和換頁符。空白會被忽略,但它們會分隔相鄰的識別符、字面值和關鍵字,否則這些識別符、字面值或關鍵字會被混淆為一個單一的識別符、字面值或關鍵字。

註解

註解以兩個字元 (* 開頭,中間沒有空白,並以字元 *) 結尾,中間沒有空白。註解被視為空白字元。註解不會出現在字串或字元字面值內。巢狀註解會被正確處理。

(* 單行註解 *) (* 多行註解,註解掉程式碼的一部分,並包含巢狀註解:let f = function | 'A'..'Z' -> "Uppercase" (* 稍後加入其他情況... *) *)

識別符

ident::= (letter ∣ _) { letter ∣ 09 ∣ _ ∣ ' } 
 
capitalized-ident::= (AZ) { letter ∣ 09 ∣ _ ∣ ' } 
 
lowercase-ident::=(az ∣ _) { letter ∣ 09 ∣ _ ∣ ' } 
 
letter::=AZ ∣ az

識別符是由字母、數字、_ (底線字元) 和 ' (單引號) 組成的序列,且開頭為字母或底線。字母至少包含 ASCII 集中的 52 個大小寫字母。目前的實作也將 ISO 8859-1 集中的某些字元識別為字母(字元 192–214 和 216–222 為大寫字母;字元 223–246 和 248–255 為小寫字母)。此功能已被棄用,為了未來相容性,應避免使用。

識別符中的所有字元都有意義。目前的實作接受長度最多為 16000000 個字元的識別符。

在許多地方,OCaml 會區分大寫的識別符和以小寫字母開頭的識別符。在此目的下,底線字元被視為小寫字母。

整數字面值

integer-literal::=[-] (09) { 09 ∣ _ }
  [-] (0x ∣ 0X) (09 ∣ AF ∣ af) { 09 ∣ AF ∣ af ∣ _ }
  [-] (0o ∣ 0O) (07) { 07 ∣ _ }
  [-] (0b ∣ 0B) (01) { 01 ∣ _ }
 
int32-literal::=integer-literall
 
int64-literal::=integer-literalL
 
nativeint-literal::=integer-literaln

整數字面值是一個或多個數字的序列,可選擇性地在前面加上負號。預設情況下,整數字面值是十進位 (基數 10)。以下前綴選擇不同的基數

前綴基數
0x, 0X十六進位 (基數 16)
0o, 0O八進位 (基數 8)
0b, 0B二進位 (基數 2)

(開頭的 0 是數字零;八進位的 O 是字母 O。)整數字面值後面可以跟著字母 lLn 之一,表示此整數的類型分別為 int32int64nativeint,而不是整數字面值的預設類型 int。超出可表示的整數值範圍的整數字面值的解釋是未定義的。

為了方便和可讀性,底線字元 (_) 在整數字面值中被接受(並忽略)。

# let house_number = 37 let million = 1_000_000 let copyright = 0x00A9 let counter64bit = ref 0L;;
val house_number : int = 37 val million : int = 1000000 val copyright : int = 169 val counter64bit : int64 ref = {contents = 0L}

浮點數字面值

float-literal::=[-] (09) { 09 ∣ _ } [. { 09 ∣ _ }] [(e ∣ E) [+ ∣ -] (09) { 09 ∣ _ }]
  [-] (0x ∣ 0X) (09 ∣ AF ∣ af) { 09 ∣ AF ∣ af ∣ _ }  [. { 09 ∣ AF ∣ af ∣ _ }] [(p ∣ P) [+ ∣ -] (09) { 09 ∣ _ }]

浮點十進位字面值由整數部分、小數部分和指數部分組成。整數部分是一個或多個數字的序列,可選擇性地在前面加上負號。小數部分是一個小數點,後面跟著零個、一個或多個數字。指數部分是字元 eE,後面跟著可選的 +- 符號,然後是一個或多個數字。它被解釋為 10 的冪。小數部分或指數部分可以省略,但不能同時省略,以避免與整數字面值混淆。超出可表示的浮點數值範圍的浮點字面值的解釋是未定義的。

浮點十六進位字面值以 0x0X 前綴表示。語法類似於浮點十進位字面值,但有以下差異。整數部分和小數部分使用十六進位數字。指數部分以字元 pP 開頭。它是以十進位寫成的,並解釋為 2 的冪。

為了方便和可讀性,底線字元 (_) 在浮點字面值中被接受(並忽略)。

# let pi = 3.141_592_653_589_793_12 let small_negative = -1e-5 let machine_epsilon = 0x1p-52;;
val pi : float = 3.14159265358979312 val small_negative : float = -1e-05 val machine_epsilon : float = 2.22044604925031308e-16

字元字面值

char-literal::= 'regular-char'
 'escape-sequence'
 
escape-sequence::= \ (\ ∣ " ∣ ' ∣ n ∣ t ∣ b ∣ r ∣ space)
 \ (09) (09) (09)
 \x (09 ∣ AF ∣ af) (09 ∣ AF ∣ af)
 \o (03) (07) (07)

字元常值以 ' (單引號) 字元分隔。兩個單引號之間可以包含一個不同於 '\ 的字元,或下列跳脫序列之一

序列表示的字元
\\反斜線 (\)
\"雙引號 (")
\'單引號 (')
\n換行 (LF)
\r歸位 (CR)
\t水平製表符 (TAB)
\b退格 (BS)
\space空格 (SPC)
\dddASCII 碼為十進制 ddd 的字元
\xhhASCII 碼為十六進制 hh 的字元
\ooooASCII 碼為八進制 ooo 的字元
# let a = 'a' let single_quote = '\'' let copyright = '\xA9';;
val a : char = 'a' val single_quote : char = '\'' val copyright : char = '\169'

字串常值

字串常值::= " { string-character } "
   {quoted-string-id| { newline
 any-char } |quoted-string-id}
 
帶引號的字串 ID::={ a...z ∣ _ }
 
字串字元::= 正規字串字元
 escape-sequence
 \u{ { 09 ∣ AF ∣ af }+}
 換行
 \newline { space ∣ tab }

字串常值以 " (雙引號) 字元分隔。兩個雙引號之間包含一系列不同於 "\ 的字元、字元常值表中給出的跳脫序列,或 Unicode 字元跳脫序列。

Unicode 字元跳脫序列會被替換為指定 Unicode 純量值的 UTF-8 編碼。Unicode 純量值是 0x0000...0xD7FF 或 0xE000...0x10FFFF 範圍內的整數,使用 1 到 6 個十六進位數字定義;允許前導零。

# let greeting = "Hello, World!\n" let superscript_plus = "\u{207A}";;
val greeting : string = "Hello, World!\n" val superscript_plus : string = "⁺"

換行序列是選擇性地以歸位字元開頭的換行字元。自 OCaml 5.2 起,字串常值中發生的換行序列會被正規化為單一換行字元。

為了允許跨行分割長字串常值,序列 \newline ‍spaces-or-tabs (行尾的反斜線,後接下一行開頭的任意數量的空格和水平製表符) 會在字串常值中被忽略。

# let longstr = "Call me Ishmael. Some years ago — never mind how long \ precisely — having little or no money in my purse, and \ nothing particular to interest me on shore, I thought I\ \ would sail about a little and see the watery part of t\ he world.";;
val longstr : string = "Call me Ishmael. Some years ago — never mind how long precisely — having little or no money in my purse, and nothing particular to interest me on shore, I thought I would sail about a little and see the watery part of the world."

跳脫換行比非跳脫換行提供更方便的行為,因為縮排不被視為字串常值的一部分。

# let contains_unexpected_spaces = "This multiline literal contains three consecutive spaces." let no_unexpected_spaces = "This multiline literal \n\ uses a single space between all words.";;
val contains_unexpected_spaces : string = "This multiline literal\n contains three consecutive spaces." val no_unexpected_spaces : string = "This multiline literal \nuses a single space between all words."

帶引號的字串常值為字串常值提供了替代的詞彙語法。它們可用於表示不需跳脫的任意內容的字串。帶引號的字串以一對匹配的 { quoted-string-id || quoted-string-id } 分隔,兩側具有相同的 quoted-string-id。帶引號的字串不會以特殊方式解譯任何字元1,但要求序列 | quoted-string-id } 本身不會出現在字串中。識別符 quoted-string-id 是小寫字母和底線 (可能為空) 的序列,可以自由選擇以避免此類問題。

# let quoted_greeting = {|"Hello, World!"|} let nested = {ext|hello {|world|}|ext};;
val quoted_greeting : string = "\"Hello, World!\"" val nested : string = "hello {|world|}"

目前的實作對字串常值的長度幾乎沒有限制。

命名標籤

為了避免歧義,在表達式中命名標籤不能僅在語法上定義為三個符號 ~ident: 的序列,而必須在詞彙層級定義。

命名標籤有兩種形式:用於正常參數的 label 和用於選擇性參數的 optlabel。它們只是由第一個字元區分,即 ~?

儘管 labeloptlabel 是表達式中的詞彙實體,但為了提高可讀性,它們的擴展 ~ label-name :? label-name : 將用於語法中。另請注意,在類型表達式內部,此擴展可以按字面意思理解, 真的有 3 個符號,它們之間有可選的空格。

前綴和中綴符號

中綴符號::=(core-operator-char ∣ % ∣ <) { operator-char }
 # { operator-char }+
 
前綴符號::= ! { operator-char }
  (? ∣ ~) { operator-char }+
 
運算符字元::= ~ ∣ ! ∣ ? ∣ core-operator-char ∣ % ∣ < ∣ : ∣ .
 
核心運算符字元::= $ ∣ & ∣ * ∣ + ∣ - ∣ / ∣ = ∣ > ∣ @ ∣ ^ ∣ |

另請參閱以下語言擴展:擴展運算符擴展索引運算符綁定運算符

諸如 <=>!! 之類的「運算符字元」序列,會從 infix-symbolprefix-symbol 類別讀取為單個符號。這些符號在表達式內部會被解析為前綴和中綴運算符,但在其他情況下行為類似於正常的識別符。

關鍵字

以下識別符保留為關鍵字,不得用於其他用途

      and         as          assert      asr         begin       class
      constraint  do          done        downto      else        end
      exception   external    false       for         fun         function
      functor     if          in          include     inherit     initializer
      land        lazy        let         lor         lsl         lsr
      lxor        match       method      mod         module      mutable
      new         nonrec      object      of          open        or
      private     rec         sig         struct      then        to
      true        try         type        val         virtual     when
      while       with


以下字元序列也是關鍵字

    !=    #     &     &&    '     (     )     *     +     ,     -
    -.    ->    .     ..    .~    :     ::    :=    :>    ;     ;;
    <     <-    =     >     >]    >}    ?     [     [<    [>    [|
    ]     _     `     {     {<    |     |]    ||    }     ~

請注意,以下識別符是現在已停止維護的 Camlp4 系統的關鍵字,為了向後相容性的原因,應避免使用。

    parser    value    $     $$    $:    <:    <<    >>    ??

歧義

詞彙歧義根據「最長匹配」規則解決:當一個字元序列可以以幾種不同的方式分解為兩個符號時,保留的分解是第一個符號最長的分解。

行號指示符

行號指示符::= # { 09 }+" { string-character } "

產生 OCaml 原始程式碼的預處理器可以在其輸出中插入行號指示符,以便編譯器產生的錯誤訊息包含參考預處理前原始檔案的行號和檔案名稱,而不是預處理後的檔案。行號指示符從一行的開頭開始,由 # (井號) 組成,後接一個正整數 (原始行號),再後接一個字元串 (原始檔案名稱)。在詞彙分析期間,行號指示符會被視為空白。

1
除了前面提到的換行序列正規化為單一換行字元之外。