運算符

目標

本教學的學習目標是

  • 將運算符作為函式使用,反之亦然,將函式作為運算符使用
  • 為自訂運算符指定正確的結合性和優先順序
  • 使用並定義自訂的 let 綁定器

使用二元運算符

在 OCaml 中,幾乎所有的二元運算符都是普通的函式。運算符底層的函式通過將運算符符號用括號括起來來引用。以下是加法、字串串接和相等函式

# (+);;
- : int -> int -> int = <fun>
# (^);;
- : string -> string -> string = <fun>
# (=);;
- : 'a -> 'a -> bool = <fun>

注意:乘法的運算符符號是 *,但不能用 (*) 來引用。這是因為 OCaml 中的註解使用 (**) 分隔。為了消除解析的歧義,必須插入空格字元才能取得乘法函式。

# ( * );;
- : int -> int -> int = <fun>

當與部分應用結合使用時,將運算符作為函式使用非常方便。例如,以下是如何使用函式 List.filter 和一個運算符來取得整數列表中大於或等於 10 的值。

# List.filter;;
- : ('a -> bool) -> 'a list -> 'a list = <fun>

# List.filter (( <= ) 10);;
- : int list -> int list = <fun>

# List.filter (( <= ) 10) [6; 15; 7; 14; 8; 13; 9; 12; 10; 11];;
- : int list = [15; 14; 13; 12; 10; 11]

# List.filter (fun n -> 10 <= n) [6; 15; 7; 14; 8; 13; 9; 12; 10; 11];;
- : int list = [15; 14; 13; 12; 10; 11]

前兩行和最後一行僅供參考。

  1. 第一行顯示 List.filter 的類型,它是一個接受兩個參數的函式。第一個參數是一個函式;第二個參數是一個列表。
  2. 第二行是 List.filter( <= ) 10 的部分應用,這是一個函式,如果應用於大於或等於 10 的數字,則返回 true

最後,在第三行,提供了 List.filter 預期的所有參數。返回的列表包含滿足 ( <= ) 10 函式的值。

定義二元運算符

也可以定義二元運算符。以下是一個範例

# let cat s1 s2 = s1 ^ " " ^ s2;;
val cat : string -> string -> string = <fun>

# let ( ^? ) = cat;;
val ( ^? ) : string -> string -> string = <fun>
# "hi" ^? "friend";;
- : string = "hi friend"

建議像範例中那樣分兩步定義運算符。第一個定義包含函式的邏輯。第二個定義僅是第一個定義的別名。這為運算符提供了一個預設的發音,並清楚地表明運算符是語法糖:一種通過使文字更緊湊來簡化閱讀的方法。

一元運算符

一元運算符也稱為前綴運算符。在某些情況下,將函式的名稱縮短為符號是有意義的。這通常用作縮短對其參數執行某種轉換的函式名稱的一種方式。

# let ( !! ) = Lazy.force;;
val ( !! ) : 'a lazy_t -> 'a = <fun>

# let rec transpose = function
   | [] | [] :: _ -> []
   | rows -> List.(map hd rows :: transpose (map tl rows));;
val transpose : 'a list list -> 'a list list = <fun>

# let ( ~: ) = transpose;;
val ( ~: ) : 'a list list -> 'a list list

這允許使用者編寫更緊湊的程式碼。但是,請注意不要編寫過於簡潔的程式碼,因為這樣更難維護。理解運算符對於大多數讀者來說必須是顯而易見的,否則它們會弊大於利。

允許的運算符

OCaml 有一個微妙的語法;並非所有內容都允許作為運算符符號。運算符符號是一個具有特殊語法的識別符號,因此它必須具有以下結構

前綴運算符

  1. 第一個字元,可以是
    • ? ~
    • !
  2. 後續字元,如果第一個字元是 ?~,則至少一個,否則為可選
    • $ & * + - / = > @ ^ |
    • % <

二元運算符

  1. 第一個字元,可以是
    • $ & * + - / = > @ ^ |
    • % <
    • #
  2. 後續字元,如果第一個字元是 #,則至少一個,否則為可選
    • $ & * + - / = > @ ^ |
    • % <
    • ! . : ? ~

這在 OCaml 手冊的 前綴和中綴符號 部分中定義。

提示

  • 不要定義廣泛範圍的運算符。將它們的範圍限制在模組或函式內。
  • 不要使用太多運算符。
  • 在定義自訂二元運算符之前,請檢查該符號是否已被使用。這可以通過兩種方式完成
    • 在 UTop 中將候選符號用括號括起來,看看它是否回應類型或 Unbound value 錯誤
    • 使用 Sherlocode 來檢查它是否已在某些 OCaml 專案中使用
  • 避免遮蔽現有的運算符。

運算符的結合性和優先順序

讓我們用一個範例來說明運算符的結合性。以下函式串聯其字串參數,並以 | 字元包圍,並以 _ 字元分隔。

# let par s1 s2 = "|" ^ s1 ^ "_" ^ s2 ^ "|";;
val par : string -> string -> string = <fun>

# par "hello" "world";;
- : string = "|hello_world|"

讓我們將 par 變成兩個不同的運算符

# let ( @^ ) = par;;
val ( @^ ) : string -> string -> string = <fun>

# let ( &^ ) = par;;
val ( &^ ) : string -> string -> string = <fun>

乍看之下,運算符 @^&^ 是相同的。但是,OCaml 解析器允許使用多個運算符形成不帶括號的表達式。

# "foo" @^ "bar" @^ "bus";;
- : string = "|foo_|bar_bus||"

# "foo" &^ "bar" &^ "bus";;
- : string = "||foo_bar|_bus|"

儘管兩個表達式都調用相同的函式(par),但它們的評估順序不同。

  1. 表達式 "foo" @^ "bar" @^ "bus" 的評估方式就好像它是 "foo" @^ ("bar" @^ "bus")。括號加在右邊,因此 @^ 向右結合
  2. 表達式 "foo" &^ "bar" &^ "bus" 的評估方式就好像它是 "(foo" &^ "bar") &^ "bus"。括號加在左邊,因此 &^ 向左結合

運算子優先順序規則決定了在沒有括號的情況下,如何解讀結合不同運算子的表達式。例如,使用相同的運算子,以下展示了如何評估同時使用兩者的表達式:

# "foo" &^ "bar" @^ "bus";;
- : string = "|foo_|bar_bus||"

# "foo" @^ "bar" &^ "bus";;
- : string = "||foo_bar|_bus|"

在這兩種情況下,數值都會先傳遞給 @^,然後才傳遞給 &^。因此,我們說 @^優先順序高於 &^。運算子優先順序的規則詳述於 OCaml 手冊的 表達式章節。它們可以總結如下。運算子的第一個字元決定其結合性和優先順序。以下是各組運算子的第一個字元。每組具有相同的結合性和優先順序。各組按照優先順序遞增的順序排序。

  1. 左結合性:$ & < = > |
  2. 右結合性:@ ^
  3. 左結合性:+ -
  4. 左結合性:% * /
  5. 左結合性:#

完整的優先順序列表更長,因為它包含了不允許用作自訂運算子的預定義運算子。OCaml 手冊有一個表格,總結了運算子結合性的規則。

繫結運算子

OCaml 允許建立自訂的 let 運算子。這通常用於與 Monad 相關的函式,例如 Option.bindList.concat_map。關於此主題的更多資訊,請參閱Monads

doi_parts 函式嘗試從預期包含數位物件識別碼 (DOI)的字串中提取註冊者和識別碼部分。

# let ( let* ) = Option.bind;;
val ( let* ) : 'a option -> ('a -> 'b option) -> 'b option = <fun>

# let doi_parts s =
  let open String in
  let* slash = rindex_opt s '/' in
  let* dot = rindex_from_opt s slash '.' in
  let prefix = sub s 0 dot in
  let len = slash - dot - 1 in
  if len >= 4 && ends_with ~suffix:"10" prefix then
    let registrant = sub s (dot + 1) len in
    let identifier = sub s (slash + 1) (length s - slash - 1) in
    Some (registrant, identifier)
  else
    None;;

# doi_parts "doi:10.1000/182";;
- : (string * string) option = Some ("1000", "182")

# doi_parts "https://doi.org/10.1000/182";;
- : (string * string) option = Some ("1000", "182")

此函式使用 Option.bind 作為對 rindex_optrindex_from_opt 呼叫的自訂繫結器。這允許僅考慮兩個搜尋都成功並返回找到字元位置的情況。如果其中任何一個失敗,doi_parts 會隱式返回 None

let open String in 建構允許在 doi_parts 的定義範圍內,呼叫模組 String 中的函式 rindex_optrindex_from_optlengthends_withsub,而無需在每個函式前面加上 String. 作為前綴。

如果找到了相關的分隔字元,則會應用函式的其餘部分。它會執行額外的檢查,並在可能的情況下從字串 s 中提取註冊者和識別碼。

仍然需要協助嗎?

協助改善我們的文件

所有 OCaml 文件都是開放原始碼。發現任何錯誤或不清楚的地方嗎?提交一個 Pull Request。

OCaml

創新。社群。安全。