OCaml 導覽

本教學介紹 OCaml 的基本功能:值、表達式、列表、函式、模式匹配等等。

不需要 OCaml 或任何函數式程式設計知識;但是,假設讀者具有一些基本的軟體開發知識。請確保您已安裝 OCaml 並設定好環境,如「安裝 OCaml」頁面所述。

我們建議您執行我們提供的範例,並嘗試它們,以了解 OCaml 的程式碼編寫方式。為此,您可以使用 UTop (Universal Toplevel)。

UTop 允許使用者透過讀取和評估 OCaml 片語(例如表達式或值定義),並在螢幕上印出結果來與 OCaml 互動。使用 utop 命令來執行 UTop。按下 Ctrl+D 退出。有關更多資訊,您可以閱讀「OCaml Toplevel 介紹」。

本導覽中的一些範例包含註解。OCaml 中的註解以 (* 開始,以 *) 結束,並且可以巢狀使用。由於 OCaml 會忽略它們,因此它們可以用在任何允許空白的地方。當在 UTop 中輸入以下程式碼時,可以省略註解。以下是一些範例

(* Here is a comment *)
(* Outside of the nested comment is still a comment. (* Here is a nested comment *) Outside of the nested comment again. *)
# 50 + (* A comment in between parts of an expression *) 50;;
- : int = 100

表達式和定義

讓我們從一個簡單的表達式開始

# 50 * 50;;
- : int = 2500

在 OCaml 中,一切都有一個值,並且每個值都有一個類型。上面的範例表示,「50 * 50 是一個類型為 int (整數) 的表達式,其求值結果為 2500。」由於它是一個匿名表達式,因此會顯示字元 - 而不是名稱。

結尾的雙分號 ;; 會告訴 toplevel 評估並印出給定片語的結果。

以下是一些其他基本值和類型的範例

# 6.28;;
- : float = 6.28

# "This is really disco!";;
- : string = "This is really disco!"

# 'a';; (* Note the single quotes *)
- : char = 'a'

# true;;
- : bool = true

OCaml 具有類型推斷。它可以自動判斷表達式的類型,而無需程式設計師的太多指導。列表專門的教學。目前,以下兩個表達式都是列表。前者包含整數,後者包含字串。

# let u = [1; 2; 3; 4];;
val u : int list = [1; 2; 3; 4]

# ["this"; "is"; "mambo"];;
- : string list = ["this"; "is"; "mambo"]

列表的類型,int liststring list,是從它們元素的類型推斷出來的。列表可以是空的 [](讀作「nil」)。請注意,第一個列表已使用 let … = … 建構給定一個名稱,這將在下面詳細介紹。列表上最基本的操作是在現有列表的前面加入一個新元素。這是使用「cons」運算子完成的,以雙冒號運算子 :: 表示。

# 9 :: u;;
- : int list = [9; 1; 2; 3; 4]

在 OCaml 中,if … then … else … 不是一個陳述式;它是一個表達式。

# 2 * if "hello" = "world" then 3 else 5;;
- : int = 10

if 開始到 5 結束的來源被解析為一個單一的整數表達式,該表達式乘以 2。OCaml 不需要兩種不同的測試建構。 三元條件運算子if … then … else … 是相同的。另請注意,這裡不需要括號,這在 OCaml 中經常出現。

可以使用 let 關鍵字為值指定名稱。這稱為將值綁定到名稱。例如

# let x = 50;;
val x : int = 50

# x * x;;
- : int = 2500

當輸入 let x = 50;; 時,OCaml 會以 val x : int = 50 回應,表示 x 是一個綁定到值 50 的識別碼。因此,x * x;; 的求值結果與 50 * 50;; 相同。

OCaml 中的綁定是不可變的,表示指定給名稱的值永遠不會改變。儘管 x 通常被稱為變數,但事實並非如此。它實際上是一個常數。使用過於簡化但可以接受的文字來說,在 OCaml 中所有變數都是不可變的。可以為可以更新的值指定名稱。在 OCaml 中,這稱為參考,將在「處理可變狀態」一節中討論。

OCaml 中沒有多載,因此在詞法範圍內,名稱只有單一值,該值僅取決於其定義。

不要在名稱中使用破折號;請改用底線。例如:x_plus_y 可行,x-plus-y 不可行。

可以為綁定指定特殊註解(有時稱為「docstring」),編輯器和工具會將其視為與綁定相關。這些註解以在註解開頭添加第二個 * 來表示。例如

(** Feet in a mile *)
let feets = 5280;;
val feets : int = 5280

這將在「odoc for Authors: Special Comments」中進一步討論。

可以使用 let … = … in … 語法在表達式內局部定義名稱

# let y = 50 in y * y;;
- : int = 2500

# y;;
Error: Unbound value y

此範例定義名稱 y 並將其綁定到值 50。然後在表達式 y * y 中使用它,產生值 2500。請注意,y 僅在 in 關鍵字之後的表達式中定義。

由於 let … = … in … 是一個表達式,因此它可以用在另一個表達式中,以便讓多個值擁有自己的名稱

# let a = 1 in
  let b = 2 in
    a + b;;
- : int = 3

這定義了兩個名稱:a 的值為 1b 的值為 2。然後,此範例在表達式 a + b 中使用它們,產生值 3

在 OCaml 中,等號符號有兩個含義。它用於定義和相等測試。

# let dummy = "hi" = "hello";;
val dummy : bool = false

這被解釋為:「定義 dummy 作為字串 "hi""hello" 之間結構相等測試的結果。」OCaml 也有雙等號運算子 ==,它代表物理相等性,但在本教學中未使用。運算子 <>= 的否定,而 !=== 的否定。

函式

在 OCaml 中,由於一切都是值,因此函式也是值。函式是使用 let 關鍵字定義的

# let square x = x * x;;
val square : int -> int = <fun>

# square 50;;
- : int = 2500

此範例定義一個名為 square 的函式,其中包含單一參數 x。它的函式主體是表達式 x * x。OCaml 中沒有「return」關鍵字。

當將 square 應用於 50 時,它會將 x * x 評估為 50 * 50,從而產生 2500

REPL 指出 square 的類型為 int -> int。這表示它是一個函式,該函式接受一個 int 作為引數(輸入),並傳回一個 int 作為結果(輸出)。函式值無法顯示,這就是為什麼改為印出 <fun> 的原因。

# String.ends_with;;
- : suffix:string -> string -> bool = <fun>

# String.ends_with ~suffix:"less" "stateless";;
- : bool = true

有些函式(例如 String.ends_with)具有帶標籤的參數。當函式有多個相同類型的參數時,標籤很有用;命名引數可以讓您猜測它們的用途。在上面,~suffix:"less" 表示 "less" 作為標籤引數 suffix 傳遞。帶標籤的引數在「帶標籤的引數」教學中詳細介紹。

匿名函式

匿名函式沒有名稱,它們是使用 fun 關鍵字定義的

# fun x -> x * x;;
- : int -> int = <fun>

我們可以編寫匿名函式,並立即將它們應用於值

# (fun x -> x * x) 50;;
- : int = 2500

具有多個參數的函式和部分應用

函式可以有多個參數,以空格分隔。

# let cat a b = a ^ " " ^ b;;
val cat : string -> string -> string = <fun>

函式 cat 有兩個 string 參數,ab,並傳回 string 類型的值。

# cat "ha" "ha";;
- : string = "ha ha"

函式不必使用它們預期的所有引數來呼叫。可以只將 a 傳遞給 cat,而無需傳遞 b

# let cat_hi = cat "hi";;
val cat_hi : string -> string = <fun>

這會傳回一個函式,該函式預期一個單一字串,這裡的 b 來自 cat 的定義。這稱為部分應用。在上面的程式碼中,cat 部分應用於 "hi"

cat 的部分應用產生的函式 cat_hi 的行為如下

# cat_hi "friend";;
- : string = "hi friend"

類型參數和高階函式

函式可以預期函式作為參數,這稱為高階函式。高階函式的一個著名範例是 List.map。以下說明如何使用它

# List.map;;
- : ('a -> 'b) -> 'a list -> 'b list = <fun>

# List.map (fun x -> x * x);;
- : int list -> int list = <fun>

# List.map (fun x -> x * x) [0; 1; 2; 3; 4; 5];;
- : int list = [0; 1; 4; 9; 16; 25]

這個函數的名稱以 List. 開頭,因為它是作用於列表的預定義函數庫的一部分。這點稍後會再討論。函數 List.map 有兩個參數:第二個參數是一個列表,而第一個參數是一個可以應用於列表元素的函數,無論它們是什麼。List.map 返回一個列表,該列表是將提供的函數應用於輸入列表的每個元素而形成的。

函數 List.map 可以應用於任何類型的列表。這裡給出的是一個整數列表,但它也可以是浮點數、字串或任何其他類型的列表。這被稱為多型(polymorphism)List.map 函數是多型的,這意味著它有兩個隱含的類型變數'a'b(發音為「alpha」和「beta」)。它們都可以是任何類型;然而,關於傳遞給 List.map 的函數

  1. 輸入列表的元素的類型與其輸入的類型相同。
  2. 輸出列表的元素的類型與其輸出的類型相同。

副作用和 unit 類型

執行作業系統層級的輸入輸出操作是使用函數完成的。以下是每個操作的範例

# read_line;;
- : unit -> string = <fun>

# read_line ();;
caramba
- : string = "caramba"

# print_endline;;
- : string -> unit = <fun>

# print_endline "¿Cuándo se come aquí?";;
¿Cuándo se come aquí?
- : unit = ()

函數 read_line 從標準輸入讀取字元,當到達行尾 (EOL) 時,將它們作為字串返回。函數 print_endline 在標準輸出上列印字串,後面跟著 EOL。

函數 read_line 不需要任何資料即可繼續,而函數 print_endline 沒有任何有意義的資料可以返回。表示這種資料的缺失是 unit 類型 的作用,它出現在它們的簽名中。unit 類型只有一個值,寫作 (),發音為「unit」。當沒有資料被傳遞或返回時,它會被用作佔位符,但仍然必須傳遞一些 token 來開始處理或表示處理已終止。

輸入輸出是在執行函數時發生的一種情況,但它不會出現在函數類型中。這稱為副作用(side-effect),並且不僅限於 I/O。unit 類型通常用於表示副作用的存在,儘管情況並非總是如此。

遞迴函數

遞迴函數在其自身的主體中呼叫自身。此類函數必須使用 let rec … = … 而不是僅使用 let 來宣告。遞迴並非在 OCaml 上執行迭代計算的唯一方法。諸如 forwhile 之類的迴圈可用,但它們旨在與可變資料一起在編寫命令式 OCaml 時使用。否則,應優先使用遞迴函數。

以下是一個建立兩個界限之間連續整數列表的函數範例。

# let rec range lo hi =
    if lo > hi then
      []
    else
      lo :: range (lo + 1) hi;;
val range : int -> int -> int list = <fun>

# range 2 5;;
- : int list = [2; 3; 4; 5]

如其類型 int -> int -> int list 所指示,函數 range 接受兩個整數作為參數,並返回一個整數列表作為結果。第一個 int 參數 lo 是範圍的下限;第二個 int 參數 hi 是上限。如果 lo > hi,則返回空範圍。這是 if … then … else 表達式的第一個分支。否則,lo 值會被添加到呼叫 range 本身所建立的列表前面;這就是遞迴。添加前面是使用 ::(OCaml 中的 cons 運算符)完成的。它通過在現有列表的前面新增一個元素來構造一個新列表。每次呼叫都會取得進展;由於 lo 剛剛被添加到列表的開頭,因此呼叫 range 時使用 lo + 1。可以這樣可視化(這不是 OCaml 語法)

   range 2 5
=> 2 :: range 3 5
=> 2 :: 3 :: range 4 5
=> 2 :: 3 :: 4 :: range 5 5
=> 2 :: 3 :: 4 :: 5 :: range 6 5
=> 2 :: 3 :: 4 :: 5 :: []
=> [2; 3; 4; 5]

每個 => 符號對應於遞迴步驟的計算,除了最後一個。OCaml 在內部處理列表,如倒數第二個表達式所示,但將它們顯示為最後一個表達式。這只是美化輸出。在最後兩個步驟之間沒有進行任何計算。

資料和類型

類型轉換和類型推斷

OCaml 具有 float 類型的浮點數值。要新增浮點數,必須使用 +. 而不是 +

# 2.0 +. 2.0;;
- : float = 4.

在 OCaml 中,+. 是浮點數之間的加法,而 + 是整數之間的加法。

在許多程式語言中,值可以自動從一種類型轉換為另一種類型。這包括隱式類型轉換提升(promotion)。例如,在這樣一種語言中,如果您寫 1 + 2.5,則第一個參數(一個整數)會被提升為浮點數,使得結果也是浮點數。

OCaml 永遠不會將值從一種類型隱式轉換為另一種類型。無法執行浮點數和整數的加法。下面的兩個範例都會拋出錯誤

# 1 + 2.5;;
Error: This expression has type float but an expression was expected of type
         int

# 1 +. 2.5;;
Error: This expression has type int but an expression was expected of type
         float
  Hint: Did you mean `1.'?

在第一個範例中,+ 旨在與整數一起使用,因此不能與 2.5 浮點數一起使用。在第二個範例中,+. 旨在與浮點數一起使用,因此不能與 1 整數一起使用。

在 OCaml 中,您需要使用 float_of_int 函數將整數顯式轉換為浮點數

# float_of_int 1 +. 2.5;;
- : float = 3.5

OCaml 要求顯式轉換的原因有很多。最重要的是,它使得可以自動計算類型。OCaml 的類型推斷演算法會計算每個表達式的類型,並且與其他語言相比,只需要很少的註釋。可以說,這比我們因更加明確而損失的時間更多。

列表

列表可能是 OCaml 中最常見的資料類型。它們是具有相同類型的值的有序集合。以下是一些範例。

# [];;
- : 'a list = []

# [1; 2; 3];;
- : int list = [1; 2; 3]

# [false; false; true];;
- : bool list = [false; false; true]

# [[1; 2]; [3]; [4; 5; 6]];;
- : int list list = [[1; 2]; [3]; [4; 5; 6]]

以上範例的讀取方式如下

  1. 空列表,nil
  2. 包含數字 1、2 和 3 的列表
  3. 包含布林值 falsefalsetrue 的列表。允許重複。
  4. 列表的列表

列表定義為空(寫作 [])或是在另一個列表 u 的前面新增一個元素 x,寫作 x :: u(雙冒號運算符發音為「cons」)。

# 1 :: [2; 3; 4];;
- : int list = [1; 2; 3; 4]

在 OCaml 中,模式匹配提供了一種檢查任何類型資料的方法,但函數除外。在本節中,它在列表上被介紹,並且它將在下一節中推廣到其他資料類型。以下是如何使用模式匹配來定義一個計算整數列表總和的遞迴函數

# let rec sum u =
    match u with
    | [] -> 0
    | x :: v -> x + sum v;;
val sum : int list -> int = <fun>

# sum [1; 4; 3; 2; 5];;
- : int = 15

列表上的多型函數

以下是如何編寫一個計算列表長度的遞迴函數

# let rec length u =
    match u with
    | [] -> 0
    | _ :: v -> 1 + length v;; (* _ doesn't define a name; it can't be used in the body *)
val length : 'a list -> int = <fun>

# length [1; 2; 3; 4];;
- : int = 4

# length ["cow"; "sheep"; "cat"];;
- : int = 3

# length [[]];;
- : int = 1

此函數不僅適用於整數列表,還適用於任何類型的列表。它是一個多型函數。它的類型指示 'a list 類型的輸入,其中 'a 是表示任何類型的類型變數。空列表模式 [] 可以是任何元素類型。因此,_ :: v 模式(作為列表頭部的值)是無關的,因為 _ 模式指示它未被檢查。由於兩種模式必須屬於同一類型,因此類型演算法會推斷出 'a list -> int 類型。

定義高階函數

可以將函數作為參數傳遞給另一個函數。將其他函數作為參數的函數稱為高階函數。先前使用函數 List.map 來說明了這一點。以下是如何使用列表上的模式匹配來編寫 map

# let square x = x * x;;
val square : int -> int

# let rec map f u =
    match u with
    | [] -> []
    | x :: u -> f x :: map f u;;
val map : ('a -> 'b) -> 'a list -> 'b list = <fun>

# map square [1; 2; 3; 4;];;
- : int list = [1; 4; 9; 16]

模式匹配,續

模式匹配不僅限於列表。任何類型的資料都可以使用它來檢查,但函數除外。模式是與被檢查的值進行比較的表達式。可以使用 if … then … else … 來執行,但模式匹配更方便。以下是一個使用 option 資料類型的範例,將在 模組和標準函式庫 章節中詳細介紹。

# #show option;;
type 'a option = None | Some of 'a

# let f opt = match opt with
    | None -> None
    | Some None -> None
    | Some (Some x) -> Some x;;
val f : 'a option option-> 'a option = <fun>

被檢查的值是 option 類型的 opt。它從上到下與模式進行比較。如果 optNone 選項,則它與第一個模式匹配。如果 optSome None 選項,則它與第二個模式匹配。如果 opt 是帶有值的雙重包裝選項,則它與第三個模式匹配。模式可以像 let 一樣引入名稱。在第三個模式中,x 指定雙重包裝選項內部的資料。

模式匹配在基本資料類型教學中以及在每個資料類型教學中都有詳細介紹。

在另一個範例中,使用 if … then … else … 和模式匹配進行相同的比較。

# let g x =
  if x = "foo" then 1
  else if x = "bar" then 2
  else if x = "baz" then 3
  else if x = "qux" then 4
  else 0;;
val g : string -> int = <fun>

# let g' x = match x with
    | "foo" -> 1
    | "bar" -> 2
    | "baz" -> 3
    | "qux" -> 4
    | _ -> 0;;
val g' : string -> int = <fun>

底線符號是一個捕獲所有模式;它可以匹配任何內容。

請注意,當模式匹配沒有捕獲所有情況時,OCaml 會拋出警告

# fun i -> match i with 0 -> 1;;
Line 1, characters 9-28:
Warning 8 [partial-match]: this pattern-matching is not exhaustive.
Here is an example of a case that is not matched:
1
- : int -> int = <fun>

對偶和元組

元組是固定長度的元素集合,可以是任何類型。對偶是具有兩個元素的元組。以下是一個 3 元組和一個對偶

# (1, "one", 'K');;
- : int * string * char = (1, "one", 'K')

# ([], false);;
- : 'a list * bool = ([], false)

對元組元件的存取是使用模式匹配完成的。例如,預定義函數 snd 返回對偶的第二個元件

# let snd p =
    match p with
    | (_, y) -> y;;
val snd : 'a * 'b -> 'b = <fun>

# snd (42, "apple");;
- : string = "apple"

注意:函數 snd 在 OCaml 標準函式庫中預定義。

元組的類型是在元件類型之間使用 * 寫成的。

變體類型

就像模式匹配概括了 switch 陳述式一樣,變體類型概括了枚舉和聯合類型。

以下是作為枚舉資料類型運作的變體類型定義

# type primary_colour = Red | Green | Blue;;
type primary_colour = Red | Green | Blue

# [Red; Blue; Red];;
- : primary_colour list = [Red; Blue; Red]

以下是作為聯合類型運作的變體類型定義

# type http_response =
    | Data of string
    | Error_code of int;;
type http_response = Data of string | Error_code of int

# Data "<!DOCTYPE html>
<html lang=\"en\">
  <head>
    <meta charset=\"utf-8\">
    <title>Dummy</title>
  </head>
  <body>
    Dummy Page
  </body>
</html>";;

- : http_response =
Data
 "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\">\n    <title>Dummy</title>\n  </head>\n  <body>\n    Dummy Page\n  </body>\n</html>"

# Error_code 404;;
- : http_response = Error_code 404

以下是一些介於兩者之間的東西

# type page_range =
    | All
    | Current
    | Range of int * int;;
type page_range = All | Current | Range of int * int

在先前的定義中,大寫的識別碼稱為建構子(constructors)。它們允許建立變體值。這與物件導向程式設計無關。

如本節第一句話所述,變體與模式匹配一起使用。以下是一些範例

# let colour_to_rgb colour =
    match colour with
    | Red -> (0xff, 0, 0)
    | Green -> (0, 0xff, 0)
    | Blue -> (0, 0, 0xff);;
val colour_to_rgb : primary_colour -> int * int * int = <fun>

# let http_status_code response =
    match response with
    | Data _ -> 200
    | Error_code code -> code;;
val http_status_code : http_response -> int = <fun>

# let is_printable page_count cur range =
    match range with
    | All -> true
    | Current -> 0 <= cur && cur < page_count
    | Range (lo, hi) -> 0 <= lo && lo <= hi && hi < page_count;;
val is_printable : int -> int -> page_range -> bool = <fun>

與函數一樣,變體如果在其自身的定義中引用自身,則可以遞迴。預定義類型 list 提供了此類變體的範例

# #show list;;
type 'a list = [] | (::) of 'a * 'a list

如先前所示,sumlengthmap 函數提供了對列表變體類型進行模式匹配的範例。

記錄

與元組一樣,記錄也會將幾種類型的元素打包在一起。但是,每個元素都會給定一個名稱。與變體類型一樣,必須先定義記錄類型才能使用。以下是一個記錄類型、一個值、對元件的存取以及對同一記錄進行模式匹配的範例。

# type person = {
    first_name : string;
    surname : string;
    age : int
  };;
type person = { first_name : string; surname : string; age : int; }

# let gerard = {
     first_name = "Gérard";
     surname = "Huet";
     age = 76
  };;
val gerard : person = {first_name = "Gérard"; surname = "Huet"; age = 76}

在定義 gerard 時,不需要宣告類型。類型檢查器將搜尋恰好具有三個名稱和類型相符欄位的記錄。請注意,記錄之間沒有類型關係。不可能宣告一個通過新增欄位來擴充另一個的記錄類型。如果記錄類型搜尋找到完全匹配,則會成功,而在任何其他情況下都會失敗。

# let s = gerard.surname;;
val s : string = "Huet"

# let is_teenager person =
    match person with
    | { age = x; _ } -> 13 <= x && x <= 19;;
val is_teenager : person -> bool = <fun>

# is_teenager gerard;;
- : bool = false

這裡,模式 { age = x; _ } 使用具有 int 類型 age 欄位的最新宣告記錄類型來輸入。類型 int 是從表達式 13 <= x && x <= 19 推斷出來的。函數 is_teenager 將僅適用於找到的記錄類型,此處為 person

處理錯誤

例外

當計算中斷時,會拋出例外。例如

# 10 / 0;;
Exception: Division_by_zero.

使用 raise 函數引發例外。

# let id_42 n = if n <> 42 then raise (Failure "Sorry") else n;;
val id_42 : int -> int = <fun>

# id_42 42;;
- : int = 42

# id_42 0;;
Exception: Failure "Sorry".

請注意,例外不會出現在函數類型中。

例外情況使用 try … with … 結構來捕捉。

# try id_42 0 with Failure _ -> 0;;
- : int = 0

標準函式庫提供了數個預定義的例外情況。也可以定義例外情況。

使用 result 類型

在 OCaml 中處理錯誤的另一種方式是回傳 result 類型的值,它可以表示正確的結果或錯誤。以下是它的定義方式:

# #show result;;
type ('a, 'b) result = Ok of 'a | Error of 'b

所以可以這樣寫:

# let id_42_res n = if n <> 42 then Error "Sorry" else Ok n;;
val id_42_res : int -> (int, string) result = <fun>

# id_42_res 42;;
- : (int, string) result = Ok 42

# id_42_res 0;;
- : (int, string) result = Error "Sorry"

# match id_42_res 0 with
  | Ok n -> n
  | Error _ -> 0;;
- : int = 0

使用可變狀態

OCaml 支援命令式程式設計。通常,let … = … 語法並不是定義變數,而是定義常數。然而,在 OCaml 中存在可變的變數。它們被稱為參考。以下是如何建立一個整數參考:

# let r = ref 0;;
val r : int ref = {contents = 0}

在語法上不可能建立一個未初始化或空值的參考。r 參考會用整數零來初始化。存取參考的內容是使用 ! 解參考運算子。

# !r;;
- : int = 0

請注意,!rr 具有不同的類型:分別為 intint ref。就像整數和浮點數無法相乘一樣,也無法更新整數或乘以參考。

讓我們更新 r 的內容。在這裡,:= 是賦值運算子;發音為「接收」。

# r := 42;;
- : unit = ()

這會回傳 (),因為變更參考的內容是一種副作用。

# !r;;
- : int = 42

使用 ; 運算子依序執行一個又一個的表達式。寫成 a; b 表示:執行 a。完成後,執行 b,只會回傳 b 的值。

# let text = ref "hello ";;
val text : string ref = {contents = "hello "}

# print_string !text; text := "world!"; print_endline !text;;
hello world!
- : unit = ()

以下是在第二行發生的副作用:

  1. 在標準輸出上顯示參考 text 的內容
  2. 更新參考 text 的內容
  3. 在標準輸出上顯示參考 text 的內容

此行為與命令式語言中的行為相同。但是,儘管 ; 未被定義為函式,它的行為就像它是 unit -> unit -> unit 類型的函式。

模組和標準函式庫

在 OCaml 中,原始碼的組織是使用稱為模組的東西來完成的。模組是一組定義。標準函式庫是一組所有 OCaml 程式都可以使用的模組。以下是如何列出標準函式庫 Option 模組中包含的定義:

# #show Option;;
module Option :
  sig
    type 'a t = 'a option = None | Some of 'a
    val none : 'a t
    val some : 'a -> 'a t
    val value : 'a t -> default:'a -> 'a
    val get : 'a t -> 'a
    val bind : 'a t -> ('a -> 'b t) -> 'b t
    val join : 'a t t -> 'a t
    val map : ('a -> 'b) -> 'a t -> 'b t
    val fold : none:'a -> some:('b -> 'a) -> 'b t -> 'a
    val iter : ('a -> unit) -> 'a t -> unit
    val is_none : 'a t -> bool
    val is_some : 'a t -> bool
    val equal : ('a -> 'a -> bool) -> 'a t -> 'a t -> bool
    val compare : ('a -> 'a -> int) -> 'a t -> 'a t -> int
    val to_result : none:'e -> 'a t -> ('a, 'e) result
    val to_list : 'a t -> 'a list
    val to_seq : 'a t -> 'a Seq.t
  end

模組提供的定義是透過在它們的名稱前面加上模組名稱來參照的。

# Option.map;;
- : ('a -> 'b) -> 'a option -> 'b option = <fun>

# Option.map (fun x -> x * x);;
- : int option -> int option = <fun>

# Option.map (fun x -> x * x) None;;
- : int option = None

# Option.map (fun x -> x * x) (Some 8);;
- : int option = Some 64

在這裡,將以幾個步驟說明如何使用函式 Option.map

  1. 顯示它的類型。它有兩個參數:一個類型為 'a -> 'b 的函式和一個 'a option
  2. 使用部分應用,只傳遞 fun x -> x * x。檢查結果函式的類型。
  3. 使用 None 應用。
  4. 使用 Some 8 應用。

當提供的選項值包含實際值時(即它是 Some 某個值),它會套用提供的函式並將其結果包裝在選項中傳回。當提供的選項值不包含任何值時(即它是 None),結果也不包含任何值(即它也是 None)。

本節前面使用的 List.map 函式也是模組的一部分,也就是 List 模組。

# List.map;;
- : ('a -> 'b) -> 'a list -> 'b list = <fun>

# List.map (fun x -> x * x);;
- : int list -> int list = <fun>

這說明了 OCaml 模組系統的第一個特性。它提供了一種透過防止名稱衝突來分離關注點的方法。如果兩個函式由不同的模組提供,則它們可以具有相同的名稱但具有不同的類型。

模組也允許有效的分別編譯。這將在下一個教學課程中說明。

結論

在本教學課程中,OCaml 是以互動方式使用的。下一個教學課程「您的第一個 OCaml 程式」將示範如何撰寫 OCaml 檔案、如何編譯它們以及如何啟動專案。

仍然需要協助嗎?

協助改進我們的文件

所有 OCaml 文件都是開源的。發現錯誤或不清楚的地方嗎?請提交提取請求。

OCaml

創新。社群。安全。