1. Giới thiệu
Lập trình hàm (FP) — là một paradigma lập trình, nơi đơn vị xây dựng chính — không phải object và không phải procedure/method, mà là hàm theo nghĩa toán học. Trong FP, chú ý chính là mô tả "cái gì cần tính", chứ không phải "làm sao để tính".
Bạn đã từng gặp một số ý tưởng FP khi làm việc với lambda expressions và LINQ. Vậy khác biệt nằm ở đâu? Trên thực tế: OOP mô tả các đối tượng và tương tác của chúng, lập trình thủ tục — một tập các bước, còn FP là composition của các hàm, truyền hành vi như một giá trị, từ chối thay đổi trạng thái (immutability) và tránh side effects.
Tại sao cần paradigma mới?
- Mã sạch hơn, dễ đoán hơn và dễ test hơn.
- Dễ hỗ trợ đa luồng hơn ("không có state — không có vấn đề").
- Súc tích và biểu đạt (code ít hơn — bug ít hơn).
- Các abstraction ở mức cao, dễ tái sử dụng.
Tượng tự
Hãy tưởng tượng nhà hàng nhận một order: "làm trứng ốp-la". Đầu bếp theo phong cách imperative thực hiện danh sách chỉ dẫn: lấy trứng, đập, đánh, rán. Đầu bếp theo phong cách chức năng nói: res = omlet(trung) — họ thao tác với các hàm, trừu tượng hóa trạng thái bếp (ừ, gần như vậy).
Trong C# ta có thể dùng cả hai cách. Điều này làm cho ngôn ngữ rất linh hoạt và mạnh mẽ — đặc biệt trong các dự án thực tế.
Những khái niệm then chốt của FP
1. Higher-order functions
Hàm có thể được truyền như tham số, trả về từ hàm khác và lưu trong biến. Bạn đã làm điều này với lambda expressions và delegates. Trong FP những "hàm trên hàm" này là nền tảng của mọi thứ.
2. Pure functions
Một hàm được coi là "pure" nếu kết quả của nó chỉ phụ thuộc vào tham số và nó không thay đổi gì bên ngoài (không có side effects). Hai lần gọi giống nhau với cùng tham số trả về cùng một kết quả.
3. Immutability
Dữ liệu không bị mutate "tại chỗ": trạng thái mới — là object mới. Điều này giúp suy luận về chương trình dễ hơn và hữu ích cho đa luồng.
4. Không có side effects
Hàm không ghi file, không thay đổi biến global, không vẽ ra màn hình — chỉ trả về kết quả. Trong đời thực side effects không thể tránh, nhưng ta cố cô lập chúng ở rìa hệ thống.
5. Function composition
Một hàm có thể được xây dựng từ các hàm khác, như ghép các khối. Ví dụ: lọc số dương, lấy bình phương và cộng lại. Mỗi thao tác là một hàm riêng, và chúng dễ kết hợp (Where → Select → Sum).
2. FP trong C#: từ lý thuyết đến thực hành
C# là ngôn ngữ đa-paradigm: nó hỗ trợ tốt OOP, procedural và phong cách functional mạnh mẽ (với lambdas, delegates, extension methods và LINQ).
Ta sẽ xem ví dụ trong ứng dụng học của chúng ta
Giả sử ta phát triển chương trình xử lý danh sách số và chuỗi. Nhiệm vụ là áp dụng các phép biến đổi theo phong cách functional lên dữ liệu này.
Ví dụ 1: Dùng higher-order functions
// Áp dụng action cho mọi phần tử của danh sách
public static void ForEach<T>(List<T> items, Action<T> action)
{
foreach (var item in items)
{
action(item);
}
}
Sử dụng:
var numbers = new List<int> { 1, 2, 3, 4, 5 };
ForEach(numbers, n => Console.WriteLine(n * n)); // Hàm làm tham số
Thấy không? Hàm có thể được "gói" vào biến hoặc truyền như giá trị — giống như đưa quả táo trong bếp!
Ví dụ 2: Pure function
Hàm không thay đổi trạng thái chương trình và chỉ phụ thuộc vào input:
int MultiplyByTwo(int x)
{
return x * 2;
}
- Không phụ thuộc gì bên ngoài.
- Không thay đổi gì bên ngoài.
- Với x = 5 luôn trả về 10.
So sánh với hàm dùng và thay đổi biến global:
int total = 0;
int AddToTotal(int x)
{
total += x;
return total;
}
Đây không còn là pure function — kết quả phụ thuộc vào state bên ngoài, và nó thay đổi state đó.
Ví dụ 3: Immutability của dữ liệu
Thay vì thay đổi dữ liệu đầu vào, ta tạo mới:
List<int> AddOneToEach(List<int> numbers)
{
return numbers.Select(n => n + 1).ToList();
}
Danh sách input không bị thay đổi. Trong chương trình đa luồng điều này rất tiện: ít lock và race condition hơn.
Ví dụ 4: Function composition
Lấy tổng các bình phương của số chẵn:
int SumOfEvenSquares(List<int> numbers)
{
return numbers
.Where(n => n % 2 == 0) // Giữ lại chỉ các số chẵn
.Select(n => n * n) // Lấy bình phương
.Sum(); // Cộng lại
}
Đọc được và mang tính khai báo: mỗi thao tác là một hàm riêng.
3. Những chi tiết hữu ích
FP, LINQ và C#
LINQ — gần như là "FP áp dụng thực tế" cho collections: bạn dùng higher-order functions (Where, Select, v.v.), nhận các sequence mới mà không mutate nguyên bản, và mỗi transform là một biểu thức riêng. Kết quả là IEnumerable<T>, mô tả cái gì cần lấy, chứ không phải cách lặp.
Bảng tương tự
| Theo lệnh (procedural/OOP) | Theo chức năng (LINQ/FP-style) |
|---|---|
|
|
| «Mutate» collection | Lấy collection mới |
| State (total += x) | Pure functions (xs.Sum()) |
| Mô tả theo kiểu: "làm cái này" | Mô tả: "cái chúng ta muốn nhận" |
FP vs OOP: hai thế giới — một C#
Không phải hai phe đối đầu. Trong dự án C# thực tế thường kết hợp: domain model tiện xây bằng classes (OOP), còn xử lý collections, aggregation và transforms thì dùng phong cách functional qua LINQ, lambdas và extension methods.
Kiến thức về delegates của bạn rất hữu dụng: Func<T, TResult>, Predicate<T>, Action<T> — là các building blocks điển hình cho phong cách FP.
Hàm filter tổng quát:
List<T> Filter<T>(List<T> items, Predicate<T> predicate)
{
var result = new List<T>();
foreach (var item in items)
{
if (predicate(item))
result.Add(item);
}
return result;
}
Gọi:
var adults = Filter(people, person => person.Age >= 18);
var bigFiles = Filter(fileNames, name => name.EndsWith(".mp4") && name.Length > 10);
Thay vì nhiều method gần giống nhau với điều kiện khác nhau — chỉ cần một hàm tổng quát.
Tại sao nhà tuyển dụng và phỏng vấn quan tâm đến FP?
- FP giúp test nhỏ các block code mà không cần dựng cả hệ thống.
- Dễ bảo trì logic: ít state — ít nguồn lỗi hơn.
- Dễ viết code song song và async — không có global state, ít race hơn.
Làm sao để không quá cuồng FP?
Đúng, FP mạnh mẽ. Nhưng C# không phải ngôn ngữ thuần functional, và không phải mọi task cần pure tuyệt đối. Đừng sợ dùng biến cục bộ và mutation có kiểm soát khi phù hợp. Quan trọng là readability, predictability và testability. Các element của FP là công cụ, không phải một giáo điều.
4. Lỗi điển hình của người mới
Rất dễ rơi vào cảm giác mã là functional trong khi thực tế không phải vậy.
Ví dụ, một hàm trả về collection mới nhưng bên trong vẫn mutate list gốc — điều này phá vỡ nguyên tắc immutability và làm hỏng kỳ vọng của caller.
Ví dụ khác: lambda truy cập biến bên ngoài và thay đổi nó. Trong paradigm functional đây là side effect và làm hành vi mã khó đoán hơn.
C# compiler sẽ không cản bạn: ngôn ngữ cho phép cả hai. Vì vậy trong thực hành FP cần chú ý để hàm "sống tự lập", không thay đổi bên ngoài và không đọc gì ngoài các tham số của nó.
GO TO FULL VERSION