แก้โจทย์ปัญหาด้วย JavaScript แบบ Functional

เปรียบเทียบโค้ดแบบ imperative style กับ functional style

สมมติมีโจทย์ปัญหาแบบข้างล่างนี้ เราจะเขียนโค้ดเพื่อแก้ปัญหานี้ยังไงครับ (โจทย์นี้นำมาจาก Project Euler — problem 1)

If we list all the natural numbers below 10 that are multiples of 3 or 5, we get 3, 5, 6 and 9. The sum of these multiples is 23.
Find the sum of all the multiples of 3 or 5 below 1000.

สิ่งแรกๆ ที่พอจะคิดได้คือ

  • ต้องมีการวน loop ตัวเลข 1 ถึง 999 (below 1000)
  • ต้องมีการเช็คว่าตัวเลขแต่ละตัว เป็นจำนวนเท่าของ 3 หรือ 5 หรือไม่ (multiple of 3 or 5)
  • ต้องมีการบวกรวมค่าของตัวเลขเก็บไว้ในตัวแปรหนึ่งใน loop เป็นผลลัพธ์ที่เราต้องการ (sum of all the multiples of 3 or 5)

ถ้าเราลองเขียนโปรแกรมเพื่อแก้ปัญหานี้แบบตรงไปตรงมา ก็จะได้ประมาณนี้

แต่ถ้าเราต้องการเขียน code ให้เข้าใจง่ายกว่านี้ และลด code ซ้ำๆ (DRY) ได้มากกว่านี้ จะทำได้มั้ยครับ และทำยังไง ลองมาดูวิธีการปรับโค้ดให้เป็น functional style กันครับ

โค้ดที่จะเขียนต่อไปใช้ library ชื่อว่า Ramda จะช่วยให้เราเขียน JavaScript ในแบบ functional style ได้ง่ายขึ้น


ฟังก์ชันเช็คจำนวนเท่าของ 3 กับ 5

ปรับ code แล้วได้แบบนี้

Line 7 เราทำฟังก์ชันเดิมให้เป็น curry ฟังก์ชัน ถ้ายังไม่รู้จัก curry ฟังก์ชันสามารถสรุปง่ายๆ ได้ตรงนี้เบื้องต้นว่า เวลาเรียกใช้จะไม่ต้องรับทุก parameter (Ramda#curry)

Line 8, 9 ทำให้เราสร้างฟังก์ชันใหม่ๆ ได้โดยไม่ต้องประกาศฟังก์ชัน แต่สร้างฟังก์ชันใหม่โดยเรียกฟังก์ชันเดิมด้วย parameter บางส่วน (3 และ 5)

ฟังก์ชันหาผลลัพธ์

ลองดู code ที่ได้แก้ไขแล้วเวอร์ชันข้างล่าง

Line 9 สร้าง array ของตัวเลขโดยใช้ (Ramda#range) Line 10–12 กรองเอาตัวเลขเฉพาะที่ต้องการจาก array โดยใช้ฟังก์ชัน isMultipleOf ตรวจสอบ (Ramda#filter) Line 13 บวกตัวเลขทั้งหมดใน array (Ramda#sum)

เราสามารถ extract ฟังก์ชัน isMultipleOf ออกมา จะได้ code แบบนี้

จาก code ด้านบน จะเห็นว่าผลลัพธ์ของ Line 3 เป็น parameter ของฟังก์ชัน Line 4 และ ผลลัพธ์ของ Line 4 เป็น parameter ของฟังก์ชัน Line 5 เราสามารถแก้ไขโค้ดเพื่อลดการประกาศตัวแปร ได้เป็นแบบนี้

จะเห็นว่าฟังก์ชัน call หลายชั้น มันมีวงเล็บเยอะ และอ่านยาก เราสามารถสร้างฟังก์ชันใหม่ ที่มีการเรียกฟังก์ชันต่อๆ กันในลักษณะนี้ได้ โดยใช้ (Ramda#compose) มาช่วย ทำให้ code สะอาดขึ้น ได้เป็นแบบนี้

จาก code ด้านบนสังเกตว่าเราเรียก R.filterกับ R.range ได้โดยไม่ต้องส่ง parameter ให้ครบ เพราะ filter, range มันเป็น curry ฟังก์ชัน (ทุกฟังก์ชันของ Ramda เป็น curry ฟังก์ชันโดยอัตโนมัติ)

สังเกตว่าผลลัพธ์ของการเรียก R.compose คือฟังก์ชัน เรียกใช้งานโดยรับค่าตัวเลข 1000 เป็นฟังก์ชัน parameter ค่า 1000 นี้จะถูกใช้เป็น parameter ตัวที่ 2 ของ R.range ต่อไป (สร้าง array ของตัวเลข 1 ถึง 999)

ผลลัพธ์ของ R.range ก็จะใช้เป็น parameter ของฟังก์ชันถัดไปทางซ้ายก็คือR.filter ส่งค่ากันต่อไปเรื่อยๆ จนถึงฟังก์ชันสุดท้ายที่เราประกาศไว้


ทิ้งท้าย

เผื่อใครอยากลอง Ramda มีฟังก์ชัน modulo (Ramda#modulo) กับ equals(Ramda#equals) ด้วย ลองดูว่าจะเอามาปรับใช้กับ ฟังก์ชัน isMultipleOf ด้านบนได้มั้ยครับ

โค้ดทั้งหมดอยู่ที่นี่ครับ