第 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
 ?(label-name [:typexpr] [=expr] )
 ?label-name:pattern
 ?label-name:(pattern [:typexpr] [=expr] )
 
local-open::= 
 letopenmodule-pathinexpr
 module-path.(expr)
 module-path.[expr]
 module-path.[|expr|]
 module-path.{expr}
 module-path.{<expr>}
 
object-expr::= 
 newclass-path
 objectclass-bodyend
 expr#method-name
 inst-var-name
 inst-var-name<-expr
 {< [ inst-var-name [=expr] { ;inst-var-name [=expr] } [;] ] >}

另請參閱下列語言擴充功能:first-class modulesopen 語句中的覆寫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>

括號括住的表達式

表達式 ( expr )begin expr end 的值與 expr 相同。這兩個結構在語意上等效,但最好在控制結構內使用 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>

括號括住的表達式可以包含類型約束,如 ( expr : typexpr )。此約束會強制 expr 的類型與 typexpr 相容。

括號括住的表達式也可以包含強制轉換 ( expr [: typexpr] :> typexpr) (請參閱下方的第 11.7.7 節)。

函式應用

函式應用以(可能帶標籤的)表達式的並列表示。expr argument1argumentn 表達式會評估 expr 表達式和出現在 argument1argumentn 的表達式。expr 表達式必須求值為函式值 f,然後將其套用至 argument1、…、argumentn 的值。

評估 exprargument1、…、argumentn 表達式的順序未指定。

# 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]

如果參數在 expr 的類型中指定為選用 (標籤帶有 ? 前綴),則對應的引數會自動以建構子 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 引入:

functionpattern1->expr1
|… 
|patternn->exprn

此表達式會求值為一個帶有一個參數的函數值。當此函數應用於值 v 時,此值會與每個模式 模式1模式n 進行匹配。如果其中一個匹配成功,也就是說,如果值 v 與某個 i 的模式 模式i 匹配,則會對與所選模式關聯的表達式 expri 進行求值,而其值會成為函數應用程式的值。expri 的求值發生在匹配期間執行的綁定所豐富的環境中。

如果多個模式與參數 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 -> expr

此表達式等同於

fun 參數1 ->fun 參數n -> expr
# 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 -> (expr : 類型表達式 )

請注意最後一個參數的類型約束之間語法上的細微差異

以及結果的類型約束

# 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 ? 標籤名稱 :( 模式 = expr0 ) -> expr 的函數等同於

fun ? 標籤名稱 : 識別碼 -> let 模式 = match 識別碼 with Some 識別碼 -> 識別碼 | None -> expr0 in expr

其中 識別碼 是一個新的變數,除非未指定何時求值 expr0

# 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 -> expr

如果我們忽略標籤(標籤僅在函數應用時才有意義),則此表達式等同於

function 模式1 ->function 模式n -> expr

也就是說,上面的 fun 表達式會求值為一個具有 n 個參數的柯里化函數:將此函數應用 n 次於值 v1vn 後,這些值將會平行地與模式 pattern1patternn 進行匹配。如果匹配成功,函數將會回傳 expr 的值,並使用匹配期間建立的綁定來擴充環境。如果匹配失敗,則會拋出 Match_failure 例外。

模式匹配中的守衛 (Guards)

模式匹配的 case (在 functionmatchtry 建構子中) 可以包含守衛表達式,這些是任意的布林表達式,必須求值為 true 才能選中該匹配 case。守衛出現在 -> 符號之前,並由 when 關鍵字引入

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

匹配的進行方式與之前描述的相同,但如果值匹配到某個具有守衛 condi 的模式 patterni,則會對表達式 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 綁定的識別符號,並期望它們的值與 expr ( let 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

這些註釋明確要求定義的值為多型(polymorphic),並允許在遞迴呼叫中使用此多型(當使用 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>

Case 表達式

表達式

matchexpr
withpattern1->expr1
|… 
|patternn->exprn

會將 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,如果 expr1expr2 的計算結果都為 true,則計算結果為 true;否則,計算結果為 false。首先會計算第一個組成部分 expr1。如果第一個組成部分的計算結果為 false,則不會計算第二個組成部分 expr2。因此,表達式 expr1 && expr2 的行為與

if expr1 then expr2 else false 完全相同。

表達式 expr1 || expr2,如果表達式 expr1expr2 其中一個的計算結果為 true,則計算結果為 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 done 會重複計算 expr2,直到 expr1 的計算結果為 true。迴圈條件 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 依序綁定到 n, n+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 依序綁定到 n, n−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
withpattern1->expr1
|… 
|patternn->exprn

會計算表達式 expr,如果計算 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 資料結構的操作

產品 (Product)

表達式 expr1 ,, exprn 的計算結果是表達式 expr1exprn 值的 n 元組。子表達式的計算順序未指定。

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

變體 (Variant)

表達式 constr expr 的計算結果為一元變體值,其建構子為 constr,而參數為 expr 的值。同樣地,表達式 constr ( expr1 ,, exprn ) 的計算結果為 n 元變體值,其建構子為 constr,而參數為 expr1, …, exprn 的值。

表達式 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

多型變體 (Polymorphic Variants)

表達式 `tag-name expr 的計算結果為多型變體值,其標籤為 tag-name,而參數為 expr 的值。

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

紀錄 (Records)

表達式 { 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 . field 會將 expr1 求值為記錄值,並傳回此記錄值中與 field 相關聯的值。

表達式 expr1 . field <- expr2 會將 expr1 求值為記錄值,然後藉由將此記錄中與 field 相關聯的值取代為 expr2 的值,對該記錄進行原地修改。只有在記錄類型的定義中已將 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 表示的陣列,將編號為 expr2 的元素取代為 expr3 的值。如果存取超出界限,則會引發 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 所表示的字串,將字元編號 expr2 的字元替換為 expr3 的值。如果存取超出邊界,會引發 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整數的按位算術右移。
+.浮點數加法。
-.(中綴)浮點數減法。
~-. -.(前綴)浮點數取負。
*.浮點數乘法。
/.浮點數除法。
**浮點數指數運算。
@ 串列串接。
^ 字串串接。
! 取值(傳回參考的目前內容)。
:=參考指定(使用第二個引數的值更新作為第一個引數給出的參考)。
= 結構相等測試。
<> 結構不等測試。
== 實體相等測試。
!= 實體不等測試。
< 「小於」測試。
<= 「小於或等於」測試。
> 「大於」測試。
>= 「大於或等於」測試。
&& &布林值合取。
|| 或布林值析取。

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 —關於 class-body 的語法,請參閱 11.9.2 節及後續章節—並立即透過 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 <- expr 會將 expr 的值賦給實例變數 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] }] >} 會返回自身的副本,並將給定的實例變數替換為相關表達式的值。單個實例變數名稱 id 代表 id = id。其他實例變數在返回的物件中與自身具有相同的值。

# 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

即使類型 typ1 是類型 typ2 的子類型,前一種運算符有時也會無法將表達式 expr 從類型 typ1 強制轉換為類型 typ2:在目前的實作中,它僅會展開包含物件和/或多型變體的兩個層級的類型縮寫,僅在類別類型中明確時才會保留遞迴。作為上述演算法的例外,如果 expr 的推論類型和 typ 都是基本類型(,不包含類型變數),則前一種運算符的行為與後一種運算符相同,將 expr 的推論類型視為 typ1。如果前一種運算符失敗,則應使用後一種運算符。

只有在 expr 的類型是 typ1 的實例(如類型註釋),並且 typ1typ2 的子類型時,才有可能將表達式 expr 從類型 typ1 強制轉換為類型 typ2。強制轉換的表達式的類型是 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 的子型別,則這可能是一個可擴展的型別。

變異性

其他型別不會引入新的子型別關係,但它們可能會傳播其參數的子型別關係。例如,當 typ1typ2 分別是 typ1typ2 的子型別時,typ1 * typ2typ1 * typ2 的子型別。對於函數型別,關係更微妙:如果 typ1typ1 的超型別,且 typ2typ2 的子型別,則 typ1 -> typ2typ1 ‍-> typ2 的子型別。因此,函數型別在其第二個參數中是協變的(如同元組),但在其第一個參數中是逆變的。如同 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 的實際值。後續將 Lazy.force 應用於 v 不會再次評估 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|]