運算符
目標
本教學的學習目標是
- 將運算符作為函式使用,反之亦然,將函式作為運算符使用
- 為自訂運算符指定正確的結合性和優先順序
- 使用並定義自訂的
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]
前兩行和最後一行僅供參考。
- 第一行顯示
List.filter
的類型,它是一個接受兩個參數的函式。第一個參數是一個函式;第二個參數是一個列表。 - 第二行是
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 有一個微妙的語法;並非所有內容都允許作為運算符符號。運算符符號是一個具有特殊語法的識別符號,因此它必須具有以下結構
前綴運算符
- 第一個字元,可以是
?
~
!
- 後續字元,如果第一個字元是
?
或~
,則至少一個,否則為可選$
&
*
+
-
/
=
>
@
^
|
%
<
二元運算符
- 第一個字元,可以是
$
&
*
+
-
/
=
>
@
^
|
%
<
#
- 後續字元,如果第一個字元是
#
,則至少一個,否則為可選$
&
*
+
-
/
=
>
@
^
|
%
<
!
.
:
?
~
這在 OCaml 手冊的 前綴和中綴符號 部分中定義。
提示
- 不要定義廣泛範圍的運算符。將它們的範圍限制在模組或函式內。
- 不要使用太多運算符。
- 在定義自訂二元運算符之前,請檢查該符號是否已被使用。這可以通過兩種方式完成
- 在 UTop 中將候選符號用括號括起來,看看它是否回應類型或
Unbound value
錯誤 - 使用 Sherlocode 來檢查它是否已在某些 OCaml 專案中使用
- 在 UTop 中將候選符號用括號括起來,看看它是否回應類型或
- 避免遮蔽現有的運算符。
運算符的結合性和優先順序
讓我們用一個範例來說明運算符的結合性。以下函式串聯其字串參數,並以 |
字元包圍,並以 _
字元分隔。
# 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
),但它們的評估順序不同。
- 表達式
"foo" @^ "bar" @^ "bus"
的評估方式就好像它是"foo" @^ ("bar" @^ "bus")
。括號加在右邊,因此@^
向右結合 - 表達式
"foo" &^ "bar" &^ "bus"
的評估方式就好像它是"(foo" &^ "bar") &^ "bus"
。括號加在左邊,因此&^
向左結合
運算子優先順序規則決定了在沒有括號的情況下,如何解讀結合不同運算子的表達式。例如,使用相同的運算子,以下展示了如何評估同時使用兩者的表達式:
# "foo" &^ "bar" @^ "bus";;
- : string = "|foo_|bar_bus||"
# "foo" @^ "bar" &^ "bus";;
- : string = "||foo_bar|_bus|"
在這兩種情況下,數值都會先傳遞給 @^
,然後才傳遞給 &^
。因此,我們說 @^
的優先順序高於 &^
。運算子優先順序的規則詳述於 OCaml 手冊的 表達式章節。它們可以總結如下。運算子的第一個字元決定其結合性和優先順序。以下是各組運算子的第一個字元。每組具有相同的結合性和優先順序。各組按照優先順序遞增的順序排序。
- 左結合性:
$
&
<
=
>
|
- 右結合性:
@
^
- 左結合性:
+
-
- 左結合性:
%
*
/
- 左結合性:
#
完整的優先順序列表更長,因為它包含了不允許用作自訂運算子的預定義運算子。OCaml 手冊有一個表格,總結了運算子結合性的規則。
繫結運算子
OCaml 允許建立自訂的 let
運算子。這通常用於與 Monad 相關的函式,例如 Option.bind
或 List.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_opt
和 rindex_from_opt
呼叫的自訂繫結器。這允許僅考慮兩個搜尋都成功並返回找到字元位置的情況。如果其中任何一個失敗,doi_parts
會隱式返回 None
。
let open String in
建構允許在 doi_parts
的定義範圍內,呼叫模組 String
中的函式 rindex_opt
、rindex_from_opt
、length
、ends_with
和 sub
,而無需在每個函式前面加上 String.
作為前綴。
如果找到了相關的分隔字元,則會應用函式的其餘部分。它會執行額外的檢查,並在可能的情況下從字串 s
中提取註冊者和識別碼。