hyunsu blog

홈강좌Rust 입문·중급 47편 중 37편째

Rust 반복자 — map·filter·collect 로 for 문을 줄인다

목차 · 3
  1. 01거르고, 바꾸고, 모은다
  2. 02반복자는 게으르다
  3. 03실습 — 번호 붙이기와 가장 큰 값

Rust 반복자(iterator)는 값을 하나씩 내주는 것이고, .iter() 로 만든다. map 으로 바꾸고 filter 로 거르고, collect 로 모으거나 sum·count·max 로 줄인다. 반복자는 게을러서, collect 같은 마무리 없이 map 만 쓰면 아무 일도 하지 않고 경고가 난다.

01 거르고, 바꾸고, 모은다

점수 목록에서 80점 이상만 고르고, 모두에게 5점을 더하고, 합계를 구해 보자. for 와 push 로 쓰면 각각 네댓 줄이 든다.

rust
fn main() {
    let scores = vec![72, 85, 90, 64, 88];

    let passed: Vec<i32> = scores.iter().copied().filter(|s| *s >= 80).collect();
    println!("{passed:?}");

    let curved: Vec<i32> = scores.iter().map(|s| s + 5).collect();
    println!("{curved:?}");

    let total: i32 = scores.iter().sum();
    println!("total {total}, passed {}", passed.len());
}
text
   Compiling iters v0.1.0 (~/projects/iters)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.19s
     Running `target/debug/iters`
[85, 90, 88]
[77, 90, 95, 69, 93]
total 399, passed 3
  • scores.iter() 는 Vec 의 원소를 하나씩 빌려 내주는 반복자다. 내주는 것이 &i32(참조)다.
  • .copied() 는 참조를 값으로 복사해 i32 로 바꾼다(Copy 타입이라 된다, 16편).
  • .filter(|s| *s >= 80) 은 클로저(36편)가 true 인 것만 남긴다. filter 는 값을 빌려서 보여 주므로 *s 로 꺼내 비교한다.
  • .map(|s| s + 5) 는 하나하나를 바꾼다.
  • .collect() 는 결과를 모아 컬렉션으로 만든다. 무엇으로 모을지 let passed: Vec<i32> 처럼 타입을 적어 알려 준다.
  • .sum() 은 모두 더한다. 이것도 결과 타입(i32)을 적어 준다.

점(.)으로 이어 붙인 한 줄이 "무엇을 하는지" 를 그대로 읽힌다. 번호를 세거나 빈 Vec 을 만들 필요가 없다.

02 반복자는 게으르다

map 만 쓰고 마무리하지 않으면 어떻게 될까. 몸체에서 println! 을 찍게 했다.

rust
fn main() {
    let scores = vec![72, 85, 90];
    scores.iter().map(|s| {
        println!("looking at {s}");
        s + 5
    });
    println!("done");
}
text
   Compiling iters v0.1.0 (~/projects/iters)
warning: unused `Map` that must be used
 --> src/main.rs:3:5
  |
3 | /     scores.iter().map(|s| {
4 | |         println!("looking at {s}");
5 | |         s + 5
6 | |     });
  | |______^
  |
  = note: iterators are lazy and do nothing unless consumed
  = note: `#[warn(unused_must_use)]` (part of `#[warn(unused)]`) on by default
help: use `let _ = ...` to ignore the resulting value
  |
3 |     let _ = scores.iter().map(|s| {
  |     +++++++

warning: `iters` (bin "iters") generated 1 warning
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.17s
     Running `target/debug/iters`
done

looking at … 이 한 줄도 찍히지 않고 done 만 나왔다. 경고가 이유를 말한다. 반복자는 게을러서, 소비되기 전에는 아무것도 하지 않는다(iterators are lazy and do nothing unless consumed). map 은 "이렇게 바꿀 것" 이라는 계획만 세운다. collect·sum·count·for 같은 마무리가 값을 하나씩 당길 때 비로소 실행된다.

그래서 아주 긴 목록에서 앞의 몇 개만 필요하면, 나머지는 아예 계산하지 않는다.

03 실습 — 번호 붙이기와 가장 큰 값

rust
fn main() {
    let names = ["ferris", "corro", "crab"];
    for (i, name) in names.iter().enumerate() {
        println!("{}. {name}", i + 1);
    }

    let lengths: Vec<usize> = names.iter().map(|n| n.len()).collect();
    let longest = lengths.iter().max();
    println!("{lengths:?} {longest:?}");
}
text
   Compiling iters v0.1.0 (~/projects/iters)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.18s
     Running `target/debug/iters`
1. ferris
2. corro
3. crab
[6, 5, 4] Some(6)
  • .enumerate() 는 값마다 0 부터 센 번호를 붙여 (번호, 값) 튜플로 내준다. for (i, name) in … 으로 풀어 받는다.
  • .map(|n| n.len()) 로 글자 수 목록을 만들었다.
  • .max() 는 가장 큰 값을 Option 으로 준다. 목록이 비어 있으면 None 이다(26편).

34편 wordfreq 의 top 함수는 for 로 짝을 옮겨 담았는데, 반복자로는 counts.into_iter().map(|(w, c)| (c, w)).collect() 한 줄이다. into_iter() 는 빌리지 않고 값을 넘겨받아 내주는 반복자다. 47편의 2판에서 이렇게 고친다. 다음 편은 값을 힙에 두는 상자, Box 다.