โหมดมืด
บทที่ 5 — Functional Programming
หลังจบบท คุณจะ:
- เข้าใจ pure (เพียวร์) function, immutability (อิม-มิว-ทา-บิ-ลิ-ตี้), higher-order (ไฮ-เออร์-ออ-เดอร์) function
- ใช้ map, filter, reduce ได้คล่อง
- เห็นว่า FP ลดบัค + เพิ่ม readability ยังไง
- ผสม FP + OOP ในงานจริง (ปี 2026 ใช้แบบนี้ — สำรวจ Effect TS, Arrow, Reactor, Cats Effect, ZIO ท้ายบท)
- รู้จัก concepts: closure, currying (เคอ-รี่-อิ้ง), monad (โม-แนด) (ระดับใช้งานได้)
ก่อนอ่าน — ต้องรู้อะไรมาก่อน?
- เขียนโปรแกรมเป็นอย่างน้อย 1 ภาษา (รู้จัก function, loop, list)
- ถ้าเคยใช้
map/filterใน Excel หรือ list comprehension ใน Python มาบ้าง จะอินเร็วขึ้น
📌 เรื่องภาษาในบทนี้ — FP เป็นแนวคิด ข้ามภาษา ตัวอย่างหลักเขียนด้วย Java (เพราะเป็นภาษาแกนของคลังนี้) แต่ concept หลัก ๆ จะมีกล่อง "เทียบ 3 ภาษา (Java / TypeScript / Python)" ให้เห็นว่าสิ่งเดียวกันหน้าตาต่างกันแค่ syntax — ถ้าคุณมาจากสาย JS/TS/Python อ่านกล่องนั้นแล้วจะเชื่อมโยงกับ Java ได้ทันที
1. Functional Programming คืออะไร
FP = paradigm (พา-รา-ไดม์ = แนวคิด/รูปแบบความคิด) ที่ treat computation = "evaluation of functions"
📖 Pure function (เพียวร์ ฟังก์ชัน) นิยามทางการ = function ที่ deterministic (เด-เทอร์-มิ-นิส-ติก = input เดิมให้ output เดิมเสมอ) + ไม่มี side effect (ไม่แตะของข้างนอก ไม่ทำ I/O ไม่แก้ตัวแปร global)
| OOP | FP | |
|---|---|---|
| Building block | Object (data + behavior) | Function (pure mapping) |
| State | Mutable เป็นปกติ | Immutable เป็นปกติ |
| Side effect | OK | Avoid / isolate |
| Iteration | for loop | map/filter/reduce |
| Logic | OO method | Function composition |
| ภาษาตัวอย่าง | Java, C# (เก่า) | Haskell, Elm |
ภาษาส่วนใหญ่ปี 2026 = multi-paradigm — Java, JS, Python, Kotlin, Scala, Swift = FP ✅
2. Pure Function
ฟังก์ชันที่:
- Same input → same output เสมอ
- No side effect
java
// ✅ Pure
int add(int a, int b) { return a + b; }
String upper(String s) { return s.toUpperCase(); }
// ❌ Not pure — depend on external state
int counter = 0;
int increment() { return ++counter; }
// ❌ Not pure — side effect
void greet(String name) {
System.out.println("Hello, " + name);
}
// ❌ Not pure — non-determinism
double random() { return Math.random(); }
LocalDateTime now() { return LocalDateTime.now(); }
// ❌ Not pure — เช่นเดียวกัน: method ที่ใช้เวลาปัจจุบันข้างใน
int getAge(LocalDate birthDate) {
return Period.between(birthDate, LocalDate.now()).getYears(); // เรียกซ้ำได้ผลต่าง
}
// ✅ Pure — รับ clock / "วันนี้" เป็น parameter (Clock injection)
int getAge(LocalDate birthDate, Clock clock) {
return Period.between(birthDate, LocalDate.now(clock)).getYears();
}
int getAge(LocalDate birthDate, LocalDate today) {
return Period.between(birthDate, today).getYears();
}🧪 เคล็ดทำ time-dependent code ให้ pure — ใน Java ใช้
Clock(java.time) ส่งเข้ามาเป็น dependency แทนเรียกLocalDate.now()ตรง ๆ ใน test ใช้Clock.fixed(...)ฝัง "วันนี้" ที่ต้องการได้ → unit test deterministic 100%
ทำไม Pure ดี
- Predictable — same in = same out
- Easy to test — no setup, no mock
- Cache-able (memoization)
- Parallelizable — no shared state
- Composable
java
// ✅ Test pure function — 1 line
assertEquals(5, add(2, 3));
// ❌ Test impure — need mock + setup
@Test
void testIncrement() {
counter = 0; // setup
increment();
assertEquals(1, counter);
}เทียบ 3 ภาษา — pure function หน้าตาเหมือนกันทุกที่ (เปลี่ยนแค่ syntax):
typescript// TypeScript const add = (a: number, b: number): number => a + b; // ✅ pure let counter = 0; const increment = () => ++counter; // ❌ พึ่ง state ข้างนอกpython# Python def add(a, b): return a + b # ✅ pure counter = 0 def increment(): # ❌ พึ่ง state ข้างนอก global counter counter += 1 return counterแก่นเดียวกัน: input เดิม → output เดิม + ไม่แตะอะไรข้างนอก = pure ไม่ว่าภาษาไหน
3. Immutability
java
// ❌ Mutable
class Point {
int x, y;
public void move(int dx, int dy) {
x += dx;
y += dy;
}
}
Point p = new Point(0, 0);
p.move(5, 5); // p = (5, 5) — original lost
// ✅ Immutable
record Point(int x, int y) {
public Point move(int dx, int dy) {
return new Point(x + dx, y + dy); // ใหม่
}
}
Point p = new Point(0, 0);
Point p2 = p.move(5, 5); // p ยังเป็น (0, 0), p2 = (5, 5)เทียบ 3 ภาษา — "อย่าแก้ของเดิม สร้างใหม่แทน":
typescript// TypeScript — spread เพื่อ copy + เปลี่ยนบางfield type Point = { x: number; y: number }; const move = (p: Point, dx: number, dy: number): Point => ({ ...p, x: p.x + dx, y: p.y + dy });python# Python — dataclass(frozen=True) ห้ามแก้ field, ใช้ replace สร้างใหม่ from dataclasses import dataclass, replace @dataclass(frozen=True) class Point: x: int y: int p2 = replace(p, x=p.x + 5, y=p.y + 5) # p เดิมไม่เปลี่ยนหลักเดียวกัน: ของเดิมไม่เคยถูกแก้ → thread-safe + คาดเดาได้ ทุกภาษา
ข้อดี
- ✅ Thread-safe ฟรี
- ✅ "Value semantics" — equal by content
- ✅ Hash-able (use as Map key)
- ✅ No "spooky action at a distance"
- ✅ Time-travel (undo, history)
Performance Concern?
java
// ❌ Concern — copy list 1000 times = expensive
List<Integer> result = List.of();
for (int i = 0; i < 1000; i++) {
result = appendImmutable(result, i); // copy ใหม่ทุกครั้ง
}→ ใช้ persistent (เพอร์-ซิส-เทนต์) data structure (โครงสร้างข้อมูลแบบคงอยู่: immutable แต่สร้างเวอร์ชันใหม่ได้เร็ว ด้วยเทคนิค structural sharing (สตรัค-เจอ-รัล แชร์-ริ่ง) = "ใช้โครงสร้างส่วนที่ไม่เปลี่ยนร่วมกัน" ไม่ก๊อปทั้งก้อน เปลี่ยนจุดไหนก็สร้างใหม่เฉพาะกิ่งนั้น)
🌳 โครงสร้าง structural sharing เห็นภาพยังไง — สมมติ list
v1 = [A,B,C,D]เก็บเป็น tree:v1 (root) ──► [A,B] ─┐ ├──► รวมเป็น [A,B,C,D] v1 (root) ──► [C,D] ─┘ เพิ่ม E → ได้ v2 = [A,B,C,D,E]: v1 (root) ──► [A,B] ◄── (ใช้ของเดิมร่วมกัน — ไม่ copy) └─► [C,D] ◄── (ใช้ของเดิมร่วมกัน — ไม่ copy) v2 (root) ──► [A,B] ◄── ชี้ไปยังกิ่งเดิม (shared) ├─► [C,D] ◄── ชี้ไปยังกิ่งเดิม (shared) └─► [E] ◄── กิ่งใหม่อันเดียวเท่านั้นที่สร้างจริงผลคือ v1 ยังคงเดิม, v2 เป็น list ใหม่ที่ใช้กิ่ง [A,B] กับ [C,D] ร่วมกับ v1 → copy แค่ root กับกิ่งใหม่ ไม่ต้อง copy ทั้ง list
Library / API ที่ใช้จริง:
- Java:
Collections.unmodifiableList(shallow — wrap แค่ชั้นนอก, element ข้างในยังแก้ได้),List.copyOf(shallow เช่นกัน — copy แค่ list ระดับบน), GuavaImmutableList, vavr persistent - Eclipse Collections (โมเดิร์น 2026, mature, immutable + primitive collections — performance ดี)
- Java 21+
SequencedCollection— interface ใหม่ที่กำหนดลำดับชัดเจน (มีgetFirst(),getLast(),reversed()) ใช้คู่กับ unmodifiable view ได้ - Clojure / Scala native
- ใน Java 21 — สำหรับ heavy mutation ใช้ mutable + return immutable ที่ end
java
// ✅ Build mutably + freeze
List<Integer> result = new ArrayList<>();
for (int i = 0; i < 1000; i++) result.add(i);
return List.copyOf(result); // immutable view4. Higher-Order Function
หัวใจของ FP คือ "function เป็นค่าตัวหนึ่ง" ที่ส่งต่อกันได้เหมือนตัวเลขหรือ string — higher-order function (HOF — ไฮ-เออร์-ออ-เดอร์) คือฟังก์ชันที่ทำอย่างน้อยหนึ่งใน 2 อย่างนี้ ทำให้เรานำพฤติกรรม (เช่น "คูณสอง") มาประกอบใหม่ได้ยืดหยุ่น:
- (1) function taking function — รับ function เป็น argument (เช่น
map(list, fn)รับfnที่จะไปแปลงแต่ละตัว) หรือ - (2) function returning function — return function ออกมา (เช่น
multiplier(2)คืนฟังก์ชันที่คูณสองให้ใช้ต่อ)
🎯 จำสั้น ๆ: HOF = "function ที่กิน function หรือถ่ม function ออกมา"
java
// รับ function
public <T, R> List<R> map(List<T> list, Function<T, R> fn) {
List<R> result = new ArrayList<>();
for (T item : list) result.add(fn.apply(item));
return result;
}
// ใช้
List<Integer> nums = List.of(1, 2, 3);
List<Integer> doubled = map(nums, n -> n * 2); // [2, 4, 6]java
// Return function
public Function<Integer, Integer> multiplier(int factor) {
return x -> x * factor;
}
Function<Integer, Integer> doubler = multiplier(2);
Function<Integer, Integer> tripler = multiplier(3);
doubler.apply(5); // 10
tripler.apply(5); // 155. Lambda + Functional Interface (Java)
Java ไม่มี "function ลอย ๆ" แต่จำลองได้ด้วย functional interface (interface ที่มี abstract method ตัวเดียว — เรียกว่า SAM: Single Abstract Method) แล้วเขียน lambda แทน implementation สั้น ๆ — Java เตรียม built-in มาให้ครบ (Predicate, Function, Consumer, Supplier, BiFunction) จึงไม่ต้องประกาศเองทุกครั้ง:
💡
@FunctionalInterfaceannotation คืออะไร — แค่ "ป้าย" บอก compiler ว่า interface นี้ตั้งใจให้มี abstract method แค่ตัวเดียว ถ้าเผลอใส่ตัวที่ 2 จะ error ทันที (กันพลาด) ไม่ใส่ annotation ก็ใช้เป็น lambda ได้ตามปกติ ใส่ไว้แค่เพื่อแจ้งเจตนาให้ชัด
java
// Functional interface = interface ที่มี abstract method 1 ตัว
@FunctionalInterface
interface Predicate<T> {
boolean test(T t);
}
// ใช้ lambda
Predicate<Integer> isPositive = x -> x > 0;
isPositive.test(5); // true
// Built-in
Predicate<String> p = s -> s.startsWith("A");
Function<Integer, Integer> f = x -> x * 2;
Consumer<String> c = s -> System.out.println(s);
Supplier<String> s = () -> UUID.randomUUID().toString();
BiFunction<Integer, Integer, Integer> add = (a, b) -> a + b;Method Reference
💡
::คืออะไร — เครื่องหมาย "method reference" (เมท-ธอด เรฟ-เฟอ-เรนซ์) เป็นทางลัดของ lambda ที่ "เรียก method ตัวนี้แทน" อ่านว่า "double colon" (ดับเบิ้ลโคลอน) —String::toUpperCaseเทียบเท่าs -> s.toUpperCase()และUser::newหมายถึง constructor ของUser(เทียบเท่า() -> new User()หรือมี argument ก็ส่งต่อให้)
java
list.forEach(System.out::println); // = (x) -> System.out.println(x)
list.stream().map(String::toUpperCase);
// Constructor reference — User::new คือ () -> new User(...)
list.stream().map(User::new);
// Instance method
String::length // s -> s.length()6. Map / Filter / Reduce ⭐ Core
Map — แปลง element
java
List<Integer> nums = List.of(1, 2, 3, 4, 5);
// Imperative
List<Integer> doubled = new ArrayList<>();
for (Integer n : nums) doubled.add(n * 2);
// Functional
List<Integer> doubled = nums.stream().map(n -> n * 2).toList();
// [2, 4, 6, 8, 10]Filter — เก็บที่ match
java
List<Integer> evens = nums.stream().filter(n -> n % 2 == 0).toList();
// [2, 4]Reduce — รวมเป็น 1 ค่า
java
int sum = nums.stream().reduce(0, (a, b) -> a + b); // 15
int max = nums.stream().reduce(Integer.MIN_VALUE, Math::max);
// Common reduce → built-in
int sum = nums.stream().mapToInt(Integer::intValue).sum();
int max = nums.stream().mapToInt(Integer::intValue).max().getAsInt();
double avg = nums.stream().mapToInt(Integer::intValue).average().getAsDouble();
long count = nums.stream().count();⚠️ ระวัง empty list —
.max()/.min()/.average()คืนOptionalInt/OptionalDoubleถ้า list ว่าง การเรียก.getAsInt()/.getAsDouble()ตรง ๆ จะโยนNoSuchElementExceptionใช้.orElse(0)หรือ.orElseThrow(...)แทนปลอดภัยกว่า:javaint max = nums.stream().mapToInt(Integer::intValue).max().orElse(0); double avg = nums.stream().mapToInt(Integer::intValue).average().orElse(0.0);
Combo
java
// Total revenue of paid orders
BigDecimal total = orders.stream()
.filter(o -> o.getStatus() == PAID)
.map(Order::getTotal)
.reduce(BigDecimal.ZERO, BigDecimal::add);→ Read like English: "filter paid orders, map to total, sum"
เทียบ 3 ภาษา — map / filter / reduce คือแก่นเดียวกัน
นี่คือจุดที่คนสาย Python/JS มักรู้สึกว่า Java "เยอะ" — จริง ๆ แล้วทำเรื่องเดียวกัน เปลี่ยนแค่ syntax โจทย์: เอาเลขคู่มายกกำลังสอง แล้วบวกรวม
java
// Java — ต้อง .stream() ... .toList() เพราะ List ไม่มี map ในตัว
int sum = nums.stream()
.filter(n -> n % 2 == 0)
.map(n -> n * n)
.reduce(0, Integer::sum);typescript
// TypeScript / JavaScript — array มี method ในตัวเลย
const sum = nums
.filter(n => n % 2 === 0)
.map(n => n * n)
.reduce((a, b) => a + b, 0);python
# Python — นิยมเขียนเป็น comprehension (อ่านเหมือนประโยค)
sum_ = sum(n * n for n in nums if n % 2 == 0)
# หรือสไตล์ map/filter ตรง ๆ ก็ได้
from functools import reduce
sum_ = reduce(lambda a, b: a + b,
map(lambda n: n * n, filter(lambda n: n % 2 == 0, nums)), 0)💡 อ่านกล่องนี้แล้วจับหลักได้: map = "แปลงทีละตัว", filter = "เก็บที่ผ่านเงื่อนไข", reduce = "ยุบเหลือค่าเดียว" — เหมือนกันทุกภาษา ต่างแค่ Java ต้องเปิด
.stream()ก่อน · JS ใช้ method บน array ตรง ๆ · Python มี comprehension เป็นทางลัด
7. Collectors (Java Stream)
หลัง map/filter เสร็จ เรามักต้อง "เก็บ" ผลลัพธ์กลับมาเป็นโครงสร้างข้อมูล — Collectors คือชุดเครื่องมือสำเร็จรูปสำหรับขั้นนี้ ทั้งแปลงเป็น List/Set/Map, จัดกลุ่ม (groupingBy), แบ่งสองฝั่ง (partitioningBy) และต่อ string (joining):
java
import static java.util.stream.Collectors.*;
// To List
List<User> activeUsers = users.stream().filter(User::isActive).collect(toList());
// หรือ Java 16+
List<User> activeUsers = users.stream().filter(User::isActive).toList();
// To Set
Set<String> uniqueEmails = users.stream().map(User::getEmail).collect(toSet());
// To Map
Map<Long, User> byId = users.stream().collect(toMap(User::getId, u -> u));
// Group by
Map<String, List<User>> byCity = users.stream()
.collect(groupingBy(User::getCity));
// Group + count
Map<String, Long> countByCity = users.stream()
.collect(groupingBy(User::getCity, counting()));
// Group + map
Map<String, List<String>> namesByCity = users.stream()
.collect(groupingBy(User::getCity, mapping(User::getName, toList())));
// Partition (2 buckets: match / not match)
Map<Boolean, List<User>> adults = users.stream()
.collect(partitioningBy(u -> u.getAge() >= 18));
// Joining string
String csv = users.stream().map(User::getName).collect(joining(", "));8. Function Composition
"composition" คือการต่อฟังก์ชันเล็ก ๆ หลายตัวเข้าเป็นฟังก์ชันใหญ่ตัวเดียว — andThen ทำตามลำดับซ้ายไปขวา ส่วน compose ทำกลับด้าน (ขวาก่อน) แนวคิดนี้ทำให้เราสร้าง pipeline แปลงข้อมูลทีละสเต็ปได้อ่านง่าย:
java
Function<Integer, Integer> double_ = x -> x * 2;
Function<Integer, Integer> plus1 = x -> x + 1;
// Chain — andThen
Function<Integer, Integer> doubleThenPlus1 = double_.andThen(plus1);
doubleThenPlus1.apply(5); // (5*2)+1 = 11
// compose (reverse)
Function<Integer, Integer> plus1ThenDouble = double_.compose(plus1);
plus1ThenDouble.apply(5); // (5+1)*2 = 12ในงานจริง — pipeline
java
String result = Stream.of(input)
.map(this::trim)
.map(this::lowercase)
.map(this::removeSpecialChars)
.findFirst()
.orElseThrow();9. Closure — Capture variable
closure คือฟังก์ชันที่ "จำ" ตัวแปรจาก scope ที่มันถูกสร้างขึ้นได้ แม้ scope นั้นจบไปแล้ว — ทำให้เราโรงงานสร้างฟังก์ชันที่ฝังค่าเฉพาะตัวไว้ได้ (เช่น adder(5) ได้ฟังก์ชันที่บวก 5 เสมอ) เป็นพื้นฐานของ counter, memoization และ partial application:
java
public Function<Integer, Integer> adder(int n) {
return x -> x + n; // ⭐ capture "n"
}
Function<Integer, Integer> add5 = adder(5);
add5.apply(10); // 15
Function<Integer, Integer> add10 = adder(10);
add10.apply(10); // 20javascript
// JS — common pattern
function makeCounter() {
let count = 0;
return () => ++count; // ⭐ capture count
}
const counter = makeCounter();
counter(); // 1
counter(); // 2⚠️ Java captured variable ต้อง effectively final (เอ็ฟ-เฟค-ทีฟ-ลี่ ไฟ-นัล = เสมือนเป็น final) — ตัวแปรที่ไม่ถูกแก้ค่าซ้ำหลังกำหนดครั้งแรก แม้ไม่ได้ใส่คำว่า final ก็ตาม (เหตุผล: lambda อาจถูกเรียกข้าม thread — ถ้าตัวแปรเปลี่ยนค่าได้จะ race condition)
java
int n = 5;
Function<Integer, Integer> add = x -> x + n; // OK
// n = 10; // ❌ would break — effectively final
// Workaround (กรณีต้องการ state จริง ๆ) — wrap ใน mutable container
AtomicInteger n = new AtomicInteger(5);⚠️ หมายเหตุ: workaround
AtomicIntegerนี้เป็นการ "หลบ" ข้อจำกัด effectively final เฉพาะกรณีที่ จำเป็น ต้องมี state จริง ๆ ภายใน lambda (เช่น counter) — มันทำให้ lambda ไม่ pure อีกต่อไป (มี side effect = mutate shared state) ขัดหลัก FP ที่เน้นทั้งบท ใช้เมื่อจำเป็นจริง ๆ เท่านั้น ทางที่ดีกว่าคือใช้reduce/count/Collectorsให้จบ
10. Currying
🟡 โซนขั้นสูง — ข้ามได้ (หัวข้อ 10 Currying): ในงาน Java ทั่วไปแทบไม่ได้ใช้ ถ้าเพิ่งเริ่มอ่านผ่าน ๆ พอ ไว้ค่อยกลับมาก็ได้ — มาต่อ §13 Optional เลยก็ได้
Currying (เคอ-รี่-อิ้ง — ตั้งชื่อตาม Haskell Curry นักคณิตศาสตร์ ไม่เกี่ยวกับแกงกะหรี่!) = แปลง function ที่รับหลาย argument ให้กลายเป็น "ลูกโซ่" (chain) ของ function ที่รับทีละ 1 argument — เรียกค่าทีละตัวต่อกันไปเรื่อย ๆ
📖 สูตรจำ:
f(a, b, c)→f(a)(b)(c)— รับทีละตัว คืน function ทุกขั้นจนครบ argument ตัวสุดท้ายค่อยได้ผลลัพธ์
java
// Normal
BiFunction<Integer, Integer, Integer> add = (a, b) -> a + b;
add.apply(2, 3); // 5
// Curried
Function<Integer, Function<Integer, Integer>> addCurried = a -> b -> a + b;
addCurried.apply(2).apply(3); // 5
// Partial application
Function<Integer, Integer> add2 = addCurried.apply(2);
add2.apply(3); // 5
add2.apply(10); // 12ใช้ในงานจริงน้อย — แต่ JS, Scala ใช้บ่อย
typescript
// TypeScript
const add = (a: number) => (b: number) => a + b;
const add2 = add(2);
add2(5); // 7Currying vs Partial Application — ต่างกันยังไง?
ทั้งสองคำคนชอบสับสน เพราะหน้าตาคล้ายกัน แต่ความหมายต่าง:
| Currying | Partial Application | |
|---|---|---|
| ความหมาย | แปลงรูปแบบ function จาก f(a, b, c) → f(a)(b)(c) ทุกตัว 1 argument | "ใส่ค่าไปบางตัว" แล้วได้ function ใหม่ที่รอค่าที่เหลือ |
| ผลลัพธ์ | ได้ chain ของ unary function (รับทีละ 1) | ได้ function ใหม่ที่ arity ลดลง (เช่น จาก 3-ary เหลือ 2-ary) |
| ตัวอย่าง | add(a)(b)(c) | add(2, _, _) ได้ฟังก์ชันรอ b, c |
javascript
// Currying — แปลงทั้งโครงสร้าง
const add = a => b => c => a + b + c;
add(1)(2)(3); // 6 — ต้องเรียกทีละชั้น
// Partial application — ใส่บางตัวล่วงหน้า
function addThree(a, b, c) { return a + b + c; }
const add1 = addThree.bind(null, 1); // ใส่ a=1 ไว้
add1(2, 3); // 6 — ที่เหลือใส่พร้อมกันได้🎯 ความเชื่อมโยง: curried function เรียกบางส่วน = partial application โดยอัตโนมัติ (เพราะ
add(1)คืน function รอ b, c) แต่ partial application ไม่จำเป็นต้องผ่าน currying — ทำผ่านbind/ wrapper ก็ได้
11. Recursion — แทน Loop
java
// Loop
int factorial(int n) {
int result = 1;
for (int i = 2; i <= n; i++) result *= i;
return result;
}
// Recursion
int factorial(int n) {
if (n <= 1) return 1;
return n * factorial(n - 1);
}ดี:
- บางครั้งอ่านง่ายกว่า (tree, divide-conquer)
- Match math definition
ระวัง:
- ⚠️ Stack overflow ถ้า deep
- Java ไม่มี tail call optimization (TCO)
- 💡 หมายเหตุ: Project Loom virtual threads (JDK 21+) ลด stack pressure สำหรับ recursion ที่ I/O หนักได้ (เพราะ virtual thread มี stack เล็ก ใช้พร้อมกันได้เยอะ) แต่ไม่ได้ทำให้ tail recursion ลึกเป็น infinite ได้เหมือน Scala — เป็นคนละปัญหา (เครื่องมือต่างกัน แก้ปัญหาคล้ายกัน คือ "call stack ลึกเกินไป")
Tail Recursion (Optimizable in some languages)
scala
// Scala
@tailrec
def factorial(n: Int, acc: Int = 1): Int =
if (n <= 1) acc
else factorial(n - 1, n * acc)12. Pattern Matching
pattern matching คือการ "แตกแยกข้อมูลตามรูปร่าง" แล้วทำงานต่างกันตามแต่ละเคส — ใช้คู่กับ sealed type ทำให้ compiler บังคับว่าเราจัดการครบทุกชนิด (exhaustive — เอ็ก-ซอส-ทีฟ = ครอบคลุมทุกกรณี) ตั้งแต่ Java 21 ยังดึงค่าข้างในออกมาได้เลย (destructuring — ดี-สตรัค-เจอ-ริ่ง = แยกค่าออกเป็นชิ้น ๆ) แทนการ cast เอง:
📐 Algebraic Data Types (ADT — อัล-เก-เบรอิก ดา-ต้า ไทพ์) — pattern matching คู่กับ sealed type คือการ implement ADT ในภาษาสมัยใหม่ ADT มี 2 แบบหลัก:
- Sum type ("type นี้เป็น A หรือ B หรือ C") =
sealed interface+ permits — เช่นShapeที่เป็นได้ทั้งCircle/Square/Rectangle(เลือก 1 อย่างจากชุดที่กำหนด)- Product type ("type นี้มี A และ B และ C ครบ") =
recordหรือ struct/tuple — เช่นRectangle(width, height)ที่มี 2 ฟิลด์เสมอรวมกัน:
sealed interface Shape { record Circle(double r) implements Shape; record Square(double s) implements Shape; }คือ "sum of products" — รากเหง้าของ type system ใน Haskell, Rust, Kotlin, Scala และตอนนี้ Java
Java 17+ — Sealed Interface
💡
sealed interfaceคืออะไร — interface ที่ "ปิดผนึก" รายชื่อ subtype ไว้ตายตัวด้วยpermits— มีได้แค่Circle,Square,Rectangleเท่านั้น คนอื่นมา implement เพิ่มไม่ได้ ข้อดี: เมื่อใช้switchแบบ pattern matching compiler จะรู้ครบทุกเคส ถ้าลืม case ไหนจะ error (exhaustive check)
java
sealed interface Shape permits Circle, Square, Rectangle {}
record Circle(double radius) implements Shape {}
record Square(double side) implements Shape {}
record Rectangle(double width, double height) implements Shape {}
double area(Shape shape) {
return switch (shape) {
case Circle c -> Math.PI * c.radius() * c.radius();
case Square s -> s.side() * s.side();
case Rectangle r -> r.width() * r.height();
};
}→ Exhaustive — compiler check ครบ ถ้าเพิ่ม Triangle เข้า permits แต่ลืม case ในสวิตช์ จะ compile ไม่ผ่าน
🌐 เทียบ Kotlin: ใช้
sealed class+whenexpression แบบเดียวกัน —when (shape) { is Circle -> ...; is Square -> ... }ก็ exhaustive ได้ (Kotlin มี feature นี้ก่อน Java หลายปี)
Destructuring (Java 21+)
java
double area(Shape shape) {
return switch (shape) {
case Circle(double r) -> Math.PI * r * r;
case Square(double s) -> s * s;
case Rectangle(double w, double h) -> w * h;
};
}13. Optional / Maybe — Avoid Null
📖 Maybe (เม-บี) = ชื่อ type ของภาษา Haskell ที่เทียบเท่า
Optionalใน Java /Optionใน Scala/Rust — หน้าตาเป็น "กล่องที่อาจมีค่า (Just x/Some x) หรือว่างเปล่า (Nothing/None)" คำว่า "Maybe monad" ที่เจอในบทความ FP ก็คือเรื่องเดียวกันนี้
Optional คือกล่องที่ "อาจมีค่าหรือไม่มี" ใช้แทนการคืน null ที่เป็นต้นเหตุ NullPointerException — แทนที่จะเช็ค null เอง เราต่อ map/orElse/ifPresent เป็น chain ได้ ทำให้ flow ปลอดภัยและอ่านง่าย:
java
// ❌ Null risk
public User findUser(Long id) {
return repo.findById(id); // null?
}
User u = findUser(1);
String name = u.getName(); // NPE
// ✅ Optional
public Optional<User> findUser(Long id) {
return repo.findById(id);
}
Optional<User> u = findUser(1);
// Use functional
String name = u.map(User::getName).orElse("Unknown");
u.ifPresent(user -> log.info("Found: {}", user));
// Chain
String city = findUser(1)
.map(User::getAddress)
.map(Address::getCity)
.orElse("Unknown");Anti-patterns
java
// ❌ Don't .get() without check
Optional<User> u = findUser(1);
User user = u.get(); // throws NoSuchElementException!
// ✅
User user = u.orElseThrow(() -> new UserNotFoundException(1L));
// ❌ Optional<List> — return empty list
public Optional<List<User>> getUsers() { ... }
// ✅ Return empty list
public List<User> getUsers() { ... }14. Result / Either — Functional Error Handling
แทนที่จะ throw exception (ซึ่งซ่อนอยู่ ไม่ปรากฏใน signature) แนว FP ทำให้ "ความผิดพลาดเป็นค่าตัวหนึ่ง" — คืน Result (หรือเรียกว่า Either — อี-เธอร์ = "ตัวใดตัวหนึ่ง" ในสองตัวเลือก) ที่เป็นได้ทั้ง Ok (สำเร็จ) หรือ Err (พลาด) บังคับให้ผู้เรียกจัดการทั้งสองทางอย่างชัดเจน อ่าน signature ก็รู้เลยว่าฟังก์ชันนี้พลาดได้:
java
sealed interface Result<T, E> {
record Ok<T, E>(T value) implements Result<T, E> {}
record Err<T, E>(E error) implements Result<T, E> {}
}
// ✅ เวอร์ชันอ่านง่าย — ไม่ใช้ explicit type witness
public Result<User, String> findUser(Long id) {
var found = repo.findById(id); // Optional<User>
if (found.isPresent()) {
return new Result.Ok<>(found.get());
} else {
return new Result.Err<>("Not found");
}
}
// 🎓 เวอร์ชันสั้น (functional chain) — มี syntax ขั้นสูง
public Result<User, String> findUserShort(Long id) {
return repo.findById(id)
.<Result<User, String>>map(Result.Ok::new) // ⚙️ syntax นี้ขั้นสูง ข้ามได้ — แค่บอก compiler ชัด ๆ ว่าผลของ map เป็นชนิด Result<User,String> (เรียกว่า explicit type witness — เอ็กซ์-พลิ-ซิท ไทพ์ วิท-เนส = บอกชนิดอย่างชัดเจน)
.orElse(new Result.Err<>("Not found"));
}
// Use (⚠️ ต้อง Java 21+ — pattern matching ที่มี generic ใน record pattern ใช้ได้ตั้งแต่ JDK 21)
Result<User, String> r = findUser(1);
switch (r) {
case Result.Ok(User user) -> System.out.println(user);
case Result.Err(String e) -> log.error(e);
}⚠️ ต้อง Java 21+ — การเขียน
case Result.Ok(User user) -> ...(record pattern ที่มี generic type) ต้องใช้ JDK 21 ขึ้นไป รุ่นก่อนหน้านี้ไม่รองรับ generics ใน pattern
🏭 ในงานจริง: อย่ารีบเขียน
Resultเองตั้งแต่ต้น — ปี 2026 มี library พร้อมใช้:
- vavr
Either<L, R>/Try<T>— ครบ ใช้กับ Spring ได้ดี- Resilience4j
Try<T>— สำหรับ wrap operation ที่ throw exception- Arrow (Kotlin) —
Either<E, A>มาตรฐาน Kotlin FP- การ hand-roll
Resultเองเหมาะกับการเรียน — ใน production ใช้ library จะปลอดภัยและมี combinator ครบกว่าRust / Kotlin มี built-in. Java พึ่งมี pattern matching → ใช้ได้
15. Side Effect Isolation
เคล็ดลับใช้ FP ในงานจริงคือ "แยกตรรกะบริสุทธิ์ (pure) ออกจาก side effect (I/O เช่น อ่าน DB, ส่งเมล)" — ดันส่วนคำนวณให้เป็น pure function ที่เทสต์ง่าย แล้วเก็บ side effect ไว้ที่ขอบนอกสุดเป็นชั้นบาง ๆ:
java
// ❌ Mix everything
public void processOrders() {
List<Order> orders = repo.findAll(); // I/O
for (Order o : orders) {
if (o.getStatus() == PENDING) { // logic
o.setStatus(CONFIRMED); // mutate
repo.save(o); // I/O
emailService.send(o.getCustomerEmail(), ...); // I/O
}
}
}
// ✅ Separate pure logic from side effects
public List<Order> confirmOrders(List<Order> orders) {
return orders.stream()
.filter(o -> o.getStatus() == PENDING)
.map(this::confirm) // pure
.toList();
}
private Order confirm(Order order) {
return order.withStatus(CONFIRMED); // pure (return new)
}
public void processOrders() {
List<Order> pending = repo.findAll();
List<Order> confirmed = confirmOrders(pending); // pure
confirmed.forEach(o -> {
repo.save(o);
emailService.send(o.getCustomerEmail(), ...);
});
}→ Test confirmOrders ง่าย — pure function
16. FP ใน React
React ออกแบบบนแนวคิด FP โดยตรง — UI คือผลลัพธ์ของฟังก์ชัน view = f(state), component ที่ดีคือ pure function ของ props, reducer คือ pure state transition, custom hook คือ function composition และ HOC คือฟังก์ชันที่คืน component:
tsx
// Pure component
function UserCard({ user }: { user: User }) {
return <div>{user.name}</div>;
}
// Pure state transition (reducer)
function reducer(state: State, action: Action): State {
switch (action.type) {
case 'INCREMENT': return { ...state, count: state.count + 1 };
case 'RESET': return { ...state, count: 0 };
default: return state;
}
}
// Custom hook (function composition)
function useAuth() {
const user = useUser();
const isLoggedIn = user !== null;
return { user, isLoggedIn };
}
// HOC = function returning component
const withLogging = (Component) => (props) => {
console.log('Rendering', props);
return <Component {...props} />;
};17. FP ใน TypeScript
TypeScript เปิดทาง FP ผ่าน type system ที่ทรงพลัง — มอง type เป็น "เซต" ที่รวม/ตัดกันได้ (union/intersection) และใช้ discriminated union (Result ที่มี field kind) แทน null/exception ทำให้ compiler บังคับให้จัดการครบทุกเคสเหมือน pattern matching:
typescript
// Type as set + intersection / union
type User = { id: number; name: string };
type Admin = User & { role: 'admin'; permissions: string[] };
// Discriminated union (ดิส-คริม-อิ-เนท-เต็ด ยูเนียน = "union ที่แยกได้ด้วย field ตัวหนึ่ง")
// แต่ละหน้า (variant) มี field พิเศษ (ในที่นี้คือ `kind`) ที่บอกชนิด → compiler รู้ทันทีว่าเข้า case ไหน
// คล้าย sealed interface ใน Java แต่เขียนแบบ structural type
type Result<T> =
| { kind: 'ok'; value: T }
| { kind: 'err'; error: string };
function process<T>(r: Result<T>) {
switch (r.kind) {
case 'ok':
console.log(r.value);
break;
case 'err':
console.error(r.error);
break;
}
}
// Higher-order
type Predicate<T> = (item: T) => boolean;
function not<T>(predicate: Predicate<T>): Predicate<T> {
return (item) => !predicate(item);
}
const isEven = (n: number) => n % 2 === 0;
const isOdd = not(isEven);
// Pipe — ใช้ library fp-ts (optional, ไม่ใช่ของ TypeScript เอง)
// fp-ts = library FP สาย Haskell-style สำหรับ TypeScript ติดตั้งเสริมเอง: npm i fp-ts
import { pipe } from 'fp-ts/function';
pipe(
[1, 2, 3, 4, 5],
arr => arr.filter(n => n > 2),
arr => arr.map(n => n * 2),
arr => arr.reduce((a, b) => a + b, 0)
);18. Monad — Light Touch (Appendix)
🟡 โซนขั้นสูง — ข้ามได้: คำว่า "monad (โม-แนด)" ฟังดูน่ากลัวแต่ในทางปฏิบัติแค่จำว่า "กล่องที่ flatMap ได้" ก็พอ ถ้ายังงงให้ข้ามทั้งหัวข้อนี้ไปก่อน — ไม่กระทบการเขียนโค้ดประจำวันเลย เก็บไว้เป็น appendix อ่านวันหลังก็ได้
💡 ทำความเข้าใจ flatMap ก่อน (นี่คือหัวใจ):
mapคือ "แปลงค่าข้างในกล่องทีละตัว" แต่ถ้าตัวแปลงนั้นคืนกล่องอีกใบ เราจะได้ "กล่องซ้อนกล่อง" (เช่นOptional<Optional<X>>) —flatMapคือ "map แล้วแบนชั้นที่ซ้อนออก" ให้เหลือกล่องชั้นเดียว สั้น ๆ คือ: flatMap = map + แกะกล่องที่ซ้อนออก 1 ชั้น
ระดับใช้งาน → คิดว่า Monad (โม-แนด) = "container (กล่องห่อค่า) ที่มีเมธอด bind/flatMap" — โดย bind เป็นชื่อทางการของสิ่งที่ Java เรียกว่า flatMap (ภาษาอื่นเรียกต่างกันแต่ทำอย่างเดียวกัน)
🪜 Functor → Applicative → Monad ladder
3 ขั้นจากง่ายไปยาก — เหมือนบันได ขึ้นทีละขั้น (ทุกขั้นบนตอบโจทย์ขั้นล่างได้หมด):
| ขั้น | ชื่อ | ความสามารถ | ตัวอย่างเมธอด |
|---|---|---|---|
| 1 | Functor (ฟัง-เตอร์) | "กล่องที่ map ได้" — แปลงค่าข้างในด้วย function | Optional.map(fn) |
| 2 | Applicative (แอป-พลิ-เค-ทิฟ) | functor + เอา function ที่ห่อในกล่องมาใช้กับค่าในกล่องอีกใบได้ | Optional.ap(fnInBox) (ใน vavr) |
| 3 | Monad (โม-แนด) | applicative + flatMap — chain operation ที่ผลลัพธ์เป็นกล่องได้ | Optional.flatMap(fn) |
🎯 จำง่าย: functor = "map ได้", monad = "map + flatten ได้" —
Optional/Stream/CompletableFutureใน Java เป็นทั้ง 3 ขั้น (มีครบmap+flatMap)
Walk-through — Monad คือกล่องที่ทำอะไร?
แทนที่จะคิดเป็นทฤษฎี ลองดู 3 ตัวอย่าง concrete:
(1) Option/Maybe monad — สำหรับ null-safety: "อาจมีค่าหรือไม่มี"
java
// แทนที่จะเช็ค null ทีละชั้น
Optional<User> user = findUser(1);
Optional<Address> addr = user.flatMap(this::findAddress); // ถ้า user ว่าง → addr ว่างอัตโนมัติ
Optional<String> city = addr.flatMap(a -> Optional.ofNullable(a.getCity()));
// เกิด null ที่ไหน chain ก็เป็น empty ไปจนจบ — ไม่ NPE(2) Result/Either monad — สำหรับ error handling: "สำเร็จหรือพลาด"
java
// คิดว่า Result = "กล่องที่เก็บค่าสำเร็จ หรือ error"
Result<User, String> user = findUser(1);
Result<Order, String> order = user.flatMap(u -> findLatestOrder(u.getId()));
// ถ้า findUser พลาด → ทั้ง chain เป็น Err โดยอัตโนมัติ ไม่ต้อง if เช็คทีละขั้น(3) Future/Task monad — สำหรับ async: "ค่าที่จะมาในอนาคต"
java
// CompletableFuture = "กล่องที่ค่ายังไม่มาแต่จะมา"
CompletableFuture<User> userFut = fetchUser(1);
CompletableFuture<Order> orderFut = userFut.thenCompose(u -> fetchOrder(u.getId()));
// thenCompose = flatMap version ของ CompletableFuture
// chain async ต่อกันได้ ไม่ต้องซ้อน callback ลึกตัวอย่าง code Java
java
// Optional = Monad
Optional<User> u = findUser(1);
Optional<Address> addr = u.flatMap(this::findAddress); // chain
// Stream/List = Monad
List<List<Integer>> nested = List.of(List.of(1,2), List.of(3,4));
List<Integer> flat = nested.stream()
.flatMap(List::stream)
.toList();
// CompletableFuture = Monad
CompletableFuture<User> user = fetchUser(1);
CompletableFuture<Order> order = user.thenCompose(u -> fetchOrder(u.getId()));→ ทุก "thing-with-flatMap" = monad (essentially) — อะไรก็ตามที่มี flatMap ถือเป็น monad โดยพื้นฐาน
ไม่ต้องเข้าใจ category theory — ใช้ flatMap ตอน chain operation ที่ wrap ใน container (ห่ออยู่ในกล่อง) ได้เลย
🏭 Monad ที่ใช้จริงในงาน 2026:
- Java:
Optional,Stream,CompletableFuture— และที่สำคัญที่สุดในงาน reactive: Project ReactorMono<T>/Flux<T>(Spring WebFlux ใช้ตัวนี้เป็นหลัก) ทั้งคู่เป็น monad reactive ที่มีflatMapครบ- Kotlin:
Arrowlibrary —Option,Either,IO,Resourceครบเซต- Scala:
Cats EffectและZIO— effect monad ระดับ production สำหรับงาน async + concurrent- TypeScript:
fp-ts(Haskell-style),Effect TS— ตัวหลังเป็น library effect monad ยุคใหม่ปี 2024+ ที่ compose ง่าย เริ่มฮิตในวง TS
📐 เชิงอรรถ — category theory คืออะไร? เป็น คณิตศาสตร์ระดับสูง (ทฤษฎีหมวดหมู่) ที่เป็นรากเหง้าเชิงทฤษฎีของคำว่า "monad" — คุณไม่จำเป็นต้องรู้เลย เพื่อใช้งานจริง ขอแค่จำว่า "monad = กล่องที่ flatMap ได้" ก็เขียนโค้ดได้สบาย ส่วนนี้ใส่ไว้เผื่อคนอยากค้นต่อเท่านั้น
18.5 FP Ecosystem ปี 2026 — Library ที่ใช้จริงตามภาษา
ข้อมูลปี 2026 ของ library FP ที่นิยมในแต่ละ stack — จดไว้เผื่อเลือกใช้:
| ภาษา | Library / Framework | บทบาท |
|---|---|---|
| Java | Project Reactor (Mono/Flux) | reactive monad — Spring WebFlux ใช้เป็นหลัก |
| Java | vavr | persistent collections + Either/Try/Option |
| Java | Eclipse Collections | immutable + primitive collections, performance ดี |
| Java | Resilience4j | Try monad สำหรับ wrap operation ที่อาจ throw |
| Kotlin | Arrow | FP toolkit ครบ — Option, Either, IO, Resource |
| Kotlin | Kotlin Coroutines | structured concurrency (ไม่ใช่ monad ตรง ๆ แต่ใช้แทน Future ได้) |
| Scala | Cats Effect | effect monad ระดับ production |
| Scala | ZIO | effect system อีกแนวที่ฮิตมาก — type-safe error + dependency injection |
| TypeScript | Effect TS | effect monad โมเดิร์น 2024+ — compose effect ได้ดี เริ่มแทน fp-ts ในโปรเจกต์ใหม่ |
| TypeScript | fp-ts | Haskell-style FP — มีครบทั้ง functor/applicative/monad |
| TypeScript | RxJS | reactive observable (สาย stream) |
| Rust | built-in | Option, Result เป็น core ของภาษา ไม่ต้องเสริม library |
💡 เลือกอย่างไร? — งาน Java ทั่วไป: Reactor (ถ้าใช้ WebFlux) หรือ vavr (ถ้าอยาก Either/Try). งาน Kotlin: Arrow. งาน Scala: Cats Effect หรือ ZIO. งาน TypeScript ใหม่ปี 2026: Effect TS แนวโน้มมาแรง
19. FP + OOP Together — Pragmatic Java
ในงานจริงเราไม่เลือกข้างระหว่าง FP กับ OOP แต่ใช้แต่ละแบบให้ถูกที่ — ใช้ OOP จัดการ domain object (encapsulate, behavior) และใช้ FP ภายในเมธอด (pure transform เช่น total() ที่ reduce) โดยเก็บ side effect ไว้ที่ชั้น service ตัวอย่างนี้ผสมทั้งสองอย่างลงตัว:
java
// Domain (OOP)
public class Order {
private final Long id;
private final List<OrderItem> items;
private final OrderStatus status;
public Order confirm() { // immutable update
return new Order(id, items, OrderStatus.CONFIRMED);
}
public BigDecimal total() { // FP — pure
return items.stream()
.map(OrderItem::subtotal)
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
}
// Service (orchestrator) — has side effects
public class OrderService {
private final OrderRepository repo;
public Order createOrder(CreateOrderRequest req) {
// Pure construct
Order order = Order.fromRequest(req);
// Pure validation
validate(order);
// Side effect (isolated)
return repo.save(order);
}
}→ Pure ตรงไหนทำได้ + Side effect ที่ boundary
20. ⚠️ Common Pitfalls
กับดักที่คนเพิ่งหัด FP มักติด — ส่วนใหญ่คือการเอา side effect/การ mutate ไปยัดใน map/filter (ซึ่งควรเป็น pure) หรือดันใช้ FP กับทุกอย่างจนอ่านยากกว่าเดิม ตารางนี้จับคู่ "สิ่งที่พลาด" กับ "สิ่งที่ควรทำ":
| ❌ | ✅ |
|---|---|
Mutate inside map/filter | Pure transform — return new |
| Side effect in stream | Side effect after collect |
Optional in field — Optional<String> name | T or T? — Optional for return type |
| Deep recursion → stack overflow | Iteration or tail call (other lang) |
for everywhere | Map/filter/reduce when transforming |
| Convert everything to FP | OOP for domain, FP for transform |
| Optional.get() | orElse / orElseThrow |
Stream forEach for side effect | Use traditional loop — clearer |
21. When NOT to use FP
FP ไม่ใช่ค้อนที่ทุบได้ทุกอย่าง — มีหลายสถานการณ์ที่แนวอื่นเหมาะกว่า เช่น hot path ที่ต้องเร็วสุด ๆ (mutation เร็วกว่า), งานที่ทีมยังไม่คุ้น (อ่านยาก) ลิสต์นี้ช่วยให้ตัดสินใจว่าเมื่อไหร่ "อย่าฝืน":
- Performance-critical hot path — mutation faster
- UI state machine — explicit mutation ดีกว่า
- Domain object with rich behavior — OOP shine
- I/O heavy — async/await ดีกว่า monad
- Team unfamiliar — readability suffers
→ FP for transformation pipeline, OOP for domain modeling
22. Checkpoint
แบบฝึกหัดท้ายบทให้ลองแปลง loop เป็น stream, จัดกลุ่ม/นับด้วย Collectors, refactor คลาส mutable เป็น immutable และต่อ pipeline แปลงข้อความจริง — ทำเองก่อนค่อยเทียบเฉลย จะซึมหลัก FP ได้แน่นกว่าอ่านผ่าน:
🛠️ Checkpoint 5.1 — Loop to Stream
java
List<User> result = new ArrayList<>();
for (User u : users) {
if (u.isActive() && u.getAge() >= 18) {
result.add(u);
}
}
result.sort((a, b) -> a.getName().compareTo(b.getName()));
List<String> names = new ArrayList<>();
for (User u : result) names.add(u.getName());Refactor as Stream
🛠️ Checkpoint 5.2 — Group + Count
Given List<Order>:
- Group by user, count orders per user
- Average order total per status
- Top 5 customer by revenue
🛠️ Checkpoint 5.3 — Immutable Refactor
Take a mutable class + refactor to immutable (use record or Lombok @Value)
🛠️ Checkpoint 5.4 — Compose Pipeline
สร้าง text processing pipeline:
- normalize (trim, lowercase)
- remove punctuation
- split into words
- filter stop-words
- count word frequency
- top 10
ทำเป็น stream/pipeline
23. สรุปบท
ทบทวนหัวใจของบทนี้แบบรวบยอด — FP มอง function เป็นค่าตัวหนึ่ง เน้น pure function + immutability เพื่อให้โค้ดเทสต์ง่ายและคาดเดาได้ ใช้ map/filter/reduce แทน loop ส่วนใหญ่ และผสานกับ OOP ในงานจริง:
✅ FP = functions as first-class — pure, immutable, no side effects
✅ Pure function = same input, same output, no side effect — easy to test
✅ Immutability = thread-safe, predictable — use record
✅ Higher-order function = take/return function — lambda + method ref
✅ Map/filter/reduce = core operations — replace 90% of loops
✅ Function composition — chain pure functions
✅ Closure = function + captured environment
✅ Pattern matching (Java 17+) — sealed + switch
✅ Optional > null — chain map/flatMap
✅ Isolate side effects at boundary — pure logic inside
✅ Modern Java/JS/TS = FP + OOP hybrid — best of both
24. Glossary — ศัพท์ FP พร้อมคำอ่าน
ศัพท์ FP ที่ออกเสียงยากสำหรับคนไทย รวบรวมไว้ค้นกลับได้:
| ศัพท์อังกฤษ | คำอ่านไทย | ความหมายสั้น ๆ |
|---|---|---|
| Pure function | เพียวร์ ฟังก์ชัน | function ที่ deterministic + ไม่มี side effect |
| Immutability | อิม-มิว-ทา-บิ-ลิ-ตี้ | ของเดิมแก้ไม่ได้ ต้องสร้างใหม่ |
| Higher-order function | ไฮ-เออร์-ออ-เดอร์ ฟังก์ชัน | function ที่กิน function หรือคืน function |
| Paradigm | พา-รา-ไดม์ | แนวคิด/รูปแบบความคิดในการเขียนโปรแกรม |
| Persistent | เพอร์-ซิส-เทนต์ | (data structure) คงอยู่ของเดิม สร้างเวอร์ชันใหม่ได้เร็ว |
| Structural sharing | สตรัค-เจอ-รัล แชร์-ริ่ง | ใช้โครงสร้างส่วนที่ไม่เปลี่ยนร่วมกันระหว่างเวอร์ชัน |
| Closure | โคล-เชอร์ | function ที่ "จำ" ตัวแปรจาก scope รอบนอกได้ |
| Effectively final | เอ็ฟ-เฟค-ทีฟ-ลี่ ไฟ-นัล | ตัวแปร Java ที่ "เสมือนเป็น final" — ไม่ถูกแก้หลังกำหนดครั้งแรก |
| Currying | เคอ-รี่-อิ้ง | แปลง f(a,b,c) → f(a)(b)(c) (ตั้งชื่อตาม Haskell Curry) |
| Partial application | พาร์-เชียล แอป-พลิ-เค-ชัน | ใส่ค่าบางตัวล่วงหน้า ได้ function ใหม่ที่รอค่าที่เหลือ |
| Functor | ฟัง-เตอร์ | "กล่องที่ map ได้" — ขั้นแรกของบันได functor→applicative→monad |
| Applicative | แอป-พลิ-เค-ทิฟ | functor + เอา function ที่ห่อในกล่องมาใช้ได้ |
| Monad | โม-แนด | "กล่องที่ flatMap ได้" — ใช้ chain operation ที่ wrap ในกล่อง |
| Maybe | เม-บี | ชื่อ type ใน Haskell ที่เทียบเท่า Optional ใน Java |
| Either | อี-เธอร์ | "ตัวใดตัวหนึ่ง" จากสองตัวเลือก (มัก = Result แบบ Ok / Err) |
| Algebraic data type | อัล-เก-เบรอิก ดา-ต้า ไทพ์ | ADT — type system ที่รวม sum type + product type |
| Destructuring | ดี-สตรัค-เจอ-ริ่ง | แยกค่าออกเป็นชิ้น ๆ (เช่น case Circle(double r) -> ...) |
| Exhaustive | เอ็ก-ซอส-ทีฟ | ครอบคลุมทุกกรณี (compiler บังคับให้จัดการครบ) |
| Explicit type witness | เอ็กซ์-พลิ-ซิท ไทพ์ วิท-เนส | บอกชนิดอย่างชัดเจน (เช่น .<Result<U,S>>map(...)) |
| Discriminated union | ดิส-คริม-อิ-เนท-เต็ด ยูเนียน | union ที่แยก variant ด้วย field พิเศษ (เช่น kind) |
| Method reference | เมท-ธอด เรฟ-เฟอ-เรนซ์ | ทางลัด :: ของ lambda (เช่น String::toUpperCase) |
| Deterministic | เด-เทอร์-มิ-นิส-ติก | input เดิมให้ output เดิมเสมอ |