Signatures That Don't Lie

Signatures That Don't Lie

เคยไหมครับ อ่านชื่อฟังก์ชันในโค้ด เห็น Input และ Output ดูปกติมาก แต่พอรันจริงกลับเจอ Runtime Error พ่นใส่หน้า หรือเจอ Null โผล่มาแบบไม่ได้รับเชิญ ทั้งที่ Type ไม่ได้บอกไว้...

ในโลกของ Functional Programming (FP) เรามีเป้าหมายอย่างหนึ่งคือการเขียน "Honest Functions" หรือฟังก์ชันที่มี Signature "ซื่อสัตย์" ต่อผู้ใช้ วันนี้เราจะมาเจาะลึกกันว่าแนวคิดนี้คืออะไร และทำไมมันถึงช่วยให้เรานอนหลับเต็มอิ่มขึ้นครับ 🥹

ฟังก์ชันที่ "โกหก" (Dishonest Functions) คืออะไร?

ฟังก์ชันที่โกหก คือฟังก์ชันที่ Signature (สิ่งที่มันประกาศ) กับ Behavior (สิ่งที่มันทำจริง) ไม่ตรงกัน โดยส่วนใหญ่มักจะโกหกเราใน 3 (+1 ในบางภาษา) รูปแบบหลัก:

1. Partial Functions

มันบอกว่ารับ number นะ แต่พอยัดเลข 0 เข้าไป กลับระเบิดตัวเองตาย (Crash) เพราะมันไม่ได้รองรับเลขศูนย์จริงๆ ตั้งแต่แรก แถมสัญญาว่าจะ return number แต่ดัน throw exception

2. Hidden Side Effects

มันบอกว่าคืนค่า string แต่ลึกๆ แอบไปลบไฟล์ในเครื่อง หรือไปเปลี่ยนค่าตัวแปร Global โดยที่เราไม่รู้

3. การใช้ Exceptions ในการจัดการ Business Logic

การโยน Exception ใน Functions ที่ใช้จัดการ Business ต่างๆ ก็ถือเป็นการโกหก เพราะมันคือการข้ามลำดับการทำงานปกติ (Non-local GOTO) และทำให้ผู้เรียกไม่ทราบว่าฟังก์ชันอาจจะล้มเหลวได้หากดูเพียงแค่ Signature

4. การใช้ Nulls (ในบางภาษา)

ในบางภาษา ประเภทข้อมูลอ้างอิงอาจเป็นค่าว่าง (null) ได้เสมอ ทำให้ Signature ที่ระบุว่าจะส่งกลับ Object กลายเป็นการโกหก เพราะบางครั้งมันส่งกลับ null แทน

แล้วฟังก์ชันที่ "ไม่โกหก" (Honest Functions) คืออะไร?

มันคือฟังก์ชันที่มีคุณสมบัติตรงข้ามกับฟังก์ชันที่โกหก ผ่ามๆ หยอกๆ หลักๆคือควรจะมีคุณสมบัติดังนี้:

1. บอกอินพุตที่เป็นไปได้ทั้งหมด

Signature ต้องระบุอย่างชัดเจนว่ารับค่าอะไรได้บ้าง และจะไม่มีการทำงานผิดพลาดหากส่งค่าตามประเภทที่ระบุมา

2. บอกผลลัพธ์ที่เป็นไปได้ทั้งหมด

ไม่ว่าผลลัพธ์จะสำเร็จหรือล้มเหลว (เช่น Error หรือค่าว่าง) ข้อมูลเหล่านี้ต้องถูกระบุไว้ในประเภทข้อมูลที่ส่งกลับ (Return Type)

3. No Hidden Side Effects

ฟังก์ชันต้องไม่แอบไปแก้ไขค่า Global หรือติดต่อฐานข้อมูลโดยที่ไม่ได้ระบุไว้ใน Signature ซึ่งจะช่วยให้เกิด Referential Transparency (ไม่อธิบายใน blog นี้นะ 🤣) หรือความสามารถในการแทนที่ฟังก์ชันด้วยค่าของมันได้โดยไม่ทำให้พฤติกรรมของโปรแกรมเปลี่ยนไป

เทคนิคการเบื้องต้นในการช่วยให้เราออกแบบ Honest Functions

1. การใช้ Wrapper Types

แทนที่จะส่งกลับค่าตรง ๆ ให้ใช้ประเภทข้อมูลอย่าง Maybe, Option หรือ Either เพื่อระบุชัดเจนว่าผลลัพธ์อาจจะไม่มีค่าหรือเกิดข้อผิดพลาดได้

2. การจำกัดประเภทอินพุต (Restrict Input Types)

เช่น แทนที่จะรับ int สำหรับตัวหาร ให้สร้างประเภทข้อมูลใหม่ชื่อ NonZeroInteger เพื่อให้ Compiler บังคับว่าห้ามส่งค่า 0 มาตั้งแต่ต้น

3. เน้น Immutability และ Pure Functions

การใช้ข้อมูลที่เปลี่ยนแปลงไม่ได้ช่วยให้เรามั่นใจว่าฟังก์ชันจะทำงานแค่ตามที่ระบุใน Signature เท่านั้น

จริงๆมีเทคนิคอีกมากมายในการทำให้ Function ของเรา "ซื่อสัตย์" มากขึ้นลองไปศึกษากันเพิ่มดูนะ

เปรียบเทียบ Honest Functions กับ Dishonest Functions

ลองมาดูตัวอย่าง function สำหรับหารเลข

function divide(a: number, b: number): number {
  if (b === 0) throw new Error("หารด้วยศูนย์ไม่ได้นะจ๊ะ!");
  return a / b;
}

ปัญหา: function นี้ ดูผ่านๆ ถ้าคนที่เอาฟังก์ชันนี้ไปใช้ไม่ได้เข้ามาดู "ไม่มีทางรู้เลย" ว่าต้องครอบ try-catch เว้นแต่จะเข้าไปอ่าน Code ข้างในหรือรอให้มันพังกลางอากาศ

ถ้าจะแก้ให้เป็น Honest Functions โดยส่วนมากเราจะใช้ระบบ Type มาเพื่อช่วยบอกความจริงทั้งหมด ในตัวอย่างจะสร้าง Type ใหม่เป็น Sum Type ที่ชื่อว่า Result มา โดยที่ Result จะเป็นได้ทั้ง Success และ Error ดังตัวอย่างด้านล่างนี้

type Result<T> = { status: 'success', value: T } | { status: 'error', message: string };

/**
 * Signature นี้บอกความจริง 100%: 
 * "ฉันรับ number นะ แต่อาจจะคืนเป็นผลลัพธ์ หรือข้อความ Error ก็ได้"
 */
function safeDivide(a: number, b: number): Result<number> {
  if (b === 0) {
    return { status: 'error', message: "Division by zero" };
  }
  return { status: 'success', value: a / b };
}

ข้อดี: ในบางภาษา Compiler จะบังคับให้คุณจัดการกรณี Error ทันที คุณลืมไม่ได้เพราะ Type มันค้ำคออยู่

ทำไมผมถึงแคร์ ?

เมื่อเราเขียนโปรแกรมตามหลัก Signatures that don't lie ผลลัพธ์ที่ตามมาคือ:

  1. Code คือ Documentation: คุณไม่จำเป็นต้องเขียนคอมเมนต์อธิบายยาวเหยียดว่าฟังก์ชันจะพังตอนไหน เพราะ Type มันบอกไว้หมดแล้ว
  2. Refactoring อย่างมั่นใจ: เมื่อคุณเปลี่ยน Logic ระบบ Type จะฟ้องทันทีว่าจุดไหนที่คุณยังจัดการไม่ครบ
  3. ลด Cognitive Load: สมองไม่ต้องจำว่า "ฟังก์ชันนี้ห้ามส่งค่านี้เข้านะ" เพราะ Compiler จะเป็นคนจำแทนคุณเอง
  4. ลดความซับซ้อน (Reduce Complexity): คุณสามารถทำความเข้าใจฟังก์ชันได้จากการดูแค่ Signature โดยไม่ต้องไล่โค้ดทีละบรรทัด (Local Reasoning)

สรุปส่งท้าย

การเปลี่ยนมาเขียนฟังก์ชันที่ "ไม่โกหก" อาจจะดูยุ่งยาก (มาก) ในช่วงแรก เพราะเราต้องห่อหุ้มค่า (Wrap values) และจัดการเคสต่างๆ มากขึ้น แต่ในระยะยาว มันคือการลงทุนที่คุ้มค่า เพราะมันเปลี่ยนจาก "การไล่ตามแก้ Bug" มาเป็นการ "ออกแบบระบบที่ไม่เปิดโอกาสให้เกิด Bug" ตั้งแต่แรกครับ

"Be honest with your types, and your code will be honest with you."

Happy Coding ครับ

Read more

Tuple: ปรัชญาของการปูเสื่อ และศิลปะแห่งการไม่ตั้งชื่อ

Tuple: ปรัชญาของการปูเสื่อ และศิลปะแห่งการไม่ตั้งชื่อ

ในโลกของการเขียนโปรแกรม เรามักถูกสอนให้เป็น “นักจัดระเบียบ” เราสร้างคลาส สร้าง Struct ตั้งชื่อตัวแปรให้สื่อความหมาย (Clean Code) แต่บางครั้ง ความเคร่งครัดที่มากเกินไปอาจกลายเป็นพันธนาการที่ทำให้ Code ของเราอุ้ยอ้ายโดยไม่จำเป็น 1. Naming Fatigue: ภาระของการมีตัวตน ลองนึกภาพคุณได้

By Santi
The Art of Early Return: วินัยแห่งการ “คัดออก” เพื่อสมองที่โล่งกว่าเดิม 10 เท่า

The Art of Early Return: วินัยแห่งการ “คัดออก” เพื่อสมองที่โล่งกว่าเดิม 10 เท่า

ในโลกของการพัฒนาซอฟต์แวร์ เรามักจะถูกสอนให้เป็นคนรอบคอบ ให้คิดถึงความเป็นไปได้ให้ครบทุกด้าน แต่บ่อยครั้งที่ “ความรอบคอบ” นั้นกลับกลายเป็นกับดักที่สร้างความซับซ้อนจนเราเองก็รับมือไม่ไหว วันนี้ผมอยากจะหยิบยกปรัชญาหนึ่งที่ผมพบจากการเขียนโปรแกรม โดยเฉพาะในภาษาอย่าง Rust ซึ่งมันไม่

By Santi
The Logic Trap: เมื่อ “ความถูกต้อง” กลายเป็นอาวุธที่ทำลายทีมซอฟต์แวร์

The Logic Trap: เมื่อ “ความถูกต้อง” กลายเป็นอาวุธที่ทำลายทีมซอฟต์แวร์

ในโลกของการพัฒนาซอฟต์แวร์ เราถูกสอนให้เทิดทูน Logic เป็นพระเจ้า เราใช้เหตุผลในการคัดเลือก Stack, ใช้ความถูกต้องในการทำ Code Review และใช้ตัวเลขในการวาง Roadmap แต่เคยสงสัยไหมครับ? ทั้งที่เราพูดเรื่องที่ “ถูกต้อง” และเป็น “ความจริง” ทุกประการ ทำไมผลลัพธ์ในห้องประชุ

By Santi
Change Management ต้องทำไหมนะ แล้วทำตอนไหน

Change Management ต้องทำไหมนะ แล้วทำตอนไหน

เนื่องจากช่วงนี้ได้ทำงานกับลูกค้าที่มีการเปลี่ยนแปลงทาง scope ของงานเยอะมาก อารมณ์แบบตอน baseline เป็นแบบนึง พอจะเลือกงานมาทำจริงๆ เรียกว่าเปลี่ยนไปตาม strategy ขององค์กรเลยก็ว่าได้ ในฐานะที่เราเป็นกลุ่มนักพัฒนา ที่ยังจำเป็นต้องควบคุมงบประมาณ กำหนด scope และต้องตอบให้ได้ว่า

By Thanthiya Phatharamalai