비욘드 JS: 러스트 - Iterator

dante Yoon·2023년 1월 1일
0

beyond js

목록 보기
14/20
post-thumbnail

글을 시작하며

안녕하세요, 단테입니다. 오늘은 러스트 반복자(iterator)에 대해 알아보겠습니다.

Processing a Series of Items with Iterators

Iterator는 일련의 순서를 가진 아이템에 대해 순회할 때 유용하게 쓰입니다.
러스트에서 iterator는 lazy하게 동작합니다.

let v1 = vec![1, 2, 3];

let v1_iter = v1.iter();

for val in v1_iter {
  println!("Got: {}", val);
}

iterator를 제공하지 않은 언어는 인덱스를 참조해야 하지만 러스트 표준 라이브러리에서 제공하는 iterator를 사용하면 인덱스 참조로 인해 벌어질 수 있는 문제들을 미연에 방지할 수 있습니다.

The Iterator Trait and the next Method

Iterator라는 이름의 trait을 구현한 모든 iterators들은 표준 라이브러리에 정의되어있습니다.
Iterator trait은 아래와 같이 구현되어 있습니다.

pub trait Iterator {
    type Item;

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

    // methods with default implementations elided
}

type ItemSelf::Item은 trait의 associated type이라고 부릅니다. 이 타입에 대해서는 나중에 자세히 다루도록 하겠습니다.

현재로서 이 코드들은 Iterator trait을 구현하기 위해서는 Item type을 정의해야 한다는 것을 의미한다는 정도로 이해하면 됩니다. 이 Item 타입은 next 메소드의 리턴 타입에서 사용합니다. iterator의 리턴 타입이 Item입니다.

Iterator trait을 구현하기 위해서는 next 메소드만 구현하면 됩니다. 이 메소드는 한 순회마다 Some으로 래핑된 item이나 순회가 끝나면 None이 리턴됩니다.

우리는 iterators 에 있는 next 메소드를 직접 호출할 수 있습니다.
이 때 next 메소드를 호출하는 것이 interator의 내부 상태 값을 변경하기 때문에 mut 키워드를 사용해야 합니다. 이 말은 next 메소드는 iterator를 소비하는 걸로 간주된다는 말입니다. for loop를 사용할 때는 내부적으로 vi_iter의 오너쉽을 가져가기 때문에 v1_iter를 mutable하게 사용하지 않아도 됩니다.

#[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);
    }
}

또한 next 메소드를 호출함으로 얻는 레퍼런스는 immutable reference입니다. iter 메소드는 immutable reference들의 반복자를 만들어 줍니다. 만약 벡터 리스트 v1의 오너쉽을 취득하고 싶다면 into_iter 메소드를 사용하면 됩니다. mutable reference들에 대해 반복하고 싶다면 iter_mut를 사용하면 됩니다.

Methods that Comsume the Iterator

Iterator trait은 표준 라이브러리로 구현된 여러 개의 메소드들이 있습니다. 자세한 내용을 알고 싶다면 Iterator trait에 대한 표준라이브러리 API 문서를 보면 됩니다.

여러 메소드 중 next를 호출하는 메소드들을 consuming adaptors라고 합니다. 이들은 호출될 때 iterator를 소비합니다. sum 메소드는 iterator에 대한 오너쉽을 취득하고 next를 반복적으로 호출함으로 인해 iterator를 소비합니다.

아래 코드를 보면 sum 메소드를 사용한 직후 v1_iter를 사용하는 것은 허용되지 않습니다. sum이 오너쉽을 가져가기 때문입니다.

#[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);
    }
}

Methods that Produce Other Iterators

Iterator adaptors는 Iterator trait에 구현되어 있는 메소드들로 iterator를 소비하지 않습니다. 이들은 원본 iterator를 변경함으로써 다른 iterators를 생성합니다.

아래에서 iterator adaptor 메소드 map을 호출하는 예제 코드를 보여줍니다. 이 코드에서는 각 item을 순회하며 item을 호출하기 위해 iterator adaptor 메소드인 map에서 클로저를 호출하는 것을 보여줍니다. map 메소드는 변경된 아이템을 생성할 수 있는 새로운 iterator를 반환합니다. 아래에서 클로저는 각 벡터 아이템을 1씩 증가하는 아이템을 생성하는 새로운 iterator를 반환합니다.

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

    v1.iter().map(|x| x + 1);
}

근데 이 코드는 경고문구를 띄우는데요 위에서 작성한 클로저는 호출되지 않습니다. iterator adaptors가 lazy하게 동작하기 때문에 우리는 iterator를 consume 해야 합니다.

$ cargo run
   Compiling iterators v0.1.0 (file:///projects/iterators)
warning: unused `Map` that must be used
 --> src/main.rs:4:5
  |
4 |     v1.iter().map(|x| x + 1);
  |     ^^^^^^^^^^^^^^^^^^^^^^^^^
  |
  = note: `#[warn(unused_must_use)]` on by default
  = note: iterators are lazy and do nothing unless consumed

warning: `iterators` (bin "iterators") generated 1 warning
    Finished dev [unoptimized + debuginfo] target(s) in 0.47s
     Running `target/debug/iterators`

이를 수정하기 위해서 collect 메소드를 사용합니다. 이 메소드는 iterator를 사용하고 결과 값을 컬렉션 데이터 형식으로 수집합니다.
말이 좀 어려운데 아래 코드를 보겠습니다.

벡터에 맵을 호출하여 반환된 iterator의 결과들을 수집한 값을 v2에 바인딩했습니다. 이 벡터는 오리지널 아이템의 값들을 1씩 증가한 아이템을 가진 벡터를 가지게 됩니다.

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]);
}

map이 클로저를 사용하기 때문에 우리는 각 아이템에 원하는 연산을 수행할 수 있습니다. 여러 개의 메소드를 체이닝해서 사용할 수도 있지만 모든 iterators는 lazy하기 때문에 iterator adaptors를 호출한 결과값들을 갖기 위해서는 consuming adaptor 메소드를 호출해야 합니다.

Using Closures that Capture Their Environment

많은 iterator adapters는 클로저를 argument로 받습니다. 그리고 보통 클로저는 주변 환경을 기억합니다.

예를 들어서 filter method를 사용하며 클로저를 사용한다고 합시다. 클로저는 iterator의 item을 사용하고 bool타입을 반환합니다. 클로저가 true를 반환하면, value는 filter가 반환하는 iteration에 포되고 아니면 안됩니다.

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

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

#[cfg(test)]
mod tests {
    use super::*;

    #[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_size(shoes, 10);

        assert_eq!(
            in_my_size,
            vec![
                Shoe {
                    size: 10,
                    style: String::from("sneaker")
                },
                Shoe {
                    size: 10,
                    style: String::from("boot")
                },
            ]
        );
    }
}

위 코드에서 shoe_size 변수를 기억하는 클로저를 filter에서 사용합니다. 이 필터는 Shoe struct 인스턴스의 콜렉션을 순회하며 특정 조건에 맞는 사이즈만 반환합니다.

shoes_in_size 함수는 shoes 벡터와 shoe size 파라메터에 대한 오너쉽을 취득합니다.
이 함수 바디에서 into_iter를 호출하여 벡터의 오너쉽을 취득하는 iterator를 생성합니다. 그리고 우리는 필터를 사용해 해당 반복자를 클로저가 true를 반환하는 아이템만 포함하는 새로운 반복자를 생성합니다.

이 클로저에서는 환경에 있던 shoe_size 파라메터를 이용해 각 shoe_size를 비교하고 collect 메소드를 호출해서 adapted iterator에서 반환되는 새 벡터 값을 함수에서 반환합니다.

성능 비교: Loops vs Iterators

test bench_search_for  ... bench:  19,620,300 ns/iter (+/- 915,700)
test bench_search_iter ... bench:  19,234,900 ns/iter (+/- 657,200)

iter가 더 빠르네요.

글을 마치며

오늘은 반복자에 대해 알아보았습니다.
러스트의 반복자는 lazy evaluation된다는 점을 유의해야겠습니다.
오늘도 함께 공부하시느라 수고하셨습니다.

profile
성장을 향한 작은 몸부림의 흔적들

0개의 댓글