Devfavor

Back

แก้โค้ดที่เดียว แต่ไม่พังที่อื่น — 3 หลักง่าย ๆ ของ Functional ProgrammingBlur image

เคยเจอ bug แบบนี้ไหม — แก้ฟังก์ชันหนึ่ง แล้วอีกหน้าจอที่ไม่เกี่ยวกันเลยพังตาม ไล่หาอยู่ครึ่งวันถึงเจอว่ามีโค้ดแอบไปแก้ตัวแปรตัวเดียวกันที่แชร์กันอยู่

หรือเวลาเขียนเทส ต้อง set up อะไรเต็มไปหมด — ต้อง mock database, ต้องตั้งค่า global ก่อน เพราะฟังก์ชันที่จะเทสไปดึงค่าจากที่อื่นแบบเดาทางไม่ได้

ปัญหาพวกนี้มีรากเดียวกัน คือ state ที่เปลี่ยนได้ตลอดเวลา กับ side effect ที่ซ่อนอยู่ Functional Programming เป็นแนวคิดที่ช่วยตัดต้นตอพวกนี้ทิ้ง — และคุณไม่จำเป็นต้องย้ายไปเขียน Haskell เพื่อใช้ประโยชน์จากแนวคิดนี้

มาดู 3 หลักที่เป็นหัวใจกัน: pure function, immutability และ recursion


🧩 Functional Programming คืออะไร (แบบสั้น ๆ)#

Functional Programming (FP) คือแนวคิดการเขียนโปรแกรมด้วยการประกอบฟังก์ชันเล็ก ๆ เข้าด้วยกัน มองทุกการคำนวณเป็นการเรียกใช้ฟังก์ชันแบบคณิตศาสตร์ แทนที่จะสั่งคอมพิวเตอร์ทีละขั้นว่าทำนี่แล้วทำนั่น

ความต่างหลักจากแบบ imperative ที่เราคุ้นเคย คือ FP พยายามหลีกเลี่ยงการเปลี่ยน state และเลี่ยง mutable data ให้มากที่สุด

แนวคิดนี้ไม่ใช่ของใหม่ ต้นกำเนิดอยู่ที่ lambda calculus ในช่วงทศวรรษ 1930 และภาษา FP ระดับสูงภาษาแรกคือ Lisp ในปลายทศวรรษ 1950 แต่เราจะไม่ลงลึกเรื่องประวัติ — สิ่งที่ใช้ได้จริงคือ 3 หลักข้างล่างนี้


✨ หลักที่ 1: Pure Function — ฟังก์ชันที่เดาทางได้#

Pure function คือฟังก์ชันที่มีคุณสมบัติสองข้อพร้อมกัน:

  1. input เดิม ได้ output เดิมเสมอ (deterministic)
  2. ไม่มี side effect — ไม่ไปแก้อะไรนอกตัวเอง ไม่อ่าน/เขียนไฟล์ ไม่ยิง network ไม่แตะ global

ลองดูตัวอย่างเทียบกัน:

// pure — เรียก add(5, 5) กี่ครั้งก็ได้ 10 ทุกครั้ง
function add(a, b) {
  return a + b;
}

// impure — ผลลัพธ์ขึ้นกับ state ข้างนอก
let counter = 0;
const impureAdd = (num) => (counter += num);
// impureAdd(5) ครั้งแรกได้ 5, ครั้งสองได้ 10 — input เดิมแต่ผลต่าง
javascript

impureAdd ไปแก้ตัวแปร counter ที่อยู่นอก scope นั่นแหละคือ side effect และเป็นต้นตอของ bug แบบ “แก้ตรงนี้ พังตรงโน้น” ที่เล่าตอนเปิด

ทำไม pure function ถึงดี#

เทสง่ายมาก — แค่ป้อน input แล้วเช็ค output ไม่ต้อง mock อะไร ไม่ต้องตั้ง global ก่อน เพราะพฤติกรรมของฟังก์ชันขึ้นกับ argument อย่างเดียว

อ่านง่าย ดูแลง่าย — เวลาอ่านโค้ด คุณไม่ต้องเก็บไว้ในหัวว่า “ตอนนี้ global ตัวไหนเป็นค่าอะไร” ทุกอย่างที่ฟังก์ชันต้องใช้อยู่ใน argument หมด ลด cognitive load ลงเยอะ

คุณสมบัตินี้มีชื่อเรียกว่า referential transparency — แปลว่าคุณแทนที่การเรียกฟังก์ชันด้วยค่าผลลัพธ์ได้เลยโดยโปรแกรมไม่เปลี่ยนพฤติกรรม

แต่ side effect จำเป็นนะ#

โปรแกรมที่ pure ล้วน ๆ จะทำอะไรไม่ได้เลย เพราะการอ่าน database, ยิง API, เขียน log, จัดการ DOM ล้วนเป็น side effect ทั้งนั้น

หลักปฏิบัติที่ใช้กันจริงคือ “core pure, shell impure” — เก็บ logic หลักไว้ในฟังก์ชัน pure ส่วนงานที่ต้องแตะโลกภายนอกดันออกไปไว้ขอบนอกสุด แนวคิดนี้ React และ Redux ก็บังคับใช้โดย default (component ควรเป็น pure ต่อ props/state)


🔒 หลักที่ 2: Immutability — สร้างใหม่ แทนที่จะแก้ของเดิม#

Immutability คือ ข้อมูลที่สร้างแล้วแก้ไม่ได้ ถ้าจะเปลี่ยนค่า ให้สร้างโครงสร้างใหม่ขึ้นมาแทน

ฟังดูสิ้นเปลือง แต่นี่คือสิ่งที่ป้องกัน bug จาก shared state — ถ้าไม่มีใครแก้ของเดิมได้ ก็ไม่มีทางที่โค้ดส่วนหนึ่งจะไปทำให้อีกส่วนพังโดยไม่รู้ตัว

ใน JavaScript#

const user = { name: "Ann", age: 30 };

// แทนที่จะแก้ user ตรง ๆ ให้ spread แล้วสร้างใหม่
const olderUser = { ...user, age: 31 };
// user ยังเป็น age: 30 อยู่ — ของเดิมไม่ถูกแตะ

// array methods ที่คืนค่าใหม่โดยไม่แก้ต้นฉบับ
const nums = [1, 2, 3];
const doubled = nums.map((n) => n * 2); // [2, 4, 6], nums ยังเหมือนเดิม
javascript

map, filter, slice คืน array ใหม่เสมอ ส่วน Object.freeze() ก็ช่วย lock object ได้ แต่ระวัง — Object.freeze() เป็น shallow คือ nested object ข้างในยังถูกแก้ได้อยู่

ใน Python#

Python มี immutable type ติดมาในตัวหลายอย่าง: int, float, str เป็น immutable โดยธรรมชาติ ส่วน tuple คือเวอร์ชันแก้ไม่ได้ของ list และ frozenset คือ set ที่แก้ไม่ได้ — ข้อดีของ frozenset คือเอาไปเป็น dictionary key หรือ element ของ set อื่นได้ (เพราะแก้ไม่ได้จึง hash ได้)

ประโยชน์ตัวจริง: concurrency#

ข้อดีที่เห็นชัดที่สุดของ immutability คือเรื่อง concurrency — เมื่อไม่มีใครแก้ข้อมูลได้ ก็ไม่มี race condition และไม่ต้องใช้ lock ในการแชร์ข้อมูลข้าม thread เพราะทุกคนอ่านของก้อนเดียวกันที่การันตีว่าไม่เปลี่ยน


🔁 หลักที่ 3: Recursion — วนซ้ำด้วยการเรียกตัวเอง#

ในเมื่อ FP เลี่ยงการเปลี่ยน state แล้ว loop แบบ for / while ที่ต้องคอยอัปเดตตัวนับก็ขัดกับแนวคิด FP จึงมักวนซ้ำด้วย recursion แทน — คือฟังก์ชันเรียกตัวเองเพื่อแก้ปัญหาที่ย่อยลงเรื่อย ๆ จนถึง base case

function factorial(n) {
  if (n <= 1) return 1; // base case — จุดหยุด
  return n * factorial(n - 1); // เรียกตัวเองด้วยปัญหาที่เล็กลง
}
javascript

กับดักที่ต้องระวัง#

Recursion ที่เขียนแบบตรงไปตรงมา (แบบที่ไม่ optimize อะไรเลย) อาจช้าได้มากจนน่าตกใจ ตัวอย่างคลาสสิกคือ Fibonacci:

function fib(n) {
  if (n < 2) return n;
  return fib(n - 1) + fib(n - 2); // ถูกต้องทางคณิตศาสตร์ แต่โตแบบ exponential
}
javascript

โค้ดนี้คำนวณค่าเดียวกันซ้ำอยู่มหาศาล fib(40) ก็เริ่มอืดแล้ว และที่สำคัญกว่านั้น คือทุกครั้งที่เรียกตัวเอง call stack จะเพิ่ม frame ขึ้นเรื่อย ๆ ถ้าวนลึกพอก็จะเกิด stack overflow

Tail recursion ช่วยได้ (ถ้าภาษารองรับ)#

Tail recursion คือการเขียนให้ recursive call เป็น “คำสั่งสุดท้าย” ของฟังก์ชัน ไม่มีงานเหลือทำหลังจากนั้น เมื่ออยู่ในรูปนี้ compiler บางตัวจะ optimize ให้กลายเป็น loop ที่ใช้ stack frame เดียวซ้ำ — เรียกว่า tail-call optimization (TCO) — ทำให้วนลึกแค่ไหนก็ไม่กิน memory เพิ่ม

Gotcha สำคัญ: ใน JavaScript นั้น TCO ถูกระบุไว้ใน ECMAScript spec ก็จริง แต่ engine ส่วนใหญ่ไม่ได้ implement จริง ดังนั้นอย่าไปพึ่ง tail recursion ใน JS เพื่อหลบ stack overflow

ทางออกที่ practical กว่าคือ ใช้ higher-order functions อย่าง map / filter / reduce ซึ่งเป็น abstraction ของ iteration pattern อยู่แล้ว แทนการเขียน raw recursion เอง


⚖️ ข้อดี vs ข้อเสีย — มองให้ครบ#

ฝั่งข้อดี FP ให้โค้ดที่ predictable (referential transparency ทำให้ผลลัพธ์เดาทางได้), เทสง่าย, ปลอดภัยใน concurrency, และ modular ขึ้นเพราะประกอบฟังก์ชันเล็ก ๆ เข้าด้วยกันได้

แต่ก็มีต้นทุนที่ต้องยอมรับ:

  • Memory / performance overhead — immutability สร้าง object ใหม่ทุกครั้งที่เปลี่ยนค่า ยิ่งข้อมูลใหญ่และซ้อนกัน ยิ่งกิน memory ทางแก้คือใช้ persistent data structure ผ่าน library อย่าง Immutable.js หรือ immer ที่แชร์ส่วนที่ไม่เปลี่ยนร่วมกัน เพื่อลด overhead นี้
  • Stack overflow จาก recursion — ถ้าภาษาไม่มี TCO จริง (เช่น JS ส่วนใหญ่) recursion ลึก ๆ จะพังได้
  • Learning curve — คนที่คุ้นกับ imperative/OOP ต้องปรับวิธีคิด การวนด้วย recursion แทน loop อาจดูไม่คุ้นตาในช่วงแรก
  • โค้ดอาจ verbose ขึ้น เพราะต้องส่ง dependency ทุกตัวเข้าไปเป็น argument

กับดักที่พบบ่อยที่สุด#

เผลอใส่ side effect โดยไม่รู้ตัว — เช่น push ค่าเข้า array ที่อยู่นอก scope ภายใน callback ของ map ตอนนั้นแหละที่ฟังก์ชันไม่ pure อีกต่อไป และคุณก็เสีย referential transparency ไปเงียบ ๆ


🤔 แล้วเมื่อไหร่ควรใช้#

อย่ามอง FP กับ imperative เป็นทางเลือกแบบ either/or — แนวทางที่ดีที่สุดมักผสมทั้งสองตามจุดแข็งของงาน

เอนไปทาง FP เมื่อ อยากลด bug จาก mutable state / side effect, อยากให้ไล่โค้ดเข้าใจง่ายและเอาไปใช้ซ้ำได้ง่าย, หรือต้อง parallelize งานกับ dataset ใหญ่

เอนไปทาง imperative เมื่อ ต้องจัดการ state ที่เปลี่ยนตามเวลาจริง ๆ, โค้ดเป็น performance-critical, หรือ control flow ซับซ้อนจนเขียนแบบ functional แล้วอ่านยากกว่า


📌 สรุป#

3 หลักของ FP จริง ๆ แล้วเป็นเครื่องมือลด bug ที่จับต้องได้:

  • Pure function — input เดิมได้ผลเดิม ไม่มี side effect → เทสง่าย เดาทางได้
  • Immutability — สร้างใหม่แทนที่จะแก้ของเดิม → ตัด bug จาก shared state, ปลอดภัยใน concurrency
  • Recursion — วนซ้ำด้วยการเรียกตัวเอง → แต่ระวัง stack overflow ใน JS ใช้ map/filter/reduce แทนได้

ข่าวดีคือคุณไม่ต้องเปลี่ยนภาษาเพื่อเริ่ม ลองเขียนฟังก์ชันถัดไปให้ pure ขึ้น เลิก mutate argument แล้วใช้ spread / map แทน เท่านี้โค้ดก็เดาทางง่ายขึ้นและเทสง่ายขึ้นทันที

ถ้าอยากต่อยอด ลองดูเรื่อง higher-order function และ function composition ซึ่งเป็นก้าวถัดไปที่ทำให้การประกอบฟังก์ชันเล็ก ๆ เข้าด้วยกันกลายเป็นเรื่องสนุก

😎 ☕

Code, coffee, and calm — the holy trinity of a good day. ☕

แก้โค้ดที่เดียว แต่ไม่พังที่อื่น — 3 หลักง่าย ๆ ของ Functional Programming
Author Coffee Stack
Published at 17 มิถุนายน 2026