콘텐츠로 이동

Rust 사용자 정의 반복자

반복자 페이지에서는 표준 라이브러리가 주는 반복자를 써 봤어요. vec.iter(), .map(), .filter(), .sum() 같은 것들이요. 그런데 반복자는 직접 만들 수도 있어요. 카운트다운 타이머, 피보나치 수, 보고서의 페이지, 나만의 파일 형식의 줄들처럼, 값을 하나씩 건네주는 것이라면 무엇이든 반복자가 될 수 있어요.

좋은 소식이 있어요. 직접 쓰는 메서드는 next 하나뿐이에요. 그러면 Rust가 map, filter, take, sum, zip을 비롯해 수십 가지 어댑터를 공짜로 줘요. 번호표 발급기를 떠올려 보세요. 내가 만드는 건 다음 번호표를 뽑아 주는 부분뿐이에요. 대기 줄도, 화면도, “지금 부르는 번호” 안내판도 모두 알아서 돌아가요.

어떤 타입을 반복자로 만들려면 Iterator 트레이트를 구현하면 돼요. 필요한 건 두 가지예요.

  • type Item: 반복자가 건네주는 값의 타입이에요(연관 타입은 고급 트레이트 페이지에서 더 자세히 다뤄요).
  • fn next(&mut self) -> Option<Self::Item>: 다음 값이 있으면 Some(값)을, 더 없으면 None을 돌려줘요.
struct Countdown {
current: u32,
}
impl Countdown {
fn new(from: u32) -> Self {
Countdown { current: from }
}
}
impl Iterator for Countdown {
type Item = u32;
fn next(&mut self) -> Option<u32> {
if self.current == 0 {
None // 끝났어요
} else {
let value = self.current;
self.current -= 1;
Some(value)
}
}
}
fn main() {
let mut countdown = Countdown::new(3);
println!("{:?}", countdown.next());
println!("{:?}", countdown.next());
println!("{:?}", countdown.next());
println!("{:?}", countdown.next());
println!("{:?}", countdown.next());
for n in Countdown::new(3) {
println!("{}...", n);
}
println!("발사!");
}

실행 결과:

Some(3)
Some(2)
Some(1)
None
None
3...
2...
1...
발사!
▶ 직접 실행해 보기

눈여겨볼 점이 몇 가지 있어요.

  • next는 반복자를 바꾸기 때문에 &mut self를 받아요. 구조체가 지금 어디까지 왔는지(current)를 기억하고, 호출할 때마다 한 칸씩 나아가요. 그래서 countdown은 let mut이어야 해요.
  • None을 돌려주면 “다 끝났어요”라는 뜻이에요. for 반복문은 next를 계속 호출하다가 처음 None이 나오면 멈춰요.
  • 이 카운트다운은 끝난 뒤에도 계속 None을 돌려줘요. 예의 바른 동작이지만, 트레이트가 요구하는 건 아니에요. 어떤 반복자는 다시 값을 내놓기 시작할 수도 있어요. 그런 보장이 필요하다면 어떤 반복자든 .fuse()를 호출하면 돼요.

반복자가 꼭 끝나야 하는 건 아니에요. 피보나치 수열(0, 1, 1, 2, 3, 5, 8, …)은 영원히 이어져요. 각 수는 바로 앞 두 수의 합이에요. 반복자는 마지막 두 수만 기억하면 돼요.

struct Fibonacci {
current: u64,
next: u64,
}
impl Fibonacci {
fn new() -> Self {
Fibonacci { current: 0, next: 1 }
}
}
impl Iterator for Fibonacci {
type Item = u64;
fn next(&mut self) -> Option<u64> {
let value = self.current;
// checked_add는 오버플로가 나면 None을 돌려주고, `?`가 그 None을 그대로 넘겨요.
// 그래서 수가 너무 커지면 프로그램이 죽는 대신 반복자가 끝나요.
let new_next = self.current.checked_add(self.next)?;
self.current = self.next;
self.next = new_next;
Some(value)
}
}
fn main() {
let first_ten: Vec<u64> = Fibonacci::new().take(10).collect();
println!("{:?}", first_ten);
}

실행 결과:

[0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
▶ 직접 실행해 보기

핵심은 .take(10)이에요. 이게 없으면 collect()가 수열 전체를 모으려고 할 거예요. 끝나지 않는 반복자에는 항상 take, take_while, find 같은 것으로 한계를 정해 주세요.

가장 좋은 부분이에요. Iterator 트레이트에는 메서드가 75개쯤 있는데, next를 뺀 나머지는 모두 next를 바탕으로 한 기본 구현이 있어요. 그래서 next를 쓰는 순간, 내 타입도 그 메서드를 전부 쓸 수 있어요.

struct Countdown {
current: u32,
}
impl Countdown {
fn new(from: u32) -> Self {
Countdown { current: from }
}
}
impl Iterator for Countdown {
type Item = u32;
fn next(&mut self) -> Option<u32> {
if self.current == 0 {
None
} else {
let value = self.current;
self.current -= 1;
Some(value)
}
}
}
fn main() {
let total: u32 = Countdown::new(5).sum();
println!("합계: {}", total);
let evens: Vec<u32> = Countdown::new(10).filter(|n| n % 2 == 0).collect();
println!("짝수: {:?}", evens);
let labels: Vec<String> = Countdown::new(3).map(|n| format!("T-{}", n)).collect();
println!("라벨: {:?}", labels);
let multiple = Countdown::new(100).find(|n| n % 7 == 0);
println!("처음 나온 7의 배수: {:?}", multiple);
let pairs: Vec<(u32, char)> = Countdown::new(3).zip(['a', 'b', 'c']).collect();
println!("zip 결과: {:?}", pairs);
}

실행 결과:

합계: 15
짝수: [10, 8, 6, 4, 2]
라벨: ["T-3", "T-2", "T-1"]
처음 나온 7의 배수: Some(98)
zip 결과: [(3, 'a'), (2, 'b'), (1, 'c')]
▶ 직접 실행해 보기

Rust 트레이트가 왜 그렇게 쓸모 있는지 잘 보여 주는 예예요. 작은 메서드 하나를 넣으면, 커다란 도구 상자가 나와요.

map이나 take 같은 어댑터는 그 자체로는 아무 일도 하지 않아요. “계획”을 세워 둘 뿐이에요. 실제 일은 무언가가 반복자를 소비할 때만 일어나요. for 반복문, collect, sum 같은 것들이요. 이걸 지연 평가(lazy evaluation)라고 해요. next가 호출될 때마다 출력하는 반복자로 직접 확인해 볼게요.

struct Noisy {
n: u32,
}
impl Iterator for Noisy {
type Item = u32;
fn next(&mut self) -> Option<u32> {
self.n += 1;
println!(" next() 호출됨, {} 돌려줌", self.n);
Some(self.n)
}
}
fn main() {
println!("체인을 만드는 중...");
let chain = Noisy { n: 0 }.map(|x| x * 10).take(3);
println!("아직 아무것도 실행되지 않았어요. 이제 소비할게요:");
for value in chain {
println!("받은 값: {}", value);
}
println!("끝");
}

실행 결과:

체인을 만드는 중...
아직 아무것도 실행되지 않았어요. 이제 소비할게요:
next() 호출됨, 1 돌려줌
받은 값: 10
next() 호출됨, 2 돌려줌
받은 값: 20
next() 호출됨, 3 돌려줌
받은 값: 30
끝
▶ 직접 실행해 보기

여기서 배울 점이 두 가지예요. 첫째, 체인을 만들 때는 아무것도 출력되지 않았어요. next()가 한 번도 호출되지 않은 거예요. 둘째, 값은 하나씩 흘러가요. next 한 번, map 한 번, “받은 값” 한 번, 그다음에 다음 값이에요. 중간에 숨은 리스트 같은 건 없어요. 그리고 Noisy는 절대 끝나지 않는데도 프로그램은 끝나요. 아이템 세 개를 받고 나면 take(3)이 더 달라고 하지 않거든요.

내 컬렉션을 for 반복문에서 쓰기

섹션 제목: “내 컬렉션을 for 반복문에서 쓰기”

지금까지는 구조체 자체가 반복자였어요. 그런데 실제로는 아이템을 담고 있는 구조체, 즉 컬렉션이 있고, 사람들이 Vec을 돌듯 그걸 돌 수 있게 하고 싶을 때가 많아요. 그럴 때는 IntoIterator를 구현해요. for 반복문이 뒤에서 쓰는 트레이트가 바로 이거예요. for x in thing은 사실 thing.into_iter()를 호출해요.

컬렉션은 보통 세 가지 방식으로 돌 수 있게 해 주는데, 내 타입에서 직접 보면 이해가 쉬워요.

이렇게 쓰면 뒤에서 호출되는 메서드 각 아이템의 타입 반복 후의 컬렉션
for s in &playlist playlist.iter() &String(읽기만 가능) 계속 쓸 수 있어요
for s in &mut playlist playlist.iter_mut() &mut String(바꿀 수 있음) 계속 쓸 수 있어요
for s in playlist playlist.into_iter() String(소유) 이동되어 사라져요

세 가지를 모두 지원하는 Playlist를 만들어 볼게요. 이 구조체는 노래를 Vec에 담고 있으니까, 새 반복자를 직접 쓰는 대신 Vec의 반복자를 그대로 건네주면 돼요.

struct Playlist {
songs: Vec<String>,
}
impl Playlist {
fn iter(&self) -> std::slice::Iter<'_, String> {
self.songs.iter()
}
fn iter_mut(&mut self) -> std::slice::IterMut<'_, String> {
self.songs.iter_mut()
}
}
// `for song in &playlist`가 동작하게 해 줘요
impl<'a> IntoIterator for &'a Playlist {
type Item = &'a String;
type IntoIter = std::slice::Iter<'a, String>;
fn into_iter(self) -> Self::IntoIter {
self.iter()
}
}
// `for song in &mut playlist`가 동작하게 해 줘요
impl<'a> IntoIterator for &'a mut Playlist {
type Item = &'a mut String;
type IntoIter = std::slice::IterMut<'a, String>;
fn into_iter(self) -> Self::IntoIter {
self.iter_mut()
}
}
// `for song in playlist`가 동작하게 해 줘요(그리고 playlist를 다 써 버려요)
impl IntoIterator for Playlist {
type Item = String;
type IntoIter = std::vec::IntoIter<String>;
fn into_iter(self) -> Self::IntoIter {
self.songs.into_iter()
}
}
fn main() {
let mut playlist = Playlist {
songs: vec![String::from("intro"), String::from("main theme"), String::from("outro")],
};
for song in &playlist {
println!("읽기: {}", song);
}
for song in &mut playlist {
*song = song.to_uppercase();
}
println!("가장 긴 곡: {:?}", playlist.iter().max_by_key(|s| s.len()));
for song in playlist {
println!("가져가기: {}", song);
}
// 마지막 반복문이 `playlist`를 이동시켰으니, 여기서는 쓸 수 없어요.
}

실행 결과:

읽기: intro
읽기: main theme
읽기: outro
가장 긴 곡: Some("MAIN THEME")
가져가기: INTRO
가져가기: MAIN THEME
가져가기: OUTRO
▶ 직접 실행해 보기

곡 제목은 영어 그대로 두었어요. to_uppercase()는 한글을 바꾸지 못하고, s.len()은 글자 수가 아니라 바이트 수를 세기 때문이에요(한글은 한 글자에 3바이트예요).

줄이 꽤 많지만, impl은 모두 같은 모양이에요. Item이 무엇인지 적고, 돌려줄 반복자 타입(IntoIter)을 적고, 그 반복자를 돌려주면 끝이에요. 'a는 라이프타임이에요. “빌려준 아이템은 빌려준 재생 목록만큼 살아 있어요”라는 뜻이에요. Vec을 감싸는 컬렉션이라면 거의 어디에나 이 패턴을 그대로 가져다 쓸 수 있어요.

알아 두면 좋은 트레이트가 두 개 더 있어요. 자주 필요하지는 않지만, .rev()나 .len() 같은 어댑터가 이것들에 기대고 있어요.

  • DoubleEndedIterator: 끝에서부터 아이템을 꺼내는 next_back을 더해요. 이걸 구현하면 .rev()를 쓸 수 있어요.
  • ExactSizeIterator: 남은 아이템이 정확히 몇 개인지 안다고 약속해요. 이걸 구현하면 .len()을 쓸 수 있어요. Iterator에서 정확한 size_hint도 함께 제공해야 해요.
// low, low+1, ..., high-1을 내놓아요(범위 low..high처럼요)
struct Between {
low: u32,
high: u32,
}
impl Iterator for Between {
type Item = u32;
fn next(&mut self) -> Option<u32> {
if self.low >= self.high {
return None;
}
let value = self.low;
self.low += 1;
Some(value)
}
fn size_hint(&self) -> (usize, Option<usize>) {
let left = self.high.saturating_sub(self.low) as usize;
(left, Some(left))
}
}
impl DoubleEndedIterator for Between {
fn next_back(&mut self) -> Option<u32> {
if self.low >= self.high {
return None;
}
self.high -= 1;
Some(self.high)
}
}
impl ExactSizeIterator for Between {}
fn main() {
let backwards: Vec<u32> = Between { low: 1, high: 6 }.rev().collect();
println!("{:?}", backwards);
let mut both_ends = Between { low: 1, high: 6 };
println!("앞 {:?}, 뒤 {:?}", both_ends.next(), both_ends.next_back());
println!("{}개 남음", both_ends.len());
}

실행 결과:

[5, 4, 3, 2, 1]
앞 Some(1), 뒤 Some(5)
3개 남음
▶ 직접 실행해 보기

양쪽 끝이 서로를 향해 다가오다가, 만나면 반복자가 끝나요. impl ExactSizeIterator for Between {}는 비어 있어요. len()은 이미 작성한 size_hint를 쓰거든요.

구조체와 impl을 쓰는 방식은 여러 번 다시 쓸 반복자에 좋아요. 한 번만 잠깐 쓸 반복자라면, 표준 라이브러리의 std::iter에 도우미 함수가 두 개 있어요.

  • iter::from_fn(closure): 값이 필요할 때마다 클로저를 호출해요. 클로저는 next처럼 Some(값)이나 None을 돌려줘요.
  • iter::successors(first, closure): 첫 번째 값에서 시작하고, 클로저가 이전 값으로부터 다음 값을 계산해요. None을 돌려주면 끝나요.
use std::iter;
fn main() {
// from_fn: 클로저가 자기 상태(count)를 들고 있어요
let mut count = 3;
let countdown: Vec<u32> = iter::from_fn(|| {
if count == 0 {
None
} else {
count -= 1;
Some(count + 1)
}
})
.collect();
println!("{:?}", countdown);
// successors: 각 값을 바로 앞 값으로부터 계산해요
let powers: Vec<u32> = iter::successors(Some(1u32), |&n| n.checked_mul(10)).collect();
println!("{:?}", powers);
// 한 줄짜리 피보나치: 마지막 두 수를 쌍으로 들고 다녀요
let fib: Vec<u64> = iter::successors(Some((0u64, 1u64)), |&(a, b)| Some((b, a + b)))
.map(|(a, _)| a)
.take(10)
.collect();
println!("{:?}", fib);
}

실행 결과:

[3, 2, 1]
[1, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000, 1000000000]
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
▶ 직접 실행해 보기

powers는 저절로 멈춰요. 다음 10의 거듭제곱인 10,000,000,000(100억)은 u32에 들어가지 않으니까, checked_mul이 None을 돌려주고 반복자가 끝나요. 좋은 기준이 하나 있어요. 함수 하나 안에서만 쓸 거라면 이 지름길을 쓰고, 다른 코드에서도 그 반복자를 만들어야 하거나 상태가 많다면 이름 있는 구조체를 만드세요.

next를 직접 호출하면서 mut를 빼먹기. next는 반복자를 바꾸니까, 변수가 가변이어야 해요.

let countdown = Countdown { current: 3 };
println!("{:?}", countdown.next());
error[E0596]: cannot borrow `countdown` as mutable, as it is not declared as mutable
--> src/main.rs:20:22
|
20 | println!("{:?}", countdown.next());
| ^^^^^^^^^ cannot borrow as mutable
|
help: consider changing this to be mutable
|
19 | let mut countdown = Countdown { current: 3 };
| +++

“countdown이 가변으로 선언되지 않았으니 가변으로 빌릴 수 없다”는 에러예요. mut를 붙이면 돼요. (for 반복문에는 필요 없어요. 반복문이 반복자를 가져가서 알아서 처리해 주거든요.)

type Item 빼먹기. 트레이트는 무엇을 건네주는지 알아야 해요.

error[E0046]: not all trait items implemented, missing: `Item`
--> src/main.rs:5:1
|
5 | impl Iterator for Countdown {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ missing `Item` in implementation
|
= help: implement the missing item: `type Item = /* Type */;`

“트레이트 아이템 중 Item이 구현되지 않았다”는 뜻이에요. impl 안에 type Item = u32;(또는 반복자가 내놓는 타입)를 추가하세요.

끝나지 않는 반복자를 collect하기. take 없이 쓴 Fibonacci::new().collect::<Vec<_>>()는 컴파일은 잘 되지만, 쓸모 있는 방식으로 끝나지 않아요. 수가 오버플로될 때까지 돌거나, next가 절대 None을 돌려주지 않는다면 영원히 돌아요. 컴파일러는 이걸 잡아 주지 못해요. 끝나지 않을 수도 있는 반복자라면, 소비하기 전에 항상 take, take_while, find를 붙이세요.

  • type Item과 fn next(&mut self) -> Option<Self::Item>으로 Iterator를 구현해요. 끝나면 None을 돌려줘요.
  • next만 있으면 map, filter, take, sum, zip을 비롯해 수십 가지 메서드가 공짜로 생겨요.
  • 반복자는 게을러요. for 반복문, collect, sum 같은 것이 소비하기 전까지는 아무 일도 일어나지 않고, 아이템은 하나씩 흘러가요.
  • &MyType, &mut MyType, MyType에 IntoIterator를 구현하면, 빌리거나, 가변으로 빌리거나, 소비하는 for 반복문을 지원할 수 있어요.
  • DoubleEndedIterator는 .rev()를, ExactSizeIterator는 .len()을 쓸 수 있게 해 줘요.
  • 한 번만 잠깐 쓸 반복자라면 std::iter::from_fn이나 std::iter::successors를 쓰세요.

이해도 점검

1. 반복자에 더 이상 아이템이 없을 때 next는 무엇을 돌려줘야 할까요?

2. next만 구현했을 때, 내 반복자에서 .map()과 .sum()을 호출할 수 있을까요?

3. 지연 평가 예제에서 next()가 처음 호출되는 건 언제일까요?

4. for song in &playlist가 동작하게 해 주는 구현은 무엇일까요?

5. 내 반복자에서 .rev()를 쓰려면 어떤 트레이트가 필요할까요?

점수: 0 / 5