홈강좌Rust 입문·중급 47편 중 37편째
Rust 반복자 — map·filter·collect 로 for 문을 줄인다
Rust 반복자(iterator)는 값을 하나씩 내주는 것이고, .iter() 로 만든다. map 으로 바꾸고 filter 로 거르고, collect 로 모으거나 sum·count·max 로 줄인다. 반복자는 게을러서, collect 같은 마무리 없이 map 만 쓰면 아무 일도 하지 않고 경고가 난다.
01 거르고, 바꾸고, 모은다
점수 목록에서 80점 이상만 고르고, 모두에게 5점을 더하고, 합계를 구해 보자. for 와 push 로 쓰면 각각 네댓 줄이 든다.
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());
} 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 3scores.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! 을 찍게 했다.
fn main() {
let scores = vec![72, 85, 90];
scores.iter().map(|s| {
println!("looking at {s}");
s + 5
});
println!("done");
} 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`
donelooking at … 이 한 줄도 찍히지 않고 done 만 나왔다. 경고가 이유를 말한다. 반복자는 게을러서, 소비되기 전에는 아무것도 하지 않는다(iterators are lazy and do nothing unless consumed). map 은 "이렇게 바꿀 것" 이라는 계획만 세운다. collect·sum·count·for 같은 마무리가 값을 하나씩 당길 때 비로소 실행된다.
그래서 아주 긴 목록에서 앞의 몇 개만 필요하면, 나머지는 아예 계산하지 않는다.
03 실습 — 번호 붙이기와 가장 큰 값
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:?}");
} 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 다.