編碼 · 程式語言
OCaml 入門教學
簡答
OCaml 是 ML 家族中的靜態型別函數式語言。它的編譯器會替你推論型別,在程式執行前就抓出許多錯誤,並產生快速的原生程式碼。程式由不可變的值、variant(帶標籤的聯集)、模式比對與模組組成。用 opam 安裝後,可以用 ocaml hello.ml 執行檔案,或用 ocamlopt 編譯。
OCaml 是什麼、誰在使用
OCaml 誕生於法國的 Inria,現在由包含 Jane Street 與 Tarides 等公司在內的開放原始碼社群開發。它結合函數式程式設計與可選的命令式特性(可變參照、陣列、迴圈),以及大部分程式碼用不到的物件系統。OCaml 5 透過 domain 與 effect handler 加入了共享記憶體的平行運算。
OCaml 用於編譯器與程式分析工具、Rocq(前身為 Coq)等證明輔助器、交易系統(Jane Street 是最知名的業界使用者),以及 Meta 的 Flow 與 Infer 等開發者工具。它也是教授函數式程式設計與型別系統的常見語言。
安裝 OCaml 並執行 Hello, world
安裝 OCaml 的標準方式是 opam,也就是 OCaml 的套件管理工具。安裝 opam 後執行一次 opam init,它會設定好編譯器。接著用 opam install dune utop 安裝建置工具 dune 與較友善的 REPL utop。
(* hello.ml *)
let () = print_endline "Hello, world!"ocaml hello.ml # run with the interpreter
ocamlopt -o hello hello.ml
./hello # native executable
# Hello, world!let () = ... 是撰寫程式進入點的慣用寫法:它會對運算式求值,並檢查其回傳為 unit(沒有值)。在 REPL(utop 或 ocaml)中,每個片段要以 ;; 結尾;在原始碼檔案中則不需要 ;;。對於實際的專案,dune init project myapp 會建立專案,dune build 則負責編譯。
值與基本型別
| 型別 | 範例 | 說明 |
|---|---|---|
| int | 42 | 在 64 位元系統上是 63 位元;運算子為 + - * / mod |
| float | 3.14 | 有自己的運算子:+. -. *. /. |
| string | "hello" | 不可變的位元組序列;用 ^ 連接 |
| char | 'a' | 單一位元組 |
| bool | true, false | &&, ||, not |
| unit | () | 沒有結果的運算式之值 |
let name = "Ana"
let age = 30
let height = 1.68 *. 100.0
let () = Printf.printf "%s is %d, %.0f cm\n" name age height
(* Ana is 30, 168 cm *)
let count = ref 0
let () = incr count; Printf.printf "count = %d\n" !count繫結是不可變的。ref 是一個可變的儲存格:!r 讀取它,r := v 寫入它。OCaml 絕不會在 int 與 float 之間自動轉換;請使用 float_of_int 與 int_of_float。
流程控制與函式
if 是運算式。函式用 let 定義;遞迴函式需要 let rec。引數以空格分隔,而且每個函式都是柯里化的,所以部分套用不需要額外成本。
let sign n =
if n > 0 then "positive"
else if n < 0 then "negative"
else "zero"
let rec factorial n = if n <= 1 then 1 else n * factorial (n - 1)
let add x y = x + y (* int -> int -> int *)
let add5 = add 5
let () =
for i = 1 to 3 do
Printf.printf "%d squared is %d\n" i (i * i)
done;
Printf.printf "%s %d %d\n" (sign (-2)) (factorial 5) (add5 10)
(* negative 120 15 *)請注意 sign (-2) 的括號:沒有括號的話,OCaml 會把 sign - 2 解讀成減法。要串接回傳 unit 的運算式,請使用 ;。
串列、陣列、元組與 record
| 結構 | 字面值 | 說明 |
|---|---|---|
| List | [1; 2; 3] | 不可變的單向鏈結串列;x :: xs 在前端新增元素 |
| Array | [| 1; 2; 3 |] | 大小固定、可變,以 a.(0) 存取索引 |
| Tuple | ("Ana", 30) | 一組固定的、型別可以不同的值 |
| Record | { name = "Ana"; age = 30 } | 具名欄位,需先宣告型別 |
| Hashtbl / Map | Hashtbl.create 16 | 可變的雜湊表 / 不可變的平衡樹 map |
type person = { name : string; age : int }
let people = [ { name = "Ana"; age = 30 }; { name = "Ben"; age = 17 } ]
let () =
people
|> List.filter (fun p -> p.age >= 18)
|> List.map (fun p -> p.name)
|> List.iter print_endline
(* Ana *)
let older = { (List.hd people) with age = 31 }管線運算子 x |> f 的意思是 f x。List 模組提供 map、filter、fold_left、find_opt 等許多函式。
招牌特色:variant、模式比對與模組
Variant 型別會列出一個值所有可能的案例。模式比對依案例拆解值,如果漏掉某個案例,編譯器會發出警告,讓重構更安全。
type shape =
| Circle of float
| Rectangle of float * float
let area = function
| Circle r -> Float.pi *. r *. r
| Rectangle (w, h) -> w *. h
let () =
List.iter (fun s -> Printf.printf "%.2f\n" (area s))
[ Circle 1.0; Rectangle (2.0, 3.0) ]
(* 3.14 then 6.00 *)
module StringMap = Map.Make (String)
let ages = StringMap.(empty |> add "Ana" 30 |> add "Ben" 17)
let () = Printf.printf "%d\n" (StringMap.find "Ana" ages) (* 30 *)模組將型別與函式分組,而且每個檔案都是一個模組(list.ml 就是 List)。Functor 是從模組到模組的函式:Map.Make (String) 會建立一個以字串為鍵的 map 模組。簽章(.mli 檔案)則隱藏實作細節。
錯誤處理
OCaml 有三種工具:option 用於可能缺值的值,result 用於可能失敗並附帶資訊的操作,例外則用於例外的狀況。
let parse_age s =
match int_of_string_opt s with
| Some n when n >= 0 -> Ok n
| Some _ -> Error "Age must not be negative"
| None -> Error "Not a number"
let () =
match parse_age "abc" with
| Ok n -> Printf.printf "Age %d\n" n
| Error msg -> print_endline ("Error: " ^ msg)
(* Error: Not a number *)
exception Empty_input
let first = function [] -> raise Empty_input | x :: _ -> x
let () =
try print_int (first []) with Empty_input -> print_endline "empty list"完整的小程式:字詞頻率
let () =
let text = "the cat and the hat and the bat" in
let counts = Hashtbl.create 16 in
String.split_on_char ' ' text
|> List.iter (fun w ->
let n = Option.value (Hashtbl.find_opt counts w) ~default:0 in
Hashtbl.replace counts w (n + 1));
Hashtbl.to_seq counts
|> List.of_seq
|> List.sort (fun (_, a) (_, b) -> compare b a)
|> List.filteri (fun i _ -> i < 2)
|> List.iter (fun (w, n) -> Printf.printf "%s %d\n" w n)
(* the 3
and 2 *)這個程式用可變的雜湊表統計字詞,把表轉成 (word, count) 配對的串列,依次數由大到小排序,並印出前兩個。~default:0 是帶標籤的引數。
接下來可以學什麼
- ocaml.org:安裝指南、「A Tour of OCaml」以及 Docs 底下的其他教學。
- OCaml 手冊,涵蓋完整的語言與標準函式庫參考。
- 學習用 dune 建置專案,以及用 opam 安裝函式庫。
- 試試免費的線上書籍「Real World OCaml」,裡面有較大型的實用範例。
常見問題
OCaml 很難學嗎?
語法本身很精簡,但如果你來自命令式語言,用型別、遞迴與模式比對的方式思考需要一些練習。型別檢查器的錯誤訊息很快就會變得容易閱讀。
為什麼 OCaml 的浮點數有獨立的運算子?
OCaml 不會多載算術運算子,所以 + 只能用於 int,+. 只能用於 float。這讓型別推論保持簡單且可預測。
OCaml 與 F# 有什麼差別?
F# 衍生自 OCaml,核心語法大多相同,但 F# 運行在 .NET 上並使用具語意的縮排,而 OCaml 則編譯成原生程式碼或位元組碼,且擁有含 functor、功能更強大的模組系統。
OCaml 什麼時候需要 ;;?
只有在互動式 toplevel(ocaml 或 utop)中才需要,它是用來告訴 REPL 對你輸入的內容求值。在 .ml 原始碼檔案中通常省略。