你的第一個 OCaml 程式

要完成本教學,您需要安裝 OCaml。 可選擇性地,我們建議設定您的編輯器

我們將使用包含 OCaml 原始碼的檔案,並將它們編譯以產生可執行的二進位檔。然而,這並不是關於 OCaml 編譯、專案模組化或依賴關係管理的詳細教學;它只會讓您對這些主題有所了解。目的是概略描繪出全貌,以避免迷失在細節中。換句話說,我們採取廣度優先的學習,而不是深度優先的學習。

在先前的教學中,大多數指令是在 UTop 中輸入的。在本教學中,大多數指令應在終端機中輸入。以錢字符號 $ 開頭的程式碼範例旨在終端機中輸入,而以井字符號 # 開頭的行旨在 UTop 中輸入。

完成本教學後,您應該能夠使用 Dune(OCaml 的建置系統)建立、編譯和執行 OCaml 專案。您將能夠使用檔案、在模組內建立私有定義,並了解如何安裝和使用 opam 套件。

注意:說明本教學的檔案可作為Git 儲存庫使用。

在 opam Switch 中工作

當您安裝 OCaml 時,會自動建立全域 opam switch。本教學可以在此全域 opam switch 中工作時完成。

當您同時處理多個 OCaml 專案時,應該建立更多的 opam switch。有關如何執行此操作的說明,請參閱opam Switches 簡介

編譯 OCaml 程式

預設情況下,OCaml 具有兩個編譯器:一個將原始碼翻譯成原生二進位檔,另一個將原始碼轉換為位元組碼格式。 OCaml 還帶有一個用於該位元組碼格式的直譯器。本教學示範如何使用原生編譯器來編寫 OCaml 程式。

我們先使用 Dune 設定一個傳統的「Hello, World!」專案。請務必安裝 3.12 或更新版本。以下會建立一個名為 hello 的專案

$ opam exec -- dune init proj hello
Success: initialized project component named hello

注意 1:如果您在目前終端機工作階段開始時執行了 eval $(opam env),或者如果您在執行 opam init 時回答了 yes,則可以從 dune 指令的開頭省略 opam exec --

注意 2:在本教學中,Dune 產生的輸出可能會因安裝的 Dune 版本而略有不同。本教學顯示 Dune 3.12 的輸出。如果您想要取得最新版本的 Dune,請在終端機中執行 opam update; opam upgrade dune

專案儲存在名為 hello 的目錄中。 tree 指令會列出建立的檔案和目錄。如果您沒有看到以下內容,可能需要安裝 tree。例如,透過 Homebrew 執行 brew install tree

注意 3:如果您在 Apple Silicon macOS 上從 Homebrew 收到此錯誤,則可能是從 Intel 到 ARM 的架構切換問題。請參閱ARM64 修復以解決 ARM64 錯誤。

注意 4:與 Unix 中包含已編譯的二進位檔不同,目錄 libbin 分別包含函式庫和程式的原始碼檔案。這是許多 OCaml 專案使用的慣例,也是 Dune 建立的罐裝專案。所有建置的成品以及原始碼的副本都儲存在 _build 目錄中。您不應編輯 _build 目錄中的任何內容。

$ cd hello
$ tree
.
├── bin
│   ├── dune
│   └── main.ml
├── _build
│   └── log
├── dune-project
├── hello.opam
├── lib
│   └── dune
└── test
    ├── dune
    └── hello.ml

4 directories, 8 files

OCaml 原始碼檔案具有 .ml 副檔名,代表「Meta Language」。 Meta Language (ML) 是 OCaml 的祖先。「OCaml」中的「ml」也是這個意思。以下是 bin/main.ml 檔案的內容

let () = print_endline "Hello, World!"

專案範圍的元資料在 dune-project 檔案中提供。它包含有關專案名稱、相依性和全域設定的資訊。

每個包含需要建置的原始碼檔案的目錄都必須包含一個 dune 檔案來描述如何建置。

這會建置專案

$ opam exec -- dune build

這會啟動它建立的可執行檔

$ opam exec -- dune exec hello
Hello, World!

讓我們看看直接編輯 bin/main.ml 檔案時會發生什麼事。在您的編輯器中開啟它,並將 World 一詞替換為您的名字。像以前一樣使用 dune build 重新編譯專案,然後使用 dune exec hello 再次啟動它。

瞧!您剛寫了您的第一個 OCaml 程式。

在本教學的其餘部分,我們將對此專案進行更多變更,以說明 OCaml 的工具。

監看模式

在我們深入探討之前,請注意您通常會想要使用 Dune 的監看模式來持續編譯並選擇性地重新啟動您的程式。這可確保語言伺服器具有關於您專案的最新資料,因此您的編輯器支援將是一流的。若要使用監看模式,只需新增 -w 旗標

$ opam exec -- dune build -w
$ opam exec -- dune exec hello -w

為什麼沒有 main 函式?

雖然 bin/main.ml 的名稱表示它包含專案的應用程式進入點,但它不包含專用的 main 函式,並且專案不需要包含具有該名稱的檔案才能產生可執行檔。編譯的 OCaml 檔案的行為就像該檔案逐行輸入到 toplevel 中一樣。換句話說,可執行 OCaml 檔案的進入點是它的第一行。

在原始碼檔案中不需要像在 toplevel 中那樣使用雙分號。語句只是從上到下依序處理,每個語句都會觸發它可能具有的副作用。定義會新增到環境中。忽略來自匿名運算式的值。所有這些的副作用會以相同的順序發生。這就是 OCaml main。

然而,通常的做法是挑出一個觸發所有副作用的值,並將其標記為預期的主要進入點。在 OCaml 中,這就是 let () = 的作用,它會評估右側的運算式,包括所有副作用,而不會建立名稱。

模組和標準函式庫,續

讓我們總結一下在OCaml 導覽中關於模組的內容

  • 模組是具名值的集合。
  • 來自不同模組的相同名稱不會衝突。
  • 標準函式庫是多個模組的集合。

模組有助於組織專案;可以將關注點分離到隔離的模組中。這會在下一節中概述。在我們自己建立模組之前,我們先示範如何使用標準程式庫模組中的定義。將檔案 bin/main.ml 的內容變更為以下內容:

let () = Printf.printf "%s\n" "Hello, World!"

這會將函式 print_endline 替換為標準程式庫中 Printf 模組的函式 printf。建置並執行這個修改後的版本應該會產生與之前相同的輸出。使用 dune exec hello 自己試試看。

每個檔案都會定義一個模組

每個 OCaml 檔案在編譯後都會定義一個模組。這就是 OCaml 中獨立編譯的運作方式。每個足夠獨立的關注點都應該隔離到一個模組中。對外部模組的參照會建立相依性。不允許模組之間有循環相依性。

若要建立模組,讓我們建立一個名為 lib/en.ml 的新檔案,其中包含以下內容:

let v = "Hello, world!"

以下是 bin/main.ml 檔案的新版本:

let () = Printf.printf "%s\n" Hello.En.v

現在執行產生的專案:

$ opam exec -- dune exec hello
Hello, world!

檔案 lib/en.ml 會建立名為 En 的模組,而該模組會定義名為 v 的字串值。Dune 會將 En 包裝到另一個名為 Hello 的模組中;這個名稱是由檔案 lib/dune 中的 stanza name hello 所定義的。字串定義是 bin/main.ml 檔案中的 Hello.En.v

Dune 可以啟動 UTop 以互動方式存取專案公開的模組。方法如下:

$ opam exec -- dune utop

然後,在 utop 頂層中,可以檢查我們的 Hello.En 模組:

# #show Hello.En;;
module Hello : sig val v : string end

現在使用 Ctrl-D 結束 utop,或輸入 #quit;; 再前往下一節。

注意:如果在 lib 目錄中新增名為 hello.ml 的檔案,Dune 會將其視為整個 Hello 模組,並使 En 無法存取。如果您希望您的模組 En 可見,您需要在您的 hello.ml 檔案中新增以下內容:

module En = En

定義模組介面

UTop 的 #show 命令會顯示一個 API(在軟體程式庫的意義上):模組提供的定義清單。在 OCaml 中,這稱為模組介面.ml 檔案會定義模組。以類似的方式,.mli 檔案會定義模組介面。對應於模組檔案的模組介面檔案必須具有相同的基本名稱,例如,en.mli 是模組 en.ml 的模組介面。建立一個 lib/en.mli 檔案,其中包含以下內容:

val v : string

請注意,只有模組簽章宣告的清單(在 #show 輸出中的 sigend 之間)已寫入介面檔案 lib/en.mli。在專門針對模組的教學中會更詳細地說明這一點。

模組介面也用於建立私有定義。如果模組定義未在其對應的模組介面中列出,則該定義為私有。如果不存在模組介面檔案,則所有內容都是公開的。

在您偏好的編輯器中修改 lib/en.ml 檔案;將其中的內容替換為以下內容:

let hello = "Hello"
let v = hello ^ ", world!"

也像這樣編輯 bin/main.ml 檔案:

let () = Printf.printf "%s\n" Hello.En.hello

嘗試編譯會失敗。

$ opam exec -- dune build
File "hello/bin/main.ml", line 1, characters 30-43:
1 | let () = Printf.printf "%s\n" Hello.En.hello
                                  ^^^^^^^^^^^^^^
Error: Unbound value Hello.En.hello

這是因為我們尚未變更 lib/en.mli。由於它沒有列出 hello,因此它是私有的。

在程式庫中定義多個模組

可以在單一程式庫中定義多個模組。為了示範這一點,建立一個名為 lib/es.ml 的新檔案,其中包含以下內容:

let v = "¡Hola, mundo!"

並在 bin/main.ml 中使用新模組:

let () = Printf.printf "%s\n" Hello.Es.v
let () = Printf.printf "%s\n" Hello.En.v

最後,執行 dune builddune exec hello 以查看新的輸出,使用您剛在 hello 程式庫中建立的模組。

$ opam exec -- dune exec hello
¡Hola, mundo!
Hello, world!

有關模組的更詳細介紹,請參閱模組

從套件安裝和使用模組

OCaml 有一個活躍的開源貢獻者社群。大多數專案都可以使用 opam 套件管理器取得,您在安裝 OCaml 教學課程中已安裝該套件管理器。以下章節說明如何從 opam 的開源儲存庫安裝和使用套件。

為了說明這一點,讓我們更新 hello 專案,以使用 Sexplib 來剖析包含 S-表達式的字串,並將其列印回字串。首先,執行 opam update 來更新 opam 的套件清單。然後,使用以下命令安裝 Sexplib 套件:

$ opam install sexplib

接下來,在 bin/main.ml 中定義包含有效 S-表達式的字串。使用 Sexplib.Sexp.of_string 函式將其剖析為 S-表達式,然後使用 Sexplib.Sexp.to_string 將其轉換回字串並列印出來。

(* Read in Sexp from string *)
let exp1 = Sexplib.Sexp.of_string "(This (is an) (s expression))"

(* Do something with the Sexp ... *)

(* Convert back to a string to print *)
let () = Printf.printf "%s\n" (Sexplib.Sexp.to_string exp1)

您輸入的代表有效 S-表達式的字串會剖析為 S-表達式類型,該類型定義為 Atom (字串) 或 S-表達式 List(它是遞迴類型)。如需詳細資訊,請參閱Sexplib 文件

在範例建置和執行之前,您需要告知 Dune 它需要 Sexplib 來編譯專案。方法是將 Sexplib 新增至 bin/dune 檔案的 library stanza。完整的 bin/dune 檔案應與下列內容相符。

(executable
 (public_name hello)
 (name main)
 (libraries hello sexplib))

有趣的事實:Dune 設定檔是 S-表達式。

最後,像之前一樣執行:

$ opam exec -- dune exec hello
(This(is an)(s expression))

使用預處理器來產生程式碼

注意:此範例已使用 DkML 2.1.0 在 Windows 上成功測試。執行 dkml version 以查看版本。

假設我們希望 hello 像在 UTop 中顯示字串清單一樣顯示其輸出:["hello"; "using"; "an"; "opam"; "library"]。若要執行此操作,我們需要一個將 string list 轉換為 string 的函式,並新增方括號、空格和逗號。我們不要自己定義,而是使用套件自動產生它。我們將使用 ppx_deriving。以下是如何安裝它的方法:

$ opam install ppx_deriving

需要告知 Dune 如何使用它,這是在 lib/dune 檔案中完成的。請注意,這與您先前編輯的 bin/dune 檔案不同!開啟 lib/dune 檔案,並將其編輯為如下所示:

(library
 (name hello)
 (preprocess (pps ppx_deriving.show)))

程式碼 (preprocess (pps ppx_deriving.show)) 表示在編譯之前,需要使用套件 ppx_deriving 提供的預處理器 show 來轉換來源。不需要寫入 (libraries ppx_deriving),Dune 會從 preprocess stanza 推斷出來。

也需要編輯檔案 lib/en.mllib/en.mli

lib/en.mli

val string_of_string_list : string list -> string
val v : string list

lib/en.ml

let string_of_string_list = [%show: string list]

let v = String.split_on_char ' ' "Hello using an opam library"

讓我們從下往上讀取此程式碼:

  • v 的類型為 string list。我們使用 String.split_on_char,透過以空格字元分割字串,將 string 轉換為 string list
  • string_of_string_list 的類型為 string list -> string。這會將字串清單轉換為字串,並套用預期的格式。

最後,您也需要編輯 bin/main.ml

let () = print_endline Hello.En.(string_of_string_list v)

以下是結果:

$ opam exec -- dune exec hello
["Hello"; "using"; "an"; "opam"; "library"]

Dune 作為一站式商店的預覽

本節說明先前未提及的 dune init proj 所建立的檔案和目錄的用途。

在 OCaml 的歷史中,已使用過多種建置系統。在撰寫本教學課程時(2023 年夏季),Dune 是主流的建置系統,這也是本教學課程中使用它的原因。Dune 會自動從檔案中擷取模組之間的相依性,並以相容的順序編譯它們。每個有要建置的內容的目錄只需要一個 dune 檔案。dune init proj 建立的三個目錄具有以下用途:

  • bin:可執行程式
  • lib:程式庫
  • test:測試

將會有一個專門介紹 Dune 的教學課程。本教學課程將介紹 Dune 的許多功能,其中一些功能在此處列出:

  • 執行測試
  • 產生文件
  • 產生封裝中繼資料(此處在 hello.opam 中)
  • 使用通用規則建立任意檔案

_build 目錄是 Dune 儲存其產生之所有檔案的位置。它可以隨時刪除,但後續的建置會重新建立它。

最小設定

在最後一節中,讓我們建立一個最低限度的專案,突出顯示 Dune 運作真正需要的內容。首先,我們建立一個新的專案目錄:

$ cd ..
$ mkdir minimo
$ cd minimo

至少,Dune 只需要兩個檔案:dune-project 和一個 dune 檔案。以下是如何以最少的文字撰寫它們的方法:

dune-project

(lang dune 3.6)

dune

(executable (name minimo))

minimo.ml

let () = print_endline "My name is Minimo"

就是這樣!這足以讓 Dune 建置並執行 minimo.ml 檔案。

$ opam exec -- dune exec ./minimo.exe
My name is Minimo

注意minimo.exe 不是檔案名稱。這是在告知 Dune 使用 OCaml 的原生編譯器而非位元組碼編譯器來編譯 minimo.ml 檔案。有趣的是,請注意,空檔案是有效的 OCaml 語法。您可以使用它來進一步減少 minimo;當然,它不會顯示任何內容,但它將是一個有效的專案!

結論

本教學課程是「入門」系列的最後一個。接下來,您可以從其他教學課程中挑選並選擇,以遵循自己的學習路徑。

仍然需要協助嗎?

協助改善我們的文件

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

OCaml

創新。社群。安全。