Skip to content

บทที่ 5 — Functional Programming

← บทที่ 4 | สารบัญ | บทที่ 6 →

หลังจบบท คุณจะ:

  • เข้าใจ 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)

OOPFP
Building blockObject (data + behavior)Function (pure mapping)
StateMutable เป็นปกติImmutable เป็นปกติ
Side effectOKAvoid / isolate
Iterationfor loopmap/filter/reduce
LogicOO methodFunction composition
ภาษาตัวอย่างJava, C# (เก่า)Haskell, Elm

ภาษาส่วนใหญ่ปี 2026 = multi-paradigm — Java, JS, Python, Kotlin, Scala, Swift = FP ✅


2. Pure Function

ฟังก์ชันที่:

  1. Same input → same output เสมอ
  2. 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 ระดับบน), Guava ImmutableList, 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 view

4. 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);  // 15

5. 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) จึงไม่ต้องประกาศเองทุกครั้ง:

💡 @FunctionalInterface annotation คืออะไร — แค่ "ป้าย" บอก 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(...) แทนปลอดภัยกว่า:

java
int 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);  // 20
javascript
// 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);   // 7

Currying vs Partial Application — ต่างกันยังไง?

ทั้งสองคำคนชอบสับสน เพราะหน้าตาคล้ายกัน แต่ความหมายต่าง:

CurryingPartial 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 + when expression แบบเดียวกัน — 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 ขั้นจากง่ายไปยาก — เหมือนบันได ขึ้นทีละขั้น (ทุกขั้นบนตอบโจทย์ขั้นล่างได้หมด):

ขั้นชื่อความสามารถตัวอย่างเมธอด
1Functor (ฟัง-เตอร์)"กล่องที่ map ได้" — แปลงค่าข้างในด้วย functionOptional.map(fn)
2Applicative (แอป-พลิ-เค-ทิฟ)functor + เอา function ที่ห่อในกล่องมาใช้กับค่าในกล่องอีกใบได้Optional.ap(fnInBox) (ใน vavr)
3Monad (โม-แนด)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 Reactor Mono<T> / Flux<T> (Spring WebFlux ใช้ตัวนี้เป็นหลัก) ทั้งคู่เป็น monad reactive ที่มี flatMap ครบ
  • Kotlin: Arrow library — 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บทบาท
JavaProject Reactor (Mono/Flux)reactive monad — Spring WebFlux ใช้เป็นหลัก
Javavavrpersistent collections + Either/Try/Option
JavaEclipse Collectionsimmutable + primitive collections, performance ดี
JavaResilience4jTry monad สำหรับ wrap operation ที่อาจ throw
KotlinArrowFP toolkit ครบ — Option, Either, IO, Resource
KotlinKotlin Coroutinesstructured concurrency (ไม่ใช่ monad ตรง ๆ แต่ใช้แทน Future ได้)
ScalaCats Effecteffect monad ระดับ production
ScalaZIOeffect system อีกแนวที่ฮิตมาก — type-safe error + dependency injection
TypeScriptEffect TSeffect monad โมเดิร์น 2024+ — compose effect ได้ดี เริ่มแทน fp-ts ในโปรเจกต์ใหม่
TypeScriptfp-tsHaskell-style FP — มีครบทั้ง functor/applicative/monad
TypeScriptRxJSreactive observable (สาย stream)
Rustbuilt-inOption, 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/filterPure transform — return new
Side effect in streamSide effect after collect
Optional in field — Optional<String> nameT or T? — Optional for return type
Deep recursion → stack overflowIteration or tail call (other lang)
for everywhereMap/filter/reduce when transforming
Convert everything to FPOOP for domain, FP for transform
Optional.get()orElse / orElseThrow
Stream forEach for side effectUse 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 behaviorOOP 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 เดิมเสมอ

← บทที่ 4 | บทที่ 6 → Architecture Patterns