blog.dopana

Back

Bài viết thứ 14 trong series Rust trung cấp. Sau khi đã thành thạo smart pointers, chúng ta sẽ chuyển sang những tính năng lập trình hàm của Rust. Rust rút mượn nhiều khái niệm từ các ngôn ngữ lập trình hàm: nó cung cấp closures (hàm ẩn danh) và iterators (chuỗi giá trị lười biếng, có thể kết hợp). Lời hứa chính là zero-cost abstractions — những cấu trúc cấp cao này biên dịch xuống cùng mã máy với vòng lặp viết tay.

graph TD
    A["Lập Trình Hàm Rust"] --> B["Closures<br/>Fn, FnMut, FnOnce"]
    A --> C["Iterators<br/>Đánh giá lười biếng"]
    A --> D["Zero-Cost Abstractions"]
    A --> E["map, filter, fold"]

Closures: Hàm Ẩn Danh#

Một closure là một hàm ẩn danh có thể bắt giữ môi trường xung quanh nó. Không giống như các hàm thông thường, closures có thể “đóng” (close over) các biến từ phạm vi bao quanh.

Cú Pháp Cơ Bản#

fn main() {
    let add_one = |x: i32| x + 1;
    println!("Kết quả: {}", add_one(5)); // Kết quả: 6

    // Closures có thể bắt giữ môi trường
    let multiplier = 3;
    let multiply = |x: i32| x * multiplier;
    println!("Kết quả: {}", multiply(5)); // Kết quả: 15
}
rust

Cú pháp |x: i32| định nghĩa danh sách tham số, tương tự như closures ở các ngôn ngữ khác.

Bắt Giữ Môi Trường#

Closures bắt giữ các biến từ môi trường xung quanh theo ba cách:

  1. Fn — bắt giữ bằng tham chiếu (&T)
  2. FnMut — bắt giữ bằng tham chiếu thay đổi (&mut T)
  3. FnOnce — bắt giữ bằng giá trị (chuyển quyền sở hữu)

[!NOTE] Một closure cài đặt FnOnce cũng có thể được sử dụng như FnMut hoặc Fn, nhưng ngược lại thì không. Compiler sẽ tự động suy luận trait ít hạn chế nhất cần thiết.

Closures Làm Đối Số Của Hàm#

Closures thực sự mạnh mẽ khi được truyền làm đối số cho các hàm bậc cao:

fn apply_to_list<T>(list: &[T], f: impl Fn(&T)) {
    for item in list {
        f(item);
    }
}

fn main() {
    let numbers = vec![1, 2, 3, 4, 5];
    apply_to_list(&numbers, |x| println!("Số: {x}"));
}
rust

Closures Là Giá Trị Trả Về#

Bạn cũng có thể trả về closures từ một hàm bằng cách sử dụng impl Fn:

fn make_multiplier(factor: i32) -> impl Fn(i32) -> i32 {
    |x| x * factor
}

fn main() {
    let double = make_multiplier(2);
    let triple = make_multiplier(3);

    println!("Gấp đôi 5: {}", double(5)); // Gấp đôi 5: 10
    println!("Gấp ba 5: {}", triple(5)); // Gấp ba 5: 15
}
rust

Iterators: Chuỗi Giá Trị Lười Biếng#

Một iterator là một cấu trúc sản sinh ra một chuỗi giá trị. Trong Rust, iterators là lười biếng — chúng không làm gì cho đến khi bạn gọi một phương thức tiêu thụ như collect() hoặc for.

Tạo Iterators#

Mỗi collection trong Rust đều có phương thức .iter() trả về một iterator:

fn main() {
    let numbers = vec![1, 2, 3, 4, 5];

    // .iter() trả về một iterator
    let iter = numbers.iter();

    // Iterators là lười biếng — chưa có gì xảy ra
    for num in iter {
        println!("{num}");
    }
}
rust

Các Bộ Điều Hợp Iterator#

Các bộ điều hợp (adapter) nhận một iterator và trả về một iterator mới. Các bộ điều hợp phổ biến nhất là map, filterfold:

fn main() {
    let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

    let result: Vec<i32> = numbers
        .iter()
        .filter(|&&x| x % 2 == 0)   // giữ lại số chẵn
        .map(|&x| x * 2)           // nhân đôi
        .collect();                // thu thập vào Vec

    println!("{:?}", result); // [4, 8, 12, 16, 20]
}
rust
graph LR
    A["[1,2,3,4,5,6,7,8,9,10]"] --> B["filter: chẵn"]
    B --> C["[2,4,6,8,10]"]
    C --> D["map: x2"]
    D --> E["[4,8,12,16,20]"]

Phương Thức fold#

fold là phiên bản hàm của vòng lặp for với bộ tích lũy:

fn main() {
    let numbers = vec![1, 2, 3, 4, 5];

    // Tính tổng
    let sum: i32 = numbers.iter().fold(0, |acc, &x| acc + x);
    println!("Tổng: {sum}"); // Tổng: 15

    // Tìm giá trị lớn nhất
    let max = numbers.iter().fold(0, |acc, &x| if x > acc { x } else { acc });
    println!("Max: {max}"); // Max: 5
}
rust

Tạo Iterator Tùy Chỉnh Với impl Iterator#

Bạn có thể tạo iterator riêng bằng cách cài đặt trait Iterator:

Zero-Cost Abstractions#

Điểm nhấn của các tính năng lập trình hàm trong Rust là chúng biên dịch xuống cùng mã máy với vòng lặp viết tay. Hãy chứng minh:

// Phong cách hàm
fn functional_sum(numbers: &[i32]) -> i32 {
    numbers.iter().filter(|&&x| x > &0).map(|&x| x * 2).sum()
}

// Phong cách thủ tục
fn imperative_sum(numbers: &[i32]) -> i32 {
    let mut sum = 0;
    for &x in numbers {
        if x > 0 {
            sum += x * 2;
        }
    }
    sum
}
rust

Cả hai hàm đều tạo ra mã lắp ráp (assembly) giống hệt nhau. Compiler tối ưu toàn bộ chuỗi iterator.

[!TIP] Iterators của Rust là ví dụ điển hình về zero-cost abstractions. Bạn được sở hữu sự biểu cảm của lập trình hàm với hiệu suất của C tối ưu thủ công.

Ví Dụ Trực Quan (ELI5): Dây Chuyền Sản Xuất#

Hãy tưởng tượng iterators như một dây chuyền sản xuất trong nhà máy:

  • Nguyên liệu (collection gốc) sẽ đến ở đầu dây chuyền.
  • Mỗi bộ điều hợp (map, filter, v.v.) là một trạm làm việc biến đổi hoặc loại bỏ các mặt hàng.
  • Băng tải (iterator) là lười biếng — các mặt hàng chỉ di chuyển khi một công nhân ở cuối kéo chúng.
  • Bộ tiêu thụ cuối cùng (collect, sum, for) là công nhân lấy sản phẩm hoàn thiện từ băng tải.

Vì băng tải là lười biếng, nếu công nhân chỉ cần 3 mặt hàng, nhà máy chỉ xử lý 3 — nó không lãng phí năng lượng xử lý toàn bộ lô hàng!

Tổng Kết#

  • Closures là hàm ẩn danh có thể bắt giữ môi trường, với ba chế độ bắt giữ: Fn, FnMut, và FnOnce.
  • Iterators là chuỗi giá trị lười biếng sản sinh giá trị khi cần, với các bộ điều hợp mạnh mẽ như map, filter, và fold.
  • Zero-cost abstractions đồng nghĩa với việc code phong cách hàm biên dịch thành cùng mã máy với vòng lặp thủ tục.
  • Các iterator tùy chỉnh có thể được tạo bằng cách cài đặt trait Iterator.

Trong bài viết tiếp theo, chúng ta sẽ đi sâu vào Fearless Concurrency — cách tiếp cận lập trình đồng thời an toàn của Rust với threads, channels và trạng thái chia sẻ.

Tài liệu tham khảo#