🧠 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) => V
thà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
withAge
trả 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
get
và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ưget
set
: 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
concat
và 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ó
map
và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
null
mộ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
/undefined
từ 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.