มาเรียนรู้พื้นฐาน Functional Programming กัน

/ 10 min read

Share on social media

functional-programming-basic สามารถดู video ของหัวข้อนี้ก่อนได้ ดู video

Functional Programming คืออะไร ?

Functional Programming คือ รูปแบบวิธีคิดการเขียน program รูปแบบหนึ่ง โดยหยิบไอเดียมาจากการทำ function คณิตศาสตร์ที่จะไม่เกี่ยวข้องกับตัวของ state ตัวอื่นๆนอกเหนือจาก function เป็นเพียงการนำข้อมูลใส่ function และให้ได้ผลลัพธ์ออกมาเพียงเท่านั้น

ไอเดียใหญ่ๆของ Functional Programming คือ การรับข้อมูลเข้ามาจัดการข้อมูลและส่งผลลัพธ์ของ function ออกมา “โดยไม่เปลี่ยนแปลงข้อมูลต้นฉบับหรือตัวของโปรแกรม” ถ้าเราจะเปรียบเทียบให้เข้าใจง่าย เปรียบเทียบเหมือนการทำงานเป็นลำดับขั้นตอนแต่ละขั้นตอนแยกออกจากกัน เหมือนการชงกาแฟที่ต้องมี step ของ

  1. ต้มน้ำ
  2. บดกาแฟ
  3. เทน้ำร้อนลงบนกาแฟกด

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

lambda-calculus

Ref: https://www.researchgate.net/figure/lxgc-reduction-graph-for-lylzzyx_fig1_228386201

พื้นฐานของ Functional Programming นั้นจริงๆแล้วมาจากพื้นฐานของ Lambda calculus

Lambda calculus นั้นถูกคิดค้นโดยนักคณิตศาสตร์ Alonzo Church ในช่วงปี 1930 ซึ่งเป็นสิ่งที่วางรากฐานสำคัญของการทำสิ่งที่เรียกว่า “function” ในคณิตศาสตร์เลยก็ว่าได้ Lambda calculus คือการพยายามจินตนาการโลกว่า “ทุกอย่างในโลกนี้คือ function” ไม่ว่าจะเป็นตัวเลข, operator, ข้อมูลใดๆ หรือ function อื่นๆ เป็น function ทั้งหมด โดยการใส่ input เข้ามา ใส่สิ่งนี้เรียกว่า “function” นี้ ก็จะสามารถได้ output ของ function ที่เรามีการนิยมออกมาได้ (ใช่ครับ มันคือการนิยาม function ออกมานั่นแหละ)

  • ใน Lambda calculus จะใช้สัญลักษณ์ λ (lambda) เป็นการกำหนด anonymous functions โดยสามารถกำหนด parameter เป็นเหมือน input ของข้อมูลที่เราจะจัดการด้วย และผลลัพธ์ที่ออกมาจาก lambda + parameter = ผลลัพธ์ของการทำงาน function ออกมาได้
  • Lambda calculus คือ framework เชิง concept ที่ช่วยทำให้เราสามารถสร้างสิ่งที่เรียกว่า function ออกมาในรูปแบบคณิตศาสตร์ออกมาได้ (เหมือนกับภาพด้านบน) เหมือนกับการกำหนดภาษาในการคุย function ออกมา
  • โดย idea หลักของ function นั้นนอกเหนือจากการทำงานภายในและส่งข้อมูลออกมาได้ ยังสามารถทำงานต่อกันจาก function ตัวอื่นๆไปต่อได้ ส่งผลทำให้เราสามารถสร้าง function ที่ซับซ้อนขึ้น จากการซ้อน function ออกมาได้

ซึ่งสิ่งนี้เป็นต้นแบบของการมอง function ว่าเป็น “purely functional” คือ function จะต้องทำงานตามจุดประสงค์ที่ใส่ไป และต้องไม่ถูกรบกวนจากสิ่งอื่นจนได้ผลลัพธ์ออกมา เพื่อส่งผลทำให้ function สามารถได้ผลลัพธ์ออกมาเหมือนเดิมทุกรอบตามนิยามของคณิตศาสตร์ออกมา ซึ่งสิ่งนี้พอมาอยู่ในโลกของการเขียน programming จึงกลายเป็นข้อดีที่ส่งผลทำให้ function (ในทาง program) สามารถที่จะทำงานตามจุดประสงค์ที่กำหนดเท่านั้น และง่ายต่อความเข้าใจต่อตัว code ดวยเช่นเดียวกัน

  • รวมถึง idea ของ Higher-order functions ที่เป็นพื้นฐานสำคัญของ Functional Programming ซึ่งสามารถรับ function เป็น input และส่งออกเป็น output ออกมาได้ ก็เป็นพื้นฐานมาจาก Lambda calculus เช่นกัน

และนี่คือสิ่งที่เรียกกันว่า Functional Programming เดี๋ยวเราจะเริ่มมาเจาะลึกกันเชิงของ program บ้างว่า เราสามารถนำมาประยุกต์ใช้กับการเขียนโปรแกรมยังไงได้บ้าง

ไอเดียการวางระบบแตกต่างกับ OOP ยังไง ?

แน่นอน เชื่อว่าหลายๆคนน่าจะรู้จักการเขียนโปรแกรมเชิงวัตถุอย่าง Object-oriented programming (OOP) ซึ่งก็ถือเป็นรูปแบบการเขียนโปรแกรมรูปแบบหนึ่งเช่นเดียวกันกับ Functional Programming ซึ่งทั้ง 2 อย่างนี้มีจุดแข็งและจุดพิจารณา รวมถึงมุมมองต่างกันพอสมควร เราจะมาลองเล่าให้ฟังกันทีละจุดแบบนี้นะครับ

  1. Concept
  • OOP focus การมองของออกเป็น object ทำการห่อคุณสมบัติทุกอย่างไว้ภายใน object ไว้ (เช่น รถสีแดง วิ่งได้ความเร็วสูงสุด 120 km/h) และพยายามเชื่อมโยง object ทุกอย่างเข้าหากัน ด้วยสิ่งที่เรียกว่า พฤติกรรม (behavior) (เช่น รถสีแดงถูกใช้โดยคุณหมอและกำลังพยายามขับรถไปยังสถานที่ปลายทาง)
  • Functional Programming คือการเปลี่ยน “ทุกอย่าง” เป็น function ออกมา เหมือนคณิตศาสตร์ โดยพยายามนำเสนอทุกอย่างต้องเกิดจากการใส่ input และได้ผลลัพธ์เป็น ouput ออกมาเสมอ โดยจะต้องไม่มีการแก้ไขหรือแทรกแทรงอะไรระหว่างดำเนินการ

มันจะเหมือนกับภาพนี้ เวลาที่เราพูดถึง function คณิตศาสตร์ มันก็คือการใส่ input และได้ output ของ function นั้นออกมา และก็จะเป็นแบบนี้เสมอ ตราบเท่าที่ใช้ function ตัวเดิมออกมา

function

Ref: https://media.geeksforgeeks.org/wp-content/uploads/20231119175537/Domain-and-Range.png

  1. Data กับ state
  • OOP data ทั้งหมดจะโดนเก็บไว้ object และถูกแก้ไขจาก method ของ object นั้นๆ โดยจะอนุญาตให้เกิด “mutable state” ได้เนื่องจาก สิ่งที่เรียกว่า state นั้นจะถูกเก็บไว้ใน object นั้นอยู่แล้ว (เช่น Bank Account ที่เราสามารถฝากเงินหรือถอนเงินเพิ่มได้ ซึ่งจำนวนเงินที่เป็น state ก็จะเก็บไว้อยู่ภายใน object นั้น)

bank-account-example Ref: https://stackoverflow.com/questions/53395244/class-diagram-of-a-bank-confusion

  • Functional Programming data ทั้งหมดจะไม่สามารถ access หรือโดนดัดแปลงได้จากภายใน function ดังนั้น การจัดการ data จึงต้องเกิดจากการส่งต่อจาก function สู่ function ออกมาเท่านั้น จะไม่ได้มีการเก็บ state ไว้ใน function แต่จะเป็นแค่การนำข้อมูล (ที่เป็น state เริ่มต้น) ไปจัดการผ่าน function และรับผลลัพธ์ตัวใหม่ออกมาแค่นั้น
bank-account-function

Ref: https://www.chegg.com/homework-help/questions-and-answers/1-create-structure-customer-specify-data-customers-bank-data-stored-account-number-name-ba-q83837741

  1. Control Flow
  • OOP ทำงานเหมือนการจัดการเขียนโปรแกรมเป็นลำดับขั้นตอน (Imperative Programming) ใช้ loop control flow เป็น step by step เหมือนเขียนโปรแกรมแบบปกติ
  • Functional Programming เป็นการเขียน program เชิงผลลัพธ์ (Declarative Programming) โดยจะเป็นการ function ที่เราต้องการออกมา และส่งต่อสิ่งที่เราต้องการนั้น เข้า function อื่นๆต่อเข้าไป เพื่อให้ได้ผลลัพธ์ออกมา (เป็นการประกอบ function ของ function ให้ได้ผลลัพธ์ที่ต้องการออกมา แทนที่จะเขียนเป็น step by step ตามปกติ)

นี่คือ 3 ไอเดียใหญ่ๆ ที่มีความแตกต่างจะเห็นว่า มุมมองการเขียนโปรแกรมทั้ง 2 แบบแตกต่างกันอย่างสิ้นเชิง

  • OOP มองไปสิ่ง state ของสิ่งที่เรากำลังทำอยู่ และพยายามจัดการกับ state ของสิ่งที่เราสร้างมา
  • แต่ Funtional programming มองแค่การสร้าง “function ของจุดประสงค์” และนำ function เหล่านั้นมาประกอบกันเป็นผลลัพธ์ที่เราต้องการออกมา

ดังนั้น 2 Idea นี้หากเลือกใช้รูปแบบตัวนั้น ก็ต้องใช้ตัวนั้นทั้ง program เพื่อป้องกันการสับสนจากมุมมองของการ design program ด้วยเช่นกัน เดี๋ยวเราจะลองมาแชร์การเขียน program ฉบับ Functional Programming style เทียบกับการเขียนแบบ OOP (ในเคสง่ายๆ) กันว่า มุมมอง code 2 ตัวนี้แตกต่างกันอย่างไร ผ่านภาษา Javascript กัน (ขอเลือกเป็นภาษานี้เพราะเป็นภาษาที่อ่านง่ายและ Support ความเป็น functional จากหลายๆ function ของ Javascript อยู่แล้วเช่นกัน)

มารู้จักเทคนิค functional แต่ละแบบ

ผมจะแบ่งการเล่าออกเป็น 2 ส่วนคือ ส่วนที่เป็นแกนหลักที่มีความแตกต่างระหว่่าง Functional กับ การเขียน program แบบ imparative ทั่วไป (ที่อาจจะมี OOP บางเคสบ้าง) กับ เทคนิคเพิ่มเติมของ Functional เมื่อ function เริ่มมีความซับซ้อนมากขึ้น เพื่อให้เห็นภาพการเขียน program แบบ functional มากขึ้น

Pure function

คุณสมบัติสำคัญอันแรกของ Functional Programming คือ ต้องเป็น “pure function” เสมอ โดย concept หลักของ “pure function” คือ

  1. input เหมือนเดิม ต้องได้ output เหมือนเดิม “เสมอ”
  2. ต้องไม่มีใครสามารถแก้ไข state ระหว่างกลางของ function ได้ (เช่นการใช้ global variable, การดึง API ข้างนอก)
  3. เราสามารถแทน function นี้ตรงตำแหน่งใดๆก็ได้ที่ใช้ “output ตัวเดียวกัน” ได้ (output เหมือนเดิมเสมอ)

นี่คือตัวอย่างของ Impure function (ที่ไม่ตรงตาม practice ของ Functional Programming)

let counter = 0;
function incrementCounter() {
counter++;
return counter;
}
const value1 = incrementCounter(); // value1 will be 1
const value2 = incrementCounter(); // value2 will be 2
console.log(value1 === value2); // false, because the counter state changes

สังเกตว่า

  • incrementCounter() นั้นไม่ได้มีการรับ input อะไรเข้าไป แต่ทุกครั้งที่รับค่ามาใหม่ จะได้รับค่าเพิ่มขึ้นเสมอ เนื่องจากมีการใช้ตัวแปร counter ที่เป็น Global variable อยู่
  • จึงส่งผลให้ทุกรอบที่ run function นี้ได้ผลลัพธ์ที่ไม่เหมือนเดิมออกมา จาก counter ที่เพิ่มขึ้นในแต่ละรอบนั่นเอง

และนี่คือ Pure function

function add(a, b) {
return a + b;
}
const result1 = add(2, 3); // result1 will always be 5
const result2 = add(2, 3); // result2 will also be 5
console.log(result1 === result2);

สังเกตว่า function add(a, b) จะทำหน้าที่เพียงอย่างเดียวคือการบวกตัวเลข a + b เสมอดังนั้นไม่ว่าเราจะ run function นี้กี่รอบ output ก็จะได้เหมือนเดิมและมีหน้าที่เพียงแค่อย่างเดียว

Declarative style

Declarative style ใน Functional Programming คือการอธิบายส่วนของ function ออกมาเป็น “ผลลัพธ์” แทนที่จะเป็นการจัดการ code แบบ step by step Declarative เป็นการเรียก function การใช้งานตามจุดประสงค์ออกมาแทน (Declarative เป็นการ focus สิ่งที่เราต้องการทำให้เสร็จที่อยู่ระดับ high-level)

เพื่อให้เห็นภาพมากขึน เราจะลองมาดู code ตัวอย่างกัน สมมุติว่า โจทย์เราคือ “สร้าง array ใหม่ที่เอาเฉพาะตัวที่หาร 2 ลงตัว”

ถ้าเป็นการเขียน program ตามแบบ imparative style code ก็จะออกมาตรงไปตรงมาตามโจทย์เลยแบบนี้

// Filter an array of numbers to keep only even values (imperative)
const numbers = [1, 2, 3, 4, 5];
const evenNumbers = [];
for (let i = 0; i < numbers.length; i++) {
if (numbers[i] % 2 === 0) {
evenNumbers.push(numbers[i]);
}
}
console.log(evenNumbers);

ซึ่งการเขียนแบบนี้ เราก็จะเป็นการเขียนตาม step by step เลย ต้องมี loop ที่ใช้สำหรับการวนแต่ละตัวและ condition สำหรับการแยกเคสออกมา

แต่ถ้าเป็นการเขียน program ตามแบบ declarative style เราจะทำการ

  • เพิ่ม function สำหรับการหาตัวเลขหาร 2 ลงตัวออกมา และคืนผลลัพธ์ออกมา
  • เมื่อรับผลลัพธ์มาใหม่ก็จะสามารถนำผลลัพธ์จาก function นั้นออกมาใส่ตัวแปรใหม่ได้
  • โดยตัว code นั้นเราจะไม่มีการใช้ loop หรือ condition โดยตรง (เป็นการใช้งานผ่าน function .filter() ออกมาแทน)
// Filter an array of numbers to keep only even values
const numbers = [1, 2, 3, 4, 5];
const evenNumbers = numbers.filter(number => number % 2 === 0);
console.log(evenNumbers); // Output: [2, 4]

ซึ่งสิ่งนี้ก็จะยังคงคุณสมบัติของ pure function เอาไว้ (ไม่ว่าจะ run กี่รอบก็จะได้ผลลัพธ์นี้ออกมาได้)

High order function

High-order functions (ขอย่อเป็น HOFs) คือ function ที่สามารถ “นำ function มาเป็น input” (หรือ arguments function) ได้ หรือ สามารถ return ค่าออกมาเป็น function ในฐานะ output ออกมาได้ เพื่อใช้คุณสมบัติของ “pure function” ให้สามารถ reuse ต่อกันได้

เรามาลองทำ 2 โจทย์นี้คือ “เพิ่มตัวเลขใน array เป็น 2 เท่า” และ “หาเลขคู่จาก array”

หากเราเขียนโดยไม่ใช้ HOFs ก็จะได้ code ลักษณะแบบนี้ออกมา (code ตรงไปตรงมาคือ วน loop > สร้าง condition และนำไปใส่อีก Array หนึ่งออกมา)

// Doubling numbers without map()
const doubledNumbers = [];
for (let i = 0; i < numbers.length; i++) {
doubledNumbers.push(numbers[i] * 2);
}
// Filtering even numbers without filter()
const evenNumbers = [];
for (let i = 0; i < numbers.length; i++) {
if (numbers[i] % 2 === 0) {
evenNumbers.push(numbers[i]);
}
}

ซึ่งเมื่อเราเขียนเป็น HOFs ก็จะมีลักษณะเป็นแบบนี้ออกมาแทน

const numbers = [1, 2, 3];
const doubledNumbers = numbers.map(number => number * 2); // [2, 4, 6]
const evenNumbers = numbers.filter(number => number % 2 === 0); // [2]

อย่างที่เราเห็น ทั้ง .map() และ .filter() ได้ทำการใส่ argument ตัวหนึ่งเข้าไปนั่นคือ function ของการทำงานนั้นๆ โดย

  • .map() function ที่เราใส่เป็น argument คือ function สำหรับการคูณตัวเลขที่ใส่เป็น parameter ใน function ไป ทำให้เมื่อใช้คำสั่ง .map() คำสั่งนี้ก็จะทำ function ส่งเข้าไปในข้อมูลแต่ละตัวและทำการ return ค่าใหม่จากการใช้ function argument นั้นเข้าไปได้นั่นเอง
  • เช่นเดียวกันกับ case ของ .filter() ที่เราใส่เป็น argument คือ function สำหรับการหาเลขหาร 2 ลงตัวเข้าไปแทน

ซึ่งใน Javascript นั้นมี function ที่ช่วยทำให้ Javascript นั้นสามารถใช้คุณสมบัติของ Functional Programming ไว้ได้อย่าง .map(), .filter() รวมถึง reduce() (ใช้สำหรับการรวมผล), sort() (ใช้สำหรับการเรียง) เช่นแบบนี้

numbers = [1, 2, 3]
const sum = numbers.reduce((accumulator, number) => accumulator + number, 0); // 6
const sortedNumbers = numbers.sort((a, b) => a - b); // [1, 2, 3]

ซึ่งถือเป็นตัวที่ช่วยอำนวยความสะดวกในการเขียน Functional Programming ของ Javascript เช่นเดียวกัน (จริงๆยังมีคำสั่งอีกหลายตัวที่ใช้งานได้นะครับ สามารถ search keyword High order function javascript เพิ่มเติมได้ครับ)

หรืออีกตัวอย่างหนึ่งในกรณีที่เราไม่ได้ใช้งานร่วมกับ array หรือ object เราก็สามารถส่ง function เข้าไปทำงานตรงๆได้เช่นเดียวกัน เช่นแบบนี้

function repeat(n, fn) {
for (let i = 0; i < n; i++) {
fn();
}
}
function logMessage() {
console.log("Hello!");
}
repeat(3, logMessage); // "Hello!" printed 3 times

จาก code นี้

  • เรามี repeat(n, fn) ที่รับ 2 ค่าคือ n = จำนวนครั้งที่จะทำ และ fn() คือ function ที่เราจะทำงานซ้ำออกมา
  • เมื่อเราสร้าง function ใหม่ logMessage() ออกมา เราก็จะสามารถส่ง function นั้นเป็น argument ใหม่ ผ่าน repeat(3, logMessage()) ออกมาได้ และใน repeat ก็จะทำงานใช้งาน logMessage() ผ่าน fn() ที่เป็นตัวแทนของ Parameter ออกมาได้

มา Step up กันอีกสักหน่อย

จาก 3 ข้อเมื่อมันคือแกนหลักของการทำ functional (เอาจริงๆ ใครที่ get idea ของทั้ง 3 อัน สามารถทำ functional มาต่อยอดกันได้แล้ว) ทีนี้ เราจะมาแนะนำ technique อื่นๆเพิ่มเติมกัน

Composition

Composition คือ concept ของ Functional Programming ที่จะเพิ่มความสามารถในการ “รวม function” เข้าด้วยกันได้ โดยการรวม function ที่ทำงานแบบเรียบง่ายหลายๆ function เข้าด้วยกันจนกลายเป็น “complex function” ออกมาได้ ลักษณะเหมือนการนำ output ของอีก function หนึ่งส่งต่อไปเป็น input ของอีก function หนึ่งออกมานั่นเอง

fog-function

Ref: https://danielpecos.com/2014/06/24/function-composition/

เรามาลองดูผ่านตัวอย่างกัน สมมุติว่าเรามี 2 functions คือ

  1. double(x) คือ function สำหรับการเพิ่มตัวเลข 2 เท่าและ return ออกมา
  2. increment(x) คือ function สำหรับการเพิ่มค่าให้กับตัวเลขที่ input เข้าไป 1 และ return ออกมา

หน้าตา function เป็นแบบนี้

function double(x) {
return x * 2;
}
function increment(x) {
return x + 1;
}

มาดูตัวอย่างแบบไม่ใช้ Composition กันก็จะเหมือนกับเราเขียน programming ทั่วๆไปเลย

  • เรียกใช้ทีละ function ตั้งแต่ double(x), increment(x) โดย increment(x) ก็จะนำผลลัพธ์จากตัวแปรตัวก่อนมาใส่และทำการส่งผลลัพธ์ออกไปได้
function doubleAndIncrement(x) {
const doubled = double(x);
const incremented = increment(doubled);
return incremented;
}
console.log(doubleAndIncrement(3)); // Outputs: 7

ทีนี้เมื่อลองใช้ Composition เราก็จะสามารถเขียนเป็นรูปแบบนี้ออกมาได้

  • ให้ double(x) ที่คำนวนได้นั้นตอนได้ output ออกมา ให้ไปเป็น input ของ increment(x) ต่อ และทำการ return เป็น function ออกมา
function doubleThenIncrement(x) {
return increment(double(x));
}
console.log(doubleThenIncrement(3)); // Outputs: 7 (3 * 2 = 6, 6 + 1 = 7)

สังเกตว่า ทั้ง 2 เคสได้ผลลัพธ์ออกมาเหมือนกันแต่ Function ของ Composition นั้นจะอ่านง่ายกว่า รวมถึงไม่จำเป็นต้องมีตัวแปรออกมาโดยไม่จำเป็นด้วย นี่จึงเป็นอีก 1 technique ของ functional ที่มักมีการใช้กัน

(จริงๆถ้าเราลองดูดีๆมันคือการใช้ความสามารถของ High order function นั่นแหละ)

Currying

Curry คือ เทคนิคของ Functional Programming ที่สามารถทำการแปลง function ที่ต้องรับทีละหลาย arguments ให้กลายเป็น sequence ของ function ที่รับ “ทีละ argument ออกมาได้”

โดยจาก concept ของตัวนี้นั้น เราจะทำการ break function จาก n argument มาสู่ n function (ที่รับทีละ 1 argument) และค่อยๆใช้งาน argument ทีละตัวผ่าน input function และส่งต่อ function พร้อมการรับ argument ต่อไปผ่าน output function ออกมาแทน

เพื่อให้เห็นภาพมากขึ้น เรามาดูผ่านตัวอย่างนี้กัน เรามากำหนดโจทย์กันว่า เราจะสร้าง function สำหรับการลดราคาสินค้าโดย

  • สร้าง function applyDiscount() ที่สามารถรับราคาสินค้าและส่วนลดสินค้าเข้าไปได้
  • ouput ที่ออกมาก็จะเป็นราคาหลังจากที่ลบ discount ตามที่รับออกไปแล้ว

หากไม่ได้ใช้เทค Curry function ทั่วไปก็จะมีหน้าตาประมาณนี้

function applyDiscount(price, discount) {
return price - price * discount;
}
// เมื่อใช้งานลด 20% ก็จะสามารถส่งเข้าไปตรงๆได้
console.log(applyDiscount(100, 0.2)); // Product 1
console.log(applyDiscount(200, 0.2)); // Product 2
console.log(applyDiscount(300, 0.2)); // Product 3

หากใช้เทคนิค Curry สิ่งที่เราจะทำคือ

  • สร้าง function ขึ้นมา 1 ตัวรับค่า discount เข้าไป
  • ทำการ return ออกมาเป็น function ที่รับ argument price ออกมา
  • เมื่อใครก็ตามที่ใช้งาน function ผ่าน output ตัวนี้ จะทำการใช้งาน discount ที่เป็น curry function ที่ผ่านการใส่ discount แล้วออกมา = ส่งผลทำให้ทุกคนที่เรียกใช้งานผ่าน curry function จะได้รับ discount ไปใช้งานได้เลย โดยไม่ต้องส่ง argument มาใหม่ทุกรอบ

หน้าตา code ก็จะเป็นประมาณนี้

function curriedApplyDiscount(discount) {
return function(price) {
return price - price * discount;
};
}
// Currying the function with a 20% discount
const applyTwentyPercentDiscount = curriedApplyDiscount(0.2);
// Applying the curried function to different products
console.log(applyTwentyPercentDiscount(100)); // Product 1
console.log(applyTwentyPercentDiscount(200)); // Product 2
console.log(applyTwentyPercentDiscount(300)); // Product 3

นี่คือประโยชน์ของเทคนิค Curry มันจะสามารถ “reuse argument” ผ่านการส่งต่อเข้าแต่ละ function ออกมาได้

Partial

Partial เป็นอีก 1 เทคนิคของ Functional Programming ที่มี concept คือการลดจำนวน argument ลง โดย “เปลี่ยน argument หลายตัวเป็น function แทน” ซึ่ง function เหล่านั้นก็จะเป็นส่วนที่ทำตัวเหมือน “prefilled” ค่าของ argument เอาไว้ก่อน ซึ่งส่งผลทำให้ argument ของ function ลดลง และช่วยลดความซับซ้อนของ function ลงได้

ถ้าอ่านๆดู “มันเหมือนกับ Curry เลยนี่นา” ใช่ครับ พื้นฐานไอเดีย 2 ตัวนี้จะเหมือนกัน แต่จุดที่แตกต่างของ Partial คือ Partial จะสามารถ “รับ argument มากกว่า 1 ตัว”มาจัดการได้ แตกต่างกับ Curry ที่ต้องกระจายให้กลายเป็น function ละ 1 argument และส่งต่อไป

จุดประสงค์ของ Partial จึงเป็นเหมือนลดความซ้ำซ้อนของตัว function จากเทคนิคของ Curry ลงมากกว่า

ลองมาดูผ่านตัวอย่างกันครับ เช่น โจทย์คือ เราจะสร้าง function สำหรับ log ออกมาโดย log จะต้องส่งข้อมูลออกมาทั้งหมด 3 อย่างคือ

  1. level เป็น log ระดับไหน (Info, Warning, Error)
  2. message ข้อความที่จะแสดง log ออกมา
  3. timestamp เวลาที่มีการบันทึก log

หากเราใช้เทคนิค Curry ก็จะได้หน้าตาประมาณนี้ออกมา

function curriedLog(level) {
return function(message) {
return function(timestamp) {
console.log(`[${level}] ${timestamp}: ${message}`);
};
};
}
// Using the curried function
const infoLog = curriedLog('INFO')('This is an informational message');
infoLog(new Date().toISOString()); // Outputs "[INFO] 2024-01-12T08:00:00.000Z: This is an informational message"

แต่เมื่อใช้เทคนิค Partial จะได้หน้าตาแบบนี้ออกมาแทน

function partialLog(level, message) {
return function(timestamp) {
console.log(`[${level}] ${timestamp}: ${message}`);
};
}
// Using the partially applied function
const infoLogToday = partialLog('INFO', 'This is an informational message');
infoLogToday(new Date().toISOString()); // Outputs "[INFO] 2024-01-12T08:00:00.000Z: This is an informational message"

Key idea ใหญ่ๆที่แยกการใช้งานระหว่าง Curry และ Partial นั้นโดยปกติจะมี

  1. จำนวนของ argument ว่าจำนวนเยอะเกินกว่าที่จะนำ function มาซ้ำซ้อนกันหรือไม่
  2. ความยืดหยุ่น ว่าต้องการหรือต้องทำบางอย่างกับหลาย argument พร้อมกันหรือไม่ก่อนส่งต่อ (ถ้าไม่มีเหตุจำเป็นอย่างเคสนี้ เอาจริงๆก็สามารถใช้ Curry ได้)

มาดูตัวอย่าง use case โดยประมาณกัน

ตอนนี้เรารู้จัก Functional Programming กันโดยประมาณและ (เอาจริงไอเดียหลักๆของ Functional Programming มันก็ประมาณนี้แหละ) มาเรียนรู้ผ่านตัวอย่างเพิ่มเติมกัน โดยเราจะลองเทียบกันระหว่าง OOP กับ functional เพื่อเติมเต็มความเข้าใจที่มากขึ้น

เราจะลองมาทำโจทย์นี้กัน

เราจะมีข้อมูลของ user เก็บไว้ใน array โดยจะเก็บข้อมูล ชื่อ (name), อายุ (age) และ isActive (user ยังใช้งานอยู่หรือไม่) โดยโจทย์ที่เราต้องการคือ
1. filter เอาเฉพาะ active user ออกมา (isActive เป็น true)
2. ทำการเพิ่มอายุเป็น 2 เท่าให้กับทุก active user (age * 2)
3. ทำการรวมอายุทุกคนของข้อ 2 และส่งมาเป็นคำตอบ

ตีโจทย์แบบ OOP

ใน OOP นั้นเราจะ focus เป็น class ออกมา ดังนั้นสิ่งที่เราจะทำคือ

  • สร้าง class User และ method doubleAge() คู่กับ user เอาไว้ เพื่อให้สามารถ คูณ 2 อายุของ User ได้
  • และในแต่ละขั้นของโจทย์ เราก็จะค่อยๆทำตามลำดับไปเลย
class User {
constructor(name, age, isActive) {
this.name = name;
this.age = age;
this.isActive = isActive;
}
doubleAge() {
this.age *= 2;
}
}
// ข้อมูล user
const users = [
new User('Alice', 25, true),
new User('Bob', 30, false),
new User('Carol', 35, true)
];
// ทำข้อที่ 1
let activeUsers = users.filter(user => user.isActive);
// ทำข้อที่ 2 = mutate state เพิ่มอายุใน object เอง
activeUsers.forEach(user => user.doubleAge());
// ทำข้อที่ 3
let totalAge = activeUsers.reduce((sum, user) => sum + user.age, 0);
console.log(totalAge); // Output: 120

สังเกตจาก code นี้

  • ตรงข้อที่ 2 นั้นจะมีการเปลี่ยนแปลงค่าภายในของ object ด้วยคำสั่ง doubleAge() ส่งผลทำให้ ตัวแปร age ที่อยู่ใน object ไม่เหมือนเดิม หลังผ่านคำสั่งนี้ไป
  • สำหรับ OOP นี่คือการเก็บสิ่งที่เรียกว่า “คุณสมบัติ” คู่กับ object เอาไว้ แต่มันก็จะแลกมาด้วยการยอมให้ state สามารถแก้ไขตัวมันเองได้

ตีโจทย์แบบ Functional Programming

สิ่งที่เราจะทำคือ เมื่อมี 3 โจทย์ที่เราต้องทำ = ก็ต้องมี “3 function” ที่ใช้สำหรับตอบโจทย์นั้นออกมานั่นคือ

  • doubleAge() สำหรับการเพิ่มอายุ 2 เท่า
  • sumAges() สำหรับการรวมอายุ
  • filter() สำหรับการคัดกรองคนเฉพาะ isActive ออกมา

เมื่อเรานำทุก function มาใช้งานร่วมกัน ก็จะได้ลักษณะ code ออกมาเป็นแบบนี้

const users = [
{ name: 'Alice', age: 25, isActive: true },
{ name: 'Bob', age: 30, isActive: false },
{ name: 'Carol', age: 35, isActive: true }
];
const doubleAge = user => ({ ...user, age: user.age * 2 });
const sumAges = (total, user) => total + user.age;
const totalAge = users
.filter(user => user.isActive)
.map(doubleAge)
.reduce(sumAges, 0);
console.log(totalAge); // Output: 120

สังเกตจาก code นี้

  • ทุก function จะเป็นเพียงแค่การรับ argument ของ user เข้ามาแค่นั้น จะไม่มีการเปลี่ยนแปลงข้อมูลอะไรของ user เลย
  • ทุก function จะทำงานต่อกันผ่าน output ของ function ที่ส่งออกมา (ใน javascript การใช้ function chain ต่อกัน โดยการใช้ . = เป็นการนำ output ไปสู่ function ต่อไป)
  • ดังนั้น แม้จะ run ทุก function ไป users ที่เป็นต้นฉบับข้อมูลก็จะยังคงเหมือนเดิม ไม่ได้มีการเปลี่ยนแปลงอะไรจากการ run function ที่เกิดขึ้น ตามคุณสมบัติ “pure function” ของ Functional Programming นั่นเอง

สรุป

จากที่บทความเล่ามา Functional Programming คือ 1 ในรูปแบบวิธีคิดการเขียน Programming อีกหนึ่งรูปแบบ ที่เป็นการปรับไอเดียจากการคิด program ตามลำดับ (imparative) เป็นการคิด program ตามจุดประสงค์และผลลัพธ์​ (declarative) ออกมาแทน เหมือนการมองโลกทุกอย่างเป็น function ออกมา ตามไอเดียของ Lambda Calculus

ไม่มีข้อสรุปใดๆว่า Functional Programming หรือ OOP นั้นไอเดียไหนดีกว่ากัน ทั้งนี้จะขึ้นอยู่กับว่า “มุมมองไหน ส่งผลทำให้ระบบ design ออกมาง่ายและ Test ออกมาง่ายมากกว่ากัน” ตัวนั้นก็จะเหมาะสมกับงานนั้นๆกว่า

จากประสบการณ์ของผมเอง

  • OOP จะเหมาะกับเคสที่เล่นกับ Data structure จำนวนเยอะมาก (เช่น พวกระบบหลังบ้านที่ต้องจัดการกับ Database) จะส่งผลทำให้จัดการ code ได้ง่ายกว่า
  • Functional Programming นั้น จะเหมาะกับเคสที่ต้องการสร้าง cover test ให้ครบทุก function รวมถึงเคสของ concurrency หรือเคสอะไรก็ตามที่ต้องนำ function กลับมาใช้ซ้ำบ่อยๆ (เช่น ระบบจัดการแบบ realtime, ระบบ data pipeline ที่ต้องส่งต่อข้อมูลไปมาหากัน) จะส่งผลทำให้ maintain ได้ง่ายขึ้นมากกว่า

แต่อย่างที่ย้ำไปครับ “ไม่มีคำตอบตายตัวสำหรับเรื่องนี้” ดังนั้น จะ Functional Programming หรือ OOP ก็ล้วนเป็น Idea ทาง programming ที่ดีทั้งคู่ หากปรับใช้อย่างเหมาะสมและตรงตามหลักการของทั้ง 2 วิธีนี้ อย่างน้อยที่สุดก็ส่งผลทำให้ code เราอ่านได้ง่ายขึ้น และ maintain ได้ง่ายขึ้นอย่างแน่นอน ลองศึกษา use case พิจารณาใช้กันตามความเหมาะสมกันด้วยนะครับ


Related Post

Share on social media