第 11 章 OCaml 语言

7 表达式

expr::= value-path
 constant
 (expr)
 beginexprend
 (expr:typexpr)
 expr { ,expr }+
 constrexpr
 `tag-nameexpr
 expr::expr
 [expr { ;expr } [;] ]
 [|expr { ;expr } [;] |]
 {field [:typexpr] [=expr]{ ;field [:typexpr] [=expr] } [;] }
 {exprwithfield [:typexpr] [=expr]{ ;field [:typexpr] [=expr] } [;] }
 expr { argument }+
 prefix-symbolexpr
 -expr
 -.expr
 exprinfix-opexpr
 expr.field
 expr.field<-expr
 expr.(expr)
 expr.(expr)<-expr
 expr.[expr]
 expr.[expr]<-expr
 ifexprthenexpr [ elseexpr ]
 whileexprdoexprdone
 forvalue-name=expr ( to ∣ downto ) exprdoexprdone
 expr;expr
 matchexprwithpattern-matching
 functionpattern-matching
 fun { parameter }+ [ :typexpr ] ->expr
 tryexprwithpattern-matching
 let [rec] let-binding { andlet-binding } inexpr
 letexceptionconstr-declinexpr
 letmodulemodule-name { (module-name:module-type) } [ :module-type ]  =module-exprinexpr
 (expr>typexpr)
 (expr:typexpr>typexpr)
 assertexpr
 lazyexpr
 local-open
 object-expr
 
argument::= expr
 ~label-name
 ~label-name:expr
 ?label-name
 ?label-name:expr
 
pattern-matching::=[ | ] pattern [whenexpr] ->expr { |pattern [whenexpr] ->expr }
 
let-binding::= pattern=expr
 value-name { parameter } [:typexpr] [>typexpr] =expr
 value-name:poly-typexpr=expr
 
parameter::= pattern
 ~label-name
 ~(label-name [:typexpr] )
 ~label-name:pattern
 ?label-name
 ?(标签名称 [:类型表达式] [=表达式] )
 ?标签名称:模式
 ?标签名称:(模式 [:类型表达式] [=表达式] )
 
local-open::= 
 letopen模块路径in表达式
 模块路径.(表达式)
 模块路径.[表达式]
 模块路径.[|表达式|]
 模块路径.{表达式}
 模块路径.{<表达式>}
 
object-expr::= 
 new类路径
 object类体end
 表达式#方法名称
 实例变量名称
 实例变量名称<-表达式
 {< [ 实例变量名称 [=表达式] { ;实例变量名称 [=表达式] } [;] ] >}

另请参阅以下语言扩展:一等模块在 open 语句中覆盖Bigarray 访问语法属性扩展节点扩展索引操作符

7.1 优先级和结合性

下表显示了运算符和非闭合构造的优先级和结合性。优先级较高的构造排在最前面。对于中缀和前缀符号,我们写“*…” 表示“任何以 * 开头的符号”。

构造或运算符结合性
前缀符号
. .( .[ .{ (见第 12.11 节)
#左结合
函数应用、构造函数应用、标签应用、assertlazy左结合
- -. (前缀)
** lsl lsr asr右结合
* / % mod land lor lxor左结合
+ -左结合
::右结合
@ ^右结合
= < > | & $ !=左结合
& &&右结合
or ||右结合
,
<- :=右结合
if
;右结合
let match fun function try

测试或刷新一下你的理解很容易

# 3 + 3 mod 2, 3 + (3 mod 2), (3 + 3) mod 2;;
- : int * int * int = (4, 4, 0)

7.2 基本表达式

常量

由常量组成的表达式计算结果为该常量。例如,3.14[||]

值路径

由访问路径组成的表达式计算结果为当前评估环境中绑定到该路径的值。该路径可以是值名称,也可以是访问模块值组件的访问路径。

# Float.ArrayLabels.to_list;;
- : Float.ArrayLabels.t -> float list = <fun>

带括号的表达式

表达式 ( 表达式 )begin 表达式 end 的值与 表达式 相同。这两个构造在语义上等效,但建议在控制结构中使用 beginend

        if … then begin … ; … end else begin … ; … end

而在其他分组情况中使用 ()

# let x = 1 + 2 * 3 let y = (1 + 2) * 3;;
val x : int = 7 val y : int = 9
# let f a b = if a = b then print_endline "Equal" else begin print_string "Not Equal: "; print_int a; print_string " and "; print_int b; print_newline () end;;
val f : int -> int -> unit = <fun>

带括号的表达式可以包含类型约束,例如 ( 表达式 : 类型表达式 )。此约束强制 表达式 的类型与 类型表达式 兼容。

带括号的表达式还可以包含强制转换 ( 表达式 [: 类型表达式] :> 类型表达式) (见下文第 11.7.7 小节)。

函数应用

函数应用用(可能带标签的)表达式的并置表示。表达式 表达式 参数1参数n 计算 表达式 及其出现在 参数1参数n 中的表达式。表达式 表达式 必须计算为一个函数值 f,然后将其应用于 参数1、…、参数n 的值。

表达式 表达式参数1、…、参数n 计算的顺序未指定。

# List.fold_left ( + ) 0 [1; 2; 3; 4; 5];;
- : int = 15

参数和参数根据各自的标签匹配。参数顺序无关紧要,除非参数具有相同的标签,或者没有标签。

# ListLabels.fold_left ~f:( @ ) ~init:[] [[1; 2; 3]; [4; 5; 6]; [7; 8; 9]];;
- : int list = [1; 2; 3; 4; 5; 6; 7; 8; 9]

如果在 表达式 的类型中,某个参数被指定为可选参数(标签以 ? 为前缀),则相应的参数将被自动包装在构造函数 Some 中,除非参数本身也以 ? 为前缀,在这种情况下,参数将按原样传递。

# let fullname ?title first second = match title with | Some t -> t ^ " " ^ first ^ " " ^ second | None -> first ^ " " ^ second let name = fullname ~title:"Mrs" "Jane" "Fisher" let address ?title first second town = fullname ?title first second ^ "\n" ^ town;;
val fullname : ?title:string -> string -> string -> string = <fun> val name : string = "Mrs Jane Fisher" val address : ?title:string -> string -> string -> string -> string = <fun>

如果传递了一个没有标签的参数,并且它对应的参数之前有一个或多个可选参数,那么这些参数将被默认将为它们传递值 None。所有其他缺少参数(没有对应参数),包括可选参数和非可选参数,都将保留,并且函数的结果将仍然是这些缺少参数对 f 体的函数。

# let fullname ?title first second = match title with | Some t -> t ^ " " ^ first ^ " " ^ second | None -> first ^ " " ^ second let name = fullname "Jane" "Fisher";;
val fullname : ?title:string -> string -> string -> string = <fun> val name : string = "Jane Fisher"

在所有情况下,除了顺序和标签完全匹配,并且没有可选参数之外,函数类型都应该在应用点已知。这可以通过添加类型约束来确保。在 -principal 模式下可以检查推导的原则性。

作为一个特殊情况,OCaml 支持labels-omitted 全应用:如果函数具有已知的元数,所有参数都是未标记的,并且它们的数量与非可选参数的数量匹配,则会忽略标签,并且非可选参数会按照其定义顺序匹配。可选参数将被赋予默认值。不建议省略标签,并且会导致警告,请参见 13.5.1

函数定义

提供了两种语法形式来定义函数。第一种形式由关键字 function 引入:

function模式1->表达式1
|… 
|模式n->表达式n

此表达式计算为具有一个参数的函数值。当此函数应用于值 v 时,此值将与每个模式 模式1模式n 匹配。如果其中一项匹配成功,也就是说,如果值 v 与模式 模式i 匹配,则与所选模式关联的表达式 表达式i 将被计算,并且它的值将成为函数应用的值。在匹配期间执行的绑定所丰富环境中,将进行 表达式i 的计算。

如果多个模式与参数 v 匹配,则选择函数定义中第一个出现的模式。如果没有任何模式与参数匹配,则会引发异常 Match_failure

# (function (0, 0) -> "both zero" | (0, _) -> "first only zero" | (_, 0) -> "second only zero" | (_, _) -> "neither zero") (7, 0);;
- : string = "second only zero"

函数定义的另一种形式由关键字 fun 引入:

fun 参数1参数n -> 表达式

此表达式等效于

fun 参数1 ->fun 参数n -> 表达式
# let f = (fun a -> fun b -> fun c -> a + b + c) let g = (fun a b c -> a + b + c);;
val f : int -> int -> int -> int = <fun> val g : int -> int -> int -> int = <fun>

可以在 -> 之前添加一个可选的类型约束 类型表达式,以强制结果的类型与约束 类型表达式 兼容

等效于

fun 参数1 ->fun 参数n -> (表达式 : 类型表达式 )

注意最后参数的类型约束

和结果的类型约束

# let eq = fun (a : int) (b : int) -> a = b let eq2 = fun a b : bool -> a = b let eq3 = fun (a : int) (b : int) : bool -> a = b;;
val eq : int -> int -> bool = <fun> val eq2 : 'a -> 'a -> bool = <fun> val eq3 : int -> int -> bool = <fun>

参数模式 ~标签名~(标签名 [: 类型]) 分别是 ~标签名:标签名~标签名:(标签名 [: 类型]) 的简写,其可选对应项也是如此。

# let bool_map ~cmp:(cmp : int -> int -> bool) l = List.map cmp l let bool_map' ~(cmp : int -> int -> bool) l = List.map cmp l;;
val bool_map : cmp:(int -> int -> bool) -> int list -> (int -> bool) list = <fun> val bool_map' : cmp:(int -> int -> bool) -> int list -> (int -> bool) list = <fun>

形式为 fun ? 标签名 :( 模式 = 表达式0 ) -> 表达式 的函数等效于

fun ? 标签名 : 标识符 -> let 模式 = match 标识符 with Some 标识符 -> 标识符 | None -> 表达式0 in 表达式

其中 标识符 是一个新的变量,除了在计算 表达式0 时没有指定它。

# let open_file_for_input ?binary filename = match binary with | Some true -> open_in_bin filename | Some false | None -> open_in filename let open_file_for_input' ?(binary=false) filename = if binary then open_in_bin filename else open_in filename;;
val open_file_for_input : ?binary:bool -> string -> in_channel = <fun> val open_file_for_input' : ?binary:bool -> string -> in_channel = <fun>

经过这两个转换之后,表达式将具有以下形式

fun [标签1] 模式1 ->fun [标签n] 模式n -> 表达式

如果我们忽略标签(在函数应用时才有意义),则它等效于

function 模式1 ->function 模式n -> 表达式

也就是说,上面的 fun 表达式计算为具有 n 个参数的柯里化函数:在将此函数应用于值 v1vn n 次之后,这些值将与模式 模式1模式n 平行匹配。如果匹配成功,则函数将在匹配期间执行的绑定所丰富环境中返回 表达式 的值。如果匹配失败,则会引发异常 Match_failure

模式匹配中的保护

模式匹配的 case(在 functionmatchtry 结构中)可以包含 guard 表达式,它们是任意布尔表达式,必须计算为 true 才能选择匹配的 case。Guard 位于 -> 标记之前,由 when 关键字引入。

functionpattern1   [when   cond1]->expr1
|… 
|patternn    [when   condn]->exprn

匹配过程如前所述,除了如果值匹配某个模式 patterni 并且该模式具有 guard condi,那么表达式 condi 将被评估(在由匹配过程中执行的绑定扩展的环境中)。如果 condi 评估为 true,那么 expri 将被评估,其值将作为匹配结果返回,如常。但如果 condi 评估为 false,匹配将继续对 patterni 之后的模式进行。

# let rec repeat f = function | 0 -> () | n when n > 0 -> f (); repeat f (n - 1) | _ -> raise (Invalid_argument "repeat");;
val repeat : (unit -> 'a) -> int -> unit = <fun>

局部定义

letlet rec 结构在本地绑定值名称。结构

let pattern1 = expr1 andand patternn = exprn in expr

以某种未指定顺序评估 expr1exprn,并将它们的值与模式 pattern1patternn 进行匹配。如果匹配成功,expr 在由匹配过程中执行的绑定扩展的环境中进行评估,expr 的值将作为整个 let 表达式的值返回。如果其中一个匹配失败,将引发异常 Match_failure

# let v = let x = 1 in [x; x; x] let v' = let a, b = (1, 2) in a + b let v'' = let a = 1 and b = 2 in a + b;;
val v : int list = [1; 1; 1] val v' : int = 3 val v'' : int = 3

提供了一种替代语法来将变量绑定到函数值:而不是编写

let ident = fun parameter1parameterm -> expr

let 表达式中,可以改写为

# let f = fun x -> fun y -> fun z -> x + y + z let f' = fun x y z -> x + y + z let f'' x y z = x + y + z;;
val f : int -> int -> int -> int = <fun> val f' : int -> int -> int -> int = <fun> val f'' : int -> int -> int -> int = <fun>

递归定义名称由 let rec 引入

let rec pattern1 = expr1 andand patternn = exprn in expr

与上面描述的 let 结构唯一的区别是,模式匹配执行的名称到值的绑定在评估表达式 expr1exprn 时被认为已经执行。也就是说,表达式 expr1exprn 可以引用由模式 pattern1,…,patternn 中的其中一个绑定的标识符,并期望它们与 exprlet rec 结构的主体)中的值相同。

# let rec even = function 0 -> true | n -> odd (n - 1) and odd = function 0 -> false | n -> even (n - 1) in even 1000;;
- : bool = true

如果表达式 expr1exprn 是函数定义(fun … 或 function …),并且模式 pattern1patternn 只是值名称,如

let rec name1 = funandand namen = funin expr

这将 name1namen 定义为对 expr 局部的相互递归函数。

其他形式的 let rec 定义的行为取决于实现。当前实现还支持一定范围的非函数值的递归定义,如第 ‍12.1 节所述。

局部异常

(在 OCaml 4.04 中引入)

可以在表达式中定义局部异常:let exception constr-decl in expr

# let map_empty_on_negative f l = let exception Negative in let aux x = if x < 0 then raise Negative else f x in try List.map aux l with Negative -> [];;
val map_empty_on_negative : (int -> 'a) -> int list -> 'a list = <fun>

异常构造函数的语法范围是内部表达式,但没有任何东西可以阻止使用此构造函数创建的异常值逃逸此范围。上面定义的两个执行将导致两个不兼容的异常构造函数(与任何异常定义一样)。例如

# let gen () = let exception A in A let () = assert(gen () = gen ());;
Exception: Assert_failure ("expr.etex", 3, 9).

显式多态类型注解

(在 OCaml 3.12 中引入)

let 定义中的多态类型注解的行为与多态方法类似

let pattern1 : typ1typn . typexpr = expr

这些注解明确要求定义的值是多态的,并且允许在递归出现时使用这种多态性(在使用 let rec 时)。但请注意,这是一个普通的多态类型,可以与自身的任何实例进行统一。

7.3 控制结构

序列

表达式 expr1 ; expr2 首先计算 expr1 的值,然后计算 expr2 的值,并返回 expr2 的值。

# let print_pair (a, b) = print_string "("; print_string (string_of_int a); print_string ","; print_string (string_of_int b); print_endline ")";;
val print_pair : int * int -> unit = <fun>

条件表达式

表达式 if expr1 then expr2 else expr3 的值,如果 expr1 的值为布尔值 true,则为 expr2 的值;如果 expr1 的值为布尔值 false,则为 expr3 的值。

# let rec factorial x = if x <= 1 then 1 else x * factorial (x - 1);;
val factorial : int -> int = <fun>

else expr3 部分可以省略,在这种情况下,它默认为 else ()

# let debug = ref false let log msg = if !debug then prerr_endline msg;;
val debug : bool ref = {contents = false} val log : string -> unit = <fun>

模式匹配表达式

表达式

matchexpr
with模式1->表达式1
|… 
|模式n->表达式n

expr 的值与模式 pattern1patternn 匹配。如果与 patterni 的匹配成功,则计算关联的表达式 expri,其值将成为整个 match 表达式的值。 expri 的计算在通过匹配进行绑定的环境中进行。如果多个模式与 expr 的值匹配,则选择在 match 表达式中首先出现的模式。

# let rec sum l = match l with | [] -> 0 | h :: t -> h + sum t;;
val sum : int list -> int = <fun>

如果没有任何模式与 expr 的值匹配,则会引发 Match_failure 异常。

# let unoption o = match o with | Some x -> x ;;
Warning 8 [partial-match]: 此模式匹配不完整。这是一个未匹配情况的示例:None val unoption : 'a option -> 'a = <fun>
# let l = List.map unoption [Some 1; Some 10; None; Some 2];;
Exception: Match_failure ("expr.etex", 2, 2).

布尔运算符

表达式 expr1 && expr2 的值为 true,当且仅当 expr1expr2 的值都为 true;否则,其值为 false。第一个分量 expr1 首先计算。如果第一个分量的值为 false,则不会计算第二个分量 expr2。因此,表达式 expr1 && expr2 的行为与以下表达式完全相同:

if expr1 then expr2 else false.

表达式 expr1 || expr2 的值为 true,如果 expr1expr2 中至少有一个表达式的值为 true;否则,其值为 false。第一个分量 expr1 首先计算。如果第一个分量的值为 true,则不会计算第二个分量 expr2。因此,表达式 expr1 || expr2 的行为与以下表达式完全相同:

if expr1 then true else expr2.

布尔运算符 &or 是 (分别) &&|| 的过时同义词。

# let xor a b = (a || b) && not (a && b);;
val xor : bool -> bool -> bool = <fun>

循环

表达式 while expr1 do expr2 doneexpr1 的值为 true 时,重复计算 expr2。循环条件 expr1 在每次迭代开始时计算并测试。整个 whiledone 表达式的值为单元值 ()

# let chars_of_string s = let i = ref 0 in let chars = ref [] in while !i < String.length s do chars := s.[!i] :: !chars; i := !i + 1 done; List.rev !chars;;
val chars_of_string : string -> char list = <fun>

作为一种特殊情况,while true do expr done 被赋予一个多态类型,使其可以用于任何表达式(例如作为任何模式匹配的分支)。

表达式 for name = expr1 to expr2 do expr3 done 首先计算表达式 expr1expr2 (边界)为整数值 np。然后,循环体 expr3 在环境中重复计算,其中 name 依次绑定到值 nn+1、…、p−1、p。如果 n > p,则循环体永远不会计算。

# let chars_of_string s = let l = ref [] in for p = 0 to String.length s - 1 do l := s.[p] :: !l done; List.rev !l;;
val chars_of_string : string -> char list = <fun>

表达式 for name = expr1 downto expr2 do expr3 done 的计算类似,只是 name 依次绑定到值 nn−1、…、p+1、p。如果 n < p,则循环体永远不会计算。

# let chars_of_string s = let l = ref [] in for p = String.length s - 1 downto 0 do l := s.[p] :: !l done; !l;;
val chars_of_string : string -> char list = <fun>

在这两种情况下,整个 for 表达式的值为单元值 ()

异常处理

表达式

try ‍expr
with模式1->表达式1
|… 
|模式n->表达式n

对表达式 expr 进行求值,如果求值过程中没有抛出异常,则返回其值。如果对 expr 的求值抛出异常,则将异常值与模式 pattern1patternn 进行匹配。如果与 patterni 的匹配成功,则对关联的表达式 expri 进行求值,其结果成为整个 try 表达式的值。对 expri 的求值是在通过匹配进行绑定后,在扩展的环境中进行的。如果多个模式与 expr 的值匹配,则选择在 try 表达式中首先出现的那个。如果没有任何模式与 expr 的值匹配,则会再次抛出异常值,从而透明地“通过” try 结构。

# let find_opt p l = try Some (List.find p l) with Not_found -> None;;
val find_opt : ('a -> bool) -> 'a list -> 'a option = <fun>

7.4 数据结构的操作

表达式 expr1 ,, exprn 求值为表达式 expr1exprn 的值的 n 元组。子表达式的求值顺序未指定。

# (1 + 2 * 3, (1 + 2) * 3, 1 + (2 * 3));;
- : int * int * int = (7, 9, 7)

变体

表达式 constr expr 求值为构造函数为 constr,参数为 expr 值的一元变体值。类似地,表达式 constr ( expr1 ,, exprn ) 求值为构造函数为 constr,参数为 expr1, …, exprn 的值的 n 元变体值。

表达式 constr (expr1, …, exprn) 求值为构造函数为 constr,参数为 expr1exprn 值的变体值。

# type t = Var of string | Not of t | And of t * t | Or of t * t let test = And (Var "x", Not (Or (Var "y", Var "z")));;
type t = Var of string | Not of t | And of t * t | Or of t * t val test : t = And (Var "x", Not (Or (Var "y", Var "z")))

对于列表,提供了一些语法糖。表达式 expr1 :: expr2 代表将构造函数 ( :: ) 应用于参数 ( expr1 , expr2 ),因此求值为一个列表,其头部是 expr1 的值,尾部是 expr2 的值。表达式 [ expr1 ;; exprn ] 等效于 expr1 :::: exprn :: [],因此求值为一个列表,其元素是 expr1exprn 的值。

# 0 :: [1; 2; 3] = 0 :: 1 :: 2 :: 3 :: [];;
- : bool = true

多态变体

表达式 `tag-name expr 求值为一个多态变体值,其标签为 tag-name,参数为 expr 的值。

# let with_counter x = `V (x, ref 0);;
val with_counter : 'a -> [> `V of 'a * int ref ] = <fun>

记录

表达式 { field1 [= expr1] ;; fieldn [= exprn ]} 求值为记录值 { field1 = v1; …; fieldn = vn },其中 viexpri 的值,对于 i = 1,… , n。单个标识符 fieldk 代表 fieldk = fieldk,限定标识符 module-path . fieldk 代表 module-path . fieldk = fieldk。字段 field1fieldn 必须都属于同一个记录类型;该记录类型的每个字段都必须在记录表达式中出现一次,但可以以任何顺序出现。求值 expr1exprn 的顺序未指定。可以在每个字段之后添加可选的类型约束 { field1 : typexpr1 = expr1 ;; fieldn : typexprn = exprn },以强制 fieldk 的类型与 typexprk 兼容。

# type t = {house_no : int; street : string; town : string; postcode : string} let address x = Printf.sprintf "The occupier\n%i %s\n%s\n%s" x.house_no x.street x.town x.postcode;;
type t = { house_no : int; street : string; town : string; postcode : string; } val address : t -> string = <fun>

表达式 { expr with field1 [= expr1] ;; fieldn [= exprn] } 用于构建一个新的记录,其中字段 field1fieldn 等于 expr1exprn,而所有其他字段的值与记录 expr 中的值相同。换句话说,它返回记录 expr 的浅拷贝,除了字段 field1fieldn,这些字段被初始化为 expr1exprn。如前所述,单个标识符 fieldk 代表 fieldk = fieldk,限定标识符 module-path . fieldk 代表 module-path . fieldk = fieldk,并且可以为每个正在更新的字段添加一个可选的类型约束,如 { expr with field1 : typexpr1 = expr1 ;; fieldn : typexprn = exprn }

# type t = {house_no : int; street : string; town : string; postcode : string} let uppercase_town address = {address with town = String.uppercase_ascii address.town};;
type t = { house_no : int; street : string; town : string; postcode : string; } val uppercase_town : t -> t = <fun>

表达式 expr1 . fieldexpr1 评估为一个记录值,并返回该记录值中与 field 关联的值。

表达式 expr1 . field <- expr2expr1 评估为一个记录值,然后通过用 expr2 的值替换该记录中与 field 关联的值,原地修改该记录。仅当 field 在记录类型定义中被声明为 mutable 时,此操作才允许。整个表达式 expr1 . field <- expr2 评估为单元值 ()

# type t = {mutable upper : int; mutable lower : int; mutable other : int} let stats = {upper = 0; lower = 0; other = 0} let collect = String.iter (function | 'A'..'Z' -> stats.upper <- stats.upper + 1 | 'a'..'z' -> stats.lower <- stats.lower + 1 | _ -> stats.other <- stats.other + 1);;
type t = { mutable upper : int; mutable lower : int; mutable other : int; } val stats : t = {upper = 0; lower = 0; other = 0} val collect : string -> unit = <fun>

数组

表达式 [| expr1 ;; exprn |] 评估为一个 n 元素数组,其元素分别用 expr1exprn 的值初始化。这些表达式评估的顺序是不确定的。

表达式 expr1 .( expr2 ) 返回 expr1 所表示的数组中元素编号为 expr2 的值。第一个元素的编号为 0;最后一个元素的编号为 n−1,其中 n 是数组的大小。如果访问超出范围,则会引发异常 Invalid_argument

表达式 expr1 .( expr2 ) <- expr3 会原地修改 expr1 所表示的数组,用 expr3 的值替换元素编号为 expr2 的元素。如果访问超出范围,则会引发异常 Invalid_argument。整个表达式的值为 ()

# let scale arr n = for x = 0 to Array.length arr - 1 do arr.(x) <- arr.(x) * n done let x = [|1; 10; 100|] let _ = scale x 2;;
val scale : int array -> int -> unit = <fun> val x : int array = [|2; 20; 200|]

字符串

表达式 expr1 .[ expr2 ] 返回 expr1 所表示的字符串中字符编号为 expr2 的值。第一个字符的编号为 0;最后一个字符的编号为 n−1,其中 n 是字符串的长度。如果访问超出范围,则会引发异常 Invalid_argument

# let iter f s = for x = 0 to String.length s - 1 do f s.[x] done;;
val iter : (char -> 'a) -> string -> unit = <fun>

表达式 expr1 .[ expr2 ] <- expr3 会原地修改 expr1 所表示的字符串,用 expr3 的值替换字符编号为 expr2 的字符。如果访问超出范围,则会引发异常 Invalid_argument。整个表达式的值为 ()注意: 此功能仅为了向后兼容 OCaml 的旧版本而提供,将在未来的版本中移除。新代码应使用字节序列和 Bytes.set 函数。

7.5 运算符

来自类 infix-symbol 的符号,以及关键字 *+--.=!=<>or||&&&:=modlandlorlxorlsllsrasr 可以出现在中缀位置(两个表达式之间)。来自类 prefix-symbol 的符号,以及关键字 --. 可以出现在前缀位置(表达式前面)。

# (( * ), ( := ), ( || ));;
- : (int -> int -> int) * ('a ref -> 'a -> unit) * (bool -> bool -> bool) = (<fun>, <fun>, <fun>)

中缀和前缀符号没有固定的含义:它们只是被解释为绑定到与符号相对应的名称的函数的应用。表达式 prefix-symbol expr 被解释为应用 ( prefix-symbol ) expr。类似地,表达式 expr1 infix-symbol expr2 被解释为应用 ( infix-symbol ) expr1 expr2

下表列出了初始环境中定义的符号及其初始含义。(有关更多详细信息,请参阅第 ‍28 章中对核心库模块 Stdlib 的描述)。它们的含义可以使用 let ( infix-op ) name1 name2 = … 在任何时候更改。

# let ( + ), ( - ), ( * ), ( / ) = Int64.(add, sub, mul, div);;
val ( + ) : int64 -> int64 -> int64 = <fun> val ( - ) : int64 -> int64 -> int64 = <fun> val ( * ) : int64 -> int64 -> int64 = <fun> val ( / ) : int64 -> int64 -> int64 = <fun>

注意:运算符 &&||~- 的处理方式有所不同,不建议更改它们的含义。

关键字 --. 可以同时用作中缀运算符和前缀运算符。当它们用作前缀运算符时,它们分别被解释为函数 (~-)(~-.)

运算符初始含义
+整数加法。
- (中缀)整数减法。
~- - (前缀)整数取反。
*整数乘法。
/整数除法。如果第二个参数为零,则引发 Division_by_zero
mod整数取模。如果第二个参数为零,则引发 Division_by_zero
land整数的按位逻辑“与”。
lor整数的按位逻辑“或”。
lxor整数的按位逻辑“异或”。
lsl整数的按位逻辑左移。
lsr整数的按位逻辑右移。
asr整数的按位算术右移。
+.浮点数加法。
-. (中缀)浮点数减法。
~-. -. (前缀)浮点数取反。
*.浮点数乘法。
/.浮点数除法。
**浮点数求幂。
@ 列表连接。
^ 字符串连接。
! 解除引用(返回引用的当前内容)。
:=引用赋值(使用第二个参数的值更新作为第一个参数给出的引用)。
= 结构相等性测试。
<> 结构不等性测试。
== 物理相等性测试。
!= 物理不等性测试。
< 测试“小于”。
<= 测试“小于或等于”。
> 测试“大于”。
>= 测试“大于或等于”。
&& &布尔合取。
|| or布尔析取。

7.6 对象

对象创建

class-path 评估为类体时,new class-path 评估为一个新对象,包含该类的实例变量和方法。

# class of_list (lst : int list) = object val mutable l = lst method next = match l with | [] -> raise (Failure "empty list"); | h::t -> l <- t; h end let a = new of_list [1; 1; 2; 3; 5; 8; 13] let b = new of_list;;
class of_list : int list -> object val mutable l : int list method next : int end val a : of_list = <obj> val b : int list -> of_list = <fun>

class-path 评估为类函数时,new class-path 评估为一个函数,该函数期望相同数量的参数并返回该类的新的对象。

立即对象创建

直接通过 object class-body end 结构创建对象在操作上等同于在本地定义一个 class class-name = object class-body end——参见第 11.9.2 节及以下部分,了解 class-body 的语法——并立即通过 new class-name 从中创建一个单个对象。

# let o = object val secret = 99 val password = "unlock" method get guess = if guess <> password then None else Some secret end;;
val o : < get : string -> int option > = <obj>

立即对象的类型与显式定义类的类型略有不同,主要体现在两个方面。首先,推断出的对象类型可能包含自由类型变量。其次,由于立即对象的类体永远不会扩展,因此它的自我类型可以与封闭的对象类型统一。

方法调用

表达式 expr # method-name 调用由 expr 表示的对象的方法 method-name

# class of_list (lst : int list) = object val mutable l = lst method next = match l with | [] -> raise (Failure "empty list"); | h::t -> l <- t; h end let a = new of_list [1; 1; 2; 3; 5; 8; 13] let third = ignore a#next; ignore a#next; a#next;;
class of_list : int list -> object val mutable l : int list method next : int end val a : of_list = <obj> val third : int = 2

如果 method-name 是一个多态方法,则它的类型应该在调用位置已知。例如,如果 expr 是一个新对象的名称(let ident = new class-path … )或如果存在类型约束。可以在 -principal 模式下检查推导的原理性。

访问和修改实例变量

类的实例变量只在定义在同一类或继承自定义实例变量的类的类的方法体中可见。表达式 inst-var-name 评估为给定实例变量的值。表达式 inst-var-name <- exprexpr 的值赋值给实例变量 inst-var-name,该实例变量必须是可变的。整个表达式 inst-var-name <- expr 评估为 ()

# class of_list (lst : int list) = object val mutable l = lst method next = match l with (* 访问实例变量 *) | [] -> raise (Failure "empty list"); | h::t -> l <- t; h (* 修改实例变量 *) end;;
class of_list : int list -> object val mutable l : int list method next : int end

对象复制

可以使用库函数 Oo.copy(参见模块 Oo)复制对象。在方法内部,表达式 {< [inst-var-name [= expr] { ; inst-var-name [= expr] }] >} 返回 self 的副本,其中给定的实例变量被关联表达式的值替换。单个实例变量名 id 代表 id = id。其他实例变量在返回对象中与 self 中具有相同的值。

# let o = object val secret = 99 val password = "unlock" method get guess = if guess <> password then None else Some secret method with_new_secret s = {< secret = s >} end;;
val o : < get : string -> int option; with_new_secret : int -> 'a > as 'a = <obj>

7.7 强制转换

类型包含对象或多态变体类型的表达式可以显式强制转换为(弱化为)超类型。表达式 (expr :> typexpr) 将表达式 expr 强制转换为类型 typexpr。表达式 (expr : typexpr1 :> typexpr2) 将表达式 expr 从类型 typexpr1 强制转换为类型 typexpr2

前一个运算符有时无法将表达式 expr 从类型 typ1 强制转换为类型 typ2,即使类型 typ1 是类型 typ2 的子类型:在当前实现中,它只扩展包含对象和/或多态变体的类型缩写两个级别,仅保留类类型(对于对象)中显式递归。作为上述算法的例外,如果 expr 的推断类型和 typ 都是地面类型( 不包含类型变量),则前一个运算符的行为与后一个运算符相同,将 expr 的推断类型作为 typ1。如果前一个运算符失败,则应使用后一个运算符。

只有当 expr 的类型是 typ1 的实例(如类型注释)时,才能将表达式 expr 从类型 typ1 强制转换为类型 typ2,并且 typ1typ2 的子类型。强制转换表达式的类型是 typ2 的实例。如果类型包含变量,则它们可能会被子类型算法实例化,但这只有在确定 typ1 是否是 typ2 的潜在子类型之后才会进行。这意味着在后一个统一步骤期间,类型可能失败,即使 typ1 的某些实例是 typ2 的某些实例的子类型。在以下段落中,我们将描述所使用的子类型关系。

对象类型

固定对象类型承认任何包含其所有方法的对象类型作为子类型。方法的类型应是超类型中方法的子类型。也就是说,

< met1 : typ1 ;; metn : typn >

< met1 : typ1 ;; metn : typn ; metn+1 : typn+1 ;; metn+m : typn+m  ‍[; ..] >

的超类型,如果每个 typi 都是对应 typi 的超类型,则该超类型可能包含省略号 ..

单态方法类型可以是多态方法类型的超类型。也就是说,如果 typtyp′ 的实例,则 'a1'an . typ′ 是 typ 的子类型。

在类定义内部,新定义的类型不可用于子类型化,因为类型缩写尚未完全定义。有一个例外是将 self 强制转换为其类的(精确)类型:如果 self 的类型没有出现在类类型中,则允许这样做。 , 如果没有二元方法。

多态变体类型

如果 typ 的上限( 可能出现在 typ 的实例中的构造函数的最大集合)包含在 typ′ 的下限中,则多态变体类型 typ 是另一个多态变体类型 typ′ 的子类型,并且 typ 的构造函数的参数类型是 typ′ 中的子类型。也就是说,

[[<] `C1 of typ1 || `Cn of typn ]

可能是可缩减类型,是

[[>] `C1 of typ1 || `Cn of typn | `Cn+1 of typn+1 || `Cn+m of typn+m ]

如果每个 typi 都是 typi 的子类型,那么它可能是一个可扩展类型。

方差

其他类型不会引入新的子类型,但它们可能会传播其参数的子类型关系。例如,typ1 * typ2typ1 * typ2 的子类型,当 typ1typ2 分别是 typ1typ2 的子类型时。对于函数类型,关系更加微妙:typ1 -> typ2typ1 ‍-> typ2 的子类型,如果 typ1typ1 的超类型,而 typ2typ2 的子类型。因此,函数类型在其第二个参数(如元组)中是协变的,但在其第一个参数中是逆变的。可变类型,例如 arrayref 既不是协变也不是逆变,它们是非变的,也就是说它们不会传播子类型关系。

对于用户定义的类型,方差会自动推断:如果参数只有协变出现,则它是协变的;如果参数只有逆变出现,则它是逆变的;如果参数没有出现,则它是无方差的;否则它是逆变的。无方差参数可以通过子类型自由更改,它不必是子类型或超类型。对于抽象和私有类型,必须显式指定方差(参见第 ‍11.8.1 节),否则默认值为非变的。这对于类型定义中的约束参数也是如此。

7.8 其他

断言检查

OCaml 支持 assert 结构来检查调试断言。表达式 assert expr 会评估表达式 expr,如果 expr 评估为 true,则返回 ()。如果它评估为 false,则会抛出异常 Assert_failure,并以源文件名和 expr 的位置作为参数。可以使用 -noassert 编译器选项关闭断言检查。在这种情况下,expr 根本不会被评估。

# let f a b c = assert (a <= b && b <= c); (b -. a) /. (c -. b);;
val f : float -> float -> float -> float = <fun>

作为一种特殊情况,assert false 会被简化为 raise (Assert_failure ...),这会给它一个多态类型。这意味着它可以用于任何表达式的替代(例如作为任何模式匹配的分支)。这也意味着 assert false “断言” 不能通过 -noassert 选项关闭。

# let min_known_nonempty = function | [] -> assert false | l -> List.hd (List.sort compare l);;
val min_known_nonempty : 'a list -> 'a = <fun>

惰性表达式

表达式 lazy expr 返回一个类型为 Lazy.t 的值 v,它封装了 expr 的计算。参数 expr 在程序的此时不会被评估。相反,它的评估将在第一次将函数 Lazy.force 应用于值 v 时执行,并返回 expr 的实际值。后续对 vLazy.force 应用不会再次评估 expr。通过模式匹配可以隐式应用 Lazy.force(参见 ‍11.6)。

# let lazy_greeter = lazy (print_string "Hello, World!\n");;
val lazy_greeter : unit lazy_t = <lazy>
# Lazy.force lazy_greeter;;
Hello, World! - : unit = ()

本地模块

表达式 let module module-name = module-expr in expr 在表达式 expr 的评估过程中,将模块表达式 module-expr 局部绑定到标识符 module-name。然后它返回 expr 的值。例如

# let remove_duplicates comparison_fun string_list = let module StringSet = Set.Make(struct type t = string let compare = comparison_fun end) in StringSet.elements (List.fold_right StringSet.add string_list StringSet.empty);;
val remove_duplicates : (string -> string -> int) -> string list -> string list = <fun>

本地打开

表达式 let open module-path in exprmodule-path.(expr) 严格等价。这些结构在表达式 expr 的各自范围内局部打开模块路径 module-path 所引用的模块。

# let map_3d_matrix f m = let open Array in map (map (map f)) m let map_3d_matrix' f = Array.(map (map (map f)));;
val map_3d_matrix : ('a -> 'b) -> 'a array array array -> 'b array array array = <fun> val map_3d_matrix' : ('a -> 'b) -> 'a array array array -> 'b array array array = <fun>

当本地打开表达式的正文由 [ ][| |]{ } 分隔时,可以省略括号。对于表达式,对于 {< >} 也可以省略括号。例如,module-path.[expr] 等价于 module-path.([expr]),而 module-path.[| expr |] 等价于 module-path.([| expr |])

# let vector = Random.[|int 255; int 255; int 255; int 255|];;
val vector : int array = [|220; 90; 247; 144|]