Functional Programming in Typescript Part 1: Fundamentals

Pitchayut CheeseJa
odds.team
Published in
7 min readJun 1, 2024

ภายในชุมชนชาว 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 ซึ่งมีลักษณะและแนวคิดสำคัญดังนี้:

  1. Pure Functions: ฟังก์ชันที่ไม่มี side effects ซึ่งหมายความว่าผลลัพธ์ของฟังก์ชันขึ้นอยู่กับ arguments ที่ถูกส่งเข้าไปเท่านั้น และไม่มีการเปลี่ยนแปลง “state” ภายนอกใดๆ
  2. Immutability: ค่าคงที่ที่ไม่สามารถเปลี่ยนแปลงได้ ช่วยลดข้อผิดพลาดที่เกิดจากการเปลี่ยนแปลง state โดยไม่คาดคิด
  3. State Management: state จะถูกจัดการอย่างระมัดระวัง โดยมักจะใช้ฟังก์ชันที่ส่งค่าใหม่กลับมาแทนการเปลี่ยนแปลงค่าเดิม
  4. First-Class Functions: ฟังก์ชันสามารถถูกส่งเป็น argument คืนค่าเป็นผลลัพธ์ หรือเก็บในตัวแปรได้
  5. Declarative Nature: เน้นการบอกว่าเราต้องการทำอะไรแทนที่จะบอกว่าทำอย่างไร (what to do instead of how to do it)
  6. Higher-Order Functions: ฟังก์ชันที่สามารถรับฟังก์ชันอื่นเป็น argument หรือคืนค่าฟังก์ชันเป็นผลลัพธ์

อันนี้คร่าวๆสำหรับ Concept ของ Functional Programming

ก่อนจะลงลึกไปในแต่ละหัวข้อ ขอแวะมาที่หัวข้อถัดไปก่อน

Cover photo created by Ahad Rahman

แล้วเจ้า Functional Programming มีประโยชน์ยังไง ???

ที่สรุปมาจะมีประมาณนี้

  1. Testable: ถ้าเราพยายามเขียนในรูปแบบ Pure Function เราจะสามารถเขียน Unit Test ได้ง่ายมาก เนื่องจาก Function จะเล็กและไม่เกิด side effect
  2. Readable: เล็ก สั้น กระชับ เข้าใจง่าย ตัว function จะยุ่งกับแค่ arguments ที่ส่งเข้ามา ไม่ต้องกังวลเรื่องตัวแปรภายนอกใดๆเลย
  3. Concurrency: function ส่วนใหญ่จะเป็น stateless ทำให้รองรับการ scale ได้
  4. 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

  1. Testability: เพราะไม่มี side effects ทำให้สามารถทดสอบได้ง่ายมาก เนื่องจากเรารู้ว่าผลลัพธ์ที่ได้จะเป็นอย่างไรเมื่อให้ค่า arguments เข้าไป
  2. Reusability: สามารถนำมาใช้ซ้ำได้ในหลายๆ ที่โดยไม่ต้องกังวลว่าจะเกิดผลกระทบต่อส่วนอื่นๆ ของ code
  3. Readability: เพราะทำงานเฉพาะกับ arguments ของมัน ทำให้เราสามารถเข้าใจการทำงานของฟังก์ชันได้ง่ายขึ้น
  4. 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) มักใช้กับการเขียนโค้ดที่ต้องการผลลัพธ์ที่แน่นอน โดยไม่ต้องกำหนดขั้นตอนการทำงานทีละขั้นตอน

ข้อดี:

  1. อ่านง่ายและเข้าใจง่าย: โค้ดมักจะสั้นและเข้าใจง่าย เนื่องจากเน้นการบอกว่าต้องการทำอะไร
  2. บำรุงรักษาง่าย: การเปลี่ยนแปลงหรือปรับปรุงทำได้ง่าย เพราะไม่ต้องเปลี่ยนแปลง logic ที่ซับซ้อน
  3. ลดข้อผิดพลาด: เนื่องจากไม่ต้องระบุขั้นตอนการทำงานทีละขั้นตอน โอกาสเกิดข้อผิดพลาดจากการเขียนโค้ดที่ซับซ้อนจึงลดลง

ตัวอย่าง:

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) ต้องกำหนดทุกขั้นตอนในการทำงานของโปรแกรมเพื่อให้ได้ผลลัพธ์ที่ต้องการ

ข้อดี:

  1. ควบคุมได้เต็มที่: สามารถควบคุมทุกขั้นตอนการทำงานของโปรแกรมได้ ทำให้สามารถปรับแต่งการทำงานให้มีประสิทธิภาพสูงสุด
  2. เข้าใจการทำงานเชิงลึก: ช่วยให้เข้าใจการทำงานภายในของโปรแกรมได้ดีขึ้น

ตัวอย่าง:

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 ซึ่งช่วยลดข้อผิดพลาดและทำให้โค้ดมีความเชื่อถือได้มากขึ้น

ข้อดี:

  1. ลดข้อผิดพลาด: ช่วยลดข้อผิดพลาดได้โดยการทำให้ข้อมูลไม่สามารถเปลี่ยนแปลงได้
  2. การทดสอบง่ายขึ้น: ฟังก์ชันที่รับข้อมูลที่ไม่เปลี่ยนแปลงจะให้ผลลัพธ์ที่คาดเดาได้เสมอ
  3. เพิ่มความน่าเชื่อถือ: การมีข้อมูลที่ไม่เปลี่ยนแปลงทำให้โปรแกรมมีความน่าเชื่อถือและสามารถทำนายผลลัพธ์ได้
  4. 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 ได้ ซึ่งหมายความว่าฟังก์ชันสามารถ:

  1. ถูกกำหนดให้กับตัวแปร
  2. ถูกส่งเป็น arguments ให้กับฟังก์ชันอื่น
  3. ถูกคืนค่าจากฟังก์ชันอื่น

ข้อดี

  1. ยืดหยุ่น: ฟังก์ชันสามารถถูกใช้เหมือนกับข้อมูลอื่นๆ
  2. การสร้าง Higher-Order Functions: สามารถสร้างฟังก์ชันที่รับหรือคืนค่าฟังก์ชันอื่นได้ ซึ่งช่วยให้การประมวลผลเชิงฟังก์ชันมีประสิทธิภาพและเข้าใจง่ายขึ้น
  3. ลดการทำซ้ำ: ช่วยให้สามารถสร้างโค้ดที่ใช้ซ้ำได้ง่ายขึ้น โดยการส่งฟังก์ชันเป็น 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

  1. ความยืดหยุ่น: สามารถใช้ฟังก์ชันอื่นเป็น argument ทำให้สามารถสร้างฟังก์ชันที่ยืดหยุ่นและปรับแต่งได้ง่าย
  2. การใช้ซ้ำ: ช่วยให้เราสามารถใช้โค้ดซ้ำได้ โดยไม่ต้องเขียนฟังก์ชันใหม่ทุกครั้งที่ต้องการเปลี่ยนแปลงพฤติกรรมบางอย่าง
  3. การแยกความรับผิดชอบ: ช่วยให้เราสามารถแยกความรับผิดชอบออกเป็นส่วน ๆ ทำให้บำรุงรักษาได้ง่ายขึ้น

ตัวอย่าง ฟังก์ชันที่รับฟังก์ชันเป็น 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 ต่ำ

  1. ความเข้าใจง่าย: ฟังก์ชันที่มี arguments น้อยจะเข้าใจง่ายกว่าและสามารถบำรุงรักษาได้ง่ายกว่า
  2. ความยืดหยุ่น: ฟังก์ชันที่มี Arity ต่ำสามารถนำมาผสมผสานกันได้ง่ายกว่าในการสร้างฟังก์ชันใหม่
  3. 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 ต่อๆไปนะครับ

--

--