1. Function Composition là gì?
Function Composition là kỹ thuật kết hợp hai hoặc nhiều hàm để tạo ra một hàm mới. Kết quả của hàm đầu tiên sẽ được truyền vào hàm thứ hai, và cứ tiếp tục như vậy. Trong toán học, điều này được biểu diễn như sau:
[ (f ∘ g)(x) = f(g(x)) ]
2. Triển khai Function Composition trong JavaScript
2.1 Composition cơ bản
// Các hàm đơn giản
const addOne = (x) => x + 1;
const multiplyByTwo = (x) => x * 2;
const square = (x) => x * x;
// Composition thủ công
const result = square(multiplyByTwo(addOne(5)));
console.log(result); // 144 = ((5 + 1) * 2)^2
// Helper function cho composition
const compose =
(...fns) =>
(x) =>
fns.reduceRight((acc, fn) => fn(acc), x);
// Sử dụng compose
const calculate = compose(square, multiplyByTwo, addOne);
console.log(calculate(5)); // 144
2.2 Point-free Style
// Các hàm utility
const prop = (key) => (obj) => obj[key];
const map = (fn) => (arr) => arr.map(fn);
const filter = (predicate) => (arr) => arr.filter(predicate);
const join = (separator) => (arr) => arr.join(separator);
// Ví dụ sử dụng
const users = [
{ name: "John", age: 30 },
{ name: "Jane", age: 25 },
{ name: "Bob", age: 35 },
];
const getNames = compose(
join(", "),
map(prop("name")),
filter((user) => user.age > 25)
);
console.log(getNames(users)); // "John, Bob"
3. Triển khai Function Composition trong TypeScript
3.1 Type-safe Composition
type Func<T, R> = (arg: T) => R;
function compose<A, B, C>(f: Func<B, C>, g: Func<A, B>): Func<A, C> {
return (x) => f(g(x));
}
// Ví dụ sử dụng với type safety
const toString = (x: number): string => x.toString();
const length = (x: string): number => x.length;
const isEven = (x: number): boolean => x % 2 === 0;
const isLengthEven = compose(isEven, compose(length, toString));
console.log(isLengthEven(123)); // false (length is 3)
console.log(isLengthEven(12)); // true (length is 2)
3.2 Pipeline Operator — proposal TC39 (chưa standardized)
Trạng thái đến 2025: Pipeline operator
|>vẫn là TC39 proposal Stage 2, chưa có trong JavaScript chuẩn. TypeScript cũng không hỗ trợ ở chế độ mặc định. Muốn chạy cú pháp|>bạn cần Babel với plugin@babel/plugin-proposal-pipeline-operator— không phảiexperimentalDecorators(flag đó dành cho decorator cũ của TS, hoàn toàn không liên quan đến pipeline).
Trong lúc chờ, có hai lựa chọn thực dụng:
Lựa chọn 1: Helper pipe/flow của thư viện (lodash-fp, Ramda, Effect, fp-ts):
import { pipe } from "lodash/fp"; // hoặc: import { pipe } from "effect/Function"
const double = (x: number): number => x * 2;
const addTen = (x: number): number => x + 10;
const format = (x: number): string => `Result: ${x}`;
const run = pipe(double, addTen, format);
console.log(run(5)); // "Result: 20"
// Hoặc gọi trực tiếp với giá trị đầu vào:
const result = pipe(5, double, addTen, format); // "Result: 20"
Lựa chọn 2: Pipeline qua Babel (nếu thật sự cần |>):
// babel.config.json
{
"plugins": [
[
"@babel/plugin-proposal-pipeline-operator",
{ "proposal": "hack", "topicToken": "%" },
],
],
}
// Cú pháp "hack" proposal — dùng `%` làm topic token
const result = 5 |> double(%) |> addTen(%) |> format(%);
console.log(result); // "Result: 20"
Vì proposal chưa cố định, khuyến nghị cho production code là dùng
pipe()helper — API ổn định, type inference tốt với TS 5+, không phụ thuộc build flag đặc biệt.
4. Ví dụ Thực Tế: Xử Lý Dữ Liệu
interface User {
id: number;
name: string;
email: string;
role: string;
}
interface UserViewModel {
displayName: string;
contact: string;
isAdmin: boolean;
}
// Các hàm transform
const normalizeEmail = (email: string): string => email.toLowerCase();
const formatName = (name: string): string =>
name
.split(" ")
.map((part) => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase())
.join(" ");
const transformUser = (user: User): UserViewModel => ({
displayName: formatName(user.name),
contact: normalizeEmail(user.email),
isAdmin: user.role === "admin",
});
const sortByName = (users: UserViewModel[]): UserViewModel[] =>
[...users].sort((a, b) => a.displayName.localeCompare(b.displayName));
const filterAdmins = (users: UserViewModel[]): UserViewModel[] =>
users.filter((user) => user.isAdmin);
// Composition của các transform
const processUsers = compose(filterAdmins, sortByName, map(transformUser));
// Sử dụng
const users: User[] = [
{ id: 1, name: "john doe", email: "[email protected]", role: "admin" },
{ id: 2, name: "jane smith", email: "[email protected]", role: "user" },
{ id: 3, name: "bob wilson", email: "[email protected]", role: "admin" },
];
const result = processUsers(users);
console.log(result);
/*
[
{
displayName: "Bob Wilson",
contact: "[email protected]",
isAdmin: true
},
{
displayName: "John Doe",
contact: "[email protected]",
isAdmin: true
}
]
*/
5. Ưu điểm và Nhược điểm
5.1 Ưu điểm
- Tái sử dụng: Các hàm nhỏ có thể được tái sử dụng và kết hợp
- Dễ test: Các hàm nhỏ dễ test hơn
- Dễ đọc: Code trở nên rõ ràng và có cấu trúc
- Không có side effects: Khuyến khích pure functions
5.2 Nhược điểm
- Performance: Có thể chậm hơn do nhiều lần gọi hàm
- Debugging: Có thể khó debug khi chuỗi composition dài
- Learning curve: Yêu cầu tư duy khác với lập trình hướng đối tượng
- Type inference: Có thể phức tạp trong TypeScript
6. Khi nào nên sử dụng Function Composition?
Function Composition phù hợp khi:
- Cần xử lý dữ liệu qua nhiều bước
- Muốn tái sử dụng các hàm nhỏ
- Cần code dễ test và maintain
- Muốn tránh side effects và mutation
7. Kết luận
Function Composition là một mẫu thiết kế mạnh mẽ trong lập trình hàm, cho phép xây dựng các hàm phức tạp từ các hàm đơn giản. Pattern này đặc biệt hữu ích trong việc xử lý dữ liệu và tạo ra code dễ maintain.