Trước khi vào bài viết, mình muốn giới thiệu ngắn gọn 2 phong cách lập trình cho các bạn cùng nắm.
Imperative programming
Mô tả cách để đạt được kết quả bằng cách liệt kê từng bước thực hiện.
Đặc điểm:
- Tập trung vào quy trình (how).
- Thường sử dụng vòng lặp, biến trạng thái, câu lệnh điều kiện.
// Imperative: tính tổng
let sum = 0;
for (let i = 0; i < arr.length; i++) {
sum += arr[i];
}Declarative Programming
Mô tả cái gì cần đạt được, để hệ thống quyết định cách thực hiện.
Đặc điểm:
- Tập trung vào kết quả (what).
- Ít quan tâm đến chi tiết thực thi.
// Declarative: tính tổng
const sum = arr.reduce((a, b) => a + b, 0);Nếu qua 2 ví dụ trên bạn chưa hiểu thì mình sẽ lấy ví dụ thực tế nè:
Có một em gái xinh đẹp tới hỏi đường bạn:
“Anh ơi cho em hỏi thờ Đức bà ở đâu vậy ạ?“
Hai phong cách lập trình trên sẽ trả lời như sau:
- Imperative: Em đi thẳng đường Nguyễn Văn Trỗi, xong tới Nam Kì Khởi Nghĩa, gặp Lê Duẩn rẽ trái đi 200m là tới
- Declarative: 01 Công xã Paris, Bến Nghé, Quận 1, Thành phố Hồ Chí Minh
Đọc ví dụ là hiểu liền.
Rồi, bây giờ chúng ta quay lại về Functional programming (FP)
Trong thế giới kiếm hiệp Kim Dung, Trương Tam Phong là nhân vật có võ công cao nhất (theo Kim Dung). Bởi vì ông sở hữu bộ võ công Thái Cực Quyền và Thái Cực Kiếm. Tinh hoa võ công khác hẳn hoàn toàn với thế giới võ công đương đại. Lấy nhu khắc cương.
Thế giới lập trình cũng vậy. Lập trình hướng đối tượng (Object oriented programming OOP) luôn được xem là tinh hoa của võ công.
Các bạn tiếp cận với OOP rất sớm, ngay lúc mới bắt đầu học lập trình.
Có rất nhiều người cho rằng OOP là mô hình để thiết kế phần mềm tốt nhất. Và mình xin nhắc lại, OOP là mô hình lấy đối tượng (object) làm gốc.
Mình ví OOP như là thế giới võ công đương đại trong Kim Dung. Vậy Thái Cực Quyền trong thế giới lập trình là gì?
Bạn đã bao giờ nghe về Functional programming (FP) chưa? Lập trình theo phong cách lấy hàm (function) là trung tâm.
Bây giờ, hãy cùng đi vào thế giới hoàn toàn khác với những gì chúng ta thường biết với OOP.
Lược sử
Một chút lược sử về FP:
- Lambda Calculus (1930s) do Alonzo Church giới thiệu là nền tảng lý thuyết cho FP.
- Các ngôn ngữ FP tiêu biểu: Lisp (1958), ML/F#, Haskell (1990), Erlang/Elixir, Clojure, Scala, Elm.
- OOP hình thành từ Simula (1960s) và Smalltalk (1970s), rồi phổ biến với C++, Java, C#, v.v.
Ngày nay, rất nhiều ngôn ngữ “đa phong cách” (multi-paradigm) tích hợp các đặc tính FP: JavaScript, Python, Java, C#, Rust, v.v.
Thật ra, đã có những bài viết trình bày rất rõ về FP. Họ là những tín đồ, fan cuồng của FP. Ví dụ như bài viết “So You Want to be a Functional Programmer” của Charles Scalfani. Thậm chí, Scalfani còn đề cao Functional Programming như nấc thang tiến hóa trong lịch sử lập trình. Anh ấy còn có một bài viết khác gây tranh cãi rất nhiều! Goodbye, Object Oriented Programming.
Trên thực tế! FP hay OOP là hai trường phái tu luyện khác nhau. Phần nào cũng có cái lợi thế hay bất lợi riêng. Chúng ta không nên so sánh bên nào ngon hơn! Mà đúng hơn:
Functional Programming (FP) không đối lập với Object-Oriented Programming (OOP); đó là một cách tiếp cận bổ sung. Viết phần mềm tốt thường là biết dùng “đúng công cụ cho đúng việc”, và đôi khi là phối hợp cả hai.
Vậy Functional programming là gì?
FP là phong cách lập trình trong đó hàm là đơn vị trung tâm. Thay vì “ra lệnh” (imperative) cho máy tính cách làm từng bước, FP ưu tiên cách diễn đạt khai báo (declarative): mô tả cái gì cần làm thông qua việc kết hợp các hàm nhỏ, thuần.
Khác biệt cốt lõi FP vs OOP
- FP: dữ liệu bất biến, không chia sẻ trạng thái, pure functions, composition, ...
- OOP: đối tượng có trạng thái thay đổi theo thời gian, phương thức gắn với dữ liệu, kế thừa, đa hình.
Áp dụng FP tốt giúp giảm coupling, dễ kiểm thử, và đơn giản hóa.
Nguyên lý cốt lõi của FP
Immutability (Tính bất biến)
Ý tưởng: Tránh thay đổi dữ liệu sau khi đã tạo; dùng bản sao mới thay vì sửa đổi bản cũ.
// Ví dụ bất biến với JavaScript
const user = { id: 1, name: "Thanh" };
// Tạo đối tượng mới, không mutate 'user'
const updatedUser = { ...user, name: "Le Nhat Thanh" };
const arr = [1, 2, 3];
const appended = [...arr, 4]; // [1,2,3,4]
const replaced = arr.map(x => x*2) // [2,4,6]
``Tại sao Immutability quan trọng?
- Dễ reasoning: Khi dữ liệu không đổi, bạn không phải lo lắng về việc một hàm nào đó âm thầm thay đổi trạng thái.
- Ít bug do shared state: Trong hệ thống đa luồng hoặc bất đồng bộ, việc chia sẻ dữ liệu mutable dễ gây race condition.
- Dễ kiểm thử: Hàm thuần + dữ liệu bất biến → dễ dự đoán kết quả.
- Thân thiện với concurrency: Không cần lock phức tạp vì không thay đổi trạng thái.
Pure Functions (Hàm thuần)
Định nghĩa: Cùng input → luôn trả về cùng output, không tạo hiệu ứng phụ - side effect (không ghi log, không gọi HTTP, không mutate tham số/vùng nhớ bên ngoài).
Lợi ích:
- Dễ unit test: Chỉ cần kiểm tra input/output.
- Dễ reasoning: Không phải lo lắng về trạng thái bên ngoài.
- Có thể cache (memoization): Vì kết quả luôn giống nhau cho cùng input.
- Thân thiện với concurrency: Không có race condition vì không chia sẻ trạng thái mutable.
// Pure function
const calcPrice = (base, taxRate) => base * (1 + taxRate);
// Impure function (có side effect - biến ngoại lai)
let discount = 0.1;
const priceWithGlobal = base => base * (1 - discount); Một chút về Functional Core, Imperative Shell: phần lõi làm toàn các hàm thuần; phần vỏ chịu trách nhiệm các side effects (IO, HTTP, DB).
Đây là một kĩ năng cực kì quan trọng khi áp dụng FP vào thực tế, mà mình sẽ trình bày ở cuối bài viết.
// Functional core (pure)
const toOrderSummary = orders =>
orders
.filter(o => o.status === "PAID")
.map(o => ({
id: o.id,
total: o.items.reduce((s, i) => s + i.price * i.qty, 0)
}));
// Imperative shell (side effects)
async function sendSummary(orders, api) {
const summary = toOrderSummary(orders);
return api.post("/summary", summary); // side effect (HTTP)
}Higher-Order Functions (HOF)
Hàm nhận hàm làm tham số, hoặc trả về hàm. Đây là nền tảng cho việc kết hợp và tái sử dụng logic.
const twice = (fn, x) => fn(fn(x));
const addOne = x => x + 1;
console.log(twice(addOne, 5)); // 7Các HOF phổ biến: map, filter, reduce, flatMap… giúp thay thế vòng lặp imperative.
Function Composition (hợp thành hàm)
Xây pipeline bằng cách ghép các hàm nhỏ, rõ ràng.
Khi xây dựng được pipeline như thế này thì code sẽ rất hạn dễ bị duplicate và các dev khác đọc sẽ dễ hiểu hơn nhiều (so với cách làm truyền thống - dùng các vòng lặp)
const compose = (...fns) => x => fns.reduceRight((v, f) => f(v), x);
const pipe = (...fns) => x => fns.reduce((v, f) => f(v), x);
const trim = s => s.trim();
const toLower= s => s.toLowerCase();
const slug = s => s.replace(/\s+/g, "-");
const toSlug = compose(slug, toLower, trim);
console.log(toSlug(" Xin Chào Functional Programming ")); // "xin-chao-functional-programming"Currying và Partial Application
Giúp “điền sẵn” một phần tham số để tạo hàm chuyên biệt.
Thường thì phần này mình ít sử dụng hơn các phần trên.
const multiply = a => b => a * b;
const double = multiply(2);
console.log([1, 2, 3].map(double)); // [2, 4, 6]Recursion vs Loops (và cảnh báo)
FP ưa dùng đệ quy (thay vì vòng lặp). Tuy nhiên:
- JavaScript không đảm bảo Tail-Call Optimization (TCO); đệ quy sâu có thể gây ra stack overflow.
- Thực tế nên ưu tiên
map/reduce, hoặc vòng lặp khi cần.
// Đệ quy đuôi (tail recursion) - nhưng JS không bảo đảm TCO
const sumRec = (arr, acc = 0) =>
arr.length === 0 ? acc : sumRec(arr.slice(1), acc + arr[0]);
// An toàn và dễ hiểu hơn
const sum = arr => arr.reduce((a, b) => a + b, 0);Ví dụ refactor từ imperative sang FP
Chúng ta cùng thực hiện bài toán này sau: Tính tổng doanh thu của các đơn hàng PAID sau một ngày cutoff.
Imperative (vòng lặp lồng nhau):
// Code minh họa
let total = 0;
for (let i = 0; i < orders.length; i++) {
const o = orders[i];
if (o.status === "PAID" && new Date(o.date) >= cutoff) {
for (let j = 0; j < o.items.length; j++) {
const it = o.items[j];
total += it.price * it.qty;
}
}
}FP với filter/map/reduce:
// Sau khi refactor
const isPaid = o => o.status === "PAID";
const after = cutoff => o => new Date(o.date) >= cutoff;
const orderTotal = o => o.items.reduce((s, it) => s + it.price * it.qty, 0);
const revenue = (orders, cutoff) =>
orders
.filter(isPaid)
.filter(after(cutoff))
.map(orderTotal)
.reduce((a, b) => a + b, 0);Kết quả tương đương, nhưng FP cho pipeline rõ ràng, dễ test từng bước, và không mutate trạng thái.
FP trong hệ sinh thái hiện đại
- JavaScript/TypeScript: Functional components & hooks trong React khuyến khích bất biến và pure render functions; Redux (Reducers là pure functions). Ngày xưa thì ReactJS hổ trợ class component những đã chuyển đổi sang functional component rồi.
- Ngôn ngữ FP chuyên sâu: Haskell (lazy evaluation, strong type system), F#, Elixir/Erlang (concurrency/actor model), Clojure (JVM FP + persistent data structures).
- Ngôn ngữ đa phong cách: Python, Java (Streams, Optional), C# (LINQ), Rust (immutability & ownership, iterators). PHP cũng đã hổ trợ một số functional style function.
Khi nào dùng FP, khi nào dùng OOP?
Ở đây mình chia sẽ ngắn gọn một xíu.
Dùng FP khi:
- Xử lý biến đổi dữ liệu (ETL, pipelines).
- Song song/đồng thời (hạn chế shared mutable state).
- Logic tính toán thuần cần dễ test và dễ tái sử dụng.
Dùng OOP khi:
- Mô hình hóa đối tượng giàu hành vi với vòng đời và trạng thái thay đổi (domain rich model).
- Tổ chức hệ thống theo thực thể (service, repository, controller) dễ quản trị.
Kết hợp cả hai:
- Functional Core (pure) + Imperative/OOP Shell (side effects, orchestration).
- Ví dụ: class OrderService (OOP) gọi các pure functions để tính toán, sau đó ghi DB (side effect).
Kinh nghiệm áp dụng FP
Mình là fullstack developer, mình có phát triển cả backend lẫn frontend.
Bạn hãy cố gắng tách biệt phần side effect nhiều nhất có thể.
Tập trung vào phần nghiệp vụ, những gì là bất biến, là pure thì tách ra hết. Ở đây chúng ta sẽ viết unit test cho nó rất dễ dàng. Điều này cực kì quan trọng, dễ maintain sau này và tránh việc gây bug.
Frontend
Ưu tiên sử dụng các core function của ngôn ngữ hổ trợ như map, reduce, filter, sort, .... Nhưng trong trường hợp mảng có quá nhiều phần tử, thì hãy xem xét sử dụng vòng lặp để giảm độ phức tạp của thuật toán.
Khi cần sử dụng side effect như call API để fetch dữ liệu, bạn có thể tách thành các service riêng lẽ. Ví dụ như bên dưới:
// Cô lập side effect lại và sử dụng nó trong chương trình chính
export const useGetLeave = (calendarId: number) => {
const endpoint = `calendar/${calendarId}/leave`;
return useQuery<LeaveInformationResponse, Error>({
queryKey: ['calendar.leave', calendarId],
queryFn: async () =>
(await Api.get<LeaveInformationResponse>(endpoint)).data,
gcTime: 0,
});
};
// Ở trong chương trình chính
const leave = useGetLeave(calendarId);Và những thứ như xử lý dữ liệu, logic, nghiệp vụ (không có side effect), nếu được hãy áp dụng FP để dễ cho người đọc (maintain) và dễ viết unit test.
Backend
Thông thường nếu các bạn áp dụng các concept của Domain-Driven Design thì các logic nghiệp vụ (business logic) sẽ thông thường tập trung ở các Aggregate, Entity, Value Object, các domain service, ... Vậy thì hãy áp dụng FP vào những nơi như vậy, vì chúng không có side effect. Và những nơi có side effect hãy để các layer phía ngoài đảm nhiệm như infrastructure layer.
- Tóm lại lõi nghiệp vụ, có thể sử dụng functional programming, cô lập chúng lại trong một layer duy nhất (domain layer), ở trong này có xuất hiện class (lúc này ko hoàn toàn là FP hay OOP nữa mà kết hợp OOP và FP), nhưng bên trong các method hoàn toàn là xử lý logic, nghiệp vụ. Layer này sẽ viết unit test để cover hoàn toàn phần nghiệp vụ. Hoặc đơn giản có thể tách thành các service "thuần" xử lý nghiệp vụ, tính toán dữ liệu, ... Lúc này chúng ta có thể tận dụng tối đa FP.
- Side effect, hay những thành phần khác thì hãy để các layer khác như infrastructure đảm nhiệm.
Nhược điểm và lưu ý để bạn tránh “over-FP”
- Hiệu năng: quá nhiều bản sao dữ liệu lớn → tốn bộ nhớ/thời gian.
- Đường cong học tập: currying, composition, HOF… có thể khó cho team chưa quen.
- Đệ quy: Javascript không TCO → dễ stack overflow.
- Khả năng đọc hiểu của dev: lạm dụng point-free style làm code “khó dò, khó đọc” hơn.
Lời khuyên: dùng FP pragmatically. Chọn những chỗ biến đổi dữ liệu và logic thuần để áp dụng trước; đo lường hiệu năng; giữ code minh bạch cho team. Không phải lúc nào cũng cố chấp sử dụng FP, hãy linh hoạt giữa FP và OOP.
KẾT
Functional Programming mang lại tính dự đoán, đơn giản hóa reasoning, và độ tin cậy cao trong các hệ thống hiện đại. Thay vì xem FP là “đối thủ” của OOP, hãy coi đó là một bộ kỹ năng bổ sung. Khi phối hợp đúng cách, bạn sẽ có code gọn – đúng – đẹp – dễ test.
Bài viết này, mình chỉ trình bày sơ bộ về functional programming. Và so sánh đôi chút nó với lập trình hướng đối tượng.
Đương nhiên chỉ với giới hạn một bài viết. Không thể cover hết tinh hoa của functional programming. Mình để những tinh hoa đó cho chúng ta cùng nhau research.
Functional programming đòi hỏi người luyện phải chuyên tâm, kiên trì. Không phải ngày một ngày hai mà có thể luyện thành.
Chúc các bạn thành công trên con đường tu luyện với Functional Programming!

