Iterators는 for loops보다 더 많은 것을 할 수 있다.
만약 당신이 Iterator trait에 대한 documentation을 본다면,
Iterators를 다양한 방법으로 transform하고 filter하고 combine하는
methods의 많은 collections를 찾을 수 있을 것이다.
그 중 대중적인 것들을 이야기해보자.
- map은 iterator의 각 element에 function을 적용한다.
- filter는 조건을 만족하는 elements만 keep한다.
- filter_map은 filter와 map을 한번에 결합한다.
- cloned는 각 element를 clone하여, references의 iterator를 values의 iterator로 변환한다.
- enumerate는 (index, value) pairs를 yield하는 새로운 iterator를 return한다.
- skip은 iterator의 처음 n개의 elements를 skip한다.
- take는 n개의 elements에 iterate후에 stop한다.
- chain은 두개의 iterator를 하나로 결합한다.
이 method들은 combinators라 불린다.
그것들은 복잡한 transformations를 정확하고 읽기 쉬운 방법으로 만들기위해,
함께 chain하여 사용된다.
let numbers = vec![1, 2, 3, 4, 5];
// The sum of the squares of the even numbers
let outcome: u32 = numbers.iter()
.filter(|&n| n % 2 == 0)
.map(|&n| n * n)
.sum();
위의 filter와 map methods는 어떻게 되는 것일까?
그들은 closures를 arguments로 가져간다.
Closures는 anonymous functions다.
anonymous functions는 fn syntax로 정의하지 않은 functions다.
그들은 |args| body syntax를 사용해 정의되며,
args는 arguments고, body는 function의 body다.
body는 code block이 될 수도 있고, single expression이 될 수도 있다.
예를 들면 다음과 같다.
// An anonymous function that adds 1 to its argument
let add_one = |x| x + 1;
// Could be written with a block too:
let add_one = |x| { x + 1 };
Closures는 두개 이상의 arguments를 take할 수도 있다.
let add = |x, y| x + y;
let sum = add(1, 2);
Closures는 그들의 환경에서 variables를 capture할수도 있다.
let x = 42;
let add_x = |y| x + y;
let sum = add_x(1);
즉, 위 코드에서는 x가 42로 들어가고, y에 1이 들어가는 것이다.
만약 필요하다면, 당신은 arguments의 types나 return type을 specify할 수 있다.
// Just the input type
let add_one = |x: i32| x + 1;
// Or both input and output types, using the `fn` syntax
let add_one: fn(i32) -> i32 = |x| x + 1;
당신이 combinators를 사용하여 iterator를 transforming했다면 무슨 일이 일어날까?
당신은 for loop를 사용하여, transformed values를 iterate하거나,
collection에 collect할 수 있다.
collection에 collect하는 것이 collect method다.
collect는 iterator를 consume하고 당신이 고른 collection에 elements들을 collect한다.
예를 들어, 당신은 아래 코드로 짝수의 제곱을 Vec에 collect할 수 있다.
let numbers = vec![1, 2, 3, 4, 5];
let squares_of_evens: Vec<u32> = numbers.iter()
.filter(|&n| n % 2 == 0)
.map(|&n| n * n)
.collect();
collect는 return type에 대해 generic하다.
그래서 당신은 올바른 type을 compiler가 추론할 수 있도록 type hint를 제공하여 도와줘야한다.
위의 예시에서, 우리는 squares_of_evens의 type을 Vec<u32>로 annotate했다.
다른 방법으로, 당신은 type을 specify하기 위해 turbofish syntax를 사용할 수 있다.
let squares_of_evens = numbers.iter()
.filter(|&n| n % 2 == 0)
.map(|&n| n * n)
// Turbofish syntax: `<method_name>::<type>()`
// It's called turbofish because `::<>` looks like a fish
.collect::<Vec<u32>>();
07_combinators exercise는 closures와 chain을 잘 결합하여 해결할 수 있었다.

chain은 항상 return되는 것을 다음 입력으로 준다는것을 기억하자.
TicketStore::to_dos는 Vec<&Ticket>을 return한다.
이 signature는 to_dos가 호출될 때마다, 새로운 heap allocation이 일어난다는 것을 의미한다.
실제로 caller가 return되는 것으로 무엇을 할지에 따라 불필요할 수도 있는 일이다.
만약 to_dos가 Vec 대신 iterator를 return한다면 더 나을 수 있다.
이렇게 caller가 결과들을 Vec로 collect할 지, 그저 iterate할 지,
결정할 수 있는 권한을 부여할 수 있다.
아래와 같이 implement한다면, to_dos의 return type은 무엇일까?
impl TicketStore {
pub fn to_dos(&self) -> ??? {
self.tickets.iter().filter(|t| t.status == Status::ToDo)
}
}
filter method는 std::iter::Filter의 instance를 return한다.
그리고 definition은 다음과 같다.
pub struct Filter<I, P> { /* fields omitted */ }
I는 filter하려하는 iterator의 type이고
P는 elements를 filter하는 조건이다.
우리는 이 경우 I가 std::slice::Iter<'_, Ticket>이라는 것을 안다.
하지만 P가 무엇일까?
P는 anonymous function인 closure다.
이름이 암시하듯이, closures는 name이 없다.
그래서 우리는 우리의 code에 write할 수 없다.
Rust는 impl trait를 통해 이를 해결했다.
impl Trait은 name을 specify하지 않아도 type을 return할 수 있게 해주는 feature다.
당신이 해당 type이 implement하는 traits를 선언하면, Rust가 알아서 나머지를 해준다.
이 경우, 우리는 Tickets에 대한 references의 iterator를 return하길 원한다.
impl TicketStore {
pub fn to_dos(&self) -> impl Iterator<Item = &Ticket> {
self.tickets.iter().filter(|t| t.status == Status::ToDo)
}
}
이러면 끝이다!
return 위치의 impl Trait은 generic parameter가 아니다.
Generics는 function의 caller에 의해 채워지는 types의 placeholders다.
generic parameter가 있는 function은 polymorphic하다.
서로 다른 types로 call될 수 있고,
그에 따라 compiler는 다른 implementation을 generate한다.
하지만 impl Trait에서는 아니다.
impl Trait의 function return type은 compile time에 fix되고,
compiler는 single implementation을 generate한다.
이것이 impl Trait이 opaque return type이라고 불리는 이유다.
caller는 return value의 정확한 type은 모르고,
specify된 traits를 implement한다는 것만 알 수 있다.
하지만, compiler는 정확한 type을 알고 있으므로, polymorphism하곤 관련없다.
RPIT는 "Return Position Impl Trait"으로 약어로 많이 사용된다.
그것은 return position에 impl Trait을 사용한다는 것을 의미한다.
08_impl_trait의 exercise는 impl Trait을 사용하여 iterator를 return하는 것이었다.

위에서 배운대로 하니까 쉽게 됐다.
우리는 name을 specify하지 않고 type을 return할 때, impl Trait을 사용되는 것을 배웠다.
같은 syntax로 argument position에 사용될 수 있다.
fn print_iter(iter: impl Iterator<Item = i32>) {
for i in iter {
println!("{}", i);
}
}
print_iter는 i32들의 iterator를 가져가서 각 element를 print한다.
argument position에 사용될 때, impl Trait은 trait bound의 generic parameter와 같다.
fn print_iter<T>(iter: T)
where
T: Iterator<Item = i32>
{
for i in iter {
println!("{}", i);
}
}
경험상 argument position의 impl Trait보단 generic을 선호한다.
Generics는 caller가 turbofish syntax(::<>)를 사용하여,
explicit하게 argument의 type을 specify할 수 있도록 해준다.
이것은 disambiguous하게 해줘서 유용할 수 있다.
하지만 impl Trait의 경우, 그렇지 않다.
09_impl_trait_2 exercise는 impl trait을 argument position에 사용한 코드를
trait bound를 사용한 generic parameter로 바꾸는 것이었다.

todo에 많은 내용이 포함되어 있어서 쉽게 했다.
Iterator의 여러 기능들과
무엇이 좋고 안좋은지 등을 배웠다.
머릿속에서 어느정도 정리해가며 해야겠다.
'Rust Programming' 카테고리의 다른 글
| [Rust] 6. Index trait, IndexMut trait (0) | 2024.10.06 |
|---|---|
| [Rust] 6. Ticket Management: Slices, Mutable slices, Two states (0) | 2024.10.02 |
| [Rust] 6. Ticket Management: Iterators, Iter, Lifetimes (0) | 2024.08.07 |
| [Rust] 6. Ticket Management: Arrays, Vectors, Resizing (0) | 2024.08.07 |
| [Rust] 5. Ticket v2: TryFrom trait, Error::source (0) | 2024.08.06 |