#20 반복자

Pt J·2020년 9월 9일
0

[完] Rust Programming

목록 보기
23/41
post-thumbnail

이 시리즈는 Rust 공식문서를 통해 공부한 흔적임을 밝힙니다.

벡터의 원소와 같은 일련의 아이템을 차례로 접근해야 하는 상황이 있다.
이런 상황에서 그것의 순회를 위해 사용할 수 있는 게 반복자다.
이걸 통해 일련의 아이템을 순회하는 코드를 직접 구현하지 않고 사용할 수 있다.
주로 for 문과 함께 사용하거나 반복자의 메서드를 사용하곤 한다.

반복자 Iterator

Rust의 반복자는 게으른 녀석이다.
반복자를 생성해도 그것을 실제로 사용할 때까지 반복자는 생성되지 않는다.
예를 들어, 벡터를 순회하는 벡터 반복자를 생성하기 위해
벡터 v에 대하여 v.iter()를 수행해도 이 반환값에 접근할 때까지 반복자는 생성되지 않는다.

우리는 다음과 같이 반복자와 for 문을 통해 일련의 아이템을 순차적으로 접근할 수 있다.

peter@hp-laptop:~/rust-practice/chapter13$ cargo new iter_for
     Created binary (application) `iter_for` package
peter@hp-laptop:~/rust-practice/chapter13$ cd iter_for/
peter@hp-laptop:~/rust-practice/chapter13/iter_for$ vi src/main.rs

src/main.rs

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

    let v1_iter = v1.iter();

    for val in v1_iter {
        println!("Got: {}", val);
    }
}
peter@hp-laptop:~/rust-practice/chapter13/iter_for$ cargo run
   Compiling iter_for v0.1.0 (/home/peter/rust-practice/chapter13/iter_for)
    Finished dev [unoptimized + debuginfo] target(s) in 0.21s
     Running `target/debug/iter_for`
Got: 1
Got: 2
Got: 3
peter@hp-laptop:~/rust-practice/chapter13/iter_for$ 

여기서 v1_iter의 반복자가 생성되는 건 for 문이 시작되는 시점이다.
반복자는 벡터뿐만 아니라 여러 컬렉션에 대해 이와 같은 순회 기능을 제공한다.

Iterator 트레이트

모든 반복자는 표준 라이브러리의 Iterator 트레이트를 구현한다.
Iterator 트레이트는 다음과 같이 작성되어 있다.


pub trait Iterator {
    type Item;

    fn next(&mut self) -> Option<Self::Item>;

    // methods with default implementations elided
}

여기엔 우리가 아직 본 적 없는 typeSelf가 포함되어 있다.
// self는 본 적 있지만 Self는 처음이다.
type은 이 트레이트의 연관 자료형을 설정하기 위한 것으로
어떤 트레이트를 구현하는 녀석이 사용할 자료형을 특정할 때 사용된다.
이에 대해서는 추후에 좀 더 자세히 알아보도록 하겠다.
단지 연관 자료형이라는 게 있다는 것과 next 메서드의 반환값의 자료형이 그것임을 알고 넘어가자.
next를 제외한 Iterator 트레이트의 메서드는 모두 디폴트로 구현되어 있다.

next 메서드는 컬렉션에서 다음 값을 가져와 Some 열것값으로 반환하고
다음 값이 존재하지 않을 경우 None 열것값을 반환하는데
구체적인 구현 방법은 Iterator 트레이트를 구현할 때 구현된다.

벡터에서 next 메서드를 사용하는 예제를 간단히 확인해보자.

peter@hp-laptop:~/rust-practice/chapter13/iter_for$ cd ..
peter@hp-laptop:~/rust-practice/chapter13$ cargo new vector_next --lib
     Created library `vector_next` package
peter@hp-laptop:~/rust-practice/chapter13$ cd vector_next/
peter@hp-laptop:~/rust-practice/chapter13/vector_next$ vi src/lib.rs

src/lib.rs

#[cfg(test)]
mod tests {
    #[test]
    fn iterator_demonstration() {
        let v1 = vec![1, 2, 3];

        let mut v1_iter = v1.iter();

        assert_eq!(v1_iter.next(), Some(&1));
        assert_eq!(v1_iter.next(), Some(&2));
        assert_eq!(v1_iter.next(), Some(&3));
        assert_eq!(v1_iter.next(), None);
    }
}
peter@hp-laptop:~/rust-practice/chapter13/vector_next$ cargo test
   Compiling vector_next v0.1.0 (/home/peter/rust-practice/chapter13/vector_next)
    Finished test [unoptimized + debuginfo] target(s) in 0.29s
     Running target/debug/deps/vector_next-02943be62aa73540

running 1 test
test tests::iterator_demonstration ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

   Doc-tests vector_next

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

peter@hp-laptop:~/rust-practice/chapter13/vector_next$ 

next 메서드를 사용하면 어디까지 순회했는지 추적함으로써 반복자 내부 상태가 변하기 때문에
반복자를 가변성을 부여하여 선언해야 한다.
이런 식으로 현재 가리키는 아이템을 넘겨가는 방식을 소비Consume라고 한다.

iter 메서드로 생성하는 반복자의 아이템은 기본적으로 불변 참조인데
반복자의 각 아이템의 소유권을 가져가며 대여하고자 한다면 into_iter 메서드를,
가변 참조를 통해 순회하고자 한다면 iter_mut 메서드를 사용할 수 있다.

Iterator 트레이트에 디폴트로 구현되어 있는 메서드 중 일부는
내부적으로 next 메서드를 호출하여 사용한다.
그런 의미에서 Iterator 트레이트를 구현한다면 next 메서드를 반드시 구현하도록 하자.
next 메서드를 통해 내부적으로 반복자를 소비하는 메서드를
소비 어댑터Consuming Adapter라고 한다.
이러한 소비 어댑터의 예로는
각 아이템을 순회하며 각각의 값을 모두 더해 최종 결과를 반환하는 sum 메서드가 있다.

peter@hp-laptop:~/rust-practice/chapter13/vector_next$ cd ..
peter@hp-laptop:~/rust-practice/chapter13$ cargo new iterator_sum --lib
     Created library `iterator_sum` package
peter@hp-laptop:~/rust-practice/chapter13$ cd iterator_sum/
peter@hp-laptop:~/rust-practice/chapter13/iterator_sum$ vi src/lib.rs

src/lib.rs

#[cfg(test)]
mod tests {
    #[test]
    fn iterator_sum() {
        let v1 = vec![1, 2, 3];
        let v1_iter = v1.iter();

        let total: i32 = v1_iter.sum();

        assert_eq!(total, 6);
    }
}
peter@hp-laptop:~/rust-practice/chapter13/iterator_sum$ cargo test
   Compiling iterator_sum v0.1.0 (/home/peter/rust-practice/chapter13/iterator_sum)
    Finished test [unoptimized + debuginfo] target(s) in 0.31s
     Running target/debug/deps/iterator_sum-b15933f29710f601

running 1 test
test tests::iterator_sum ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

   Doc-tests iterator_sum

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

peter@hp-laptop:~/rust-practice/chapter13/iterator_sum$ 

sum 메서드는 반복자의 소유권을 가져간다.
따라서 이것을 호출한 후에는 그 반복자 변수를 더 이상 사용할 수 없다.

Iterator 트레이트의 메서드 중에는 반복자 어댑터Iterator Adapter라고 불리는 녀석들도 있다.
이 녀석들은 어떤 반복자를 다른 종류의 반복자로 변경하는 데 사용된다.
예를 들어, map 메서드는 반복자의 각 아이템에 특정한 연산을 한 결과를 반복자로 반환한다.
각 아이템에 수행할 연산은 클로저로서 인자로 전달된다.
반복자를 소비하며 수집한 결과값을 컬렉션에 저장하여 반환하는 collect 메서드와 함께 사용하면
다음과 같은 예제를 작성할 수 있다.

peter@hp-laptop:~/rust-practice/chapter13/iterator_sum$ cd ..
peter@hp-laptop:~/rust-practice/chapter13$ cargo new map_collect
     Created binary (application) `map_collect` package
peter@hp-laptop:~/rust-practice/chapter13$ cd map_collect/
peter@hp-laptop:~/rust-practice/chapter13/map_collect$ vi src/main.rs

src/main.rs

fn main() {
    let v1: Vec<i32>  = vec![1, 2, 3];

    let v2: Vec<_> = v1.iter().map(|x| x + 1).collect();

    assert_eq!(v2, vec![2, 3, 4]);
}
peter@hp-laptop:~/rust-practice/chapter13/map_collect$ cargo run
   Compiling map_collect v0.1.0 (/home/peter/rust-practice/chapter13/map_collect)
    Finished dev [unoptimized + debuginfo] target(s) in 0.28s
     Running `target/debug/map_collect`
peter@hp-laptop:~/rust-practice/chapter13/map_collect$

filter 메서드를 사용하면 반복자가 가진 아이템 중 일부만 가진 새 반복자를 생성할 수 있다.
이 녀석은 bool 값을 반환하는 클로저를 인자로 받으며
그 결과가 true인 아이템만 모아 반복자로 만들어 반환한다.

peter@hp-laptop:~/rust-practice/chapter13/map_collect$ cd ..
peter@hp-laptop:~/rust-practice/chapter13$ cargo new filter --lib
     Created library `filter` package
peter@hp-laptop:~/rust-practice/chapter13$ cd filter/
peter@hp-laptop:~/rust-practice/chapter13/filter$ vi src/lib.rs

src/lib.rs

#[derive(PartialEq, Debug)]
struct Shoe {
    size: u32,
    style: String,
}

fn shoes_in_my_size(shoes: Vec<Shoe>, shoe_size: u32) -> Vec<Shoe> {
    shoes.into_iter()
        .filter(|s| s.size == shoe_size)
        .collect()
}

#[test]
fn filters_by_size() {
    let shoes = vec![
        Shoe {
            size: 10,
            style: String::from("sneaker"),
        },
        Shoe {
            size: 13,
            style: String::from("sandal"),
        },
        Shoe {
            size: 10,
            style: String::from("boot"),
        },
    ];

    let in_my_size = shoes_in_my_size(shoes, 10);

    assert_eq!(
        in_my_size,
        vec![
            Shoe {
                size: 10,
                style: String::from("sneaker"),
            },
            Shoe {
                size: 10,
                style: String::from("boot"),
            },
        ]
    );
}
peter@hp-laptop:~/rust-practice/chapter13/filter$ cargo test
   Compiling filter v0.1.0 (/home/peter/rust-practice/chapter13/filter)

# snip

    Finished test [unoptimized + debuginfo] target(s) in 0.37s
     Running target/debug/deps/filter-bc7b31213b667a13

running 1 test
test filters_by_size ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

   Doc-tests filter

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

peter@hp-laptop:~/rust-practice/chapter13/filter$ 

반복자 직접 구현하기

반복자는 Iterator 트레이트를 구현한다.
바꿔 말하면, 우리는 iter, into_iter, iter_mut 메서드로 반복자를 생서할 수도 있지만
Iterator 트레이트를 통해 반복자를 직접 구현할 수도 있다.
이를 위해선 우린 단지 연관 자료형을 설정하고 next 메서드를 구현하면 된다.

예를 들어, u32 변수를 저장하며 이를 통해 1부터 5까지 세는 반복자 Counter를 만들어보자.
이것은 count라는 u32 변수 하나를 필드로 가지며 생성할 땐 0으로 초기화하고
next 메서드를 호출할 때마다 1씩 증가하며 5를 넘어서면 None을 반환한다.

peter@hp-laptop:~/rust-practice/chapter13/filter$ cd ..
peter@hp-laptop:~/rust-practice/chapter13$ cargo new counter --lib
     Created library `counter` package
peter@hp-laptop:~/rust-practice/chapter13$ cd counter/
peter@hp-laptop:~/rust-practice/chapter13/counter$ vi src/lib.rs

src/lib.rs

struct Counter {
    count: u32,
}

impl Counter {
    fn new() -> Counter {
        Counter { count: 0 }
    }
}

impl Iterator for Counter {
    type Item = u32;

    fn next(&mut self) -> Option<Self::Item> {
        self.count += 1;

        if self.count < 6 {
            Some(self.count)
        } else {
            None
        }
    }
}

#[test]
fn calling_next_directly() {
    let mut counter = Counter::new();

    assert_eq!(counter.next(), Some(1));
    assert_eq!(counter.next(), Some(2));
    assert_eq!(counter.next(), Some(3));
    assert_eq!(counter.next(), Some(4));
    assert_eq!(counter.next(), Some(5));
    assert_eq!(counter.next(), None);
}
peter@hp-laptop:~/rust-practice/chapter13/counter$ cargo test
   Compiling counter v0.1.0 (/home/peter/rust-practice/chapter13/counter)

# snip

running 1 test
test calling_next_directly ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

   Doc-tests counter

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

peter@hp-laptop:~/rust-practice/chapter13/counter$ 

그리고 우리는 next 메서드를 구현한 것만으로
next 메서드를 내부적으로 사용하는 Iterator의 다른 메서드들도 사용할 수 있다.
예를 들어, 다음과 같은 코드의 작성이 가능하다.

peter@hp-laptop:~/rust-practice/chapter13/counter$ vi src/lib.rs

src/lib.rs

// snip

#[test]
fn using_another_iterator_trait_methods() {
    let sum: u32 = Counter::new().zip(Counter::new().skip(1))
        .map(|(a, b)| a * b)
        .filter(|x| x % 3 == 0)
        .sum();
  
    assert_eq!(18, sum);
}
peter@hp-laptop:~/rust-practice/chapter13/counter$ cargo test
   Compiling counter v0.1.0 (/home/peter/rust-practice/chapter13/counter)

# snip

    Finished test [unoptimized + debuginfo] target(s) in 0.30s
     Running target/debug/deps/counter-22dd9d0ca2a41960

running 2 tests
test calling_next_directly ... ok
test using_another_iterator_trait_methods ... ok

test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

   Doc-tests counter

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

peter@hp-laptop:~/rust-practice/chapter13/counter$ 

skip 메서드는 앞에서부터 전달된 인자 개수만큼의 아이템을 무시한다.
그리고 zip 메서드는 그것을 호출하는 녀석과 인자로 전달된 녀석 중
하나라도 None이라면 None을 반환한다.
따라서 우리는 [(1, 2), (2, 3), (3, 4), (4, 5)]를 생성하고
map 메서드를 통해 [2, 6, 12, 20]로 변환한 뒤
filter 메서드를 통해 [6, 12]로 변환하여 sum 메서드를 통해 그 합을 구하게 된다.

유사 grep 개선하기

우리는 이러한 반복자를 통해 우리가 지난 번에 만든 minigrep을 개선할 수 있다.
우리는 Config 구조체에서 clone 메서드를 사용하면서도
조금 비효율적이지만 일단 넘어가자고 한 바가 있다.
우리는 원래 명령줄 인수의 문자열 벡터를 Config 연관함수 new의 인자로 받았다.
env::args는 명렬줄 인수가 담긴 반복자를 반환하는데
우리는 이것을 벡터로 변환하여 사용하고 있던 것이다.
이것을 벡터로 변환하지 않고 있는 그대로의 반복자를 사용하도록 변경해보자.

peter@hp-laptop:~/rust-practice/chapter13/counter$ cd ..
peter@hp-laptop:~/rust-practice/chapter13$ cargo new minigrep --lib
     Created library `minigrep` package
peter@hp-laptop:~/rust-practice/chapter13$ cp ../chapter12/minigrep/src/ 
lib.rs   main.rs  
peter@hp-laptop:~/rust-practice/chapter13$ cp ../chapter12/minigrep/src/lib.rs minigrep/src/lib.rs 
peter@hp-laptop:~/rust-practice/chapter13$ cp ../chapter12/minigrep/src/main.rs minigrep/src/main.rs
peter@hp-laptop:~/rust-practice/chapter13$ cd minigrep/
peter@hp-laptop:~/rust-practice/chapter13/minigrep$ vi src/main.rs

src/main.rs

use std::env;
use std::process;
use minigrep::Config;

fn main() {
    let config = Config::new(env::args()).unwrap_or_else(|err| {
        eprintln!("Problem parsing arguments: {}", err);
        process::exit(1);
    });

//    println!("Search for {}", config.query);
//    println!("In file {}", config.filename);

    if let Err(e) = minigrep::run(config) {
        eprintln!("Application error: {}", e);
        process::exit(1);
    }
}
peter@hp-laptop:~/rust-practice/chapter13/minigrep$ vi src/lib.rs

src/lib.rs

// snip

impl Config {
    pub fn new(mut args: env::Args) -> Result<Config, &'static str> {
        if args.len() < 3 {
            return Err("not enough arguments");
        }
        let query = match args.next() {
            Some(arg) => arg,
            None => return Err("Didn't get a query string"),
        };
        let filename = match args.next() {
            Some(arg) => arg,
            None => return Err("Didn't get a file name"),
        };
        let case_sensitive = env::var("CASE_INSENSITIVE").is_err();

        Ok(Config { query, filename, case_sensitive })
    }
}

// snip

그리고 search 메서드 또한 반복자와 filter 메서드를 통해 개선할 수 있다.
이것을 사용하면 우리가 원하는 조건을 충족하는 아이템만의 반복자를 생성할 수 있으니 말이다.

peter@hp-laptop:~/rust-practice/chapter13/minigrep$ vi src/lib.rs

src/lib.rs

// snip

pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
    contents.lines()
        .filter(|line| line.contains(query))
        .collect()
}

pub fn search_case_insensitive<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
    let query = query.to_lowercase();
    contents.lines()
        .filter(|line| line.to_lowercase().contains(&query))
        .collect()
}

// snip

반복자는 Rust의 무비용 추상화 기능 중 하나로,
반복문을 통해 구현한 것보다 효율적인 프로그램을 구현할 수 있게 해준다.
그런 의미에서 둘 다 사용 가능한 상황이라면 이왕이면 반복자를 사용하록 하자.

이 포스트의 내용은 공식문서의 13장 2절 Processing a Series of Items with Iterators ~ 13장 4절 Comparing Performance: Loops vs. Iterators에 해당합니다.

profile
Peter J Online Space - since July 2020

0개의 댓글