Programming/Rust

러스트 반복자와 클로저

2mukee 2025. 10. 8. 21:40
320x100
320x100

함수형 프로그래밍과 러스트

러스트에서는 클로저와 반복자를 통해 함수형 스타일의 프로그래밍을 지원

 

- 클로저 (closure)

변수에 저장할 수 있는 함수와 유사한 구조의 익명함수

 

- 반복자 (iterator)

일련의 요소들을 처리할 수 있는 방법

 

 

 

 

클로저 (자신의 환경을 캡처하는 익명 함수)

러스트의 클로저는 변수에 저장하거나 다른 함수에 인수로 전달할 수 있는 익명 함수

한곳에서 클로저를 만들고 다른 콘텍스트의 다른 곳에서 이를 호출하여 평가할 수 있음

함수와 다르게 클로저는 정의된 스코프에서 값을 캡쳐할 수 있음

 

#[derive(Debug, PartialEq, Copy, Clone)]
enum ShirtColor {
    Red,
    Blue,
}

struct Inventory {
    shirts: Vec<ShirtColor>,
}

impl Inventory {
    fn giveaway(&self, user_preference: Option<ShirtColor>) -> ShirtColor {
        user_preference.unwrap_or_else(|| self.most_stocked()) # 클로저
    }
    
    fn most_stocked(&self) -> ShirtColor {
        let mut num_red = 0;
        let mut num_blue = 0;
        
        for color in &self.shirts {
            match color {
                ShirtColor::Red => num_red += 1,
                ShirtColor::Blue => num_blue += 1,
            }
        }
        
        if num_red > num_blue {
            ShirtColor::Red
        } else {
            ShirtColor::Blue
        }
    }
}

fn main() {
    let store = Inventory {
        shirts: vec![ShirtColor::Blue, ShirtColor::Red, ShirtColor::Blue],
    };
    
    let user_pref1 = Some(ShirtColor::Red);
    let givaway1 = store.giveaway(user_pref1);
    println!("User with preference {:?} gets {:?}", user_pref1, giveaway1);
    
    let user_pref2 = None;
    let givaway2 = store.giveaway(user_pref2);
    println!("User with preference {:?} gets {:?}", user_pref2, giveaway2);
}

 

 

 

 

 

함수와 클로저의 차이

1. 클로저는 인스턴스의 불변 참조자를 캡처하여 특정 메서드에 넘겨주는 등의 일을 할 수 있으나, 함수는 불가함

2. 클로저는 보통 매개변수나 반환값의 타입을 명시하도록 요구하지 않음

3. 클로저는 함수처럼 노출된 인터페이스로 사용되지 않음 (이름 없이 라이브러리의 사용자에게 노출되지 않은 채로 변수에 저장되고 사용됨

4. 컴파일러는 변수에 대한 타입을 추론하는 방법과 비슷한 식으로 클로저의 매개변수와 반환 타입을 추론 (드물게 컴파일러가 클로저의 타입을 명시하도록 요구하는 경우도 있음)

 

- 클로저에 매개변수와 반환 값의 타입을 추가적으로 명시하기

    expensive_closure = |num: u32| -> u32 {
        println!("calculating");
        thread::sleep(Duration::from_secs(2));
        num
    };

 

- 함수와 클로저 비교

fn add_one_v1(x: u32) -> u32 { x + 1 };

// 모든 것이 명시된 클로저
let add_one_v2 = |x: u32| -> u32 { x + 1 };

// 타입 명시를 제거한 클로저
let add_one_v3 = |x| -> { x + 1 };

// 중괄호까지 제거한 클로저
let add_one_v4 = |x| x + 1;

 

- 클로저의 추론

let ex_closure = |x| x;

// 여기서는 컴파일러가 클로저의 매개변수와 반환 값이 String 이라고 추론
let s = ex_closure(String::from("hello"));

// 오류 발생. 이미 클로저의 타입이 추론되었는데 다른 타입을 넘겼기 때문
let n = ex_closure(5);

 

 

 

 

 

 

 

클로저의 값 캡쳐 방법들

- 불변으로 빌려오기

fn main() {
    let list = vec![1, 2, 3];
    println!("Before defining closure: {:?}", list);
    
    // 특정 변수가 클로저 정의에 바인딩 됨
    let only_borrows = || println!("From closure: {:?}", list);
    
    println!("Before calling closure: {:?}", list);
    
    only_borrows();
    println!("After calling closure: {:?}", list);
}

 

- 가변으로 빌려오기

fn main() {
    let mut list = vec![1, 2, 3];
    
    println!("Before defining closure: {:?}", list);
    
    let mut borrows_mutably = || list.push(7);
    
    // 클로저와 정의와 호출 사이에는 출력을 위한 불변 대여가 허용되지 않는다 (가변 대여가 있을 때는 다른 대여가 허용되지 않기 때문)
    // println!("Before calling closure: {:?}", list);
    
    borrows_mutably();
    
    println!("After calling closure: {:?}", list);
}

 

- 소유권 이동

use std::thread;

fn main() {
    let list = vec![1, 2, 3];
    println!("Before defining closure: {:?}", list);
    
    // list의 소유권을 새 쓰레드에 제공될 클로저로 이동 시키기 위해 move 키워드 사용
    thread::spawn(move || {
        println!("From thread: {:?}", list)
    }).join().unwrap();
}

 

 

 

 

 

 

Option<T> unwrap_or_else

impl<T> Option<T> {
    pub fn unwrap_or_else<F>(self, f: F) ->
    where
        F: FnOnce() -> T
    {
        match self {
            Some(x) => x,
            None => f(),
        }
    }
}

 

 

 

 

 

 

반복자

일련의 아이템들에 대해 순서대로 어떤 작업을 수행할 수 있도록 해줌

 

let v1 = vec![1, 2, 3];
let v1_itar = v1.itar(); // Vec<T>에 정의된 iter 메서드를 호출하여 반복자 생성 (코드 자체의 동작은 없음)

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

 

 

 

 

 

Iterator 트레이트와 next 메서드

모든 반복자는 표준 라이브러리에 정의된 Iterator라는 이름의 트레이트를 구현

pub trait Iterator {
    type Item;
    
    fn next(&mut self) -> Option<Self::Item>; // 연관 타입 (associated type)
    
    // 기본 구현 생략
}

 

Iterator 트레이트는 구현하는 이에게 next 메서드 정의만을 요구

이 메서드는 Some 으로 감싼 반복자의 아이템을 하나 씩 반환하고 반복자가 종료될 때는 None을 반환

 

- 반복자의 next 메서드 호출하기

#[test]
fn iterator_demonstration() {
    let v1 = vec![1, 2, 3];
    
    // 반복자에 대한 next 메서드 호출은 반복자 내부의 상태를 변경하여 반복자가 현재 시퀀스의 어디에 있는지 추적
    let mut v1_iter = v1.iter();
    
    // next 메서드 호출은 반복자로 부터 아이템을 소비
    // next 호출로 얻어온 값들은 벡터 내 값들에 대한 불변 참조자
    // 소유권을 얻고 싶다면 iter 대신 iter_into를 사용
    // 비슷하게 가변 참조자에 대한 반복자가 필요하면 iter_mut를 사용
    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);
}

 

참고로 for 루프를 사용할 때는 반복자를 가변으로 만들 필요가 없다

루프가 반복자의 소유권을 가지고 내부적으로 가변으로 만들기 때문

 

 

 

 

 

소비 어댑터 (consuming adaptor)

Iterator 트레이트 내 next를 호출하는 메서드들을 의미. 호출하면 반복자를 소비하기 때문 

대표적으로 sum 메서드가 있는데, 

이는 반복자의 소유권을 가져온 다음 반복적으로 next를 호출하는 방식으로 순회하며 반복자를 소비

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

 

 

 

 

 

반복자 어댑터 (iterator adaptor)

Iterator 트레이트에 정의된 메서드로 반복자를 소비하지 않는 대신 원본 반복자의 어떤 측면을 바꿔 다른 반복자를 제공

대표적으로 map 메서드가 있는데,

클로저를 인수로 받아서 각 아이템에 대해 호출하여 아이템 전체를 순회하고 

수정된 아이템들을 생성하는 새로운 반복자를 반환

여기서 클로저는 벡터의 각 아이템에서 1이 증가한 새로운 반복자를 만듦

#[test]
fn collect_map() {
    let v1: Vec<i32> = vec![1, 2, 3];
    
    // collect 메서드를 호출하여 결괏값을 모아서 컬렉션 타입으로 반환해야함
    let v2: Vec<_> = v1.iter().map(|x| x + 1).collect();
    
    assert_eq(v2, vec![2, 3, 4]);
}

 

 

 

 

 

루프와 반복자 중 선택하기

대부분의 러스트 프로그래머는 반복자 스타일을 선호

반복자는 고수준의 추상화이지만 컴파일이 되면 직접 작성한 저수준의 코드로 내려가며,

이는 러스트의 비용 없는 추상화(zero-cost abstraction)로 런타임 오버헤드가 없음을 의미

즉, 반복자와 클로저는 코드를 좀 더 고수준으로 보이도록 하지만, 런타임 성능에 불이익을 주지 않는다

 

 

 

300x250
728x90