티스토리 뷰

Rust-Language

Closure, 클로저 함수

kmj24 2021. 8. 5. 01:41

Rust의 Closure는 변수에 저장하거나, 다른 함수에 인자로 넘길 수 있는 익명함수이다.

클로저의 특성은 호출되는 Scope에서 클로저함수의 Lexical 환경을 사용할 수 있다.

javascript에서도 클로저 함수가 있는데, 다른 Scope에서 클로저의 환경을 사용할 수 있다는 개념이 비슷해보인다.

 

Closure의 Use case

운동 관리 앱을 만든다고 가정했을때, 필요한 기능은 아래의 내용으로 가정한다.

 - 평소 운동 강도가 일정 이하일 경우 운동한 내용 출력.

 - 평소 운동 강도가 일정 이상 넘어가고 랜덤 숫자와 일치할 경우 금일 운동 제외.

 - 랜덤 숫자와 일치하지 않을 경우 금일 운동한 시간 출력

필요한 기능을 구현하기 위해 프로그램에서 돌아갈 간단한 알고리즘을 만들었다.

use std::thread;
use std::time::Duration;

fn simulated_expensive_calculation(intensity: u32) -> u32 {
  println!("calculating slowly...");
  thread::sleep(Duration::from_secs(2));
  intensity
}

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

fn main(){
  let simulated_user_specified_value = 10;
  let simulated_random_number = 7;

  generate_workout(simulated_user_specified_value, simulated_random_number);
}

이때 클라이언트가 앞으로 알고리즘 호출 방식을 변경해야 한다고 한다.

이럴 경우 업데이트를 단순화 하기 위해 코드를 리팩토링하여 simulated_expensive_calculation함수를 한번만 호출하려고 한다.

이때 클로저를 사용할 수 있다.

한곳에서 코드를 정의해두고, 실제로 결과가 필요한 곳에서만 그 코드를 실행할 필요가 있을때 클로저를 사용할 수 있다.

fn generate_workout(intensity: u32, random_number: u32) -> u32 {
  let expensive_closure = |num: u32| -> u32 {
      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));
    }
  }
}

simulated_expensive_calculation 함수를 expensive_closure 클로저 함수로 바꾸었다.

//closure를 정의하는 형태
|parameter1, parameter2, ...| -> T{
   // ...
   T
};

클로저는 변수에 할당 할수 있고, | 키워드를 이용하여 parameter를 정의한다.

중괄호 내 함수에서 필요한 로직을 작성한다.

반환값이 있다면 반환값을 정의한다.

//모두 동일한 결과를 출력하는 closure함수이다.
fn add_one (x: u32) -> u32 { x + 1 }
let add_one = |x: u32| -> u32 { x + 1 };
let add_one = |x: u32| { x + 1 };
let add_one = |x: u32| x + 1 ;

 

제너릭 파라미터와 Fn 트레잇 사용하여 클로저 저장

운동 관리 앱으로 돌아가서 비용이 높은 계산을 하는 클로저를 필요한것 보다 많이 호출하고 있다.

클로저와 클로저를 호출한 결과값을 가지고 있는 구조체를 만들 수 있다.

구조체는 결과값을 필요로 할 때만 클로저를 호출 할것이고, 결과 값을 Caching한다. 이를 memoization또는 lazy evaluation이라고 한다.

Fn트레잇은 표준 라이브러리에서 제공한다. 모든 클로저들은 Fn, FnMut, FnOnce 중 하나의 트레잇을 구현한다.

여기서는 Fn을 사용한다.

함수는 세개의 Fn 트레잇도 모두 구현한다. 
환경에서 값을 캡처할 필요 가 없다면, Fn 트레잇을 구현한 어떤것을 필요로 하는 곳에 클로저 대신 함수를 사용할 수 있다.

클로저가 이 트레잇 바운드에 맞춰야 하는 parameter와 return value의 타입을 표현하기 위해 Fn 트레잇 바운드에 타입을 추가한다. 여기서는 u32 타입을 사용한다.

//calculation에 클로저를 담고, 선택적인 결과를 value에 담는 Cacher 구조체
struct Cacher<T> where T: Fn(u32) -> u32 {
  calculation: T,
  value: Option<u32>,
}

Cacher에 대한 Caching로직

impl<T> Cacher<T> where T: Fn(u32) -> u32 {
  fn new(calculation: T) -> Cacher<T> {
    Cacher {
      calculation,
      value: None,
    }
  }

  fn value(&mut self, arg: u32) -> u32 {
    match self.value {
      Some(v) => v,
      None => {
        let v = (self.calculation)(arg);
        self.value = Some(v);
        v
      },
    }
  }
}

new 메서드는 제네릭 parameter T를 받고, 구조체와 동일한 트레잇 바운드를 갖도록 정의되어있다.

calculation필드에 명시된 클로저를 포함하고 클로저를 아직 실행한 적이 없기 때문에 value필드는 None이 반환 된다.

클로저의 결과값을 원할 때 클로저를 직접 호출하기보다, value 메서드를 호출하여 Some을 이용하여 결과값을 가지고 있는지 확인하여 가지고 있다면 클로저를 실행하지 않고 Some의 value를 반환한다.

만약 self.value가 None이라면 self.caculation에 저장된 클로저를 호출하고, 나중에 재사용하기 위해 self.value에 저장한 다음 그 값을 반환한다.

fn chacing_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));
    }
  }
}

클로저로 환경 캡처하기

위의 예제에서 클로저를 단지 인라인 익명함수로 사용했다.

그러나 클로저는 환경을 캡처하여 클로저가 정의된 스코프의 변수들에 접근이 가능하다.

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

위의 코드에서 equal_to_x 클로저는 동일한 스코프의 변수인 x를 사용할 수 있다.

일반 함수로는 이와 동일한 동작이 불가능하다.

 

 

참조

https://rinthel.github.io/rust-lang-book-ko/ch13-01-closures.html#%ED%81%B4%EB%A1%9C%EC%A0%80-%ED%99%98%EA%B2%BD%EC%9D%84-%EC%BA%A1%EC%B2%98%ED%95%A0-%EC%88%98-%EC%9E%88%EB%8A%94-%EC%9D%B5%EB%AA%85-%ED%95%A8%EC%88%98

 

클로저: 환경을 캡쳐할 수 있는 익명 함수 - The Rust Programming Language

러스트의 클로저는 변수에 저장하거나 다른 함수에 인자로 넘길 수 있는 익명 함수입니다. 한 곳에서 클로저를 만들고 다른 문맥에서 그것을 평가하기 위해 호출할 수 있습니다. 함수와 다르게

rinthel.github.io

 

'Rust-Language' 카테고리의 다른 글

trait: 공유 동작 정의  (0) 2021.09.08
반복자, iterator  (0) 2021.09.03
스마트 포인터  (0) 2021.05.16
collection - vector  (0) 2021.04.23
if let 흐름 제어  (0) 2021.04.13
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/07   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30 31
글 보관함