Rust 클로저

mohadang·2023년 8월 20일
0

Rust

목록 보기
28/30
post-thumbnail
let expensive_closure = |num| {
    println!("calculating slowly...");
    thread::sleep(Duration::from_secs(2));
    num
};

클로저 특징

함수와 다르게 호출되는 스코프로부터 변수들을 캡처할 수 있다

클로저가 겉으로 봤을때 익명 함수와 비슷하다. 하지만 클로저는 함수와 다르게 호출되는 스코프로부터 변수들을 캡처할 수 있다.

클로저

fn main() {
    let x = 4;
    
    let equal_to_x = |z| z == x;
    
    let y = 4;
    
    assert!(equal_to_x(y));
}

함수

fn main() {
    let x = 4;
    
    fn equal_to_x(z: i32) -> bool { z == x }
    
    let y = 4;
    
    assert!(equal_to_x(y)); // 에러 발생
}

error[E0434]: can't capture dynamic environment in a fn item; use the || { ...
} closure form instead
 --> src/main.rs
  |
4 |     fn equal_to_x(z: i32) -> bool { z == x }
  |     

변수 캡처시 복사나 소유권 이동을 할 수 있다

다음은 스코프에 변수가 복사되어 문제가 없다.

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

    let equal_to_x = |z| z == x;

    println!("can't use x here: {:?}", x);

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

    assert!(equal_to_x(y));
}

move 키워드를 사용하면 스코프 캡처시 소유권 이동이 발생하여 컴파일 에러 발생한다.

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

    let equal_to_x = move |z| z == x;

    println!("can't use x here: {:?}", x);

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

    assert!(equal_to_x(y));
}

5 |
6 |     println!("can't use x here: {:?}", x);
  |                                        ^ value borrowed here after move

타입 추론

클로저를 사용하면 컴파일러가 타입을 유추하여 클로저를 동작 시킨다.

example_closure는 첫번째 클로저 사용으로 x 가 String으로 해석되었다. 이 상태에서 5 인자를 전달하면 컴파일 에러가 발생한다.

클로저에 타입 어노테이션을 추가하여 선언하는 것이 가능하다.

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  ;

클로저를 사용한 리팩토링

Step1

simulated_expensive_calculation 함수가 필요 이상으로 중복 호출되고 있다.

fn generate_workout(intensity: u32, random_number: u32) {
    if intensity < 25 {
        println!(
            "Today, do {} pushups!",
            simulated_expensive_calculation(intensity)    <--
        );
        println!(
            "Next, do {} situps!",
            simulated_expensive_calculation(intensity)    <-- 중복 호출
        );
    } else {
        if random_number == 3 {
            println!("Take a break today! Remember to stay hydrated!");
        } else {
            println!(
                "Today, run for {} minutes!",
                simulated_expensive_calculation(intensity)
            );
        }
    }
}

Step 2

필요 이상의 호출을 제거하였지만 호출하지 않아도 되는 상황에서 함수를 호출한다.

fn generate_workout(intensity: u32, random_number: u32) {
    let expensive_result =
        simulated_expensive_calculation(intensity);

    if intensity < 25 {
        println!(
            "Today, do {} pushups!",
            expensive_result
        );
        println!(
            "Next, do {} situps!",
            expensive_result
        );
    } else {
        if random_number == 3 {    
        	--> 호출하지 않아도 되는 상황 <--
            println!("Take a break today! Remember to stay hydrated!");
        } else {
            println!(
                "Today, run for {} minutes!",
                expensive_result
            );
        }
    }
}\

Step 3 클로저 사용 (1/2)

expensive_closure 클로저를 정의하여 사용 하였지만 아직이다. 중복 호출 문제가 다시 발생 하고 있다.

fn generate_workout(intensity: u32, random_number: u32) {
    let expensive_closure = |num| {
        println!("calculating slowly...");
        thread::sleep(Duration::from_secs(2));
        num
    };

    if intensity < 25 {
        println!(
            "Today, do {} pushups!",
            expensive_closure(intensity)
        );
        println!(
            "Next, do {} situps!",
            expensive_closure(intensity)    <-- 중복 호출
        );
    } else {
        if random_number == 3 {
            println!("Take a break today! Remember to stay hydrated!");
        } else {
            println!(
                "Today, run for {} minutes!",
                expensive_closure(intensity)
            );
        }
    }
}

Step 4 클로저 사용 (2/2)

Cacher와 클로저를 사용하여 이미 계산한 값을 재활용 하여 불필요한 연산을 줄였다. 또한 호출할 필요 없는 상황에서 호출하는 문제도 해결 하였다.

struct Cacher<T>
where
    T: Fn(u32) -> u32,
{
    calculation: T,
    values: HashMap<u32, u32>,
}

impl<T> Cacher<T>
where
    T: Fn(u32) -> u32, // T의 타입이 u32 인자 하나를 받는 함수/클로저
{
    fn new(calculation: T) -> Cacher<T> {
        Cacher {
            calculation,
            values: Default::default(),
        }
    }
    fn value(&mut self, arg: u32) -> u32 {
        if let Some(&v) = self.values.get(&arg) {
            v
        } else {
            let v = (self.calculation)(arg);
            self.values.insert(arg, v);
            v
        }
    }
}

fn generate_workout(intensity: u32, random_number: u32) {
    let mut expensive_result = Cacher::new(|num| {
        println!("calculating slowly...");
        thread::sleep(Duration::from_secs(2));
        num
    });

    if intensity < 25 {
        println!(
            "Today, do {} pushups!",
            expensive_result.value(intensity)
        );
        println!(
            "Next, do {} situps!",
            expensive_result.value(intensity)
        );
    } else {
        if random_number == 3 {
            println!("Take a break today! Remember to stay hydrated!");
        } else {
            println!(
                "Today, run for {} minutes!",
                expensive_result.value(intensity)
            );
        }
    }
}

이 패턴을 메모이제이션(memoization) 혹은 지연 평가(lazy evaluation)로 불린다.

클로저 타입

클로저는 3가지 방식으로 값을 받아올 수 있다.

  • 소유권 받기
  • 불변으로 빌려오기
  • 가변으로 빌려오기

이 3 방식은 트레잇으로 표현된다.

  • FnOnce : 캡처한 변수를 소비하기 위해 클로저는 이 변수의 소유권을 가져온다. 이름의 일부인 Once 는 클로저가 동일한 변수들에 대해 한번이상 소유권을 얻을수 없다는 사실을 의미하며, 그래서 한 번만 호출 될 수 있다.
  • Fn : 값들을 불변으로 빌려 옵니다.
  • FnMut : 값들을 가변으로 빌려오기 때문에 값을 변경할 수 있다.

클로저 타입 사용

Rust에서도 함수를 인자로 받는 함수 포인터가 사용 가능 하다.

fn add_onoe(x: i32) -> i32 {
    x + 1
}
fn do_twich(f: fn(i32) -> i32, arg: i32) -> i32 {
    f(arg) + f(arg)
}
fn main() {
    let ans = do_twich(add_onoe, 5);
    println!("The answer is: {}", ans);
}

이를 클로저 타입으로 받을 수 도 있다.

fn add_onoe(x: i32) -> i32 {
    x + 1
}
fn do_twich<T>(f: T, arg: i32) -> i32
    where T: Fn(i32) -> i32 {    <--i32 인자 하나를 받고 i32를 반환하는 클로저 타입
    f(arg) + f(arg)
}

fn main() {
    let ans = do_twich(add_onoe, 5);
    println!("The answer is: {}", ans);
}

클로저를 사용하면 인자로 로직을 전달할 수 있다(C++의 함수자와 유사). 대표적인 사용 예가 map이다. map은 인자로 클로저를 받는다.

fn main() {
    let list_of_numbers = vec![1, 2, 3];
    let list_of_strings: Vec<String> = list_of_numbers
        .iter()
        .map(|i| i.to_string())    <-- map 인자에 클로저를 넘기고 있다.
        .collect();
}

클로저 타입은 함수도 받을 수 있기에 클로저 대신 함수도 넘길 수 있다.

fn main() {
    let list_of_numbers = vec![1, 2, 3];
    let list_of_strings: Vec<String> = list_of_numbers
        .iter()
        .map(ToString::to_string)    <-- 함수 사용
        .collect();
}

클로저는 튜플 구조체도 받을 수 있다

enum Status {
    Value(u32),// tuple struct
    Stop,
}

fn main() {
    let list_of_statuses: Vec<Status> = (0_u32..20).map(Status::Value).collect();
    println!("{:?}", list_of_statuses);
}

함수를 반환하는 클로저

클로저는 인자뿐만 아니라 반환 타입에도 사용 가능 하다.

fn returns_closure() ->
                     impl Fn(i32) -> i32 { // impl Fn(i32) -> i32 와 같은 클로저를 반환
    |x| x + 1
}

fn main() {
    let closure = returns_closure();
    assert_eq!(2, closure(1));
}

여기서 인자를 받아서 사용하는 클로저라면 문제가 발생한다.

fn returns_closure(base: i32) ->
                     impl Fn(i32) -> i32 {
    |x| x + 1 + base
}

fn main() {
    let closure = returns_closure(10);
    assert_eq!(2, closure(1));
}

 --> src\main.rs:3:5
  |
3 |     |x| x + 1 + base
  |     ^^^         ---- `base` is borrowed here
  |     |
  |     may outlive borrowed value `base`

base는 클로저에서 참조(borrowed)되었다. Rust의 참조 규칙은 참조된 데이터는 원본 데이터보다 오래 살 수 없다. 이 클로저는 원본이 없어 졌음에도 사용될 수 있기에 메모리 위험이 있다. 이럴 경우 클로저에 소유권을 이전하는 방법이 있다.

fn returns_closure(base: i32) ->
                     impl Fn(i32) -> i32 {
    move |x| x + 1 + base
}

fn main() {
    let closure = returns_closure(10);
    assert_eq!(12, closure(1));
}

만약 인자 값에 따라 다른 클로저를 반환 한다면 또다른 문제가 발생한다.

fn returns_closure(base: i32) ->
                     impl Fn(i32) -> i32 {
    if base > 10 {
        move |x| x + 10 + base
    } else {
        move |x| x + 0 + base
    }

}

fn main() {
    let closure = returns_closure(5);
    assert_eq!(6, closure(1));
}

 --> src\main.rs:6:9
  |
3 | /     if base > 10 {
4 | |         move |x| x + 10 + base
  | |         ----------------------
  | |         |
  | |         the expected closure
  | |         expected because of this
5 | |     } else {
6 | |         move |x| x + 0 + base
  | |         ^^^^^^^^^^^^^^^^^^^^^ expected closure, found a different closure
7 | |     }
  | |_____- `if` and `else` have incompatible types

클로저는 타입 추론을 하여 컴파일 타임에 정의한다. 컴파일 타임에 인자값이 무엇인지 알 수 없기에 문제가 발생한다.

이 문제를 해결하기 위해 반환되는 트레잇 오브젝트가 동적으로 결정됨을 의미하는 dyn 키워드와 힙 메모리에 할당 가능한 Box 스마트 포인터를 조합하여 사용한다. Box를 사용하는 이유는 컴파일 타임에 반환되는 트레잇 오브젝트 크기를 계산할 수 없기에 사용한다.

fn returns_closure(base: i32) ->
                     Box<dyn Fn(i32) -> i32> {
    if base > 10 {
        Box::new(move |x| x + 10 + base)
    } else {
        Box::new(move |x| x + 0 + base)
    }

}

fn main() {
    let closure = returns_closure(5);
    assert_eq!(6, closure(1));
}
profile
mohadang

0개의 댓글