Best Practice: Typescript trong functional programming

🧠 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:

  1. Deterministic: Cùng input → luôn ra cùng output.
  2. 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 getset:
    • 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ới
  • over: 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 MonoidSemigroup + 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ó mapflatMap (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ủ

  1. Left identity:
    M.of(a).flatMap(f) === f(a)
  2. Right identity:
    m.flatMap(M.of) === m
  3. 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