Danh sách bài viết

Bài 206: Adapter: map, filter, take, skip

Bài 206 của series Rust Cơ Bản — bộ tứ adapter map, filter, take, skip. Bài 204 giới thiệu lazy evaluation và bài 205 đi qua nhóm consumer. Bài này quay lại nhóm còn lại — adapter, những method trả về một iterator mới thay vì một value cuối cùng. Bốn adapter phổ biến nhất là map (transform từng element, có thể đổi kiểu Item), filter (giữ element thoả predicate, closure nhận reference nên hay thấy pattern |&x|), take(n) (giới hạn n phần tử đầu — bắt buộc với infinite iterator để chain có thể kết thúc), và skip(n) (bỏ n phần tử đầu, trả về phần còn lại). Bài cũng đi qua hai biến thể predicate-based take_while / skip_while (dừng hoặc vượt qua theo điều kiện, có short-circuit) và step_by(n) (lấy mỗi phần tử thứ n). Tất cả đều lazy — chỉ tạo struct mô tả công việc, chưa làm gì cho đến khi có consumer (collect, sum, count...) trigger ở cuối chain. Cuối bài là idiom Rust kinh điển: v.iter().filter(...).map(...).take(N).collect() — pipeline đọc trái sang phải, ghép nhiều adapter rồi đóng bằng đúng một consumer.

09/06/2026
11 phút đọc
1 lượt xem
1

Mục Tiêu Bài Học

Sau bài học, bạn sẽ:

  • Hiểu khái niệm adapter: method của Iterator trait trả về một iterator mới (kiểu wrapper như Map, Filter, Take, Skip) — lazy, chain được, và không trigger evaluation; cần một consumer ở cuối chain mới thực sự chạy.
  • Dùng .map(|x| ...) để biến đổi từng element bằng closure — biết rằng kiểu của Item có thể thay đổi sau khi map (ví dụ &i32String).
  • Dùng .filter(|&x| pred) để giữ lại element thoả predicate — hiểu vì sao closure nhận reference tới Item và pattern |&x| chỉ là cú pháp destructure copy thuận tiện cho kiểu Copy.
  • Dùng .take(n) để giới hạn n phần tử đầu — đặc biệt quan trọng để chain trên iterator vô hạn vẫn kết thúc; và .skip(n) để bỏ qua n phần tử đầu và lấy phần còn lại.
  • Dùng .take_while(pred) / .skip_while(pred) — biến thể predicate-based: dừng khi pred fail, hoặc skip cho đến khi pred fail; cả hai đều short-circuit.
  • Dùng .step_by(n) để lấy mỗi phần tử thứ n(0..10).step_by(2) ra 0,2,4,6,8.
  • Đọc và viết được idiom Rust điển hình v.iter().filter(...).map(...).take(10).collect() — chain nhiều adapter rồi đóng bằng đúng một consumer.

Bài này kế thừa Bài 204 (lazy evaluation) và Bài 205 (consumer). Bài 207 sẽ tiếp tục với hai adapter "ghép cặp" — enumerate đánh số thứ tự và zip ghép song song hai iterator.

2

Adapter Là Gì

Adapter (tiếng Việt hay gọi là method chuyển đổi) là phương thức của Iterator trait trả về một iterator mới — thường là một struct wrapper có tên trùng với method viết hoa: map trả Map, filter trả Filter, take trả Take, skip trả Skip. Mỗi struct này tự implement Iterator nên có thể gọi tiếp .x().y().z(), tạo ra chain dài tuỳ ý.

Đặc tính quan trọng nhất của adapter là lazy — gọi xong không làm gì cả, chỉ ghi nhớ "công việc cần làm". v.iter().map(|x| { println!("{x}"); x * 2 }) không in ra dòng nào, mặc dù trông như đã chạy closure. Lý do: chưa có ai gọi .next() trên iterator kết quả, nên closure không bị invoke. Chỉ khi có một consumer ở cuối chain (.collect(), .sum(), .for_each(), vòng for, ...) thì runtime mới thực sự "kéo" element đi qua các tầng adapter.

Cách nhận biết adapter khi đọc doc của Iterator: nhìn return type. Nếu trả impl Iterator<Item = ...> hoặc một wrapper iterator như Map, Filter, Take, Skip, StepBy — đó là adapter. Nếu trả về kiểu khác (usize, Option<T>, Vec<T>, (), ...) — đó là consumer. Bài này đi qua sáu adapter cốt lõi: map, filter, take, skip, take_while/skip_while, và step_by.

3

.map(|x| ...) Transform

.map áp một closure lên từng element và trả về iterator yield kết quả closure. Signature đơn giản: fn map<B, F: FnMut(Self::Item) -> B>(self, f: F) -> Map<Self, F>. Điều đáng chú ý: kiểu B không nhất thiết trùng Self::Item — closure có thể đổi kiểu, ví dụ map từ &i32 sang String, hay từ (u32, u32) sang u32.

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

    // Map giữ kiểu — i32 → i32
    let doubled: Vec<i32> = v.iter().map(|x| x * 2).collect();
    println!("{doubled:?}"); // [2, 4, 6, 8, 10]

    // Map đổi kiểu — i32 → String
    let labels: Vec<String> = v.iter().map(|x| format!("item-{x}")).collect();
    println!("{labels:?}"); // ["item-1", "item-2", ...]

    // Map trên tuple iter
    let rects = [(3, 4), (5, 2), (6, 6)];
    let areas: Vec<i32> = rects.iter().map(|(w, h)| w * h).collect();
    println!("{areas:?}"); // [12, 10, 36]
}

Lưu ý closure nhận Self::Item trực tiếp — không phải reference. Trên v.iter(), Item = &i32 nên closure nhận &i32; trên v.into_iter(), Item = i32 nên closure nhận i32 sở hữu. Khác biệt này quan trọng khi map các kiểu không Copyiter() trả ref (closure phải clone nếu muốn sở hữu), into_iter() trả value (consume Vec gốc nhưng dùng thoải mái).

4

.filter(|&x| pred) Lọc

.filter giữ lại element thoả mãn predicate, bỏ qua element còn lại. Signature: fn filter<P: FnMut(&Self::Item) -> bool>(self, p: P) -> Filter<Self, P>. Điểm khác biệt so với map: closure nhận reference tới Item chứ không nhận Item trực tiếp — kể cả khi Item đã là &T rồi, closure vẫn nhận &&T.

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

    // Pattern thông dụng — destructure &x cho kiểu Copy
    let evens: Vec<i32> = v.iter().filter(|&&x| x % 2 == 0).copied().collect();
    println!("{evens:?}"); // [2, 4, 6, 8]

    // Hoặc deref bằng tay
    let odds: Vec<i32> = v.iter().filter(|x| *x % 2 == 1).copied().collect();
    println!("{odds:?}"); // [1, 3, 5, 7]

    // Trên into_iter — Item = i32 nên filter nhận &i32
    let big: Vec<i32> = v.into_iter().filter(|&x| x > 4).collect();
    println!("{big:?}"); // [5, 6, 7, 8]
}

Hai pattern |&x||x| đều dùng được — chỉ là cú pháp destructure. Với kiểu Copy (int, char, bool, ...), |&x| auto-copy giá trị ra biến x tiện viết tiếp x % 2; với kiểu không Copy (String, Vec) thì giữ |x| rồi deref khi cần — nếu destructure copy sẽ lỗi move out of reference.

Quan sát code trên: sau filter thường thấy .copied() hoặc .cloned() rồi .collect(). Nguyên do: filter không đổi kiểu Item — đầu vào là &i32 thì đầu ra cũng &i32. Muốn collect thành Vec<i32> cần một bước copy/clone để có giá trị sở hữu.

5

.take(n) Lấy n Đầu

.take(n) giới hạn iterator ở n phần tử đầu — sau khi yield đủ n element thì trả None bất kể underlying còn hay không. Nếu underlying ngắn hơn n, take dừng sớm theo underlying — không panic.

fn main() {
    let v = vec![10, 20, 30, 40, 50];

    // Lấy 3 đầu
    let first3: Vec<i32> = v.iter().take(3).copied().collect();
    println!("{first3:?}"); // [10, 20, 30]

    // take(n) lớn hơn length — chỉ trả những gì có
    let all: Vec<i32> = v.iter().take(100).copied().collect();
    println!("{all:?}"); // [10, 20, 30, 40, 50]

    // Use case quan trọng nhất: chặn infinite iterator
    let first5_squares: Vec<u64> = (1u64..)            // 1, 2, 3, ... vô hạn
        .map(|n| n * n)
        .take(5)
        .collect();
    println!("{first5_squares:?}"); // [1, 4, 9, 16, 25]
}

Use case "vàng" của take là chặn infinite iterator. Range không có cận trên (1..) là infinite — gọi collect() trực tiếp sẽ chạy mãi và ngốn hết RAM. Đặt .take(n) vào giữa biến cái vô hạn thành hữu hạn, đó là idiom rất phổ biến khi generate dữ liệu mẫu, sinh test fixture, hoặc lấy top-N của một stream.

6

.skip(n) Bỏ n Đầu

.skip(n) đối xứng với take — bỏ qua n phần tử đầu, trả về phần còn lại. Nếu underlying ngắn hơn n, kết quả là iterator rỗng (cũng không panic). Khi đến lượt phần còn lại được kéo bởi consumer, skip sẽ tiêu n lần .next() trong nội bộ để "vượt qua" rồi mới yield element thứ n+1.

fn main() {
    let v = vec![10, 20, 30, 40, 50];

    // Bỏ 2 đầu, lấy phần còn lại
    let rest: Vec<i32> = v.iter().skip(2).copied().collect();
    println!("{rest:?}"); // [30, 40, 50]

    // Bỏ nhiều hơn length — iterator rỗng
    let none: Vec<i32> = v.iter().skip(100).copied().collect();
    println!("{none:?}"); // []

    // Combine skip + take = "trang" dữ liệu (paging)
    let all = (1..=20).collect::<Vec<_>>();
    let page_size = 5;
    let page_index = 2; // page 3 (0-indexed)
    let page: Vec<i32> = all.iter().skip(page_index * page_size).take(page_size).copied().collect();
    println!("{page:?}"); // [11, 12, 13, 14, 15]
}

Cặp .skip(offset).take(size) là idiom paging cổ điển — đọc trái sang phải đúng nghĩa "bỏ offset, lấy size phần tử". Lưu ý skip trên slice không phải O(1) như indexing trực tiếp &v[offset..]; vẫn phải kéo lần lượt n phần tử. Nếu n lớn và underlying là Vec thì dùng slice indexing nhanh hơn.

7

.take_while / .skip_while

Hai biến thể predicate-based của take/skip: take_while(pred) lấy element cho đến khi pred trả false rồi dừng; skip_while(pred) bỏ qua element cho đến khi pred trả false thì bắt đầu lấy phần còn lại. Cả hai đều short-circuit — không duyệt hết iterator, không gọi predicate lại sau khi đã chuyển trạng thái.

fn main() {
    let v = vec![2, 4, 6, 7, 8, 10, 12];

    // take_while: lấy cho đến khi gặp số lẻ
    let lead_evens: Vec<i32> = v.iter().take_while(|&&x| x % 2 == 0).copied().collect();
    println!("{lead_evens:?}"); // [2, 4, 6]  -- dừng ở 7, KHÔNG có 8, 10, 12

    // skip_while: bỏ phần đầu chẵn, lấy từ số lẻ trở đi
    let from_odd: Vec<i32> = v.iter().skip_while(|&&x| x % 2 == 0).copied().collect();
    println!("{from_odd:?}"); // [7, 8, 10, 12]  -- giữ NGUYÊN 8, 10, 12 dù lại chẵn

    // Khác biệt với filter: filter duyệt toàn bộ, gọi pred mọi element
    let evens: Vec<i32> = v.iter().filter(|&&x| x % 2 == 0).copied().collect();
    println!("{evens:?}"); // [2, 4, 6, 8, 10, 12]
}

So sánh ba pattern trên cùng input: filter giữ mọi số chẵn (kể cả sau số lẻ); take_while chỉ giữ prefix chẵn rồi dừng; skip_while chỉ bỏ prefix chẵn rồi giữ tất cả còn lại. Ba thứ rất hay nhầm — nhớ kỹ "while" mang nghĩa "trong khi điều kiện còn đúng" với prefix duy nhất.

Short-circuit là điểm vượt trội của take_while: trên iterator dài hoặc tốn kém, dừng sớm tiết kiệm rất nhiều cost. (1..).take_while(|&n| n < 100) chỉ kéo 99 element rồi dừng — vẫn safe trên infinite range.

8

.step_by(n) Lấy Mỗi n

.step_by(n) lấy element đầu (index 0), rồi mỗi n phần tử lấy một — bỏ qua n-1 phần tử giữa. Yêu cầu n > 0 (truyền 0 sẽ panic). Hữu ích khi cần subsample đều, hoặc duyệt theo block size cố định.

fn main() {
    // Range step_by — pattern hay nhất
    let even_idx: Vec<i32> = (0..10).step_by(2).collect();
    println!("{even_idx:?}"); // [0, 2, 4, 6, 8]

    let every3: Vec<i32> = (0..20).step_by(3).collect();
    println!("{every3:?}"); // [0, 3, 6, 9, 12, 15, 18]

    // step_by(1) tương đương không step gì
    let same: Vec<i32> = (1..=5).step_by(1).collect();
    println!("{same:?}"); // [1, 2, 3, 4, 5]

    // Trên Vec: subsample mỗi 3 dòng log
    let logs = vec!["a", "b", "c", "d", "e", "f", "g", "h", "i"];
    let sample: Vec<&&str> = logs.iter().step_by(3).collect();
    println!("{sample:?}"); // ["a", "d", "g"]
}

Khác với (0..10).filter(|x| x % 2 == 0) — kết quả giống nhau ([0,2,4,6,8]) nhưng step_by không gọi closure nào, hiệu năng tốt hơn cho range. filter linh hoạt hơn (predicate tuỳ ý) nhưng phải duyệt mọi element. Với bước cố định nên ưu tiên step_by; với điều kiện phức tạp dùng filter.

9

Combine Chain

Bộ tứ adapter chỉ thực sự phát huy khi được chain. Idiom Rust kinh điển: .iter() mở chain → một loạt adapter biến đổi → một consumer ở cuối. Đọc trái sang phải đúng nghĩa pipeline; compiler monomorphize và inline mạnh nên hiệu năng tương đương loop viết tay — không có Vec trung gian.

fn main() {
    let nums: Vec<i32> = (1..=100).collect();

    // Lấy 10 bình phương đầu của các số chia hết cho 3
    let result: Vec<i32> = nums.iter()
        .filter(|&&x| x % 3 == 0)
        .map(|x| x * x)
        .take(10)
        .collect();
    println!("{result:?}");
    // [9, 36, 81, 144, 225, 324, 441, 576, 729, 900]

    // Skip 5 đầu, lấy 5 tiếp, cộng tổng
    let sum: i32 = nums.iter().skip(5).take(5).sum();
    println!("{sum}"); // 6+7+8+9+10 = 40

    // step_by + take_while + map + collect
    let labels: Vec<String> = (0u32..)
        .step_by(2)
        .take_while(|&n| n < 12)
        .map(|n| format!("even-{n}"))
        .collect();
    println!("{labels:?}");
    // ["even-0", "even-2", "even-4", "even-6", "even-8", "even-10"]

    // Pattern thông dụng: top-N từ stream
    let words = ["hi", "hello", "rust", "iterator", "fn"];
    let short: Vec<&&str> = words.iter()
        .filter(|w| w.len() <= 5)
        .take(3)
        .collect();
    println!("{short:?}"); // ["hi", "hello", "rust"]
}

Mẹo đọc chain dài: tách dòng cho mỗi method, mỗi dòng một adapter, dòng cuối là consumer. Khi đọc lại sau, mắt quét dọc thấy ngay pipeline gồm các bước nào. Nếu chain dài quá 5-6 bước, cân nhắc tách thành biến trung gian để đặt tên cho từng phase — không cần ép phải one-liner.

Một lưu ý nhỏ về thứ tự: .filter().take(N) khác với .take(N).filter(). Pattern đầu: filter trước rồi lấy N cái thoả mãn; pattern sau: lấy N đầu rồi mới filter (kết quả có thể ít hơn N). Khi cần "N đầu tiên thoả mãn điều kiện" thì luôn dùng filter trước take.

10

Tổng Kết

  • Adapter là method của Iterator trả về iterator mới — lazy, chain được, không trigger evaluation; cần một consumer ở cuối để chạy.
  • map(|x| ...) transform từng element, có thể đổi kiểu Item (closure nhận Self::Item trực tiếp).
  • filter(|&x| pred) giữ element thoả predicate — closure nhận reference, pattern |&x| destructure copy cho kiểu Copy.
  • take(n) giới hạn n phần tử đầu — bắt buộc với infinite iterator; skip(n) bỏ n đầu, trả phần còn lại.
  • take_while / skip_while dùng predicate cho prefix duy nhất, short-circuit; khác filter ở chỗ chỉ áp lên prefix chứ không duyệt toàn bộ.
  • step_by(n) lấy mỗi phần tử thứ n, hiệu năng tốt hơn filter khi bước cố định.
  • Idiom Rust: .iter().filter(...).map(...).take(N).collect() — chain adapter rồi đóng bằng đúng một consumer.
11

Bài Tập Củng Cố

Tự trả lời, đáp án ở cuối:

  1. Cho let v = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];, viết chain trả về Vec<i32> chứa 3 bình phương đầu của các số chia hết cho 2.
  2. Viết một dòng generate 10 số đầu tiên của dãy 1, 4, 9, 16, ... (bình phương) từ một range vô hạn, collect ra Vec<u64>.
  3. Cho let v = vec![2, 4, 6, 5, 8, 10];. Khác biệt giữa v.iter().filter(|&&x| x % 2 == 0).count()v.iter().take_while(|&&x| x % 2 == 0).count() là gì? Tại sao?
  4. Viết hàm paginate(items: &[i32], page: usize, size: usize) -> Vec<i32> trả về trang thứ page (0-indexed) với size phần tử mỗi trang, dùng skip + take.
  5. Dùng step_by trên (0..30) để lấy [0, 5, 10, 15, 20, 25]. Cùng kết quả nếu thay step_by bằng filter thì viết thế nào?
Đáp án
  1. v.iter().filter(|&&x| x % 2 == 0).map(|x| x * x).take(3).collect::<Vec<_>>() — kết quả [4, 16, 36].
  2. (1u64..).map(|n| n * n).take(10).collect::<Vec<_>>() — kết quả [1, 4, 9, 16, 25, 36, 49, 64, 81, 100].
  3. filter trả 5 (gồm 2, 4, 6, 8, 10 — duyệt toàn bộ); take_while trả 3 (gồm 2, 4, 6 — dừng ở 5 vì pred fail, không xét 8, 10). Khác biệt: filter áp predicate lên mọi element; take_while chỉ áp lên prefix liền nhau, dừng ngay khi pred lần đầu fail.
  4. fn paginate(items: &[i32], page: usize, size: usize) -> Vec<i32> { items.iter().skip(page * size).take(size).copied().collect() }.
  5. (0..30).step_by(5).collect::<Vec<_>>(). Phiên bản filter: (0..30).filter(|x| x % 5 == 0).collect::<Vec<_>>() — cùng kết quả, nhưng filter phải duyệt cả 30 element và gọi closure 30 lần; step_by nhảy thẳng nên nhanh hơn.
12

Bài Tiếp Theo

Bài 207: enumerate, zip — hai adapter "ghép cặp" cực kỳ hữu ích. enumerate() ghép mỗi element với chỉ số của nó, yield (usize, Item) — phổ biến khi cần biết "đây là phần tử thứ mấy". zip(other) ghép song song hai iterator thành một iterator của tuple (A, B), dừng theo iterator ngắn hơn — pattern hay dùng khi cần xử lý hai mảng đồng thời. Bài cũng đi qua cách kết hợp enumerate + zip + take để build các chain thực tế.