
บทความส่วนที่ 2 เจาะลึกพื้นฐานที่นักพัฒนาระบบ (systems developer) ต้องเข้าใจก่อนเขียน OCaml 5 ระดับ production ครอบคลุม type system และ memory layout, functions กับ performance tuning, pattern matching พร้อม algebraic data types (ADT) และ error handling แบบ type-safe ทุกหัวข้อมาพร้อมตัวอย่างที่ประกอบและ run ได้ทันที
ก่อนเขียน OCaml สำหรับงาน systems ต้องเข้าใจ 2 ชั้นพร้อมกัน คือชั้น type system ที่ compiler ตรวจสอบและชั้น runtime representation ว่าค่าแต่ละตัวถูกเก็บใน memory อย่างไร การรู้ทั้งสองชั้นช่วยให้เขียน code ที่ทั้ง correct (ถูกต้อง) และ performant (เร็ว) ไปพร้อมกัน
OCaml ใช้ Hindley-Milner type inference ซึ่งสามารถ infer (อนุมาน) type ของ expression ทั้งหมดได้โดยที่ผู้เขียนไม่ต้อง annotate ทุก function แต่ compiler ยังคง static typing อย่างเข้มงวด นั่นคือทุก error ที่เกี่ยวกับ type จะถูกจับตั้งแต่ compile time
(* compiler infer type ให้อัตโนมัติ *)
let add x y = x + y (* val add : int -> int -> int *)
let concat s1 s2 = s1 ^ s2 (* val concat : string -> string -> string *)
(* polymorphic function — 'a คือ type variable *)
let identity x = x (* val identity : 'a -> 'a *)
let swap (a, b) = (b, a) (* val swap : 'a * 'b -> 'b * 'a *)
(* annotate type เองเมื่อต้องการ clarity *)
let add_int (x : int) (y : int) : int = x + y
(* ใช้งาน *)
let () =
Printf.printf "add: %d\n" (add 3 4);
Printf.printf "concat: %s\n" (concat "Hello, " "OCaml");
Printf.printf "identity: %d\n" (identity 42)
ข้อดีของ type inference ต่อ systems code:
%%{init: {'theme':'base','themeVariables':{'primaryColor':'#3c3836','primaryTextColor':'#ebdbb2','primaryBorderColor':'#fabd2f','lineColor':'#83a598','secondaryColor':'#504945','tertiaryColor':'#282828','background':'#282828','mainBkg':'#3c3836','textColor':'#ebdbb2'}}}%%
flowchart LR
A[Source Code
ซอร์สโค้ด] --> B[Lexer
แยก tokens]
B --> C[Parser
สร้าง AST]
C --> D[Type Inference
Hindley-Milner]
D --> E{Type Check
ผ่านหรือไม่}
E -->|ผ่าน| F[Code Generation
สร้าง bytecode/native]
E -->|ไม่ผ่าน| G[Compile Error
จับ bug ก่อน runtime]
F --> H[Executable]
OCaml ใช้หลักการ uniform representation — ทุกค่ามีขนาด 1 word (64 บิตบน 64-bit system) ไม่ว่าจะเป็นชนิดใด โดยแยกว่าเป็น immediate value หรือ pointer to heap block ด้วย tag bit ที่บิตต่ำสุด
┌─────────────────────────────────────────────────────┐
│ LSB = 1 → Immediate (integer, constructor tag) │
│ LSB = 0 → Pointer to heap block │
└─────────────────────────────────────────────────────┘
นี่คือสาเหตุที่ int ใน OCaml เป็น 63-bit แทนที่จะเป็น 64-bit เต็ม
โดย คือค่าต่ำสุดและ คือค่าสูงสุดของ int บน 64-bit system โดย 1 bit ถูกใช้เป็น tag
การเข้ารหัส tagged integer:
เมื่อ n คือ integer ตรรกะที่โปรแกรมมองเห็น และ tagged_repr(n) คือ representation ใน memory การใช้ bit shift ซ้าย 1 ครั้งและ OR กับ 1 ทำให้ LSB = 1 เพื่อบ่งบอก GC ว่า "นี่ไม่ใช่ pointer อย่าไล่ scan"
(* ดู internal representation ผ่าน Obj module — ใช้เพื่อการศึกษาเท่านั้น *)
let () =
let n = 42 in
(* Obj.magic n ไม่ใช้ใน production แต่ช่วยให้เห็น raw word *)
Printf.printf "int 42 → tagged bits = %d (0x%x)\n"
((n lsl 1) lor 1) ((n lsl 1) lor 1);
Printf.printf "int max_int = %d\n" max_int;
Printf.printf "int min_int = %d\n" min_int;
(* แสดงให้เห็นว่า int เป็น 63-bit *)
Printf.printf "2^62 - 1 = %d (= max_int: %b)\n"
(int_of_float (2. ** 62.) - 1)
(int_of_float (2. ** 62.) - 1 = max_int)
ตารางสรุป representation ของ primitive types:
| Type | ขนาด (bits) | Boxed? | หมายเหตุ |
|---|---|---|---|
int |
63 | ❌ immediate | 1 bit tag |
bool |
63 | ❌ immediate | false=0, true=1 (tagged) |
char |
63 | ❌ immediate | ASCII only ใน stdlib |
unit |
63 | ❌ immediate | เหมือน 0 (tagged) |
float |
64 | ✅ boxed ปกติ | unboxed ใน float array/record |
int32 |
32 | ✅ boxed | มี header + data |
int64 |
64 | ✅ boxed | มี header + data |
nativeint |
32/64 | ✅ boxed | ตาม architecture |
string |
variable | ✅ boxed | immutable, byte sequence |
bytes |
variable | ✅ boxed | mutable byte sequence |
'a array |
variable | ✅ boxed | float array special-cased (unboxed) |
Unboxed หมายถึงค่าที่ถูกเก็บ "ในตำแหน่งเดียวกับ word นั้น" โดยตรง ไม่ต้องตามไป dereference ที่ heap — เร็วกว่าและไม่สร้าง garbage ให้ GC ต้องเก็บ
Boxed หมายถึงค่าที่อยู่ใน heap block ต่างหาก — word ในตัวแปรคือ pointer ไปที่ block นั้น
(* float ปกติ boxed แต่ใน float array และ record ที่ทุก field เป็น float จะ unboxed *)
(* boxed: float อยู่ใน heap block แยก *)
let a : float option = Some 3.14 (* boxed *)
(* unboxed: array ของ float ถูก optimize ให้เก็บ double ติดกันเลย *)
let arr : float array = [| 1.0; 2.0; 3.0; 4.0 |] (* unboxed, flat *)
(* record ที่ทุก field เป็น float → unboxed เหมือน C struct *)
type point3d = { x : float; y : float; z : float }
let p = { x = 1.0; y = 2.0; z = 3.0 } (* unboxed in place *)
(* ถ้ามี field ที่ไม่ใช่ float ปนเข้ามา → boxed ปกติ *)
type mixed = { name : string; value : float }
let m = { name = "alpha"; value = 1.5 } (* boxed *)
(* Performance test: float array vs array of boxed floats *)
let bench_sum_flat () =
let a = Array.init 1_000_000 float_of_int in
let t0 = Unix.gettimeofday () in
let s = Array.fold_left (+.) 0.0 a in
let t1 = Unix.gettimeofday () in
Printf.printf "flat float array sum = %f (%.3f ms)\n"
s ((t1 -. t0) *. 1000.)
let () = bench_sum_flat ()
หลักการสำคัญสำหรับ systems code:
float array หรือ Bigarray.Array1 เพื่อหลีกเลี่ยง box/unbox
%%{init: {'theme':'base','themeVariables':{'primaryColor':'#3c3836','primaryTextColor':'#ebdbb2','primaryBorderColor':'#fabd2f','lineColor':'#83a598','secondaryColor':'#504945','tertiaryColor':'#282828','background':'#282828','mainBkg':'#3c3836','textColor':'#ebdbb2'}}}%%
flowchart TB
subgraph Stack["Stack / Register"]
V1["int 42
(tagged: 85)"]
V2["float ptr
0x7f..."]
V3["arr ptr
0x7f..."]
end
subgraph MinorHeap["Minor Heap"]
B1["Header | 3.14
boxed float"]
B2["Header | len=4
1.0 | 2.0 | 3.0 | 4.0
flat float array"]
end
V1 -.->|immediate
ไม่ต้อง deref| V1
V2 -->|pointer
ต้อง deref| B1
V3 -->|pointer| B2
ทุก composite value ที่ boxed จะอยู่ใน heap block ซึ่งมีโครงสร้างดังนี้
┌──────────────┬──────────────┬─────────────────────────┐
│ Header (1w) │ Field 0 │ Field 1 │ ... │
│ size|color │ │ │ │
│ |tag │ │ │ │
└──────────────┴──────────────┴─────────────────────────┘
Header 1 word เก็บ metadata สำหรับ GC:
(* Record — field layout ตามลำดับที่ประกาศ *)
type user = {
id : int;
name : string;
email : string;
active : bool;
}
(* memory: [header][id][name ptr][email ptr][active] *)
(* Variants — constructors ได้ tag ตามลำดับ *)
type status =
| Idle (* tag 0, unboxed immediate *)
| Busy (* tag 1, unboxed immediate *)
| Error of string (* tag 0, boxed block with 1 field *)
| Timeout of int * int (* tag 1, boxed block with 2 fields *)
(* หมายเหตุ: constructor ที่ไม่มี argument → unboxed (immediate tag)
constructor ที่มี argument → heap block แยก *)
let u = { id = 1; name = "moo"; email = "moo@example.com"; active = true }
let print_status = function
| Idle -> print_endline "idle"
| Busy -> print_endline "busy"
| Error msg -> Printf.printf "error: %s\n" msg
| Timeout (start, dur) -> Printf.printf "timeout at %d for %d\n" start dur
let () =
print_status Idle;
print_status (Error "connection refused");
print_status (Timeout (100, 30));
Printf.printf "user: %s <%s>\n" u.name u.email
สำหรับ performance ระดับ systems บางครั้งจำเป็นต้องใช้ attribute เพื่อบังคับ layout:
(* [@@unboxed] — สำหรับ record/variant ที่มี field เดียว
ทำให้ไม่มี wrapping block *)
type user_id = UserId of int [@@unboxed]
(* UserId 42 → มี representation เหมือน 42 ตรงๆ *)
(* [@@immediate] — บอก compiler ว่า type นี้ immediate เสมอ
ช่วย optimize pattern match *)
type color = Red | Green | Blue [@@immediate]
Record fields และ variable bindings เป็น immutable โดย default เว้นแต่เราประกาศ mutable อย่างชัดเจน นี่ไม่ใช่แค่เรื่อง code style — มันมีผลตรงต่อประสิทธิภาพของ GC ด้วย
type counter = { mutable value : int } (* mutable เฉพาะ field ที่ระบุ *)
type point = { x : float; y : float } (* immutable ทั้งหมด *)
(* update แบบ functional — สร้าง record ใหม่ *)
let move p dx dy = { x = p.x +. dx; y = p.y +. dy }
(* update แบบ in-place — ต้อง mutable *)
let increment c = c.value <- c.value + 1
ทำไม immutability ช่วย GC?
OCaml GC เป็น generational — assumption ว่า "most objects die young" ใช้ได้ผลเพราะ functional update สร้างของใหม่และทิ้งของเก่าตลอดเวลา หาก object ยังอยู่ใน minor heap ตอนเกิด minor GC เท่านั้นที่จะถูก promote ไป major heap ของ temporary ส่วนใหญ่ถูก reclaim ด้วย cost เกือบเป็นศูนย์
แต่ mutability ข้าม generation สร้างปัญหาที่เรียกว่า write barrier:
โดย คือต้นทุนเพิ่มเติมเมื่อ object ใน major heap ชี้ไป minor heap (OCaml ต้องจำไว้ใน remembered set)
(* code ที่ถูกกว่าในแง่ GC *)
let good () =
let p1 = { x = 1.0; y = 2.0 } in (* allocate ใน minor *)
let p2 = { p1 with x = 3.0 } in (* allocate ใหม่ใน minor *)
p2 (* p1 ตายเองตอน minor GC *)
(* code ที่ตึง GC มากกว่าเพราะ write barrier *)
let bad (cache : (int, string) Hashtbl.t) =
for i = 0 to 1_000_000 do
Hashtbl.replace cache i (string_of_int i) (* mutate ข้าม generation *)
done
แนวปฏิบัติ:
mutable เฉพาะ hot path ที่วัดแล้วว่าคุ้มFunctions ใน OCaml เป็น first-class citizen ทุกฟังก์ชันเป็น value ที่ส่งไปมาได้ การเข้าใจ currying, partial application และ tail-call optimization (TCO) เป็นพื้นฐานของ code ที่ทั้งสวยและเร็ว
ทุก function ใน OCaml รับ argument ทีละตัวจริงๆ แม้ว่าเวลาเขียนจะดูเหมือนรับหลายตัว — นี่คือ currying
(* ดูเหมือนรับ 3 arguments แต่จริงๆ เป็น function 3 ชั้น *)
let add3 a b c = a + b + c
(* val add3 : int -> int -> int -> int *)
(* อ่านว่า: int -> (int -> (int -> int)) *)
(* partial application — จ่าย argument บางตัวได้ function ใหม่ *)
let add10 = add3 10
(* val add10 : int -> int -> int *)
let add_10_20 = add3 10 20
(* val add_10_20 : int -> int *)
(* ใช้งาน *)
let () =
Printf.printf "%d\n" (add3 1 2 3); (* 6 *)
Printf.printf "%d\n" (add10 5 5); (* 20 *)
Printf.printf "%d\n" (add_10_20 100) (* 130 *)
ประโยชน์สำหรับ systems code:
|> (pipe operator) ทำให้ data pipeline อ่านง่าย(* ตัวอย่าง pipeline ด้วย partial application *)
let log_with prefix msg = Printf.printf "[%s] %s\n" prefix msg
let log_info = log_with "INFO"
let log_error = log_with "ERROR"
let process_records records =
records
|> List.filter (fun r -> r.active)
|> List.map (fun r -> r.name)
|> List.sort compare
(* โดยที่ records : user list *)
เมื่อ function มี parameter เยอะ labeled arguments ช่วยให้ call site อ่านง่ายและไม่สลับลำดับพลาด
(* labeled arguments ด้วย ~ *)
let create_socket ~host ~port ~timeout =
Printf.sprintf "connect(%s:%d, timeout=%d)" host port timeout
(* optional arguments ด้วย ? — ต้องมี default *)
let connect ?(timeout = 30) ?(retry = 3) ~host ~port () =
Printf.sprintf "host=%s port=%d timeout=%d retry=%d"
host port timeout retry
let () =
(* เรียกแบบเต็ม *)
print_endline (create_socket ~host:"localhost" ~port:8080 ~timeout:60);
(* สลับลำดับได้ — compiler ไม่ว่า *)
print_endline (create_socket ~timeout:60 ~port:8080 ~host:"localhost");
(* optional: ไม่ใส่ timeout กับ retry ก็ได้ *)
print_endline (connect ~host:"db.local" ~port:5432 ());
(* ใส่แค่บางตัว *)
print_endline (connect ~host:"db.local" ~port:5432 ~timeout:120 ())
ข้อควรระวัง: optional argument ต้องตามด้วย non-optional หรือ () (unit) ไม่อย่างนั้น compiler จะสับสนว่าเราจ่ายครบหรือยัง
TCO คือการที่ compiler เปลี่ยน tail-recursive call เป็น jump ธรรมดาแทนที่จะ push stack frame ใหม่ ทำให้ recursion ลึกแค่ไหนก็ไม่ stack overflow — สำคัญมากสำหรับ systems code ที่ต้องจัดการ data จำนวนมาก
Tail position คือจุดที่ expression เป็นผลลัพธ์สุดท้ายของ function โดยไม่ต้องทำอะไรต่อหลังจากนั้น
(* ❌ ไม่ใช่ tail call — ต้อง + x หลังจาก recursive return *)
let rec sum_bad = function
| [] -> 0
| x :: rest -> x + sum_bad rest (* x + <result> ไม่ใช่ tail position *)
(* ✅ tail-recursive ด้วย accumulator pattern *)
let sum_good lst =
let rec loop acc = function
| [] -> acc
| x :: rest -> loop (acc + x) rest (* tail call *)
in
loop 0 lst
(* ทดสอบกับ list ยาวมาก *)
let big_list = List.init 1_000_000 (fun i -> i)
let () =
(* sum_bad big_list จะ stack overflow *)
try
let s = sum_bad big_list in
Printf.printf "sum_bad: %d\n" s
with Stack_overflow ->
print_endline "sum_bad: Stack overflow!";
(* sum_good ทำงานได้ปกติ *)
let s = sum_good big_list in
Printf.printf "sum_good: %d\n" s
Accumulator pattern คือ idiom หลักสำหรับเขียน tail-recursive function:
(* reverse list แบบ tail-recursive *)
let reverse lst =
let rec loop acc = function
| [] -> acc
| x :: rest -> loop (x :: acc) rest
in
loop [] lst
(* map แบบ tail-recursive (stdlib List.map ไม่ tail-rec ใน version เก่า) *)
let map_tr f lst =
let rec loop acc = function
| [] -> List.rev acc
| x :: rest -> loop (f x :: acc) rest
in
loop [] lst
(* fold_left เป็น tail-recursive โดยธรรมชาติ — ควรใช้มากกว่า fold_right *)
let sum_fold = List.fold_left (+) 0
let () =
Printf.printf "reverse [1;2;3] = %s\n"
(String.concat ";" (List.map string_of_int (reverse [1;2;3])));
Printf.printf "sum_fold: %d\n" (sum_fold [1;2;3;4;5])
สำหรับคนที่มาจาก C/Python การเขียน for หรือ while loop อาจจะคุ้นมือกว่า แต่ใน OCaml การใช้ tail recursion มักจะทั้งอ่านง่ายกว่าและเปิดทางให้ compiler optimize ได้ดีกว่า
(* imperative style — OK แต่ต้องใช้ ref *)
let factorial_imp n =
let result = ref 1 in
for i = 1 to n do
result := !result * i
done;
!result
(* functional style — tail recursive *)
let factorial_func n =
let rec loop i acc =
if i > n then acc
else loop (i + 1) (acc * i)
in
loop 1 1
(* benchmark ดูความต่าง *)
let bench name f =
let t0 = Unix.gettimeofday () in
let _ = f () in
let t1 = Unix.gettimeofday () in
Printf.printf "%s: %.6f sec\n" name (t1 -. t0)
let () =
bench "imperative" (fun () ->
for _ = 1 to 100_000 do ignore (factorial_imp 20) done);
bench "functional" (fun () ->
for _ = 1 to 100_000 do ignore (factorial_func 20) done)
กฎทองสำหรับ performance loop:
List.fold_left แทน List.fold_right เมื่อ order ไม่สำคัญArray.unsafe_get / Array.unsafe_set ใน hot path หลังจาก bounds check แล้ว[@tail_mod_cons] หรือ manual accumulator สำหรับ function ที่ return list
%%{init: {'theme':'base','themeVariables':{'primaryColor':'#3c3836','primaryTextColor':'#ebdbb2','primaryBorderColor':'#fabd2f','lineColor':'#83a598','secondaryColor':'#504945','tertiaryColor':'#282828','background':'#282828','mainBkg':'#3c3836','textColor':'#ebdbb2'}}}%%
flowchart TB
A[Function call] --> B{อยู่ใน
tail position?}
B -->|ใช่| C[Reuse stack frame
ใช้ jump ธรรมดา]
B -->|ไม่ใช่| D[Push new stack frame
เสี่ยง stack overflow]
C --> E["O(1) stack space
เหมือน loop"]
D --> F["O(n) stack space
แตกที่ n ใหญ่"]
Algebraic Data Types (ADT) คือ superpower ของ OCaml สำหรับการ model state และ error อย่างแม่นยำ เมื่อรวมกับ pattern matching ที่ compiler ตรวจ exhaustiveness ให้ bug ประเภท "ลืม handle case นี้" จะหายไปเกือบหมดก่อน run
Sum type คือการพูดว่า "value นี้จะเป็นอย่างใดอย่างหนึ่งจากลิสต์ที่กำหนด" แต่ละทางเลือกเรียกว่า constructor และอาจมี payload หรือไม่ก็ได้
(* state machine ของ TCP connection *)
type tcp_state =
| Closed
| Listen
| Syn_sent
| Syn_received
| Established
| Fin_wait_1
| Fin_wait_2
| Close_wait
| Closing
| Last_ack
| Time_wait
(* variant with payload — แต่ละ case พกข้อมูลต่างกันได้ *)
type network_event =
| Connected of { remote : string; port : int }
| Disconnected of { reason : string; code : int }
| DataReceived of bytes
| Error of exn
(* ใช้ pattern match handle แต่ละ case *)
let handle_event = function
| Connected { remote; port } ->
Printf.printf "✓ connected to %s:%d\n" remote port
| Disconnected { reason; code } ->
Printf.printf "✗ disconnected: %s (code=%d)\n" reason code
| DataReceived data ->
Printf.printf "→ received %d bytes\n" (Bytes.length data)
| Error e ->
Printf.printf "! error: %s\n" (Printexc.to_string e)
let () =
handle_event (Connected { remote = "192.168.1.1"; port = 443 });
handle_event (DataReceived (Bytes.of_string "GET / HTTP/1.1"));
handle_event (Disconnected { reason = "timeout"; code = 408 })
Product type คือ "value นี้มีทุก field พร้อมกัน" ใช้สำหรับข้อมูลที่มีหลาย attribute
type server_config = {
host : string;
port : int;
max_connections : int;
tls_enabled : bool;
cert_path : string option;
}
(* สร้าง record *)
let default_config = {
host = "0.0.0.0";
port = 8080;
max_connections = 1000;
tls_enabled = false;
cert_path = None;
}
(* functional update ด้วย with syntax *)
let secure_config = {
default_config with
port = 443;
tls_enabled = true;
cert_path = Some "/etc/ssl/server.pem";
}
(* destructuring ใน pattern match *)
let describe_config { host; port; tls_enabled; _ } =
Printf.sprintf "%s://%s:%d"
(if tls_enabled then "https" else "http")
host port
let () =
print_endline (describe_config default_config);
print_endline (describe_config secure_config)
การรวม sum กับ product: state machine ที่แต่ละ state พกข้อมูลของตัวเอง
type connection =
| Idle
| Connecting of { attempt : int; started_at : float }
| Active of {
socket : Unix.file_descr;
peer : string;
bytes_sent : int;
bytes_received : int;
}
| Failed of { error : string; retry_after : float }
let state_summary = function
| Idle -> "idle"
| Connecting { attempt; _ } -> Printf.sprintf "connecting (try #%d)" attempt
| Active { peer; bytes_sent; bytes_received; _ } ->
Printf.sprintf "active with %s (tx=%d, rx=%d)" peer bytes_sent bytes_received
| Failed { error; _ } -> Printf.sprintf "failed: %s" error
หัวใจของ pattern matching คือ compiler เตือนเมื่อเรา handle ไม่ครบ ทุก case — เปิด warning 8 (exhaustiveness) ให้เป็น error ใน project จริง
(* ไฟล์ dune ควรเขียน *)
(* (env (_ (flags (:standard -w @8)))) *)
(* -w @8 หมายถึง treat warning 8 (non-exhaustive match) as error *)
type alert_level = Info | Warning | Critical
(* ❌ compiler จะเตือน: Warning 8: non-exhaustive match *)
let color_of_level_bad = function
| Info -> "blue"
| Warning -> "yellow"
(* ลืม Critical *)
(* ✅ handle ทุก case *)
let color_of_level = function
| Info -> "blue"
| Warning -> "yellow"
| Critical -> "red"
(* เมื่อเพิ่ม constructor ใหม่ compiler จะไล่ทุกจุดที่ต้องแก้ *)
type alert_level_v2 = Info | Warning | Critical | Fatal
(* ตอนนี้ color_of_level ต้องแก้ — compiler error ที่ทุกจุดที่ลืม *)
นี่คือ feature ที่เปลี่ยนวิธีเขียน code: การเพิ่ม state ใหม่ไม่ใช่เรื่องน่ากลัว เพราะ compiler พาเราไปที่ทุกจุดที่ต้องแก้
(* guard clause ด้วย when *)
let classify_number = function
| n when n < 0 -> "negative"
| 0 -> "zero"
| n when n < 10 -> "small"
| n when n < 100 -> "medium"
| _ -> "large"
(* or-pattern — รวม case ที่ handle เหมือนกัน *)
let is_whitespace = function
| ' ' | '\t' | '\n' | '\r' -> true
| _ -> false
(* as-pattern — bind ทั้ง whole กับ sub-pattern *)
let first_positive = function
| [] -> None
| (x :: _) as lst when x > 0 -> Some lst (* bind whole list *)
| _ :: rest -> Some rest
(* destructuring ซ้อน *)
type point = { x : float; y : float }
type shape =
| Circle of { center : point; radius : float }
| Rectangle of { tl : point; br : point }
let area = function
| Circle { radius; _ } -> Float.pi *. radius *. radius
| Rectangle { tl = { x = x1; y = y1 }; br = { x = x2; y = y2 } } ->
abs_float ((x2 -. x1) *. (y2 -. y1))
let () =
Printf.printf "%s\n" (classify_number (-5));
Printf.printf "%s\n" (classify_number 50);
Printf.printf "area circle: %.2f\n"
(area (Circle { center = { x = 0.; y = 0. }; radius = 10. }));
Printf.printf "area rect: %.2f\n"
(area (Rectangle {
tl = { x = 0.; y = 0. };
br = { x = 5.; y = 3. }
}))
Recursive types คือ type ที่อ้างอิงตัวเอง — ฐานของ data structure ทั้งหลาย
(* custom linked list *)
type 'a my_list =
| Nil
| Cons of 'a * 'a my_list
let rec length = function
| Nil -> 0
| Cons (_, rest) -> 1 + length rest
(* binary tree *)
type 'a tree =
| Leaf
| Node of { value : 'a; left : 'a tree; right : 'a tree }
let rec insert x = function
| Leaf -> Node { value = x; left = Leaf; right = Leaf }
| Node { value; left; right } ->
if x < value then Node { value; left = insert x left; right }
else if x > value then Node { value; left; right = insert x right }
else Node { value; left; right } (* duplicate: ignore *)
let rec in_order = function
| Leaf -> []
| Node { value; left; right } ->
in_order left @ (value :: in_order right)
(* trie สำหรับ string prefix lookup — ใช้ในงาน networking เช่น routing table *)
module Trie = struct
type t = {
is_end : bool;
children : (char, t) Hashtbl.t;
}
let empty () = { is_end = false; children = Hashtbl.create 4 }
let rec insert t word i =
if i >= String.length word then { t with is_end = true }
else
let c = word.[i] in
let child =
match Hashtbl.find_opt t.children c with
| Some x -> x
| None -> empty ()
in
let child' = insert child word (i + 1) in
Hashtbl.replace t.children c child';
t
let rec contains t word i =
if i >= String.length word then t.is_end
else
match Hashtbl.find_opt t.children word.[i] with
| None -> false
| Some child -> contains child word (i + 1)
let add t word = insert t word 0
let member t word = contains t word 0
end
(* ใช้งาน *)
let () =
(* BST *)
let bst =
List.fold_left (fun t x -> insert x t) Leaf [5; 3; 8; 1; 4; 7; 9]
in
let sorted = in_order bst in
Printf.printf "sorted: %s\n"
(String.concat ", " (List.map string_of_int sorted));
(* Trie *)
let t = Trie.empty () in
let _ = Trie.add t "hello" in
let _ = Trie.add t "help" in
let _ = Trie.add t "hi" in
Printf.printf "'hello' in trie: %b\n" (Trie.member t "hello");
Printf.printf "'held' in trie: %b\n" (Trie.member t "held")
%%{init: {'theme':'base','themeVariables':{'primaryColor':'#3c3836','primaryTextColor':'#ebdbb2','primaryBorderColor':'#fabd2f','lineColor':'#83a598','secondaryColor':'#504945','tertiaryColor':'#282828','background':'#282828','mainBkg':'#3c3836','textColor':'#ebdbb2'}}}%%
flowchart TB
subgraph ADT["Algebraic Data Types"]
SUM["Sum Type
(Variant / Union)
A หรือ B หรือ C"]
PROD["Product Type
(Record / Tuple)
A และ B และ C"]
REC["Recursive Type
อ้างอิงตัวเอง
tree, list, trie"]
end
SUM --> USE1["State machines
Error types
Protocol messages"]
PROD --> USE2["Configs
Entities
DTOs"]
REC --> USE3["Parse trees
Routing tables
Search structures"]
USE1 & USE2 & USE3 --> PM["Pattern Matching
+ Exhaustiveness Check
= จับ bug ตั้งแต่ compile"]
Systems code เผชิญกับ error ตลอดเวลา — network fail, disk full, parse error, timeout OCaml ไม่มี null และให้เครื่องมือ 3 อย่างในการจัดการ error: option, result และ exceptions การเลือกใช้แต่ละแบบให้เหมาะกับสถานการณ์คือทักษะสำคัญ
option แทน Null Pointeroption คือ sum type แบบ built-in สำหรับค่า "อาจไม่มี" — replacement ของ null ที่ปลอดภัยกว่าเพราะ compiler บังคับให้เรา handle กรณี None
(* definition ใน stdlib *)
(* type 'a option = None | Some of 'a *)
(* lookup ที่อาจไม่เจอ *)
let find_user users id =
List.find_opt (fun u -> u.id = id) users
type user = { id : int; name : string }
let users = [
{ id = 1; name = "moo" };
{ id = 2; name = "alice" };
]
(* ใช้งาน — บังคับให้ handle None *)
let greet users id =
match find_user users id with
| Some u -> Printf.printf "Hello, %s!\n" u.name
| None -> Printf.printf "User %d not found\n" id
let () =
greet users 1;
greet users 99
Option combinators ใน stdlib ช่วย chain operations โดยไม่ต้อง nested match:
(* Option.map — ทำกับค่าถ้ามี *)
let shout_name u = Option.map (fun u -> String.uppercase_ascii u.name) u
(* Option.bind — chain function ที่ return option *)
let find_admin users =
find_user users 1
|> Option.bind (fun u ->
if u.name = "moo" then Some u else None)
(* Option.value — ให้ default ถ้าเป็น None *)
let name_of_user u = Option.value (Option.map (fun u -> u.name) u) ~default:"<unknown>"
let () =
print_endline (name_of_user (find_user users 1));
print_endline (name_of_user (find_user users 99))
result สำหรับ Recoverable Errorsเมื่อเราต้องการ ข้อมูลว่า error คืออะไร ไม่ใช่แค่ "ไม่มี" ให้ใช้ result
(* definition ใน stdlib *)
(* type ('a, 'e) result = Ok of 'a | Error of 'e *)
(* ดีไซน์ error type ให้เฉพาะเจาะจง *)
type db_error =
| NotFound of string
| ConnectionLost
| Timeout of float
| PermissionDenied of { user : string; resource : string }
(* function ที่อาจล้มเหลว *)
let fetch_user id : (user, db_error) result =
if id < 0 then Error (NotFound (Printf.sprintf "user %d" id))
else if id = 0 then Error ConnectionLost
else Ok { id; name = "user_" ^ string_of_int id }
(* handle แต่ละ error type เฉพาะ *)
let describe_result = function
| Ok u -> Printf.sprintf "got user: %s" u.name
| Error (NotFound s) -> Printf.sprintf "not found: %s" s
| Error ConnectionLost -> "connection lost"
| Error (Timeout t) -> Printf.sprintf "timeout after %.1fs" t
| Error (PermissionDenied { user; resource }) ->
Printf.sprintf "%s cannot access %s" user resource
let () =
print_endline (describe_result (fetch_user 42));
print_endline (describe_result (fetch_user (-1)));
print_endline (describe_result (fetch_user 0))
let*OCaml 4.08+ มี binding operators (let*, let+, and*) ทำให้ chain operations ที่ return option/result อ่านเหมือน imperative code แต่ type-safe เต็มรูปแบบ
module Result_syntax = struct
let ( let* ) = Result.bind
let ( let+ ) r f = Result.map f r
end
(* ไม่มี monadic syntax — nested และอ่านยาก *)
let parse_and_validate_v1 s =
match int_of_string_opt s with
| None -> Error "not a number"
| Some n ->
if n < 0 then Error "negative"
else if n > 100 then Error "too big"
else Ok (n * 2)
(* มี monadic syntax — flat และอ่านเหมือน imperative *)
let parse_and_validate_v2 s =
let open Result_syntax in
let* n =
Option.to_result ~none:"not a number" (int_of_string_opt s)
in
let* n =
if n < 0 then Error "negative" else Ok n
in
let* n =
if n > 100 then Error "too big" else Ok n
in
Ok (n * 2)
(* ยิ่งซับซ้อน ยิ่งได้ประโยชน์ *)
type config = {
host : string;
port : int;
timeout : int;
}
let parse_config_line line =
let open Result_syntax in
match String.split_on_char '=' line with
| [ k; v ] -> Ok (String.trim k, String.trim v)
| _ -> Error (Printf.sprintf "bad line: %s" line)
let find_field fields name =
match List.assoc_opt name fields with
| Some v -> Ok v
| None -> Error (Printf.sprintf "missing field: %s" name)
let parse_int_field fields name =
let open Result_syntax in
let* v = find_field fields name in
match int_of_string_opt v with
| Some n -> Ok n
| None -> Error (Printf.sprintf "bad int: %s" v)
let parse_config lines =
let open Result_syntax in
let* fields = List.fold_left (fun acc line ->
let* acc = acc in
let* field = parse_config_line line in
Ok (field :: acc)
) (Ok []) lines in
let* host = find_field fields "host" in
let* port = parse_int_field fields "port" in
let* timeout = parse_int_field fields "timeout" in
Ok { host; port; timeout }
let () =
let lines = [ "host=localhost"; "port=8080"; "timeout=30" ] in
match parse_config lines with
| Ok c ->
Printf.printf "config: %s:%d (timeout=%d)\n" c.host c.port c.timeout
| Error e ->
Printf.printf "error: %s\n" e
OCaml มี exception และมันเร็วกว่า result สำหรับ rare errors ในบาง workload กฎคร่าวๆ:
| สถานการณ์ | ใช้ |
|---|---|
| Error ที่ caller ควรตัดสินใจ handle | result |
| Error ที่ recover ไม่ได้ (bug, invariant violated) | assert / exception |
| Performance-critical path, rare error | exception + try ... with |
| API ที่คนอื่นใช้ | result (ชัดเจนใน type) |
| Control flow ลึกๆ แบบ early return | exception (local scope) |
(* บาง case exception ดีกว่า result — early exit จาก nested loop *)
exception Found of int
let find_first_match arr pred =
try
Array.iteri (fun i x -> if pred x then raise (Found i)) arr;
None
with Found i -> Some i
(* bad practice: ใช้ exception สำหรับ expected error ที่ user ต้อง handle *)
(* good practice แทน: *)
let parse_int_safe s =
try Ok (int_of_string s)
with Failure _ -> Error (Printf.sprintf "not an int: %s" s)
(* custom exception สำหรับ invariant violation *)
exception Invariant_violated of string
let dequeue q =
if Queue.is_empty q then
raise (Invariant_violated "dequeue from empty queue")
else
Queue.pop q
let () =
let arr = [| 1; 3; 5; 7; 9; 10; 11 |] in
match find_first_match arr (fun x -> x mod 2 = 0) with
| Some i -> Printf.printf "first even at index %d\n" i
| None -> print_endline "no even found"
%%{init: {'theme':'base','themeVariables':{'primaryColor':'#3c3836','primaryTextColor':'#ebdbb2','primaryBorderColor':'#fabd2f','lineColor':'#83a598','secondaryColor':'#504945','tertiaryColor':'#282828','background':'#282828','mainBkg':'#3c3836','textColor':'#ebdbb2'}}}%%
flowchart TB
START["ต้อง return error?"] --> Q1{Error นี้
caller ต้องเห็น
ใน type signature?}
Q1 -->|ใช่| Q2{มีข้อมูล
error หลายชนิด?}
Q1 -->|ไม่| Q3{Bug หรือ
invariant ผิด?}
Q2 -->|ใช่| R1["result with
custom error variant"]
Q2 -->|ไม่ แค่มี/ไม่มี| R2["option"]
Q3 -->|ใช่| E1["raise exception
หรือ assert"]
Q3 -->|ไม่| Q4{Rare error
ใน hot path?}
Q4 -->|ใช่| E2["exception
+ try...with รอบนอก"]
Q4 -->|ไม่| R1
ตารางเปรียบเทียบในเชิง performance:
| เครื่องมือ | Type-safe | Cost เมื่อ success | Cost เมื่อ failure | ใช้ใน type signature |
|---|---|---|---|---|
option |
✅ | 1 allocation | 0 (None = immediate) | ✅ |
result |
✅ | 1 allocation | 1 allocation | ✅ |
| exception | ❌ | 0 | สูง (stack unwind) | ❌ (invisible) |
assert false |
❌ | N/A | crash | ❌ |
โดย คือต้นทุนการ allocate heap block, คือต้นทุน pattern match และ คือความลึกของ call stack ระหว่าง raise และ try
ตัวอย่างจริงที่รวมทั้ง option, result และ exception เข้าด้วยกันใน flow ของการอ่าน config แล้วเปิด connection:
(* systems/error_flow.ml *)
(* domain error types *)
type config_error =
| File_missing of string
| Parse_error of { line : int; message : string }
| Missing_field of string
type connection_error =
| Dns_failed of string
| Connection_refused
| Tls_handshake_failed
type app_error =
| Config of config_error
| Connection of connection_error
(* helper — read file ที่อาจไม่เจอ *)
let read_file_opt path =
try Some (In_channel.with_open_text path In_channel.input_all)
with Sys_error _ -> None
(* parse config — return result *)
let parse_config path =
match read_file_opt path with
| None -> Error (File_missing path)
| Some content ->
let lines = String.split_on_char '\n' content in
(* ทำ simplified parse — production ใช้ parser combinator *)
let fields =
List.filter_map (fun line ->
let line = String.trim line in
if line = "" || String.length line < 1 || line.[0] = '#' then None
else
match String.index_opt line '=' with
| Some i ->
let k = String.sub line 0 i |> String.trim in
let v =
String.sub line (i+1) (String.length line - i - 1)
|> String.trim
in
Some (k, v)
| None -> None
) lines
in
let get name =
match List.assoc_opt name fields with
| Some v -> Ok v
| None -> Error (Missing_field name)
in
let ( let* ) = Result.bind in
let* host = get "host" in
let* port_s = get "port" in
match int_of_string_opt port_s with
| None -> Error (Parse_error { line = 0; message = "port not int" })
| Some port -> Ok (host, port)
(* simulated connect — return result *)
let connect host port =
if host = "" then Error (Dns_failed host)
else if port = 0 then Error Connection_refused
else Ok (Printf.sprintf "connected to %s:%d" host port)
(* top-level flow ที่รวมทุกชั้นของ error *)
let run config_path =
let ( let* ) = Result.bind in
let* (host, port) =
parse_config config_path
|> Result.map_error (fun e -> Config e)
in
let* conn =
connect host port
|> Result.map_error (fun e -> Connection e)
in
Ok conn
(* pretty-print error *)
let describe_error = function
| Config (File_missing p) -> Printf.sprintf "config file missing: %s" p
| Config (Parse_error { line; message }) ->
Printf.sprintf "parse error line %d: %s" line message
| Config (Missing_field f) -> Printf.sprintf "config missing field: %s" f
| Connection (Dns_failed h) -> Printf.sprintf "DNS failed: %s" h
| Connection Connection_refused -> "connection refused"
| Connection Tls_handshake_failed -> "TLS handshake failed"
(* entry point *)
let () =
(* สร้าง config file ตัวอย่าง *)
Out_channel.with_open_text "/tmp/app.conf" (fun oc ->
Out_channel.output_string oc "# sample config\nhost=localhost\nport=8080\n");
match run "/tmp/app.conf" with
| Ok s -> Printf.printf "✓ %s\n" s
| Error e -> Printf.printf "✗ %s\n" (describe_error e);
(* ทดสอบ error path *)
match run "/tmp/missing.conf" with
| Ok _ -> ()
| Error e -> Printf.printf "✗ %s\n" (describe_error e)
1. เริ่มต้นด้วย result type ที่สะท้อน domain error ของคุณ
2. ใช้ exception เฉพาะ bug / invariant / early return ภายใน function เดียว
3. ที่ boundary ของระบบ (HTTP handler, main, RPC) → convert exception → result
4. เปิด warning 8 เป็น error ใน dune file → บังคับ exhaustive match
5. ใช้ let* เพื่อให้ code อ่านง่าย
%%{init: {'theme':'base','themeVariables':{'primaryColor':'#3c3836','primaryTextColor':'#ebdbb2','primaryBorderColor':'#fabd2f','lineColor':'#83a598','secondaryColor':'#504945','tertiaryColor':'#282828','background':'#282828','mainBkg':'#3c3836','textColor':'#ebdbb2'}}}%%
flowchart LR
subgraph Core["Domain Logic"]
R1[fetch_user
→ result]
R2[parse_config
→ result]
R3[validate
→ result]
end
subgraph Boundary["System Boundary"]
EX[try...with
convert exn → result]
end
subgraph Top["Top-level"]
MAIN[main /
HTTP handler]
end
R1 --> EX --> MAIN
R2 --> EX --> MAIN
R3 --> EX --> MAIN
MAIN --> LOG[log error
+ exit code]
เราครอบคลุมพื้นฐาน 4 เรื่องสำคัญที่ต้องมั่นก่อนก้าวไป memory safety ลึกๆ และ concurrency:
option, result และ exception แต่ละตัวมีที่ใช้ของตัวเอง monadic chaining ด้วย let* ทำให้ error-handling code อ่านเหมือน imperativeทั้ง 4 เรื่องนี้ประกอบกันเป็น "vocabulary" พื้นฐานของ OCaml systems programming ก่อนจะเข้าสู่ ส่วนที่ 3 — Memory Safety ใน OCaml ที่จะเจาะเรื่อง GC tuning, persistent data structures, mutable state อย่างปลอดภัย และ FFI กับ C