/static/codemoomoo.png

OCaml 5 สำหรับการพัฒนาระบบ IT: Memory Safety, Concurrency และ Multicore

Part 2 — Fundamentals for Systems Development

บทความส่วนที่ 2 เจาะลึกพื้นฐานที่นักพัฒนาระบบ (systems developer) ต้องเข้าใจก่อนเขียน OCaml 5 ระดับ production ครอบคลุม type system และ memory layout, functions กับ performance tuning, pattern matching พร้อม algebraic data types (ADT) และ error handling แบบ type-safe ทุกหัวข้อมาพร้อมตัวอย่างที่ประกอบและ run ได้ทันที


3. Type System และ Memory Layout

ก่อนเขียน OCaml สำหรับงาน systems ต้องเข้าใจ 2 ชั้นพร้อมกัน คือชั้น type system ที่ compiler ตรวจสอบและชั้น runtime representation ว่าค่าแต่ละตัวถูกเก็บใน memory อย่างไร การรู้ทั้งสองชั้นช่วยให้เขียน code ที่ทั้ง correct (ถูกต้อง) และ performant (เร็ว) ไปพร้อมกัน

3.1 Type Inference และ Static Typing

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]

3.2 Primitive Types และ Representation ใน Memory

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 เต็ม

intmin = -262 , intmax = 262 - 1

โดย intmin คือค่าต่ำสุดและ intmax คือค่าสูงสุดของ int บน 64-bit system โดย 1 bit ถูกใช้เป็น tag

การเข้ารหัส tagged integer:

taggedrepr (n) = (n«1) | 1

เมื่อ 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)

3.3 Boxed vs Unboxed Values

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:

%%{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

3.4 Records, Variants และ Heap Block Layout

ทุก 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]

3.5 Immutability by Default และผลต่อ GC

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:

cost ( assignment ) = coststore + costbarrier (หาก major-to-minor pointer)

โดย costbarrier คือต้นทุนเพิ่มเติมเมื่อ 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

แนวปฏิบัติ:

  1. ใช้ immutable data structure เป็น default
  2. ใช้ mutable เฉพาะ hot path ที่วัดแล้วว่าคุ้ม
  3. เก็บ mutable state อยู่ใน short-lived scope เพื่อลด write barrier

4. Functions, Currying และ Performance

Functions ใน OCaml เป็น first-class citizen ทุกฟังก์ชันเป็น value ที่ส่งไปมาได้ การเข้าใจ currying, partial application และ tail-call optimization (TCO) เป็นพื้นฐานของ code ที่ทั้งสวยและเร็ว

4.1 Currying และ Partial Application

ทุก 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:

(* ตัวอย่าง 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 *)

4.2 Labeled และ Optional Arguments

เมื่อ 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 จะสับสนว่าเราจ่ายครบหรือยัง

4.3 Tail-Call Optimization (TCO)

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])

4.4 เขียน Loop แบบ Tail-Recursive แทน Imperative

สำหรับคนที่มาจาก 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:

  1. ใช้ List.fold_left แทน List.fold_right เมื่อ order ไม่สำคัญ
  2. ใช้ Array.unsafe_get / Array.unsafe_set ใน hot path หลังจาก bounds check แล้ว
  3. หลีกเลี่ยงการสร้าง closure ใหม่ใน tight loop
  4. ใช้ [@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 ใหญ่"]

5. Pattern Matching และ Algebraic Data Types

Algebraic Data Types (ADT) คือ superpower ของ OCaml สำหรับการ model state และ error อย่างแม่นยำ เมื่อรวมกับ pattern matching ที่ compiler ตรวจ exhaustiveness ให้ bug ประเภท "ลืม handle case นี้" จะหายไปเกือบหมดก่อน run

5.1 Variants (Sum Types) สำหรับ Modeling States

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 })

5.2 Records (Product Types) สำหรับ Structured Data

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

5.3 Exhaustiveness Check — จับ Case ที่ลืม

หัวใจของ 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 พาเราไปที่ทุกจุดที่ต้องแก้

5.4 Pattern Matching Idioms ที่ Powerful

(* 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. }
    }))

5.5 Recursive Types — Tree, List, Trie

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"]

6. Error Handling แบบ Type-safe

Systems code เผชิญกับ error ตลอดเวลา — network fail, disk full, parse error, timeout OCaml ไม่มี null และให้เครื่องมือ 3 อย่างในการจัดการ error: option, result และ exceptions การเลือกใช้แต่ละแบบให้เหมาะกับสถานการณ์คือทักษะสำคัญ

6.1 option แทน Null Pointer

option คือ 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))

6.2 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))

6.3 Monadic Chaining ด้วย 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

6.4 Exceptions — เมื่อไหร่ควรใช้

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"

6.5 เปรียบเทียบ Exceptions vs Result — Decision Matrix

%%{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
costresult costalloc + costmatch costexception { 0ถ้า success O(depth)ถ้า raise

โดย costalloc คือต้นทุนการ allocate heap block, costmatch คือต้นทุน pattern match และ depth คือความลึกของ call stack ระหว่าง raise และ try

6.6 ตัวอย่างรวม — Systems Error Handling

ตัวอย่างจริงที่รวมทั้ง 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)

6.7 Workflow แนะนำ

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]

สรุปส่วนที่ 2

เราครอบคลุมพื้นฐาน 4 เรื่องสำคัญที่ต้องมั่นก่อนก้าวไป memory safety ลึกๆ และ concurrency:

  1. Type System และ Memory Layout — เข้าใจ tagged representation, boxed vs unboxed, และผลกระทบต่อ GC ทำให้เขียน code ที่ compiler optimize ได้เต็มที่
  2. Functions และ Performance — currying, labeled args และ tail-call optimization คือรากฐานของ code ที่ทั้งอ่านง่ายและเร็ว
  3. Pattern Matching + ADT — ใช้ sum type และ product type ร่วมกับ exhaustiveness check เพื่อ model state/error อย่างแม่นยำและจับ bug ตั้งแต่ compile
  4. Error Handlingoption, 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