CodeGym /Các khóa học /C# SELF /Ứng dụng thực tế của FP trong C#

Ứng dụng thực tế của FP trong C#

C# SELF
Mức độ, Bài học
Có sẵn

1. Giới thiệu

Từ lý thuyết đến thực hành, hợp lý nếu tự hỏi: «Tại sao mình, một developer trên .NET và C#, lại cần mấy kỹ thuật functional programming này?»
Thật ra, C# không phải là ngôn ngữ hoàn toàn functional như F# hay Haskell. Nhưng kể từ version 3.0 đến C# 14, nó có rất nhiều công cụ FP mà có thể đáng kể cải thiện chất lượng và biểu đạt của code.

Đây là những chỗ mà chúng hoạt động tốt:

  • Xử lý collection — LINQ, Map/Reduce, filter, aggregate, sort và các “phép thuật” với data.
  • Pure functions — ít bug do state và side-effects, dễ debug hơn.
  • Higher-order functions — components generic, tái sử dụng được, thân thiện khi làm việc.
  • Immutability trong multithreading — một trong những yếu tố chính để code an toàn trong parallel và async scenarios.
  • Function composition — làm cho business logic phức tạp trở nên concise, readable và testable.

Bảng: So sánh OOP-approach và FP-approach trong C#

Nhiệm vụ Imperative (OOP/old school) Functional (FP)
Filter list
foreach + if + Add
.Where(predicate)
Transform list
foreach + tính toán + Add
.Select(lambda)
Find by criteria
foreach + if/return
.FirstOrDefault(predicate)
Aggregate
vòng lặp với biến counter
.Aggregate(seed, func)
Caching
dictionary thủ công + kiểm tra
function với closure

2. LINQ: phần FP nhất trong C#

Nếu bạn nghĩ “functional programming” chỉ là về list, filter và mấy .Where, .Select, .Aggregate, — chúc mừng: đúng vậy! LINQ là tinh túy của FP trong C#.

Nhớ lại cách LINQ hoạt động

LINQ thao tác trên collections bằng chuỗi các method, nhận function làm parameter (ví dụ lambdas). Ví dụ:

var numbers = new List<int> { 1, 2, 3, 4, 5 };

// Lấy các số chẵn và nhân đôi chúng
var result = numbers
    .Where(x => x % 2 == 0)
    .Select(x => x * 2);

foreach (var number in result)
    Console.WriteLine(number);

Chuyện gì đang xảy ra?

  • .Where — một higher-order function: nhận một function (x => x % 2 == 0) và trả về một collection khác.
  • .Select — cũng nhận một function (x => x * 2).
  • Chúng ta không thay đổi collection gốc, mà tạo kết quả mới.

Style này dễ đọc và mở rộng (có thể thêm .OrderBy, .Take, .Distinct, v.v.).

Lưu ý! Lambda expressions là cách tiện để tạo delegate “on the fly”. LINQ sẽ không khả thi nếu thiếu support FP trong C#.

Sơ đồ: Xử lý function trên collection


Collection --> Where(x => bool) --> Select(x => y) --> Kết quả mới

3. Function composition và data pipeline

FP thường dùng composition: một operation phức tạp được xây từ chuỗi các hàm nhỏ, mỗi hàm làm việc của nó.

Ví dụ: pipeline xử lý chuỗi

Style thay đổi state (OOP):

string s = "   hello world   ";
s = s.Trim();
s = s.ToUpper();
s = s + "!";
Console.WriteLine(s); // HELLO WORLD!

Lại theo cách “functional” hơn — pipeline của các hàm:

Func<string, string> trim = x => x.Trim();
Func<string, string> upper = x => x.ToUpper();
Func<string, string> addBang = x => x + "!";

// Function composition — áp dụng tuần tự
Func<string, string> pipeline = x => addBang(upper(trim(x)));

Console.WriteLine(pipeline("   hello world   ")); // HELLO WORLD!

Combinator đơn giản để compose:

Func<T, R> Compose<T, U, R>(Func<T, U> f, Func<U, R> g) =>
    x => g(f(x));

// Bây giờ pipeline qua Compose:
var pipeline2 = Compose(trim, upper);
pipeline2 = Compose(pipeline2, addBang);

Console.WriteLine(pipeline2("   hello again    ")); // HELLO AGAIN!

4. Làm việc với immutability: bảo vệ khỏi bug

Immutability là viên gạch cơ bản của FP. Ta không mutate data structures mà trả về cái mới. Điều này rất quan trọng trong multithreaded apps.

Ví dụ: “sai” (mutable)

List<int> numbers = new List<int> { 1, 2, 3 };
numbers[0] = 42;

Ví dụ: “đúng” (functional)

var numbers = new List<int> { 1, 2, 3 };
var newNumbers = numbers.Select((x, i) => i == 0 ? 42 : x).ToList();

Trong C# hiện đại có collection ImmutableList<T> và các type khác trong namespace System.Collections.Immutable:

using System.Collections.Immutable;

var immutableNumbers = ImmutableList.Create(1, 2, 3);
var changed = immutableNumbers.SetItem(0, 42); // Trả về list mới!

5. Higher-order functions trong thực tế

Higher-order functions là cách viết các component generic mà không cần nhiều conditional code.

Ví dụ: Bộ lọc users chung

class User
{
    public string Name { get; set; }
    public int Age { get; set; }
}

var users = new List<User>
{
    new User { Name = "Vasya", Age = 26 },
    new User { Name = "Katya", Age = 17 },
    new User { Name = "Lyosha", Age = 35 }
};
List<User> FilterUsers(List<User> source, Predicate<User> predicate)
{
    return source.Where(u => predicate(u)).ToList();
}

// Sử dụng:
var adults = FilterUsers(users, u => u.Age >= 18);
var longNames = FilterUsers(users, u => u.Name.Length > 3);

6. Pattern matching và switch-expressions

C# hiện đại dùng pattern matching tích cực: switch-expression thường thay cho các chuỗi if nặng nề.

object value = 123;

string description = value switch
{
    int i when i > 100 => "So lon",
    string s when s.Length > 3 => "Chuoi dai",
    null => "Gia tri rong",
    _ => "Khong biet"
};

Console.WriteLine(description); // So lon

7. Memoization: caching ket qua ham

Memoization — cache kết quả của hàm cho cùng arguments. Trong C# có thể implement tay rất dễ.

Func<int, int> SlowFib = null; // Ham Fibonacci đệ quy

var cache = new Dictionary<int, int>();

SlowFib = n =>
{
    if (cache.ContainsKey(n))
        return cache[n];
    if (n <= 1)
        cache[n] = n;
    else
        cache[n] = SlowFib(n - 1) + SlowFib(n - 2);
    return cache[n];
};

Console.WriteLine(SlowFib(40)); // Nhanh như chớp!

8. Currying và partial application

Partial application — cố định một phần arguments của hàm. Trong C# tiện làm bằng lambdas.

Func<int, int, int> add = (a, b) => a + b;

// Cố định argument đầu
Func<int, int> add10 = b => add(10, b);

Console.WriteLine(add10(5));   // 15
Console.WriteLine(add10(100)); // 110

9. Declarative style với functions

Imperative:

var result = new List<int>();
foreach (var n in numbers)
{
    if (n > 0)
        result.Add(n * n);
}

Declarative:

var result = numbers
    .Where(n => n > 0)
    .Select(n => n * n)
    .ToList();

10. Bài tập thực hành

Triển khai module lọc tasks trong “task manager cho sinh viên”.

Model:

class StudentTask
{
    public string Title { get; set; }
    public bool IsCompleted { get; set; }
    public int Priority { get; set; }
}

Dữ liệu ban đầu:

var tasks = new List<StudentTask>
{
    new StudentTask { Title = "Làm bài tập về nhà", IsCompleted = false, Priority = 2 },
    new StudentTask { Title = "Uống cà phê", IsCompleted = true, Priority = 3 },
    new StudentTask { Title = "Xem bài giảng", IsCompleted = false, Priority = 1 }
};

Bộ lọc chung:

List<StudentTask> FilterTasks(
    List<StudentTask> all,
    Predicate<StudentTask> predicate)
{
    return all.Where(t => predicate(t)).ToList();
}

// Tìm tasks chưa hoàn thành với priority > 1
var importantTasks = FilterTasks(tasks, t => !t.IsCompleted && t.Priority > 1);

// In kết quả
foreach (var task in importantTasks)
    Console.WriteLine(task.Title);

Combinators cho predicates:

Predicate<StudentTask> IsActive = t => !t.IsCompleted;
Predicate<StudentTask> IsHighPriority = t => t.Priority > 1;

// Kết hợp nhiều criteria, phương án 1
var specialTasks = FilterTasks(tasks, t => IsActive(t) && IsHighPriority(t));

// Phương án 2: function-combinator của hai predicate
Predicate<StudentTask> And(Predicate<StudentTask> a, Predicate<StudentTask> b) => t => a(t) && b(t);

var specialTasks2 = FilterTasks(tasks, And(IsActive, IsHighPriority));

11. Điểm cần chú ý và lỗi thường gặp khi dùng FP trong C#

Thứ nhất, nhớ rằng C# là strongly-typed. Đôi khi cần chỉ rõ types, đặc biệt khi functions trả về delegates hoặc lambdas phức tạp. Nếu không, compiler có thể báo lỗi do type inference không đủ.

Thứ hai, đừng truyền function có side-effects vào chỗ mong đợi pure functions. Thay đổi biến ngoài scope phá vỡ tính predictability. Cố gắng để functions không mutate state ngoài phạm vi của chúng.

Thứ ba, chú ý variable capturing trong closure, đặc biệt với async và multithreaded code. Biến bị thay đổi sau khi truyền vào lambda (ví dụ trong LINQ) có thể dẫn tới bug khó ngờ.

Cuối cùng, FP quá mức có thể làm code khó maintain cho team không quen. Dùng FP khi nó thực sự đơn giản hóa giải pháp, đừng chỉ vì “đẹp”.

2
Nhiệm vụ
C# SELF, mức độ, bài học
Đã khóa
Xử lý chuỗi với composition của các hàm
Xử lý chuỗi với composition của các hàm
1
Khảo sát/đố vui
, cấp độ , bài học
Không có sẵn
Lập trình hàm
Giới thiệu về lập trình hàm
Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION