Functional Programming in Typescript Part 1: Fundamentals
ภายในชุมชนชาว ODDS จะมีสิ่งที่คล้ายๆกับชมรม แต่เราเรียกกันว่า Community of Practice (CoP) ส่วนนิยามจริงๆ ไปอ่านในนี้นะ
Community of Practice (CoP) คืออะไร?
เลยลองไป join CoP Functional Programming
และมีโอกาสได้กลับมาอ่านหนังสือ “Hands-On Functional Programming with TypeScript” อีกรอบ หลังจากปล่อยมันรองขาโต๊ะมา 1 ปี ที่สนใจเรื่อง Functional Programming เพราะบางที เขียนไปเขียนมา อ่าว เทสไงวะ ฟังก์ชันดูซับซ้อนเหลือเกิน หรือบางที Coding Style ผมขอเรียกว่า “ลายมือ” ของตัวเองมันดูอ่านยาก หรือให้คนอื่นดูแล้วงงๆ ซึ่งตัว Functional Programming เนี่ย เคลมเลยว่า เขียนง่าย test ง่าย เข้าใจง่าย ไม่รู้จริงเปล่าเลยลองหยิบมาอ่านดู และที่เลือก Typescript เพราะเนื้องานที่ทำอยู่ปัจจุบันใช้อันนี้สะเป็นส่วนใหญ่
น่าจะเป็นการเดินทางที่ยาวนาน ไม่รู้จะมีกี่ Part รอติดตามนะครับ
เกริ่นมาตั้งนาน มาเริ่มด้วยคำถามที่ basic ที่สุด คืออออ
Functional Programming คืออะไร ???
Functional Programming คือ รูปแบบการเขียนโปรแกรมที่เน้นการใช้ฟังก์ชันเป็นหน่วยพื้นฐานของ code ซึ่งมีลักษณะและแนวคิดสำคัญดังนี้:
- Pure Functions: ฟังก์ชันที่ไม่มี side effects ซึ่งหมายความว่าผลลัพธ์ของฟังก์ชันขึ้นอยู่กับ arguments ที่ถูกส่งเข้าไปเท่านั้น และไม่มีการเปลี่ยนแปลง “state” ภายนอกใดๆ
- Immutability: ค่าคงที่ที่ไม่สามารถเปลี่ยนแปลงได้ ช่วยลดข้อผิดพลาดที่เกิดจากการเปลี่ยนแปลง state โดยไม่คาดคิด
- State Management: state จะถูกจัดการอย่างระมัดระวัง โดยมักจะใช้ฟังก์ชันที่ส่งค่าใหม่กลับมาแทนการเปลี่ยนแปลงค่าเดิม
- First-Class Functions: ฟังก์ชันสามารถถูกส่งเป็น argument คืนค่าเป็นผลลัพธ์ หรือเก็บในตัวแปรได้
- Declarative Nature: เน้นการบอกว่าเราต้องการทำอะไรแทนที่จะบอกว่าทำอย่างไร (what to do instead of how to do it)
- Higher-Order Functions: ฟังก์ชันที่สามารถรับฟังก์ชันอื่นเป็น argument หรือคืนค่าฟังก์ชันเป็นผลลัพธ์
อันนี้คร่าวๆสำหรับ Concept ของ Functional Programming
ก่อนจะลงลึกไปในแต่ละหัวข้อ ขอแวะมาที่หัวข้อถัดไปก่อน
แล้วเจ้า Functional Programming มีประโยชน์ยังไง ???
ที่สรุปมาจะมีประมาณนี้
- Testable: ถ้าเราพยายามเขียนในรูปแบบ Pure Function เราจะสามารถเขียน Unit Test ได้ง่ายมาก เนื่องจาก Function จะเล็กและไม่เกิด side effect
- Readable: เล็ก สั้น กระชับ เข้าใจง่าย ตัว function จะยุ่งกับแค่ arguments ที่ส่งเข้ามา ไม่ต้องกังวลเรื่องตัวแปรภายนอกใดๆเลย
- Concurrency: function ส่วนใหญ่จะเป็น stateless ทำให้รองรับการ scale ได้
- Caching: cache จะง่ายขึ้นมากเมื่อเราสามารถทำนายผลลัพธ์ของฟังก์ชันได้จาก arguments ของมัน
Pure Functions
เป็นหัวใจหลักในการเขียนโปรแกรมรูปแบบ Functional โดยมีคุณสมบัติและแนวคิดสำคัญดังนี้
- Deterministic: ผลลัพธ์ของ pure function จะขึ้นอยู่กับ arguments ที่ส่งเข้าไปเท่านั้น หากส่งค่า argument เดียวกันเข้าไปจะได้ผลลัพธ์เดิมเสมอ
ตัวอย่าง
const add = (a: number, b: number): number => a + b;
console.log(add(2, 3)); // ผลลัพธ์: 5
console.log(add(2, 3)); // ผลลัพธ์: 5
- No Side Effects: จะไม่เปลี่ยนแปลง state ภายนอก ไม่ว่าจะเป็นการแก้ไขตัวแปรภายนอก การเขียนไฟล์ หรือการเรียกใช้ฟังก์ชันที่มี side effect
ตัวอย่าง (ไม่ใช่ pure function)
let count = 0;
const increment = (): number => {
count += 1; // มีการเปลี่ยนแปลงตัวแปรภายนอก
return count;
};
console.log(increment()); // ผลลัพธ์: 1
console.log(increment()); // ผลลัพธ์: 2
ตัวอย่าง (pure function)
const increment = (n: number): number => n + 1;
console.log(increment(0)); // ผลลัพธ์: 1
console.log(increment(1)); // ผลลัพธ์: 2
- Immutability: จะไม่แก้ไขค่า arguments ที่ส่งเข้าไปและมักจะทำงานกับข้อมูลที่ไม่เปลี่ยนแปลง (immutable data) ซึ่งช่วยลดข้อผิดพลาดที่เกิดจากการเปลี่ยนแปลง state
ตัวอย่าง (Mutable)
const appendToArray = (arr: number[], value: number): void => {
arr.push(value); // มีการแก้ไขค่า arguments ที่ส่งเข้าไป
};
const myArray = [1, 2, 3];
appendToArray(myArray, 4);
console.log(myArray); // ผลลัพธ์: [1, 2, 3, 4]
ตัวอย่าง (Immutable)
const appendToArrayPure = (arr: number[], value: number): number[] => {
return [...arr, value]; // สร้าง array ใหม่แทนการแก้ไขของเดิม
};
const myArrayPure = [1, 2, 3];
const newArray = appendToArrayPure(myArrayPure, 4);
console.log(newArray); // ผลลัพธ์: [1, 2, 3, 4]
console.log(myArrayPure); // ผลลัพธ์: [1, 2, 3] (ค่าเดิมไม่เปลี่ยนแปลง)
ข้อดีของการใช้ Pure Functions
- Testability: เพราะไม่มี side effects ทำให้สามารถทดสอบได้ง่ายมาก เนื่องจากเรารู้ว่าผลลัพธ์ที่ได้จะเป็นอย่างไรเมื่อให้ค่า arguments เข้าไป
- Reusability: สามารถนำมาใช้ซ้ำได้ในหลายๆ ที่โดยไม่ต้องกังวลว่าจะเกิดผลกระทบต่อส่วนอื่นๆ ของ code
- Readability: เพราะทำงานเฉพาะกับ arguments ของมัน ทำให้เราสามารถเข้าใจการทำงานของฟังก์ชันได้ง่ายขึ้น
- Optimization: สามารถ Optimize ได้ง่าย เช่น การใช้เทคนิคการ memoization เนื่องจากเราสามารถทำนายผลลัพธ์ได้จากค่า arguments
Side Effects
Functional Programming — FP: เป็นเรื่องปกติที่จะกล่าวว่า pure function คือฟังก์ชันที่ไม่มี side-effects ซึ่งหมายความว่าเมื่อเราเรียกใช้ เราสามารถคาดหวังได้ว่าฟังก์ชันนั้นจะไม่ก่อให้เกิดการเปลี่ยนแปลงสถานะ (state mutation) ใดๆ กับส่วนประกอบอื่นใน application ของเรา
ในบางภาษา สามารถรับประกันได้ว่า application จะไม่มี side-effects โดยใช้ระบบ “Type System” ในขณะที่ TypeScript มีข้อเสียคือระบบ type system ไม่สามารถรับประกันได้ว่า application ของเราจะไม่มี side-effects
อย่างไรก็ตาม เราสามารถใช้หลักการ Functional Programming บางอย่างเพื่อเพิ่มความปลอดภัยใน TypeScript ของเราได้
สมมติว่าเรามีฟังก์ชันที่ค้นหาสินค้าในสต็อก
ตัวอย่างที่มี side-effects ส่งผลกับ application
function findItemInStock(items: string[], itemName: string): string {
if (items.length == 0) {
throw new Error("No items in stock!"); // เกิด side effects
}
const item = items.find(item => item === itemName);
if (!item) {
throw new Error("Item not found!"); // เกิด side effects
} else {
return item;
}
}
const stock = ["item1", "item2", "item3"];
try {
const item1 = findItemInStock(stock, "item1");
console.log(`Found: ${item1}`);
} catch (error) {
console.error(error.message);
}
try {
const item4 = findItemInStock(stock, "item4");
console.log(`Found: ${item4}`);
} catch (error) {
console.error(error.message);
}
ในตัวอย่างนี้ ฟังก์ชัน findItemInStock
มี side-effects เพราะมันโยน error ที่สามารถส่งผลกระทบต่อการทำงานของส่วนอื่นใน application
ตัวอย่างที่ไม่มี side-effects
function safeFindItemInStock(items: string[], itemName: string): Promise<string> {
if (items.length == 0) {
return Promise.reject(new Error("No items in stock!")); // ไม่มี side-effect
}
const item = items.find(item => item === itemName);
if (!item) {
return Promise.reject(new Error("Item not found!")); // ไม่มี side-effect
} else {
return Promise.resolve(item); // ไม่มี side-effect
}
}
const stock = ["item1", "item2", "item3"];
safeFindItemInStock(stock, "item1")
.then(item => console.log(`Found: ${item}`))
.catch(error => console.error(error.message));
safeFindItemInStock(stock, "item4")
.then(item => console.log(`Found: ${item}`))
.catch(error => console.error(error.message));
การใช้ Promise
ทำให้ฟังก์ชัน safeFindItemInStock
ปลอดภัยจากข้อผิดพลาดที่อาจเกิดขึ้นและไม่มี side-effects เพราะมันจะจัดการข้อผิดพลาดโดยการส่งกลับ Promise.reject
แทนที่จะโยนข้อผิดพลาดโดยตรง ซึ่งทำให้ฟังก์ชันของเราเป็น pure function
Referential Transparency
การเรียกฟังก์ชันด้วย arguments ใดๆ จะได้ผลลัพธ์เดียวกันเสมอ และสามารถแทนที่การเรียกฟังก์ชันด้วยผลลัพธ์ที่ได้โดยไม่เปลี่ยนแปลงพฤติกรรมของโปรแกรม
function addItemToStockPure(stock: string[], item: string): string[] {
return [...stock, item]; // สร้าง array ใหม่แทนการแก้ไขของเดิม
}
const initialStock = ["item1", "item2", "item3"];
const updatedStock = addItemToStockPure(initialStock, "item4");
console.log(updatedStock); // ผลลัพธ์: ["item1", "item2", "item3", "item4"]
console.log(initialStock); // ผลลัพธ์: ["item1", "item2", "item3"] (ค่าเดิมไม่เปลี่ยนแปลง)
จากตัวอย่าง เราสามารถแทนที่ค่าของตัวแปร updatedStock ด้วยผลลัพธ์ที่ได้จากฟังก์ชัน addItemToStockPure ตรงๆโดยไม่กระทบต่อการทำงานส่วนอื่นๆภายใน application
function addItemToStockPure(stock: string[], item: string): string[] {
return [...stock, item]; // สร้าง array ใหม่แทนการแก้ไขของเดิม
}
const initialStock = ["item1", "item2", "item3"];
const updatedStock = ["item1", "item2", "item3", "item4"]; // แทนที่ด้วยผลลัพธ์
console.log(updatedStock); // ผลลัพธ์: ["item1", "item2", "item3", "item4"]
console.log(initialStock); // ผลลัพธ์: ["item1", "item2", "item3"] (ค่าเดิมไม่เปลี่ยนแปลง)
Stateless vs Stateful
Pure Functions ที่อ้างอิง Referentially Transparent Expressions เป็น Stateless ซึ่งหมายถึงผลลัพธ์ของมันไม่ได้รับอิทธิพลจาก statement ก่อนหน้าตัวมันเอง
function isOdd(n: number): boolean {
return n % 2 !== 0;
}
console.log(isOdd(3)); // ผลลัพธ์: true
console.log(isOdd(3)); // ผลลัพธ์: true (ผลลัพธ์ไม่เปลี่ยนแปลงตามการเรียกใช้หลายครั้ง)
ในทางตรงกันข้าม Stateful ยากต่อการทดสอบและกลายเป็นปัญหาเมื่อเราพยายามออกแบบระบบที่สามารถขยายตัวได้ (Scalable) และไม่ร่วงง่ายๆ (Resilient)
let count = 0;
function increment(): number {
count += 1; // มีการเปลี่ยนแปลงตัวแปรภายนอก
return count;
}
console.log(increment()); // ผลลัพธ์: 1
console.log(increment()); // ผลลัพธ์: 2 (ผลลัพธ์เปลี่ยนแปลงตามการเรียกใช้หลายครั้ง)
Declarative vs Imperative Programming
Declarative Programming เป็นวิธีการเขียนโปรแกรมที่เน้นการบอกว่าเราต้องการทำอะไร แทนที่จะบอกว่าเราต้องทำอย่างไร (what to do instead of how to do it) มักใช้กับการเขียนโค้ดที่ต้องการผลลัพธ์ที่แน่นอน โดยไม่ต้องกำหนดขั้นตอนการทำงานทีละขั้นตอน
ข้อดี:
- อ่านง่ายและเข้าใจง่าย: โค้ดมักจะสั้นและเข้าใจง่าย เนื่องจากเน้นการบอกว่าต้องการทำอะไร
- บำรุงรักษาง่าย: การเปลี่ยนแปลงหรือปรับปรุงทำได้ง่าย เพราะไม่ต้องเปลี่ยนแปลง logic ที่ซับซ้อน
- ลดข้อผิดพลาด: เนื่องจากไม่ต้องระบุขั้นตอนการทำงานทีละขั้นตอน โอกาสเกิดข้อผิดพลาดจากการเขียนโค้ดที่ซับซ้อนจึงลดลง
ตัวอย่าง:
const numbers = [1, 2, 3, 4, 5];
const evenNumbers = numbers.filter(n => n % 2 === 0); // บอกว่าเราต้องการตัวเลขที่หารสองลงตัว
console.log(evenNumbers); // ผลลัพธ์: [2, 4]
Imperative Programming เป็นวิธีการเขียนโปรแกรมที่เน้นการระบุขั้นตอนการทำงานทีละขั้นตอน (how to do it) ต้องกำหนดทุกขั้นตอนในการทำงานของโปรแกรมเพื่อให้ได้ผลลัพธ์ที่ต้องการ
ข้อดี:
- ควบคุมได้เต็มที่: สามารถควบคุมทุกขั้นตอนการทำงานของโปรแกรมได้ ทำให้สามารถปรับแต่งการทำงานให้มีประสิทธิภาพสูงสุด
- เข้าใจการทำงานเชิงลึก: ช่วยให้เข้าใจการทำงานภายในของโปรแกรมได้ดีขึ้น
ตัวอย่าง:
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); // ผลลัพธ์: [2, 4]
Immutability
เป็นแนวคิดที่ว่าข้อมูลไม่ควรถูกเปลี่ยนแปลงหลังจากที่ถูกสร้างขึ้นแล้ว เมื่อมีการเปลี่ยนแปลงข้อมูลใหม่ ควรสร้าง copy ใหม่ของข้อมูลนั้นแทนการเปลี่ยนแปลงข้อมูลเดิม แนวคิดนี้เป็นหัวใจสำคัญของ Functional Programming ซึ่งช่วยลดข้อผิดพลาดและทำให้โค้ดมีความเชื่อถือได้มากขึ้น
ข้อดี:
- ลดข้อผิดพลาด: ช่วยลดข้อผิดพลาดได้โดยการทำให้ข้อมูลไม่สามารถเปลี่ยนแปลงได้
- การทดสอบง่ายขึ้น: ฟังก์ชันที่รับข้อมูลที่ไม่เปลี่ยนแปลงจะให้ผลลัพธ์ที่คาดเดาได้เสมอ
- เพิ่มความน่าเชื่อถือ: การมีข้อมูลที่ไม่เปลี่ยนแปลงทำให้โปรแกรมมีความน่าเชื่อถือและสามารถทำนายผลลัพธ์ได้
- Concurrency: ไม่ต้องกังวลเกี่ยวกับการเข้าถึงและเปลี่ยนแปลงข้อมูลพร้อมกันจากหลายส่วนของโปรแกรม
interface Person {
name: string;
age: number;
}
function celebrateBirthday(person: Person): Person {
// สร้าง object ใหม่แทนการเปลี่ยนแปลงค่าเดิม
return {
...person,
age: person.age + 1
};
}
const boonsong: Person = { name: "Boonsong", age: 69 };
const olderBoonsong = celebrateBirthday(boonsong);
console.log(boonsong); // ผลลัพธ์: { name: "Boonsong", age: 69 }
console.log(olderBoonsong); // ผลลัพธ์: { name: "Boonsong", age: 70}
ในตัวอย่างนี้ ฟังก์ชัน celebrateBirthday
สร้าง object ใหม่แทนการเปลี่ยนแปลงค่าเดิมของ boonsong
Functions as first-class citizens
“Function ก็เหมือนคนคุย จะเป็นตัวจริงหรือที่คั่นเวลาก็เรื่องของเรา” หยอกกกก
ในบริบทของFunctional Programming ไอเจ้า First-Class Functions หมายถึงฟังก์ชันสามารถถูกปฏิบัติเป็นFirst-Class Objects ได้ ซึ่งหมายความว่าฟังก์ชันสามารถ:
- ถูกกำหนดให้กับตัวแปร
- ถูกส่งเป็น arguments ให้กับฟังก์ชันอื่น
- ถูกคืนค่าจากฟังก์ชันอื่น
ข้อดี
- ยืดหยุ่น: ฟังก์ชันสามารถถูกใช้เหมือนกับข้อมูลอื่นๆ
- การสร้าง Higher-Order Functions: สามารถสร้างฟังก์ชันที่รับหรือคืนค่าฟังก์ชันอื่นได้ ซึ่งช่วยให้การประมวลผลเชิงฟังก์ชันมีประสิทธิภาพและเข้าใจง่ายขึ้น
- ลดการทำซ้ำ: ช่วยให้สามารถสร้างโค้ดที่ใช้ซ้ำได้ง่ายขึ้น โดยการส่งฟังก์ชันเป็น arguments หรือคืนค่าฟังก์ชันใหม่
ตัวอย่าง การกำหนดฟังก์ชันให้กับตัวแปร
const greet = (name: string): string => {
return `Hey, ${name}!`;
};
console.log(greet("Boonsong")); // ผลลัพธ์: "Hey, Boonsong!"
การส่งฟังก์ชันเป็นอาร์กิวเมนต์ให้กับฟังก์ชันอื่น
const names = ["Boonsong", "Kasinan", "Kannika"];
const greetAll = (names: string[], greeter: (name: string) => string): string[] => {
return names.map(greeter);
};
const greetings = greetAll(names, greet);
console.log(greetings);
// ผลลัพธ์: ["Hey, Boonsong!", "Hey, Kasinan!", "Hey, Kannika!"]
การคืนค่าฟังก์ชันจากฟังก์ชันอื่น
const createMultiplier = (multiplier: number): (num: number) => number => {
return (num: number) => num * multiplier;
};
const double = createMultiplier(2);
console.log(double(5)); // ผลลัพธ์: 10
const triple = createMultiplier(3);
console.log(triple(5)); // ผลลัพธ์: 15
Higher-Order Functions
เป็นฟังก์ชันที่สามารถรับฟังก์ชันอื่นเป็น argument หรือคืนค่าฟังก์ชันเป็นผลลัพธ์ได้ ซึ่งช่วยให้การประมวลผลเชิงฟังก์ชันมีประสิทธิภาพและเข้าใจง่ายขึ้น
ข้อดีของ Higher-Order Functions
- ความยืดหยุ่น: สามารถใช้ฟังก์ชันอื่นเป็น argument ทำให้สามารถสร้างฟังก์ชันที่ยืดหยุ่นและปรับแต่งได้ง่าย
- การใช้ซ้ำ: ช่วยให้เราสามารถใช้โค้ดซ้ำได้ โดยไม่ต้องเขียนฟังก์ชันใหม่ทุกครั้งที่ต้องการเปลี่ยนแปลงพฤติกรรมบางอย่าง
- การแยกความรับผิดชอบ: ช่วยให้เราสามารถแยกความรับผิดชอบออกเป็นส่วน ๆ ทำให้บำรุงรักษาได้ง่ายขึ้น
ตัวอย่าง ฟังก์ชันที่รับฟังก์ชันเป็น argument
const numbers = [1, 2, 3, 4, 5];
// ฟังก์ชันที่กรองค่าจาก array โดยใช้ฟังก์ชัน predicate ที่ส่งเข้าไป
const filter = <T>(arr: T[], predicate: (item: T) => boolean): T[] => {
const result: T[] = [];
for (const item of arr) {
if (predicate(item)) {
result.push(item);
}
}
return result;
};
const isOdd = (n: number): boolean => n % 2 !== 0;
const oddNumbers = filter(numbers, isOdd);
console.log(oddNumbers); // ผลลัพธ์: [1, 3, 5]
ในตัวอย่างนี้ ฟังก์ชัน filter
เป็น Higher-Order Function เพราะมันรับฟังก์ชัน predicate
เป็นอาร์กิวเมนต์และใช้ฟังก์ชันนี้ในการกรองค่าจาก array
ตัวอย่าง ฟังก์ชันที่คืนค่าฟังก์ชันอื่น
// ฟังก์ชันที่สร้างฟังก์ชันตัวคูณตามค่าที่กำหนด
const createMultiplier = (multiplier: number): (num: number) => number => {
return (num: number) => num * multiplier;
};
const double = createMultiplier(2);
console.log(double(5)); // ผลลัพธ์: 10
const triple = createMultiplier(3);
console.log(triple(5)); // ผลลัพธ์: 15
ในตัวอย่างนี้ ฟังก์ชัน createMultiplier
เป็น Higher-Order Function เพราะมันคืนค่าฟังก์ชันใหม่ที่สามารถคูณค่าตัวเลขด้วยค่าที่กำหนด
Function Arity
Arity หมายถึงจำนวน arguments ที่ฟังก์ชันรับ ใน Functional Programming มักจะใช้ฟังก์ชันที่มี Arity ต่ำ ซึ่งหมายความว่าฟังก์ชันส่วนใหญ่จะรับ arguments เพียงหนึ่งหรือสองตัวเท่านั้น
ข้อดีของการใช้ฟังก์ชันที่มี Arity ต่ำ
- ความเข้าใจง่าย: ฟังก์ชันที่มี arguments น้อยจะเข้าใจง่ายกว่าและสามารถบำรุงรักษาได้ง่ายกว่า
- ความยืดหยุ่น: ฟังก์ชันที่มี Arity ต่ำสามารถนำมาผสมผสานกันได้ง่ายกว่าในการสร้างฟังก์ชันใหม่
- Currying: เป็นเทคนิคที่ใช้ในการแปลงฟังก์ชันที่มีหลาย arguments ให้เป็นฟังก์ชันที่มี arguments เดียวหลาย ๆ ฟังก์ชัน
Unary (Arity = 1)
const increment = (x: number): number => x + 1;
console.log(increment(5)); // ผลลัพธ์: 6
Binary (Arity = 2)
const add = (a: number, b: number): number => a + b;
console.log(add(3, 4)); // ผลลัพธ์: 7
Currying
เป็นเทคนิคที่ใช้ในการแปลงฟังก์ชันที่มีหลาย arguments ให้เป็นฟังก์ชันที่มี argument เดียวหลาย ๆ ฟังก์ชัน ทำให้สามารถใช้ฟังก์ชันที่มี Arity ต่ำได้ง่ายขึ้น
const add = (a: number) => (b: number): number => a + b;
const addFive = add(5);
console.log(addFive(3)); // ผลลัพธ์: 8
console.log(addFive(7)); // ผลลัพธ์: 12
ในตัวอย่างนี้ ฟังก์ชัน add
ถูกแปลงให้เป็นฟังก์ชันที่มี Arity เท่ากับ 1 โดยใช้ Currying ทำให้สามารถสร้างฟังก์ชันใหม่ addFive
ที่เพิ่ม 5 ให้กับตัวเลขที่ส่งเข้าไป
Laziness
เป็นแนวคิดที่เลื่อนการคำนวณจนกว่าจะมีความจำเป็นต้องใช้ผลลัพธ์จริง ๆ ซึ่งช่วยเพิ่มประสิทธิภาพและรองรับการจัดการ Infinite Data
Lazy Evaluation
const lazyRange = (start: number, end: number): (() => number | null) => {
let current = start;
return () => {
if (current <= end) {
return current++;
} else {
return null;
}
};
};
const nextNumber = lazyRange(1, 5);
console.log(nextNumber()); // ผลลัพธ์: 1
console.log(nextNumber()); // ผลลัพธ์: 2
console.log(nextNumber()); // ผลลัพธ์: 3
console.log(nextNumber()); // ผลลัพธ์: 4
console.log(nextNumber()); // ผลลัพธ์: 5
console.log(nextNumber()); // ผลลัพธ์: null
ในตัวอย่างนี้ ฟังก์ชัน lazyRange
สร้างฟังก์ชันที่คืนค่าลำดับของตัวเลขตั้งแต่ start
ถึง end
โดยการคืนค่าทีละตัวเมื่อเรียกใช้
การใช้ Generator เพื่อเลียนแบบ Lazy Evaluation
function* lazyRange(start: number, end: number) {
for (let i = start; i <= end; i++) {
yield i;
}
}
const rangeIterator = lazyRange(1, 5);
console.log(rangeIterator.next().value); // ผลลัพธ์: 1
console.log(rangeIterator.next().value); // ผลลัพธ์: 2
console.log(rangeIterator.next().value); // ผลลัพธ์: 3
console.log(rangeIterator.next().value); // ผลลัพธ์: 4
console.log(rangeIterator.next().value); // ผลลัพธ์: 5
console.log(rangeIterator.next().value); // ผลลัพธ์: undefined
ในตัวอย่างนี้ เราใช้ generator function เพื่อสร้าง iterator ที่คืนค่าตัวเลขทีละตัวเมื่อเรียกใช้ next()
การใช้งาน Lazy Evaluation ในการจัดการ Infinite Data
function* infiniteSequence() {
let i = 0;
while (true) {
yield i++;
}
}
const infiniteIterator = infiniteSequence();
console.log(infiniteIterator.next().value); // ผลลัพธ์: 0
console.log(infiniteIterator.next().value); // ผลลัพธ์: 1
console.log(infiniteIterator.next().value); // ผลลัพธ์: 2
// เราสามารถเรียกใช้ต่อไปได้เรื่อย ๆ
ก็จบกันไปกับ Part ที่เป็น fundamental รอติดตามใน Part ต่อๆไปนะครับ