基本資料型態與模式比對
簡介
本文檔涵蓋原子型別,例如整數和布林值;預定義的複合型別,例如字串和列表;以及使用者定義的型別,即變體和記錄。我們將展示如何在這些型別上進行模式比對。
在 OCaml 中,執行時沒有型別檢查,除非明確轉換,否則值不會變更型別。這就是靜態型別和強型別的意義。這允許安全地處理結構化資料。
注意:如同先前的教學,以 #
開頭並以 ;;
結尾的運算式適用於頂層環境,例如 UTop。
預定義型別
整數、浮點數、布林值和字元
整數
int
型別是 OCaml 中預設和基本的整數型別。當您輸入一個整數時,OCaml 會將其識別為整數,如下例所示
# 42;;
- : int = 42
int
型別表示與平台相關的有號整數。這意味著 int
並非總是具有相同的位數。它取決於底層平台的特性,例如處理器架構或作業系統。int
值的運算由 Stdlib
和 Int
模組提供。
通常,int
在 32 位元架構中具有 31 位元,在 64 位元架構中具有 63 位元,因為一位元保留給 OCaml 的執行期操作。標準函式庫還提供了 Int32
和 Int64
模組,支援 32 位元和 64 位元有號整數上與平台無關的運算。本教學不會詳細說明這些模組。
在 OCaml 中,沒有專門用於無號整數的型別。對 int
的位元運算將符號位元視為與其他位元相同。二元運算子使用標準符號。帶符號的餘數運算子寫為 mod
。OCaml 中的整數沒有預定義的冪運算子。
浮點數與型別轉換
浮點數的型別為 float
。
OCaml 不會在值之間執行任何隱式型別轉換。因此,算術運算式不能混合整數和浮點數。參數不是全部為 int
就是全部為 float
。浮點數的算術運算子不同,它們使用點字尾:+.
、-.
、*.
、/.
。
# let pi = 3.14159;;
val pi : float = 3.14159
# let tau = 2.0 *. pi;;
val tau : float = 6.28318
# let tau = 2 *. pi;;
Error: This expression has type int but an expression was expected of type
float
# let tau = 2 * pi;;
Error: This expression has type float but an expression was expected of type
int
float
的運算由 Stdlib
和 Float
模組提供。
布林值
布林值由 bool
型別表示。
# true;;
- : bool = true
# false;;
- : bool = false
# false < true;;
- : bool = true
bool
的運算由 Stdlib
和 Bool
模組提供。連詞「and」寫為 &&
,而析取「or」寫為 ||
。兩者都是短路運算,表示如果左邊的值足以決定整個運算式的值,它們就不會評估右邊的參數。
在 OCaml 中,if … then … else …
是一個條件運算式。它具有與其分支相同的型別。
# 3 * if "foo" = "bar" then 5 else 5 + 2;;
- : int = 21
測試子運算式的型別必須為 bool
。分支子運算式的型別必須相同。
條件運算式與布林值的模式比對相同
# 3 * match "foo" = "bar" with true -> 5 | false -> 5 + 2;;
- : int = 21
字元
char
型別的值對應於 Latin-1 字元集的 256 個符號。字元字面值以單引號括起來,如下所示
# 'd';;
- : char = 'd'
char
值的運算由 Stdlib
和 Char
模組提供。
Uchar
模組提供對 Unicode 字元的支援。
字串 & 位元組序列
字串
字串是不可變的,表示不可能變更字串內的字元值。
# "hello" ^ " " ^ "world!";;
- : string = "hello world!"
字串是 char
值的有限且固定大小的序列。字串串接運算子的符號為 ^
。
可以使用以下語法索引存取字串字元
# "buenos dias".[4];;
- : char : 'o'
對 string
值進行的操作由 Stdlib
和 String
模組提供。
位元組序列
# String.to_bytes "hello";;
- : bytes = Bytes.of_string "hello"
與字串類似,位元組序列是有限且固定大小的。每個單獨的位元組都由 char
值表示。與陣列類似,位元組序列是可變的,這表示它們無法擴展或縮短,但可以更新每個組件位元組。本質上,位元組序列 (類型 bytes
) 是一個無法列印的可變字串。沒有直接撰寫 bytes
的方法,因此它們必須由函式產生。
對 bytes
值進行的操作由 Stdlib
和 Bytes
模組提供。只有函式 Bytes.get
允許直接存取位元組序列中包含的字元。與陣列不同,位元組序列上沒有直接存取運算子。
bytes
的記憶體表示比 char array
更緊湊四倍。
陣列 & 列表
陣列
陣列是相同類型值的有限且固定大小的序列。以下是一些範例
# [| 0; 1; 2; 3; 4; 5 |];;
- : int array = [|0; 1; 2; 3; 4; 5|]
# [| 'x'; 'y'; 'z' |];;
- : char array = [|'x'; 'y'; 'z'|]
# [| "foo"; "bar"; "baz" |];;
- : string array = [|"foo"; "bar"; "baz"|]
陣列可以包含任何類型的值。這裡的陣列是 int array
、char array
和 string array
,但任何類型的資料都可以用在陣列中。通常,array
被認為是一種多型類型。嚴格來說,它是一個類型運算子,它接受一個類型作為引數(這裡為 int
、char
和 string
)來形成另一個類型(這裡推斷的類型)。這是空陣列。
# [||];;
- : 'a array = [||]
請記住,'a
("alpha") 是一個將被其他類型替換的類型參數。
與 string
和 bytes
類似,陣列支援直接存取,但語法不同。
# [| 'x'; 'y'; 'z' |].(2);;
- : char = 'z'
陣列是可變的,這表示它們無法擴展或縮短,但可以更新每個元素。
# let letter = [| 'v'; 'x'; 'y'; 'z' |];;
val letter : char array = [|'v'; 'x'; 'y'; 'z'|]
# letter.(2) <- 'F';;
- : unit = ()
# letter;;
- : char array = [|'v'; 'x'; 'F'; 'z'|]
左箭頭 <-
是陣列更新運算子。在上方,它表示索引 2 處的儲存格設定為值 'F'
。這與寫 Array.set letter 2 'F'
相同。陣列更新是一種副作用,並傳回 unit 值。
對陣列的操作由 Array
模組提供。有一個關於 陣列 的專門教學。
列表
作為字面值,列表非常像陣列。以下是轉換為列表的相同先前範例。
# [ 0; 1; 2; 3; 4; 5 ];;
- : int list = [0; 1; 2; 3; 4; 5]
# [ 'x'; 'y'; 'z' ];;
- : char list = ['x'; 'y'; 'z']
# [ "foo"; "bar"; "baz" ];;
- : string list = ["foo"; "bar"; "baz"]
與陣列類似,列表是相同類型值的有限序列。它們也是多型的。但是,列表是可擴展的、不可變的,並且不支援直接存取它們包含的所有值。列表在函式程式設計中扮演著核心角色,因此它們有一個 專門教學。
對列表的操作由 List
模組提供。List.append
函式會串連兩個列表。它可以作為具有符號 @
的運算子使用。
關於列表,有一些特別重要的符號
- 空列表寫為
[]
,具有類型'a list'
,並發音為「nil」。 - 列表建構子運算子寫為
::
,並發音為「cons」,用於將值新增至列表的開頭。
它們一起是用來建立列表和存取其儲存資料的基本方式。例如,以下是如何透過連續套用 cons (::
) 運算子來建立列表
# 3 :: [];;
- : int list = [3]
# 2 :: 3 :: [];;
- : int list = [2; 3]
# 1 :: 2 :: 3 :: [];;
- : int list = [1; 2; 3]
模式匹配提供了存取儲存在列表中的資料的基本方式。
# match [1; 2; 3] with
| x :: u -> x
| [] -> raise Exit;;
- : int = 1
# match [1; 2; 3] with
| x :: y :: u -> y
| x :: u -> x
| [] -> raise Exit;;
- : int = 2
在上面的表達式中,[1; 2; 3]
是要匹配的值。|
和 ->
符號之間的每個表達式都是一個模式。它們是 list
類型的表達式,僅使用 []
、::
和代表列表可能具有的各種形狀的繫結名稱來形成。模式 []
表示「如果列表為空」。模式 x :: u
表示「如果列表包含資料,則讓 x
為列表的第一個元素,u
為列表的其餘部分」。->
符號右側的表達式是每個對應情況下傳回的結果。
選項 & 結果
選項
option
類型也是一種多型類型。選項值可以儲存任何種類的資料,或表示不存在任何此類資料。選項值只能以兩種不同的方式建構:當沒有資料可用時為 None
,否則為 Some
。
# None;;
- : 'a option = None
# Some 42;;
- : int option = Some 42
# Some "hello";;
- : string option = Some "hello"
以下是選項值的模式匹配範例
# match Some 42 with None -> raise Exit | Some x -> x;;
- : int = 42
對選項的操作由 Option
模組提供。選項在 錯誤處理 指南中討論。
結果
result
類型可用於表示函式的結果可以是成功或失敗。只有兩種方式來建構結果值:使用 Ok
或 Error
以及預期含義。這兩個建構子都可以保存任何種類的資料。result
類型是多型的,但它有兩個類型參數:一個用於 Ok
值,另一個用於 Error
值。
# Ok 42;;
- : (int, 'a) result = Ok 42
# Error "Sorry";;
- : ('a, string) result = Error "Sorry"
對結果的操作由 Result
模組提供。結果在 錯誤處理 指南中討論。
元組
以下是一個包含兩個值的元組,也稱為配對。
# (3, 'K');;
- : int * char = (3, 'K')
該配對包含整數 3
和字元 'K'
;其類型為 int * char
。*
符號代表乘積類型。
這可以推廣到具有 3 個或更多元素的元組。例如,(6.28, true, "hello")
的類型為 float * bool * string
。類型 int * char
和 float * bool * string
稱為乘積類型。*
符號用於表示捆綁在乘積中的類型。
預定義的函式 fst
傳回配對的第一個元素,而 snd
傳回配對的第二個元素。
# fst (3, 'g');;
- : int = 3
# snd (3, 'g');;
- : char = 'g'
在標準程式庫中,兩者都是使用模式匹配來定義的。以下是一個函式如何提取四種類型乘積的第三個元素
# let f x = match x with (h, i, j, k) -> j;;
val f : 'a * 'b * 'c * 'd -> 'c = <fun>
請注意,類型 int * char * bool
、int * (char * bool)
和 (int * char) * bool
不相同。值 (42, 'h', true)
、(42, ('h', true))
和 ((42, 'h'), true)
不相等。在數學語言中,乘積類型運算子 *
不是結合律的。
函式
從類型 m
到類型 n
的函式類型寫為 m -> n
。以下是一些範例
# fun x -> x * x;;
- : int -> int = <fun>
# (fun x -> x * x) 9;;
- : int = 81
第一個表達式是一個類型為 int -> int
的匿名函式。該類型是從表達式 x * x
推斷出來的,它必須是類型 int
,因為 *
是一個傳回 int
的運算子。印在值位置的 <fun>
是一個符號,表示函式沒有要顯示的值。這是因為,如果它們已編譯,則它們的程式碼不再可用。
第二個表達式是函式應用。會套用引數 9
,並傳回結果 81
。
# fun x -> x;;
- : 'a -> 'a = <fun>
# (fun x -> x) 42;;
- : int = 42
# (fun x -> x) "This is really disco!";;
- : string = "This is really disco!"
第一個表達式是另一個匿名函式。它是識別函式,它可以應用於任何事物,並且傳回其引數而不做變更。這表示其引數可以是任何類型,並且其結果具有相同的類型。相同的程式碼可以套用至不同類型的資料。這稱為多型。
請記住,'a
是一個類型參數,因此任何類型的值都可以傳遞給函式,並且其類型會取代類型參數。識別函式具有相同的輸入和輸出類型,無論它可能是什麼。
以下兩個表達式顯示識別函式可以套用至不同類型的引數
# let f = fun x -> x * x;;
val f : int -> int = <fun>
# f 9;;
- : int = 81
定義函式與命名值相同,如第一個表達式所示
# let g x = x * x;;
val g : int -> int = <fun>
# g 9;;
- : int = 81
可執行的 OCaml 程式碼主要由函式組成,因此盡可能讓它們簡潔明瞭是有益的。此處使用較短、較常見且可能更直觀的語法來定義函式 g
。
在 OCaml 中,函式可能會透過擲回例外狀況 (類型為 exn
) 而終止,而不會傳回預期的類型值,這不會在其類型中顯示。如果沒有檢查其程式碼,則無法知道函式是否可能會引發例外狀況。
# raise;;
- : exn -> 'a' = <fun>
例外狀況在 錯誤處理 指南中討論。
函式可能有多個參數。
# fun s r -> s ^ " " ^ r;;
- : string -> string -> string = <fun>
# let mean s r = (s + r) / 2;;
val mean : int -> int -> int = <fun>
與乘積類型符號 *
類似,函式類型符號 ->
不是結合律的。以下兩種類型不相同
(int -> int) -> int
:此函式將類型為int -> int
的函式作為參數,並傳回int
作為結果。int -> (int -> int)
:此函式將int
作為參數,並傳回類型為int -> int
的函式作為結果。
Unit
唯一的是,類型 unit
只有一個值。它寫為 ()
,並發音為「unit」。
unit
類型有多種用途。主要來說,它在函式不需要傳遞資料或在完成其運算後沒有要傳回的任何資料時,用作符號。當函式具有副作用(例如 OS 層級的 I/O)時,就會發生這種情況。需要將函式套用至某事物才能觸發其運算,而且它們也必須傳回某事物。當沒有任何有意義的內容可以傳遞或傳回時,應使用 ()
。
# read_line;;
- : unit -> string = <fun>
# print_endline;;
- : string -> unit = <fun>
函式 read_line
從標準輸入讀取以行尾終止的字元序列,並將其作為字串傳回。當傳遞 ()
時,讀取輸入開始。
# read_line ();;
foo bar
- : string = "foo bar"
# print_endline;;
- : string -> unit = <fun>
注意:請將 foo bar
取代為您自己的文字,然後按下 Return
。
函式 print_endline
印出字串,後接標準輸出上的行尾。傳回 unit 值表示作業系統已將輸出請求加入佇列。
使用者定義的類型
使用者定義的類型一律使用 type … = …
陳述式引入。關鍵字 type
必須以小寫字母撰寫。第一個省略符號代表類型名稱,且不得以大寫字母開頭。第二個省略符號代表類型定義。有三種情況可能發生
- 變體
- 記錄
- 別名
接下來的三個章節將介紹這三種型別定義。
變體 (Variants)
變體也稱為 標籤聯合 (tagged unions)。它們與 不相交聯合 (disjoint union) 的概念相關。
枚舉資料型別 (Enumerated Data Types)
變體型別最簡單的形式對應於枚舉型別。它由一個明確的命名值列表定義。定義的值稱為建構子 (constructors),且必須以大寫字母開頭。
例如,以下是如何定義變體資料型別來表示《龍與地下城》的角色職業和陣營。
# type character_class =
| Barbarian
| Bard
| Cleric
| Druid
| Fighter
| Monk
| Paladin
| Ranger
| Rogue
| Sorcerer
| Wizard;;
type character_class =
Barbarian
| Bard
| Cleric
| Druid
| Fighter
| Monk
| Paladin
| Ranger
| Rogue
| Sorcerer
| Wizard
# type rectitude = Evil | R_Neutral | Good;;
type rectitude = Evil | R_Neutral | Good
# type firmness = Chaotic | F_Neutral | Lawful;;
type firmness = Chaotic | F_Neutral | Lawful
這種類型的變體型別也可以用來表示平日、基本方向,或任何其他可以給予名稱的固定大小的值集合。在值上會根據定義順序定義順序 (例如,Druid < Ranger
)。
可以對以上定義的型別執行模式匹配 (pattern matching)
# let rectitude_to_french = function
| Evil -> "Mauvais"
| R_Neutral -> "Neutre"
| Good -> "Bon";;
val rectitude_to_french : rectitude -> string = <fun>
請注意
unit
是一個具有唯一建構子的變體,不攜帶任何資料:()
。bool
也是一個具有兩個不攜帶資料的建構子的變體:true
和false
。
帶有資料的建構子 (Constructors With Data)
可以將資料包裝在建構子中。以下型別具有多個帶有資料的建構子 (例如,Hash of string
) 和一些不帶資料的建構子 (例如,Head
)。它表示引用 Git 修訂版本 的不同方式。
# type commit =
| Hash of string
| Tag of string
| Branch of string
| Head
| Fetch_head
| Orig_head
| Merge_head;;
type commit =
Hash of string
| Tag of string
| Branch of string
| Head
| Fetch_head
| Orig_head
| Merge_head
以下是如何使用模式匹配將 commit
轉換為 string
# let commit_to_string = function
| Hash sha -> sha
| Tag name -> name
| Branch name -> name
| Head -> "HEAD"
| Fetch_head -> "FETCH_HEAD"
| Orig_head -> "ORIG_HEAD"
| Merge_head -> "MERGE_HEAD";;
val commit_to_string : commit -> string = <fun>
在上面,使用了 function …
結構而不是之前使用的 match … with …
結構
let commit_to_string' x = match x with
| Hash sha -> sha
| Tag name -> name
| Branch name -> name
| Head -> "HEAD"
| Fetch_head -> "FETCH_HEAD"
| Orig_head -> "ORIG_HEAD"
| Merge_head -> "MERGE_HEAD";;
val commit_to_string' : commit -> string = <fun>
我們需要將檢查過的表達式傳遞給 match … with …
結構。function …
是一種特殊的匿名函數形式,它會取得一個參數並將其轉發到 match … with …
結構,如上所示。
警告:使用括號包裝乘積型別 (product types) 會將它們變成單一參數。
# type t =
| C1 of int * bool
| C2 of (int * bool);;
type t = C1 of int * bool | C2 of (int * bool)
# let p = (4, false);;
val p : int * bool = (4, false)
# C1 p;;
Error: The constructor C1 expects 2 argument(s),
but is applied here to 1 argument(s)
# C2 p;;
- : t = C2 (4, false)
建構子 C1
有兩個型別為 int
和 bool
的參數,而建構子 C2
則有一個型別為 int * bool
的參數。
遞迴變體 (Recursive Variants)
引用自身的變體定義是遞迴的。建構子可能會包裝來自正在定義的型別的資料。
以下定義就是這種情況,可以用於儲存 JSON 值。
# type json =
| Null
| Bool of bool
| Int of int
| Float of float
| String of string
| Array of json list
| Object of (string * json) list;;
type json =
Null
| Bool of bool
| Int of int
| Float of float
| String of string
| Array of json list
| Object of (string * json) list
建構子 Array
和 Object
都包含型別為 json
的值。
使用遞迴變體的模式匹配定義的函數通常也是遞迴的。此函數會檢查名稱是否存在於整個 JSON 樹中
# let rec has_field name = function
| Array u ->
List.fold_left (fun b obj -> b || has_field name obj) false u
| Object u ->
List.fold_left
(fun b (key, obj) -> b || key = name || has_field name obj) false u
| _ -> false;;
val has_field : string -> json -> bool = <fun>
在這裡,最後一個模式使用了符號 _
,它會捕獲所有內容。它在所有既不是 Array
也不是 Object
的資料上返回 false
。
多型資料型別 (Polymorphic Data Types)
重新審視預先定義的型別 (Revisiting Predefined Types)
預先定義的型別 option
是一種具有兩個建構子的變體型別:Some
和 None
。它可以包含任何型別的值,例如 Some 42
或 Some "hola"
。從這個意義上來說,option
是多型的。以下是它在標準函式庫中的定義方式
# #show option;;
type 'a option = None | Some of 'a
預先定義的型別 list
在相同的意義上也是多型的。它是一個具有兩個建構子的變體,可以保存任何型別的資料。以下是它在標準函式庫中的定義方式
# #show list;;
type 'a list = [] | (::) of 'a * 'a list
這裡唯一的魔法是將建構子轉換為符號,我們在本教學中不涵蓋這一點。型別 bool
和 unit
也是帶有相同魔法的常規變體
# #show unit;;
type unit = ()
# #show bool;;
type bool = false | true
隱式地,乘積型別也像變體型別一樣運作。例如,pairs 可以被視為此型別的佔用者
# type ('a, 'b) pair = Pair of 'a * 'b;;
type ('a, 'b) pair = Pair of 'a * 'b
(int, bool) pair
會寫成 int * bool
,而 Pair (42, true)
會寫成 (42, true)
。從開發人員的角度來看,一切都好像為每個可能的乘積形狀宣告了這樣的型別。這就是允許對乘積進行模式匹配的原因。
甚至整數和浮點數也可以被視為類似枚舉的變體型別,具有許多建構子和 有趣的語法糖 (funky syntactic sugar),這允許對這些型別進行模式匹配。
最後,唯一沒有簡化為變體的型別建構是函數箭頭型別。模式匹配允許檢查任何型別的值,除了函數之外。
使用者定義的多型 (User-Defined Polymorphic)
以下是一個變體型別的範例,它結合了帶有資料的建構子、不帶資料的建構子、多型和遞迴
# type 'a tree =
| Leaf
| Node of 'a * 'a tree * 'a tree;;
type 'a tree = Leaf | Node of 'a * 'a tree * 'a tree
它可以用來表示任意標籤的二元樹。假設這樣的樹會用整數標記,以下是一種使用遞迴和模式匹配來計算其整數總和的可能方式。
# let rec sum = function
| Leaf -> 0
| Node (x, lft, rht) -> x + sum lft + sum rht;;
val sum : int tree -> int = <fun>
以下是如何在此型別中定義 map 函數
# let rec map f = function
| Leaf -> Leaf
| Node (x, lft, rht) -> Node (f x, map f lft, map f rht);;
val map : ('a -> 'b) -> 'a tree -> 'b tree = <fun>
在 OCaml 社群以及更大的函數式程式設計社群中,多型 (polymorphism) 這個詞被寬泛地使用。它適用於以類似方式處理各種型別的事物。在這種廣泛的意義上,OCaml 的幾個功能都是多型的。每個功能都使用一種特定的多型形式,並且有一個名稱。總之,OCaml 有幾種多型形式。在大多數情況下,這些概念之間的區別是模糊的,但有時需要區分它們。
以下是適用於資料型別的術語
'a list
、'a option
和'a tree
經常被稱為多型型別。形式上,bool list
或int option
是型別,而list
和option
是型別運算子 (type operators),它們會取得型別參數並產生型別。這是一種參數多型 (parametric polymorphism)。'a list
和'a option
表示型別族 (type families),這是將型別參數套用到運算子所建立的所有型別。
記錄 (Records)
記錄就像元組一樣,將多個值捆綁在一起。在元組中,元素由它們在對應乘積型別中的位置識別。它們是第一、第二、第三或在其他位置。在記錄中,每個元素都有一個名稱和一個值。這個名稱-值對稱為欄位。這就是為什麼記錄型別必須在使用前宣告的原因。
例如,以下是記錄型別的定義,旨在部分表示《龍與地下城》的角色職業。請注意,以下程式碼取決於本教學先前定義的內容。請確保您已在枚舉資料型別區段中輸入了定義。
# type character = {
name : string;
level : int;
race : string;
class_type : character_class;
alignment : firmness * rectitude;
armor_class : int;
};;
type character = {
name : string;
level : int;
race : string;
class_type : character_class;
alignment : firmness * rectitude;
armor_class : int;
}
character
型別的值攜帶與此乘積的佔用者相同的資料:string * int * string * character_class * character_alignment * int
。
使用點符號存取欄位,如所示
# let ghorghor_bey = {
name = "Ghôrghôr Bey";
level = 17;
race = "half-ogre";
class_type = Fighter;
alignment = (Chaotic, R_Neutral);
armor_class = -8;
};;
val ghorghor_bey : character =
{name = "Ghôrghôr Bey"; level = 17; race = "half-ogre";
class_type = Fighter; alignment = (Chaotic, R_Neutral); armor_class = -8}
# ghorghor_bey.alignment;;
- : firmness * rectitude = (Chaotic, R_Neutral)
# ghorghor_bey.class_type;;
- : character_class = Fighter
# ghorghor_bey.level;;
- : int = 17
若要在不輸入未變更的欄位的情況下建構一個具有某些欄位值變更的新記錄,我們可以使用記錄更新語法,如所示
# let togrev = { ghorghor_bey with name = "Togrev"; level = 20; armor_class = -6 };;
val togrev : character =
{name = "Togrev"; level = 20; race = "half-ogre"; class_type = Fighter;
alignment = (Chaotic, R_Neutral); armor_class = -6}
請注意,記錄的行為類似於單一建構子變體。這允許對它們進行模式匹配。
# match ghorghor_bey with { level; _ } -> level;;
- : int = 17
別名 (Aliases)
型別別名 (Type Aliases)
就像值一樣,任何型別都可以給予名稱。
# type latitude_longitude = float * float;;
type latitude_longitude = float * float
這主要作為文件編寫的手段或縮短長型別表達式。
函數參數別名 (Function Parameter Aliases)
也可以使用元組和記錄的模式匹配為函數參數提供名稱。
(* Tuples as parameters *)
# let tuple_sum (x, y) = x + y;;
val tuple_sum : int * int -> int = <fun>
# let f ((x, y) as arg) = tuple_sum arg;;
val f : int * int -> int = <fun>
(* Records as parameters *)
# type dummy_record = {a: int; b: int};;
type dummy_record = { a : int; b : int; }
# let record_sum ({a; b}: dummy_record) = a + b;;
val record_sum : dummy_record -> int = <fun>
# let f ({a;b} as arg) = record_sum arg;;
val f : dummy_record -> int = <fun>
這對於匹配參數的變體值很有用。
# let meaning_of_life = function Some _ as opt -> opt | None -> Some 42;;
val meaning_of_life : int option -> int option = <fun>
完整範例:數學表達式 (A Complete Example: Mathematical Expressions)
此範例示範如何表示簡單的數學表達式,例如 n * (x + y)
,並以符號方式將其乘開以獲得 n * x + n * y
# type expr =
| Plus of expr * expr (* a + b *)
| Minus of expr * expr (* a - b *)
| Times of expr * expr (* a * b *)
| Divide of expr * expr (* a / b *)
| Var of string (* "x", "y", etc. *);;
type expr =
Plus of expr * expr
| Minus of expr * expr
| Times of expr * expr
| Divide of expr * expr
| Var of string
表達式 n * (x + y)
會寫成
# let e = Times (Var "n", Plus (Var "x", Var "y"));;
val e : expr = Times (Var "n", Plus (Var "x", Var "y"))
以下是一個會將 Times (Var "n", Plus (Var "x", Var "y"))
印成更像 n * (x + y)
的函數
# let rec to_string = function
| Plus (e1, e2) -> "(" ^ to_string e1 ^ " + " ^ to_string e2 ^ ")"
| Minus (e1, e2) -> "(" ^ to_string e1 ^ " - " ^ to_string e2 ^ ")"
| Times (e1, e2) -> "(" ^ to_string e1 ^ " * " ^ to_string e2 ^ ")"
| Divide (e1, e2) -> "(" ^ to_string e1 ^ " / " ^ to_string e2 ^ ")"
| Var v -> v;;
val to_string : expr -> string = <fun>
我們可以編寫一個函數來乘開 n * (x + y)
或 (x + y) * n
形式的表達式,為此我們將使用巢狀模式
# let rec distrib = function
| Times (e1, Plus (e2, e3)) ->
Plus (Times (distrib e1, distrib e2),
Times (distrib e1, distrib e3))
| Times (Plus (e1, e2), e3) ->
Plus (Times (distrib e1, distrib e3),
Times (distrib e2, distrib e3))
| Plus (e1, e2) -> Plus (distrib e1, distrib e2)
| Minus (e1, e2) -> Minus (distrib e1, distrib e2)
| Times (e1, e2) -> Times (distrib e1, distrib e2)
| Divide (e1, e2) -> Divide (distrib e1, distrib e2)
| Var v -> Var v;;
val distrib : expr -> expr = <fun>
以下是如何使用它
# e |> distrib |> to_string |> print_endline;;
((n * x) + (n * y))
- : unit = ()
前兩個模式是 distrib
函數如何運作的關鍵。第一個模式是 Times (e1, Plus (e2, e3))
,它會匹配 e1 * (e2 + e3)
形式的表達式。此第一個模式的右側等效於 (e1 * e2) + (e1 * e3)
。第二個模式執行相同的操作,只是針對 (e1 + e2) * e3
形式的表達式。
其餘模式不會變更表達式的形式,但它們會在其子表達式上以遞迴方式呼叫 distrib
函數。這確保了其所有子表達式也會被乘開。(如果您只想乘開表達式的最上層,您可以將所有其餘模式替換為簡單的 e -> e
規則。)
相反的操作,即將共同的子表達式因式分解,也可以以類似的方式實作。以下版本僅適用於最上層表達式。
# let top_factorise = function
| Plus (Times (e1, e2), Times (e3, e4)) when e1 = e3 ->
Times (e1, Plus (e2, e4))
| Plus (Times (e1, e2), Times (e3, e4)) when e2 = e4 ->
Times (Plus (e1, e3), e4)
| e -> e;;
val top_factorise : expr -> expr = <fun>
# top_factorise (Plus (Times (Var "n", Var "x"),
Times (Var "n", Var "y")));;
- : expr = Times (Var "n", Plus (Var "x", Var "y"))
上面的 factorise 函數引入了另一個功能:每個模式的守衛 (guards)。條件遵循 when
,這表示僅當模式匹配且 when
子句中的條件滿足時,才會執行傳回碼。
結論 (Conclusion)
本教學提供 OCaml 基本資料型別及其用法的全面概述。我們探索了內建型別,例如整數、浮點數、字元、列表、元組和字串,以及使用者定義的型別:記錄和變體。記錄和元組是將異質資料分組為有凝聚力的單位的機制。變體是一種將異質資料作為連貫替代方案公開的機制。
在本教學中,介紹了變體 (variants) 和產品 (products),它們對應於代數資料型態 (algebraic data types)。在這個層級,使用的是名義 (nominal)型別檢查演算法。從歷史上看,這是 OCaml 的第一個型別系統,它來自 ML 程式語言,也就是 OCaml 的祖先。雖然 OCaml 還有其他型別系統,但本文檔主要關注使用此演算法進行資料型別化的資料。