🧠 Phần 1: Khái niệm nền tảng trong Functional Programming
🔹 Functional Programming là gì?
- Functional Programming (FP) là một phong cách lập trình sử dụng hàm (functions) làm thành phần cốt lõi để xây dựng chương trình.
- FP khác với Design Patterns: FP là cách tiếp cận, còn design patterns là các giải pháp lặp lại.
- Trong TypeScript, FP tận dụng sức mạnh của Higher-Order Functions, closures, recursion, và được củng cố bởi static typing.
🆚 Imperative vs Declarative Programming
🔸 Imperative: Chỉ định từng bước cụ thể.
let evenSum = 0;
for (let i = 1; i <= 10; i++) {
if (i % 2 === 0) evenSum += i;
}- Phải mô tả “làm thế nào” để đạt kết quả.
- Có thể gây lỗi nếu thay đổi thứ tự lệnh.
🔸 Declarative: Mô tả cái cần đạt được.
const sum = Array.from({ length: 10 }, (_, i) => i + 1)
.filter(n => n % 2 === 0)
.reduce((acc, n) => acc + n, 0);- Đọc như một câu lệnh logic: tạo mảng → lọc số chẵn → tính tổng.
👉 Declarative được ưa chuộng trong FP vì giúp dễ hiểu và bảo trì hơn.
🧼 Pure Functions (Hàm thuần)
✅ Đặc điểm:
- Deterministic: Cùng input → luôn ra cùng output.
- Không có side effect.
function add(a: number, b: number): number {
return a + b;
}❌ Ví dụ về impure function:
let count = 0;
function incrementAndLog(value: number): number {
count++;
console.log(count); // Side effect
return value + 1;
}🔐 Closures
- Closure là một hàm giữ lại được phạm vi biến bên ngoài dù được gọi ở nơi khác.
function makeFunc() {
const name = "Alex";
return function displayName() {
console.log(name);
};
}
const myFunc = makeFunc();
myFunc(); // "Alex"📝 Closure có thể gây lỗi khó lường nếu vô tình giữ biến thay đổi trong vòng lặp.
📤 Side Effects và IO Actions
- FP không loại bỏ side effects, mà quản lý chúng có chủ đích, ví dụ như gói vào
IO<A>:
interface IO<A> { (): A; }
const getTime: IO<string> = () => new Date().toISOString();
const logMessage = (msg: string): IO<void> => () => console.log(msg);📦 IO giúp rõ ràng hóa phần nào là side effect, dễ kiểm soát và test.
🔁 Recursion – Đệ quy
✅ Cấu trúc chuẩn của đệ quy:
- Base case: điểm dừng.
- Recursive case: gọi lại chính mình với input nhỏ hơn.
function factorial(n: number): number {
if (n <= 1) return 1;
return n * factorial(n - 1);
}🌲 Ví dụ với cây nhị phân:
interface TreeNode {
value: number;
left?: TreeNode;
right?: TreeNode;
}
function inOrder(node: TreeNode | undefined): number[] {
if (!node) return [];
return [...inOrder(node.left), node.value, ...inOrder(node.right)];
}🧠 Tail Recursion
- Tối ưu đệ quy để tránh lỗi stack overflow khi input lớn:
function factorialTail(n: number, acc: number = 1): number {
if (n <= 1) return acc;
return factorialTail(n - 1, n * acc);
}⚠️ Lưu ý: TypeScript/JS không hỗ trợ Tail Call Optimization ở mức runtime → vẫn có thể bị lỗi với input lớn.
🔧 First-Class Functions
- Trong TypeScript, hàm là first-class citizen → có thể:
- Gán cho biến
- Truyền làm tham số
- Trả về từ hàm khác
- Lưu trong cấu trúc dữ liệu
const greet = (name: string) => `Hello, ${name}!`;
function execute(x: number, y: number, op: (a, b) => number): number {
return op(x, y);
}🔗 Function Composition
const double = (x) => x * 2; const increment = (x) => x + 1; const composed = (x) => increment(double(x)); // double rồi mới increment
🧩 Compose Utility:
function compose<T>(...fns: Array<(arg: T) => T>) {
return (x: T) => fns.reduceRight((acc, fn) => fn(acc), x);
}👉 Giúp ghép chuỗi xử lý theo thứ tự từ dưới lên, như pipe.
🧩 Phần 2: Currying, Referential Transparency và Tính Bất Biến (Immutability)
🔁 Currying – Biến hàm nhiều tham số thành chuỗi hàm một tham số
- Currying giúp hàm dễ kết hợp (compose) hơn bằng cách chuyển đổi từ: tsCopyEdit
(a: T, b: U) => Vthành: tsCopyEdit(a: T) => (b: U) => V
📦 Ví dụ:
function curry<T, U, V>(fn: (a: T, b: U) => V): (a: T) => (b: U) => V {
return (a) => (b) => fn(a, b);
}
const truncate = (str: string, len: number) =>
str.length > len ? str.slice(0, len) + '...' : str;
const curriedTruncate = curry(truncate);
const truncate7 = curriedTruncate(7);
console.log(truncate7("Hello TypeScript")); // "Hello T..."♻️ Referential Transparency – Tính thay thế được
- Một hàm có tính chất referential transparency nếu bạn có thể thay thế hàm bằng kết quả của nó mà không ảnh hưởng chương trình.
⚠️ Ví dụ không trong suốt:
function sortList(list: number[]): number[] {
return list.sort(); // Sắp xếp TẠI CHỖ → làm thay đổi input gốc
}✅ Ví dụ trong suốt:
function pureSort(list: number[]): number[] {
return [...list].sort(); // Tạo bản sao → không làm thay đổi input
}🧠 Khi hàm trong suốt, ta dễ kiểm thử, dễ dự đoán, dễ thay thế khi refactor.
❄️ Immutability – Tính bất biến
- Trong FP, ta cố gắng không thay đổi dữ liệu gốc. Mọi thay đổi đều tạo bản sao mới.
🧱 const không đủ để bất biến
const arr = [1, 2, 3]; arr.push(4); // Vẫn hợp lệ – chỉ bị chặn reassignment
🛡️ Readonly types:
interface User {
name: string;
age: number;
}
const user: Readonly<User> = { name: "Alice", age: 30 };
user.age = 31; // ❌ lỗi: không được sửa🧬 DeepReadonly – Bất biến sâu
type DeepReadonly<T> =
T extends (infer R)[] ? ReadonlyArray<DeepReadonly<R>> :
T extends object ? { readonly [K in keyof T]: DeepReadonly<T[K]> } :
T;
interface Department {
name: string;
employees: { id: number, name: string }[];
}
const dept: DeepReadonly<Department> = {
name: "Engineering",
employees: [{ id: 1, name: "Alice" }]
};
dept.employees.push({ id: 2, name: "Bob" }); // ❌ lỗi🏗️ Bất biến trong class – Immutable Class
class ImmutablePerson {
readonly #name: string;
readonly #age: number;
constructor(name: string, age: number) {
this.#name = name;
this.#age = age;
}
get name() { return this.#name; }
get age() { return this.#age; }
withAge(newAge: number): ImmutablePerson {
return new ImmutablePerson(this.#name, newAge);
}
}- Hàm
withAgetrả về một bản sao mới với tuổi cập nhật, giữ nguyên các trường khác.
📚 Dùng Immutable.js
- Một thư viện tạo các cấu trúc dữ liệu bất biến thật sự, ví dụ:
import { List } from 'immutable';
const list1 = List([1, 2, 3]);
const list2 = list1.push(4);
console.log(list1.toArray()); // [1, 2, 3]
console.log(list2.toArray()); // [1, 2, 3, 4]📌 push vào list1 thực chất tạo list2 mới — list1 vẫn giữ nguyên.
🔍 Phần 3: Functional Lenses – Truy cập dữ liệu bất biến, có thể compose được
🔭 Functional Lens là gì?
- Một lens là một cặp hàm
getvàset:get: lấy giá trị từ một object.set: tạo bản sao mới của object với giá trị được cập nhật.
- Lens hoạt động như một kính hiển vi chiếu vào một phần nhỏ của object mà không làm thay đổi object gốc.
- Lens rất hữu ích trong các tình huống:
- Làm việc với dữ liệu lồng nhau sâu.
- Cần cập nhật bất biến (immutable).
- Xây dựng logic composable và dễ test.
🔧 Interface của một Lens
export interface Lens<T, A> {
get: (obj: T) => A;
set: (obj: T) => (newValue: A) => T;
}T: kiểu của object cha.A: kiểu của thuộc tính bên trong.
🛠️ Hàm tạo lens từ một property
function lensProp<T, K extends keyof T>(key: K): Lens<T, T[K]> {
return {
get: (obj) => obj[key],
set: (obj) => (value) => ({ ...obj, [key]: value }),
};
}📌 set sử dụng spread operator để đảm bảo không làm thay đổi object ban đầu.
👤 Ví dụ: Lens cho thuộc tính age
interface Person {
name: string;
age: number;
email: string;
}
const person: Person = { name: "John", age: 30, email: "john@example.com" };
const ageLens = lensProp<Person, "age">("age");
const age = ageLens.get(person); // 30
const newPerson = ageLens.set(person)(35); // tạo object mới với age = 35📌 Object ban đầu không thay đổi.
🔁 Hàm hỗ trợ thao tác với lens
function view<T, A>(lens: Lens<T, A>, obj: T): A {
return lens.get(obj);
}
function set<T, A>(lens: Lens<T, A>, obj: T, value: A): T {
return lens.set(obj)(value);
}
function over<T, A>(lens: Lens<T, A>, f: (x: A) => A, obj: T): T {
return lens.set(obj)(f(lens.get(obj)));
}view: nhưgetset: cập nhật giá trị mớiover: cập nhật theo 1 hàm chuyển đổi
📌 Cách sử dụng over:
const updated = over(ageLens, x => x + 1, person); console.log(view(ageLens, updated)); // 31
📋 Use Case: Cập nhật todo trong state (kiểu Redux)
🧱 Mô hình dữ liệu:
interface TodoItem {
id: string;
title: string;
completed: boolean;
}
interface TodoListState {
allItemIds: string[];
byItemId: { [id: string]: TodoItem };
}🧩 Lens cần thiết:
const byItemIdLens = lensProp<TodoListState, 'byItemId'>('byItemId');
const completedLens = lensProp<TodoItem, 'completed'>('completed');
function todoItemLens(id: string): Lens<{ [key: string]: TodoItem }, TodoItem> {
return lensProp<{ [key: string]: TodoItem }, string>(id);
}🔁 Cập nhật trạng thái bất biến bằng lens:
function reduceState(state: TodoListState, action: UpdateTodoItemCompletedAction): TodoListState {
if (action.type === "UPDATE_TODO_ITEM_COMPLETED") {
const itemLens = todoItemLens(action.id);
const currentItem = view(itemLens, state.byItemId);
const updatedItem = over(completedLens, () => action.completed, currentItem);
const updatedByItemId = {
...state.byItemId,
[action.id]: updatedItem,
};
return set(byItemIdLens, state, updatedByItemId);
}
return state;
}✅ State được cập nhật bất biến, và thao tác gọn gàng, không cần viết logic lồng nhiều tầng.
📊 Kết quả sau khi dispatch:
const initialState: TodoListState = {
byItemId: {
'1': { id: '1', title: 'Learn TS', completed: false },
'2': { id: '2', title: 'Build App', completed: false },
},
};
const action = {
type: "UPDATE_TODO_ITEM_COMPLETED",
id: '1',
completed: true
};
const newState = reduceState(initialState, action);
console.log(newState);🔎 newState là một bản sao mới của initialState với completed của id: 1 được cập nhật thành true.
🧱 Phần 4: Functors, Applicatives, Monads và các cấu trúc nâng cao
🎁 Functors – hộp có thể biến đổi bên trong
- Functor là một cấu trúc dữ liệu có phương thức
map, dùng để áp dụng hàm lên giá trị bên trong mà không thay đổi cấu trúc.
📦 Ví dụ: Box functor
class Box<T> {
constructor(private value: T) {}
map<U>(f: (value: T) => U): Box<U> {
return new Box(f(this.value));
}
toString(): string {
return `Box(${this.value})`;
}
}
const result = new Box(5).map(x => x * 2).map(x => x + 1);
console.log(result.toString()); // Box(11)tsCopyEdit
📌 map giúp chuỗi xử lý trở nên gọn gàng và bất biến.
⚙️ Applicatives – hàm nằm trong hộp, áp dụng lên giá trị trong hộp
- Applicative mở rộng Functor bằng cách thêm phương thức
ap(apply), cho phép: “Hàm trong hộp” áp dụng lên “giá trị trong hộp”
🧾 Ví dụ: Maybe applicative
tsCopyEditconst add = (a: number) => (b: number) => a + b;
const maybeAdd = Maybe.just(add);
const result = maybeNumber1.ap(maybeAdd.ap(maybeNumber2));
⚠️ Trong TypeScript, do thiếu Higher-Kinded Types, nên phải dùng thư viện như fp-ts để giải quyết an toàn kiểu.
♻️ Semigroups – kết hợp giá trị theo luật kết hợp
- Semigroup là bất kỳ kiểu nào có thể kết hợp hai giá trị lại bằng hàm
concatvà tuân theo tính kết hợp.
🧮 Ví dụ:
class Sum {
constructor(public value: number) {}
concat(other: Sum): Sum {
return new Sum(this.value + other.value);
}
}🧩 Monoids – semigroup có phần tử trung tính
- Một Monoid là Semigroup + Identity element (phần tử không ảnh hưởng đến phép kết hợp).
💯 Ví dụ:
class Product implements Monoid<Product> {
constructor(public value: number) {}
concat(other: Product): Product {
return new Product(this.value * other.value);
}
identity(): Product {
return new Product(1); // 1 là phần tử trung tính của phép nhân
}
}tsCopyEdit
📚 Traversables – duyệt qua container để gom kết quả
- Traversable là cấu trúc hỗ trợ phương thức
traverse, giúp duyệt từng phần tử và gom kết quả vào container khác.
🧪 Ví dụ:
class TraversableList<T> {
constructor(private items: T[]) {}
traverse<A>(fn: (item: T) => A[]): A[] {
return this.items.flatMap(fn);
}
}🌀 Monads – “chất kết dính” các giá trị & hàm có thể thất bại
- Monad là cấu trúc có
mapvàflatMap(còn gọi làbind) dùng để:- Chuỗi các phép tính có thể thất bại hoặc sinh side effects.
- Ẩn đi lỗi hoặc
nullmột cách an toàn.
📌 Ví dụ: Maybe Monad
class Maybe<T> {
private constructor(private value: T | null) {}
static just<T>(value: T): Maybe<T> { return new Maybe(value); }
static nothing<T>(): Maybe<T> { return new Maybe<T>(null); }
map<U>(fn: (value: T) => U): Maybe<U> {
return this.value === null ? Maybe.nothing() : Maybe.just(fn(this.value));
}
flatMap<U>(fn: (value: T) => Maybe<U>): Maybe<U> {
return this.value === null ? Maybe.nothing() : fn(this.value);
}
}✅ Ưu điểm:
- Viết code rõ ràng, tránh lồng
if,null check. - Dễ composable.
- Dễ test vì loại bỏ
null/undefinedtừ sớm.
📉 Ví dụ xử lý chia và căn bậc hai an toàn:
function safeDivide(x: number, y: number): Maybe<number> {
return y !== 0 ? Maybe.just(x / y) : Maybe.nothing();
}
function safeSqrt(x: number): Maybe<number> {
return x >= 0 ? Maybe.just(Math.sqrt(x)) : Maybe.nothing();
}
const result = safeDivide(16, 4).flatMap(safeSqrt);
console.log(result); // Maybe { value: 2 }⚠️ Monad Laws – các quy tắc cần tuân thủ
- Left identity:
M.of(a).flatMap(f) === f(a) - Right identity:
m.flatMap(M.of) === m - Associativity:
m.flatMap(f).flatMap(g) === m.flatMap(x => f(x).flatMap(g))
✅ Giúp các phép chain logic ổn định, không bị sai lệch thứ tự.
📊 Monad khác: Either – Xử lý lỗi rõ ràng
class Either<L, R> {
static left<L, R>(val: L): Either<L, R> { ... }
static right<L, R>(val: R): Either<L, R> { ... }
}
function divide(a: number, b: number): Either<string, number> { ... }🧠 Khi lỗi → left, khi thành công → right. Dùng để thay cho throw error.
🧾 Monad State – quản lý trạng thái bất biến
class State<S, A> {
constructor(public run: (s: S) => [A, S]) {}
map<B>(f: (a: A) => B): State<S, B> { ... }
flatMap<B>(f: (a: A) => State<S, B>): State<S, B> { ... }
}🧠 Cho phép truyền trạng thái qua chuỗi xử lý mà không thay đổi biến toàn cục.
✅ Tóm tắt chương
- Functional Programming tập trung vào:
🔹 Purity – tránh side effect
🔹 Immutability – không thay đổi dữ liệu gốc
🔹 Composition – ghép hàm thành chuỗi logic
🔹 Referential Transparency – có thể thay hàm bằng giá trị mà không làm sai logic - Các cấu trúc nâng cao:
🧩 Functors →map
🔗 Applicatives →ap
🌀 Monads →flatMap
📊 Semigroups/Monoids →concat,identity
🧭 Traversables →traverse


Để lại một bình luận
Bạn phải đăng nhập để gửi bình luận.