运算符
目标
本教程的学习目标是
- 将运算符用作函数,反之,将函数用作运算符
- 为自定义运算符分配正确的结合性和优先级
- 使用和定义自定义
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
的部分应用,这是一个返回true
的函数,如果应用于大于或等于 10 的数字。
最后,在第三行中,提供了 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
运算符。这通常用于与单子相关的函数,例如 Option.bind
或 List.concat_map
。有关此主题的更多信息,请参阅 单子。
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
中提取注册人和标识符。