运算符

目标

本教程的学习目标是

  • 将运算符用作函数,反之,将函数用作运算符
  • 为自定义运算符分配正确的结合性和优先级
  • 使用和定义自定义 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 的部分应用,这是一个返回 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 具有细微的语法;并非所有内容都允许作为运算符符号。运算符符号是一个具有特殊语法的标识符,因此必须具有以下结构

前缀运算符

  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 运算符。这通常用于与单子相关的函数,例如 Option.bindList.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_optrindex_from_opt 调用的自定义绑定器。这允许仅考虑两个搜索都成功并返回找到的字符的位置的情况。如果其中任何一个失败,doi_parts 将隐式返回 None

let open String in 结构允许在 doi_parts 定义范围内调用模块 String 中的函数 rindex_optrindex_from_optlengthends_withsub,而无需在每个函数名前加上 String.

函数的其余部分如果找到了相关的分隔符,则会应用。如果可能,它会执行额外的检查并从字符串 s 中提取注册人和标识符。

仍然需要帮助?

帮助改进我们的文档

所有 OCaml 文档都是开源的。发现错误或不清楚的地方?提交拉取请求。

OCaml

创新。社区。安全。