你的第一個 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 中包含已編譯的二進位檔不同,目錄 lib
和 bin
分別包含函式庫和程式的原始碼檔案。這是許多 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
輸出中的 sig
和 end
之間)已寫入介面檔案 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 build
和 dune 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
(()())
使用預處理器來產生程式碼
注意:此範例已使用 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.ml
和 lib/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
;當然,它不會顯示任何內容,但它將是一個有效的專案!
結論
本教學課程是「入門」系列的最後一個。接下來,您可以從其他教學課程中挑選並選擇,以遵循自己的學習路徑。