1. Functors là gì?
Functors là các đối tượng có thể map qua - nghĩa là chúng có thể áp dụng một hàm lên các giá trị được bọc trong chúng. Một Functor phải tuân thủ hai quy tắc:
- Identity: Khi map qua một hàm identity (x => x), kết quả phải giống với giá trị ban đầu
- Composition: Khi map qua một hàm kết hợp (f ∘ g), kết quả phải giống với việc map qua f rồi map qua g
2. Triển khai Functors trong JavaScript
2.1 Array Functor
// Array là một Functor có sẵn trong JavaScript
const numbers = [1, 2, 3, 4, 5];
// Map qua một hàm
const doubled = numbers.map((x) => x * 2);
console.log(doubled); // [2, 4, 6, 8, 10]
// Map qua nhiều hàm (composition)
const addOne = (x) => x + 1;
const multiplyByTwo = (x) => x * 2;
const result1 = numbers.map(addOne).map(multiplyByTwo);
const result2 = numbers.map((x) => multiplyByTwo(addOne(x)));
console.log(result1); // [4, 6, 8, 10, 12]
console.log(result2); // [4, 6, 8, 10, 12]
2.2 Custom Functor
class Box {
constructor(value) {
this.value = value;
}
static of(value) {
return new Box(value);
}
map(fn) {
return Box.of(fn(this.value));
}
fold(fn) {
return fn(this.value);
}
}
// Ví dụ sử dụng
const box = Box.of(5);
const result = box
.map((x) => x * 2)
.map((x) => x + 1)
.fold((x) => x);
console.log(result); // 11
3. Triển khai Functors trong TypeScript
3.1 Generic Functor Interface
interface Functor<T> {
map<U>(fn: (value: T) => U): Functor<U>;
}
class Box<T> implements Functor<T> {
constructor(private value: T) {}
static of<T>(value: T): Box<T> {
return new Box(value);
}
map<U>(fn: (value: T) => U): Box<U> {
return Box.of(fn(this.value));
}
fold<U>(fn: (value: T) => U): U {
return fn(this.value);
}
}
// Ví dụ sử dụng
interface User {
name: string;
age: number;
}
const user = Box.of<User>({ name: "John", age: 30 });
const result = user
.map((u) => ({ ...u, age: u.age + 1 }))
.map((u) => `${u.name} is ${u.age} years old`)
.fold((x) => x);
console.log(result); // "John is 31 years old"
3.2 Maybe Functor
class Maybe<T> implements Functor<T> {
private constructor(private value: T | null) {}
static of<T>(value: T): Maybe<T> {
return new Maybe(value);
}
static nothing<T>(): Maybe<T> {
return new Maybe<T>(null);
}
isNothing(): boolean {
return this.value === null || this.value === undefined;
}
map<U>(fn: (value: T) => U): Maybe<U> {
if (this.isNothing()) {
return Maybe.nothing<U>();
}
return Maybe.of(fn(this.value!));
}
fold<U>(fn: (value: T) => U, defaultValue: U): U {
if (this.isNothing()) {
return defaultValue;
}
return fn(this.value!);
}
}
// Ví dụ sử dụng
interface Post {
title: string;
author?: {
name: string;
email: string;
};
}
const getAuthorEmail = (post: Post): string =>
Maybe.of(post.author)
.map((author) => author.email)
.fold((email) => email, "No author email");
const post1: Post = {
title: "Hello World",
author: {
name: "John",
email: "[email protected]",
},
};
const post2: Post = {
title: "No Author",
};
console.log(getAuthorEmail(post1)); // "[email protected]"
console.log(getAuthorEmail(post2)); // "No author email"
4. Dùng trong thực tế: pipeline xử lý dữ liệu
// Functor cho xử lý dữ liệu
class DataProcessor<T> implements Functor<T> {
constructor(private data: T) {}
static of<T>(data: T): DataProcessor<T> {
return new DataProcessor(data);
}
map<U>(fn: (value: T) => U): DataProcessor<U> {
return DataProcessor.of(fn(this.data));
}
validate(predicate: (value: T) => boolean): DataProcessor<T> {
if (!predicate(this.data)) {
throw new Error("Validation failed");
}
return this;
}
transform<U>(fn: (value: T) => U): U {
return fn(this.data);
}
}
// Ví dụ sử dụng
interface UserInput {
name: string;
email: string;
age: number;
}
const processUserInput = (input: UserInput): string => {
return DataProcessor.of(input)
.validate((u) => u.name.length > 0)
.validate((u) => u.email.includes("@"))
.validate((u) => u.age >= 0)
.map((u) => ({
...u,
name: u.name.trim(),
email: u.email.toLowerCase(),
}))
.transform((u) => `${u.name} (${u.email}) is ${u.age} years old`);
};
const userInput: UserInput = {
name: " John Doe ",
email: "[email protected]",
age: 30,
};
console.log(processUserInput(userInput)); // "John Doe ([email protected]) is 30 years old"
Đoạn này tiện để minh họa, nhưng nếu nhìn kỹ thì validate() đang throw. Nghĩa là DataProcessor không còn là một functor “sạch” theo nghĩa chỉ map giá trị nữa. Trong code production, mình sẽ tách rõ hai chuyện:
mapchỉ transform dữ liệu.validatetrả vềResulthoặc ném lỗi ở ranh giới ngoài cùng.
Nếu trộn cả hai, pipeline nhìn gọn nhưng debug hơi mệt vì một bước giữa chuỗi có thể nổ exception.
5. Kiểm tra functor law bằng test nhỏ
Functor đáng tin khi map không làm điều bất ngờ. Hai luật cơ bản nghe học thuật, nhưng test lại rất thực dụng: map identity phải giữ nguyên, map composition phải cho cùng kết quả.
const identity = <T>(value: T): T => value;
const double = (value: number): number => value * 2;
const addOne = (value: number): number => value + 1;
const box = Box.of(10);
const identityResult = box.map(identity).fold((value) => value);
const compositionResult = box
.map(addOne)
.map(double)
.fold((value) => value);
const composedResult = box
.map((value) => double(addOne(value)))
.fold((value) => value);
console.assert(identityResult === 10);
console.assert(compositionResult === composedResult);
Không cần test theo kiểu toán học quá trang trọng. Chỉ cần vài test như trên là đủ bắt các bug kiểu map mutate state, swallow error, hoặc đổi wrapper sai.
6. Functor hay chỉ dùng map thường?
Trong JavaScript, nhiều khi Array.map, Promise.then, Option/Result từ thư viện là đủ. Tự tạo Box, Maybe, DataProcessor chỉ đáng làm khi wrapper đó mang thêm ngữ nghĩa thật:
Maybebiểu diễn dữ liệu có thể thiếu.Resultbiểu diễn thành công/thất bại.Taskbiểu diễn async lazy, chỉ chạy khi được gọi.Parserbiểu diễn một bước parse có thể ghép tiếp.
Nếu wrapper chỉ bọc giá trị rồi gọi .map(), nó thường làm code khó đọc hơn.
7. Trade-off cần nhớ
7.1 Ưu điểm
- Composition: Cho phép kết hợp các tác vụ một cách dễ dàng
- Type safety: Đảm bảo type safety trong TypeScript
- Reusability: Có thể tái sử dụng các hàm xử lý dữ liệu
- Immutability: Không thay đổi giá trị gốc
7.2 Nhược điểm
- Độ phức tạp: Có thể làm code phức tạp hơn nếu sử dụng không đúng cách
- Learning curve: Yêu cầu hiểu biết về lập trình hàm
- Verbose: Có thể làm code dài hơn trong một số trường hợp
- Debugging: Có thể khó debug hơn do tính chất trừu tượng
8. Khi nào nên dùng Functors?
Functors phù hợp khi:
- Cần xử lý dữ liệu một cách có cấu trúc
- Cần kết hợp nhiều tác vụ xử lý dữ liệu
- Cần đảm bảo type safety trong TypeScript
- Cần xử lý các giá trị có thể null hoặc undefined
Functor hữu ích trong lập trình hàm vì nó cho phép xử lý dữ liệu một cách có cấu trúc và an toàn. Pattern này đáng chú ý trong việc xử lý dữ liệu và đảm bảo type safety trong TypeScript.