Signatures That Don't Lie

Share
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

The Five Ideals

The Five Ideals

จากหนังสือ The Unicorn Project ในนั้นจะมีที่ปรึกษาเก่ง ๆ คนหนึ่งชื่อ Erik เป็นเหมือนเซ็นเซหาตัวยาก เดี๋ยวก็มา เดี๋ยวก็ไป ถ้าโชคดีก็จะเจอเค้าว่าง แล้วเค้าก็จะสอนอะไรที่สำคัญ ๆ แต่ฟังดูล้ำ ๆ และลึกซึ้ง

By Chokchai Phatharamalai
Communication, Reliability and Modern concepts - System Design, Simply Explained

Communication, Reliability and Modern concepts - System Design, Simply Explained

บทความนี้จะอธิบายพื้นฐานของ System Design โดยต่อยอดจากตัวอย่าง Gym Application เพื่อให้เห็นภาพว่า เมื่อระบบเริ่มเติบโตและถูกแยกออกเป็นหลาย Service แต่ละ Service จะสื่อสารและทำงานร่วมกันอย่างไร งานแบบไหนควรสื่อสารแบบ Synchronous งานแบบไหนควรประมวลผลแบบ Asynchronous รวมถึงอธิบาย Concept พื้นฐานต่าง ๆ ที่ควรรู้ เช่น API

By Boonsong Srithong
Race condition

Race condition

ผมกำลังอ่านหนังสือ The Unicorn Project ซึ่งเป็นนิยายเกี่ยวกับ developer สาวคนหนึ่งที่ใช้ชีวิตในองค์กรท่ามกลางกระแส digital disruption อ่านแล้วสนุกมาก เป็นเรื่องที่ตื่นเต้น และหลาย ๆ dynamic ที่เกิดขึ้นก็คล้ายกับที่ผมพบเจอในบริษัทซอฟต์แวร์ เลยทำให้

By Chokchai Phatharamalai
Distributed Systems Concepts - System Design, Simply Explained

Distributed Systems Concepts - System Design, Simply Explained

ในช่วงแรก Product เราสามารถออกแบบระบบให้อยู่ใน Server และ Database เดียวกันได้ แต่เมื่อระบบโตขึ้น มีผู้ใช้งานเพิ่มขึ้น ข้อมูลเยอะขึ้น ระบบเริ่มทำงานช้าลดเรื่อยๆ สิ่งที่หนีไม่ได้คือการ Decomposition Service ไปเป็น Distributed System เช่น Member Service, Booking Service, Payment

By Boonsong Srithong